이미지 파일 업로드: 타입 이슈와 해결
후기 작성 시, 사용자가 여러 사진 파일을 업로드할 수 있도록 구현하였다.

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
속성의 존재 여부로 구분할 수 있다고 말했지만, 타입스크립트는 그렇지 않은 모양이었다. 아래와 같은 타입 에러가 발생하였다.

어쩔 수 없이 as 키워드를 통해 files[i]가 UploadFile임을 확언해주어야 했다.
아래와 같이 변경하였다.
if ('originFileObj' in files[i]) { formData.append(key, (files[i] as UploadFile).originFileObj); }
이 문제는 해결되었으나, 타입 오류가 또 있었다.

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