이미지 업로드가 느려 사용자에게 로딩 중임을 알려줄 로딩 UI가 필요했다.
이미지를 업로드하는 과정에서 시간이 걸려, 사용자가 이미지가 업로드되는 동안 아무런 표시가 없어 이미지가 업로드되고 있음을 인식하기 어려웠다. 그래서 사용자에게 업로드 중임을 명확히 알리기 위해서 로딩 UI가 필요했다.
나는 이미지 업로드 시 상단에 progressBar를 통해 업로드 진행률을 표시하기로 결정했다.
spinner, skeleton 등 로딩 UI 중에 progressBar를 선택한 이유는 다른 UI를 가리지 않고 전역에서 로딩 상태를 관리하여 표시하고 싶었다.
OnUploadProgress를 활용해 ProgressBar에 진행률 업데이트하기
ProgressBar 전역 상태관리
이미지 업로드뿐만 아니라 저장하기 등의 작업에서도 로딩을 사용하기 위해 전역에서 관리했다.
import { atom } from 'recoil'
const progressBarState = atom({
key: 'progressBarState',
default: {
isLoading: false,
percent: 0,
},
})
export default progressBarState
ProgressBar Component
로딩 중이 아닌 경우에는 아무것도 렌더링 하지 않고 로딩 중이라면 percent 만큼 바가 채워지도록 했다.
import { useRecoilValue } from 'recoil'
import progressBarState from '@/recoil/atoms/progressBarState'
function ProgressBar() {
const progressBar = useRecoilValue(progressBarState)
const { isLoading, percent } = progressBar
if (!isLoading) return null
return (
<div className="w-full bg-gray-200 h-2 mb-4 fixed top-0 z-50">
<div className="brand-gradient h-2" style={{ width: `${percent}%` }} />
</div>
)
}
export default ProgressBar
useImageUpload hooks
axios에서 onUploadProgress는 파일 업로드 중에 업로드 진행 상태를 추적한다. 이를 사용해 progressBar의 진행률을 업데이트해 줬다.
import httpClient from '@/lib/httpClient'
import { useSetRecoilState } from 'recoil'
import progressBarState from '@/recoil/atoms/progressBarState'
const useImageUpload = () => {
const setProgressBar = useSetRecoilState(progressBarState)
const uploadImage = async (
files: FileList | null,
type: 'thumbnail' | 'asset',
) => {
if (!files || !files[0]) throw new Error('업로드 할 파일이 없습니다.')
const file = files[0]
const formData = new FormData()
formData.append('file', file)
// 로딩 시작
setProgressBar((prev) => ({
...prev,
isLoading: true,
}))
try {
const {
data: { imageUrl },
} = await httpClient.post(`/v1/image/upload/${type}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
// loaded는 현재 업로드된 바이트 수, total은 전체 바이트 수를 나타냄
const { loaded, total } = progressEvent
if (!total) return
const percentage = Math.floor((loaded / total) * 100) // 업로드 진행률
setProgressBar((prev) => ({
...prev,
percent: percentage,
}))
},
})
// 로딩 끝
setProgressBar({
percent: 0,
isLoading: false,
})
return imageUrl
} catch (error) {
console.error('Image upload failed:', error)
throw error
}
}
return { uploadImage }
}
export default useImageUpload
문제?!
기본 네트워크 통신에서 console.log로 진행률을 확인했을 때, 0 다음 바로 100이 찍혀 progressBar가 서서히 채워지는 게 아니라 시작과 동시에 채워졌다.
Network탭에서 통신을 느리게 Slow 3G로 주니... 내가 원하던 대로 0, 7, 10,,,,100 하고 진행률이 채워졌다... 그리고 진행률이 100이 되는 시점에 업로드된 이미지가 반영되길 원했으나 진행률이 100이 되었을때도 api는 아직 pending 상태였고 api 응답 후에는 업로드된 이미지 url을 받아 이를 다시 받아오기까지에 시간이 걸렸다....
내가 원하던 건
이미지 업로드 요청 -> progressBar에 현재 진행률 업데이트 -> 진행률 100% -> 업로드 된 이미지 짠!
이런 자연스러움이었다... 🥲
내가 아예 잘못 생각하고 있던게 있다!!!!! 바로 파일이 서버로 업로드되는 것(onUploadProgress)과 서버에서 응답을 받는것은 별개이다!
onUploadProgress는 파일이 서버로 전송되는 동안에만 호출되며 파일이 서버로 전송되는 진행률을 표시한다. 따라서 진행률이 100%라는 것은 파일이 서버로 성공적으로 업로드되었음을 의미하는 것이다!
다시! 만들어 본 progressBar
결과적으로 진행률을 정확하게 표시하는건 포기했다.
우연히 구글 사이트를 보다가 progressBar 로딩을 발견했다! 이전에 나는 progressBar에는 무조건 진행률이 나타나야한다!라고 생각했는데 구글에서는 진행률과 상관없이 로딩 중이라면 progressBar가 진행되는 모습으로 나타났다!
percent를 제거하고 isLoading으로 진행중이라면 progressBar가 나타나고 애니메이션 효과를 줬다ㅎㅎ
다시 만든 전체 소스는 아래와 같다.
import { atom } from 'recoil'
const progressBarState = atom({
key: 'progressBarState',
default: false,
})
export default progressBarState
import httpClient from '@/lib/httpClient'
import { useSetRecoilState } from 'recoil'
import progressBarState from '@/recoil/atoms/progressBarState'
const useImageUpload = () => {
const setIsLoading = useSetRecoilState(progressBarState)
const uploadImage = async (
files: FileList | null,
type: 'thumbnail' | 'asset',
) => {
if (!files || !files[0]) throw new Error('업로드 할 파일이 없습니다.')
const file = files[0]
const formData = new FormData()
formData.append('file', file)
// 로딩 시작
setIsLoading(true)
try {
const {
data: { imageUrl },
} = await httpClient.post(`/v1/image/upload/${type}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
// 로딩 끝
setTimeout(() => {
setIsLoading(false)
}, 500)
return imageUrl
} catch (error) {
console.error('Image upload failed:', error)
throw error
}
}
return { uploadImage }
}
export default useImageUpload
import { useRecoilValue } from 'recoil'
import progressBarState from '@/recoil/atoms/progressBarState'
function ProgressBar() {
const isLoading = useRecoilValue(progressBarState)
if (!isLoading) return null
return (
<div className="w-full bg-gray-200 h-1.5 fixed top-0 left-0 z-50">
<div className="absolute top-0 left-0 w-5/12 brand-gradient h-1.5 animate-loader" />
</div>
)
}
export default ProgressBar
다시 수정한 최종 결과물은...!
제목과는 다른 결과가 나왔지만... onUploadProgress의 역할에 대해서 정확히 알 수 있었다.
'Project > foliohub' 카테고리의 다른 글
[Next.js] NotFound / Error 페이지 Custom하기 (0) | 2024.04.06 |
---|---|
프로젝트 1차 완성 🎉(+검색엔진에 내 사이트가 뜬다!) (0) | 2024.04.04 |
Route 53로 EC2 서버에 도메인 연결하기 (0) | 2024.03.21 |
가비아 도메인 구매 (0) | 2024.03.21 |
[patch-package] library custom하는 방법 (0) | 2024.03.11 |