이미지 파일 S3 업로드

Hee·2024년 4월 14일
0

설정화면에서 프로필 사진을 업로드하는 기능을 개발했다.

파일 유효성 검사


해당 조건에 따라 유효성 검사를 하는데,
확장자와 용량은 파일 객체를 대상으로 하고, 이미지 사이즈를 검사하기 위해서는 이미지 객체 생성이 추가로 필요했다.

새로운 Promise 객체를 만들고 이 Promise가 유효성 검사 결과를 반환하도록 했다.

파일 확장자

유효한 파일 유형을 정의한다. 여기서는 jpeg(jpg), png 유형을 허용한다.

 const validTypes = ['image/jpeg', 'image/png']

포함되어 있지 않으면 해당 오류 메시지를 사용하여 reject를 호출.

 if (!validTypes.includes(file.type)) {
            return reject('JPEG(JPG), PNG인 사진 확장자로 올려주세요.')
        }

파일 용량

비슷한 방식으로 파일의 크기가 허용된 최대 크기를 초과하는지 확인한다. 초과할 경우 해당 오류 메시지를 사용하여 reject 호출.

  const validMaxSize = 2 * 1024 * 1024
        if (file.size > validMaxSize) {
            return reject('사진 용량을 2MB 이하로 줄여서 올려주세요.')

파일을 읽기 위해 FileReader 객체를 생성하고
파일 읽기 작업이 성공하거나 실패한 경우에 대한 콜백 함수를 정의했다.

 const reader = new FileReader()
        reader.onload = (e) => {
       //파일 읽기 작업 성공시 로직 
        reader.onerror = () => {
            reject(
                '사진을 올리는 과정에서 오류가 발생했어요. 다시 시도해 주세요.'
            )
        }

이미지 크기

파일 읽기가 성공하면 데이터 URL로 변환된 이미지를 확인하고 해당 이미지의 폭과 높이를 검사한다.
이미지의 폭과 높이가 200픽셀 이상인지 확인한다.
모든 검사가 통과되면 resolve 호출하여 데이터 URL을 반환한다.

    reader.onload = (e) => {
            const result = e.target?.result

            if (typeof result === 'string') {
                const img = new Image()

                img.onload = () => {
                    if (img.width < 200 || img.height < 200) {
                        reject('200*200 픽셀 이상인 사진으로 올려주세요.')
                    } else {
                        resolve(result)
                    }
                }
                img.onerror = () => {
                    reject(
                        '사진을 올리는 과정에서 오류가 발생했어요. 다시 시도해 주세요.'
                    )
                }
                img.src = result
            }
        }

에러 토스트

위 reject에서 나오는 error 메시지를 토스트로 제공한다.

만약에 reject('사진을 올리는 과정에서 오류가 발생했어요. 다시 시도해 주세요.') 라면
toast fail 인수에 해당 메시지가 들어갈 것이다.

    const uploadFile = async (file: File) => {
        try {
            const uploadResult = await validateFile(file)
            setImageSrc(uploadResult)
            await getPresignedUrl(file)
        } catch (error) {
            fail(`${error}`)
        }
    }

S3 presigned Url

위에서 유효성 검사가 끝나면 업로드할 파일의 presigned URL을 가져오는 함수를 실행한다.

url 요청

나는 워크스페이스와 사용자 프로필을 업로드가 필요해서 prop으로 받는 타입에 따라 요청이 다르다.

서버에 'url 주세요' 요청을 해서

    const getPresignedUrl = async (file: File) => {
        try {
            const response = await (type === 'workspace'
                ? uploadService.getWorkspacePresignedUrl(id as string)
                : uploadService.getProfilePresignedUrl())
            const { url, key } = response.data.data
            await putImageFileToS3(url, file, key)
        } catch (error) {
            console.error(error)
        }
    }

성공하면 url와 key를 response로 받고
이를 이용해 s3 bucket upload에 업로드 하는 함수를 실행한다.

s3 bucket upload

response로 받은 url에 업로드 파일 put 요청을 보내고
여기서 성공하면 객체 key를 form value에 담아 보내서 서버에 저장한다.

    const putImageFileToS3 = async (url: string, file: File, key: string) => {
        try {
            await axios.put(url, file, {
                headers: {
                    'Content-Type': 'image/png',
                },
            })
            setValue(name as FieldPath<T>, key as PathValue<T, FieldPath<T>>, {
                shouldValidate: true,
            })
        } catch (error) {
            console.error(error)
        }
    }

미리보기

따라서 Preview 컴포넌트를 만들고
그 안에서 imageSrc 유무에 따라 이미지 영역을 핸들링했다.

 {imageSrc ? (
                <S.Image
                    src={imageSrc}
                    alt="preview-img"
                />
            ) : (
                <S.EmptyImage
                    type={type}
                >
                    {firstCharOfName}
                </S.EmptyImage>
            )}

사진 업로드 시

사용자가 사진을 업로드 하면 유효성검사를 성공하면
base64로 인코딩한 문자열 형태로 result가 반환되고 이를 imageSrc에 값으로 저장한다.

Preview영역에 이를 전달한다.

            const uploadResult= await validateFile(file)
            setImageSrc(uploadResult)

사진 없을 시

등록된 이미지가 없을 경우,
즉, 사용자가 등록하기 전이거나 기존 이미지를 삭제하는 경우 사용자 이름의 첫 글자를 표시한다.

Preview영역에 이를 전달한다.

      <Preview
             imageSrc={imageSrc}
             firstCharOfName={firstCharOfName}
             type={type}
         />

데이터 조회 시

설정 페이지에서 데이터를 조회할 때에는 서버에서 s3 url 주소로 받아 접근하여 볼 수 있다.

    const getFormValues = getValues()
    const firstCharOfName = getFormValues['name'].charAt(0)
    const initialImageSrc = getFormValues[name]

설정 페이지 렌더링 시 받은 initialImageSrc를 ImageSrc에 저장한다.

여기서는 react-hook-form으로 설정페이지 값들을 관리하고 있어서 formvalue에서 받아오고 Preview영역에 이를 전달한다.

  useEffect(() => {
        if (initialImageSrc && !imageSrc) {
            setImageSrc(initialImageSrc as string)
        }
profile
*^^*

0개의 댓글