HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🚀
개발 노트
/
🎨
아트집 - 본론
/
📜
<아트집> 기술 문서
/
📌
[후기 작성 페이지] 이미지 파일 업로드
📌

[후기 작성 페이지] 이미지 파일 업로드

 
이미지 파일 업로드: 타입 이슈와 해결깨달은 점
 
 

이미지 파일 업로드: 타입 이슈와 해결

후기 작성 시, 사용자가 여러 사진 파일을 업로드할 수 있도록 구현하였다.
 
notion image
Antd의 Upload 컴포넌트를 사용하였기에, 마크업은 전혀 어렵지 않았다.
반면에, 까다로웠던 것은 ‘타입’이었다.
위 이미지와 같이 파일을 업로드하면,
각 파일은 UploadFile 이라는 Antd 고유의 타입으로 정의된다.
아래는 UploadFile 의 interface 선언이다. (상당히 많은 속성들이 정의되어 있다…)
export interface UploadFile<T = any> { uid: string; size?: number; name: string; fileName?: string; lastModified?: number; lastModifiedDate?: Date; url?: string; status?: UploadFileStatus; percent?: number; thumbUrl?: string; crossOrigin?: React.ImgHTMLAttributes<HTMLImageElement>['crossOrigin']; originFileObj?: RcFile; response?: T; error?: any; linkProps?: any; type?: string; xhr?: T; preview?: string; }
이 중 originFileObj 라는 속성으로 File 객체가 담겨 있는 것을 볼 수 있다.
실제로 formData에 append할 것은 originFileObj라는 이 객체이며, 다른 속성은 불필요하다.
그런데 다시 한 번 보자.
originFileObj는 File 객체가 아니라 RcFile이라는 타입의 객체다.
File 객체를 확장하여 선언한 것이 RcFile이라는 타입이다.
export interface RcFile extends File { uid: string; }
 
상당히 복잡하지 않은가??
그렇다. 내가 필요한 것은 서버에 전송할 File 객체인데,
Antd의 고유한 타입 정의로 인해 이 File 객체를 얻어내는 것이 상당히 까다로웠다.
 
결과적으로, 나는 다음과 같이 구현하였다.
우선 아래와 같이 업로드 한 파일을 저장하는 files를 선언하였다. UploadFile의 배열이다.
const [files, setFiles] = useState<UploadFile[]>([]);
 
폼이 제출되면, 업로드한 이미지 파일을 formData에 담아야 한다.
나는 convertFilesToFormData라는 유틸 함수를 만들었고, 아래와 같이 files를 넘겨주었다.
convertFilesToFormData('files', files, formData);
 
내가 만든 convertFilesToFormData 함수의 타입 정의는 아래와 같다.
const convertFilesToFormData: (key: string, files: FileList | UploadFile[], _formData?: FormData) => FormData
 
파라미터 설명
  • key: formData에 담을 File의 key이다. 임의의 문자열을 key로 정할 수 있다.
  • files: 파일 목록이다. FileList 또는 UploadFile[]을 받을 수 있다.
    • 이 경우에는 UploadFile[]을 받겠지만, 일반적으로 사용되는 FileList도 받을 수 있도록 했다.
  • _formData: 말 그대로 formData이다. optional.
    • formData를 전달할 경우 해당 formData에 append 하며,
      그렇지 않을 경우 새로운 formData를 생성하여 append한 뒤 반환한다.
 
이제 구체적으로 convertFilesToFormData 를 살펴보자.
export const appendFilesToFormData = ( key: string, files: FileList | UploadFile[], _formData?: FormData, ) => { const formData = _formData ? _formData : new FormData(); for (let i = 0; i < files.length; i++) { if ('originFileObj' in files[i]) { formData.append(key, (files[i] as UploadFile).originFileObj as File); } else { formData.append(key, files[i] as File); } } return formData; };
 
for문에서 files에 저장된 파일을 하나씩 가져온다.
이 파일은 File 객체이거나, UploadFile 객체이다.
위에서 말했듯이, 이 둘은 엄연히 다르므로 구분하여 처리하는 것이 필요하다.
우선 originFileObj 속성의 존재 여부로 구분할 수 있다.
originFileObj 속성이 존재한다면 UploadFile 객체이며,
이 UploadFile 객체에서 originFileObj 속성만을 꺼내어 formData에 담아야 한다.
if ('originFileObj' in files[i]) { formData.append(key, files[i].originFileObj); }
 
나는 위에서 originFileObj 속성의 존재 여부로 구분할 수 있다고 말했지만,
타입스크립트는 그렇지 않은 모양이었다. 아래와 같은 타입 에러가 발생하였다.
notion image
어쩔 수 없이 as 키워드를 통해 files[i]가 UploadFile임을 확언해주어야 했다.
아래와 같이 변경하였다.
if ('originFileObj' in files[i]) { formData.append(key, (files[i] as UploadFile).originFileObj); }
이 문제는 해결되었으나, 타입 오류가 또 있었다.
notion image
 
위에서 말했듯, originFileObj는 RcFile이라는 Antd 고유의 타입이며,
이는 File 타입과 다르기 때문에 생긴 오류였다. (File은 Blob에 속한다)
 
하지만 RcFil과 File은 차이점이 크지 않다. uid라는 속성이 하나 추가되었을 뿐이다.
export interface RcFile extends File { uid: string; }
 
그렇기에 타입스크립트가 이를 File로 간주하도록 하였고, 타입 오류를 해결할 수 있었다.
if ('originFileObj' in files[i]) { formData.append(key, (files[i] as UploadFile).originFileObj as File); }
 
이상의 구현 과정을 통해, Antd의 이미지 파일을 성공적으로 업로드할 수 있었다.
아래의 결과물을 보자.
 
 
한편, files[i]가 File 객체인 경우 (사실 이것이 일반적인 경우다)
다음과 같이 formData에 담는다.
이 경우에도 files[i]가 File임을 확언해주는 선언이 필요했다.
else { formData.append(key, files[i] as File); }
이 경우에도 성공적으로 처리할 수 있다.
아래의 결과물을 보자.
유저의 프로필 이미지를 업로드하는 부분으로, Antd를 사용하지 않았다.
즉, files[i]가 UploadFile이 아니라 일반 File이다.
 
 
성공적으로 처리된 것을 확인할 수 있다 :)

깨달은 점

이 기능을 구현하면서 깨달은 것이 있는데 그것은 두 가지다.
 
  1. Antd와 같은 UI 라이브러리에는 장단점이 존재한다.
    1. Antd와 같은 UI 라이브러리를 사용하면,
      이미 만들어진 컴포넌트를 쉽게 사용할 수 있으므로 매우 편리하다. 이것이 큰 장점이다.
      하지만 타입 정의, 그리고 여기서 다루지는 않았지만 고유의 속성이나 메소드 등
      해당 컴포넌트만의 작동 방식이 존재한다.
      그 고유한 작동 방식 때문에, 디테일한 작업을 할 경우 문제가 오히려 복잡해질 수 있다.
      이 글에서 다루었던 타입 관련 이슈가 바로 그것이었다.
      그렇기 때문에, 앞으로 UI 라이브러리를 사용할 때에는
      이러한 장단점을 잘 고려해서 결정할 필요가 있을 것 같다.
 
  1. 타입스크립트의 추론은 완전하지 않다.
    1. 타입 가드를 통해 나름대로 타입을 구분하였음에도,
      생소하고 복잡한 타입인 경우 타입스크립트가 추론에 실패하는 것을 보았다.
      (물론 내가 타입 가드를 잘못한 것일 수도 있다. 그렇다면 지적해주시기 바란다)
      이 경우에는 어쩔 수 없이 타입을 확언해야만 했다.
      타입스크립트의 추론은 완전하지 않다는 것,
      그런 경우 사용자가 임의로 타입을 확언해주는 것이 필요함을 깨달았다.