진행하는 프로젝트에서 formData 형식으로 서버 측에 이미지 파일을 전달해줘야 했는데, 이번 기회에 FormData로 이미지 전송하는 전체 과정과 관련 개념들을 정리해보려고 한다.
input에 파일 업로드하는 과정, input에 업로드한 이미지 프리뷰 처리를 하는 두 가지 방식 비교, formData에 파일 추가하는 과정, console에서 formData를 확인하는 방법, 서버에 axios 요청으로 formData 데이터 전송하는 과정을 순차적으로 다룰 예정이다.
// 추후 서버 요청 시 state값 사용
const [imageFile, setImageFile] = useState<File | null>(null);
const onUploadImage = (e: React.ChangeEvent<HTMLInputElement>) => {
const test = e.target.files;
const file = e.target.files?.[0];
console.log(test);
console.log(file);
if(file){
setImageFile(file);
}
};
<input type="file" accept="image/*" onChange={onUploadImage} />
input type="file"
의 onChange
속성에서 e.target.files
를 콘솔에 확인해보면 첫 번째 찍힌 내용과 같이 File
은 0이라는 key에 담기고, length를 가진 FileList
객체가 반환되는 것을 확인할 수 있다.
이미지 업로드에 필요한 데이터는 File
이기 때문에, e.target.files?.[0]
로 업로드 이미지 파일 데이터를 가져온다.
File API
File API
는 단순 텍스트 데이터 뿐만 아니라 이미지, 오디오, 비디오 등 대용량 바이너리 데이터를 다루기 위한 API이다.
Blob, File, FileReader, FileList 객체로 구성되어 있다.
Blob
: 주로 파일 형태가 아닌 바이너리 데이터(마이크 소리, 영상, canvas 그림 등)을 다룬다.File
: Blob을 상속받은 객체로, 주로 파일 형태의 바이너리 데이터(png, mp3 파일 등)를 다룬다.
- 즉 Blob의 기능을 확장하면서 파일에 대한 추가 정보(파일 이름, 크기, 마지막 수정 날짜..)를 제공하는 객체이다.
- 로컬 파일을 참조하는 객체이다. 로컬 파일을 읽거나 쓸 수 있다.
FileReader
: File이나 Blob에 저장된 바이너리 데이터를 읽어들이는 객체다.FileList
: HTML Input Element를 통해 입력 받은 파일(File
객체 형태)들을 저장하는 유사배열객체다.
이미지 프리뷰 로드 처리를 하기 위해서는 먼저 위 과정에서 가져온 업로드 이미지 File
객체가 Blob
을 상속받는다는 것을 짚고 넘어가야 한다.
Blob이란?
Binary Large Object
- 일반적으로 이미지, 비디오, 사운드 파일과 같이 이진 형태의 대용량 멀티미디어 데이터를 처리할 때 사용된다.(물론 텍스트 데이터도 다룰 수 있음)
- Blob은 보통 사용자가 업로드한
File
객체에 상속을 한다.(File이 Blob을 상속 받는다)- Blob을 상속받은
File
객체로 로컬 파일을 다룰 수 있게 된다.- 즉 Blob은 데이터 자체라기보다, 데이터를 처리하거나 간접적으로 접근하기 위한 객체이다.
Blob을 생성하는 방법
1. 생성자 방식
new Blob(source배열, {type: "MIME Type", endings: "transparent"});
- source 배열: ArrayBuffer, ArrayBufferView, Blob, File, DOMString을 요소로 하는 배열을 입력받는다.
- option 객체: MIME Type과 문자열 처리 방식을 결정한다.
(여기서 MIME Type은 Multipurpose Internet Mail Extensions의 약자로,image/jpeg
,image/png
,image/gif
,text/plain
,text/html
과 같이 다양한 형식의 데이터를 인터넷에서 전송하고 처리하기 위해 데이터의 종류, 형식을 식별하기 위해 파일을 변환한 텍스트 문자열 형태를 말한다)
- type: MIME Type을 객체 형태로 입력받는다.("image/png", "audio/*" 등. 디폴트 """)
- ending: \n을 포함하는 문자열 처리 방식 ("transparent" | "native". 디폴트 "transparent")const blob = new Blob(["이것은", "테스트","데이터"], {type: "text/plain"}); console.log(blob); // Blob {size: 24, type: 'text/plain'}
2. 기존 Blob 객체에 slice() 메서드를 사용해 일부 취득
Blob.prototype.slice(start, end, contentType)
- 기존 Blob 데이터를 byte단위로 쪼개어 만든 새로운 Blob객체를 반환할 수 있는데, 일단 현재 게시글에서는 이미지 프리뷰를 보여주는 데 필요한 이해만 하고자 하기 때문에 이 부분은 추후에 다루고 지금은 넘어가자.
3. 화면, 마이크, 오디오를 통해 입력 받는 Blob
Blob URL
Blob들을 img 태그에 넣어 DOM에서 보여주려면URL.createObjectURL(blob)
를 활용해 브라우저 내의 Blob 객체를 가리키는 DOMString 형태의 URL로 변환 작업을 해줘야 한다.const blobURL = URL.createObjectURL(mergedBlob); console.log(blobURL); // blob: http://localhost/{hash 값}
createObjectURL
로 생성된 Blob URL은URL.revokeObjectURL(blobURL)
을 호출하기 전까지 브라우저 메모리에 상주하기 때문에, 메모리 누수를 방지하려면URL.revokeObjectURL(blobURL)
메소드를 호출해야한다.
const [imageFile, setImageFiles] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState('');
const onUploadImage = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImageFiles(file);
const imagePreviewUrl = URL.createObjectURL(file);
setImagePreview(imagePreviewUrl);
console.log(imagePreviewUrl);
}
};
// 메모리 누수 방지 - 이미지 업로드 후 Blob URL 해제
useEffect(() => {
if (imagePreview) {
return () => URL.revokeObjectURL(imagePreview);
}
}, [imagePreview]);
URL.createObjectURL()
을 사용해 File
의 URL을 만들 수 있다. 만들어진 URL을 콘솔에 찍어보면 위 사진과 같은 blob 데이터가 찍힌다.
이렇게 가져온 Url을 프리뷰 img 태그 src 속성에 넣으면 된다.
<img src={imagePreviewUrl} />
<input type="file" accept="image/*" onChange={onUploadImage} />
blob 파일은
FileReader
를 통해 읽을 수 있다.const reader = new FileReader(); reader.readAsArrayBuffer(blob);
const [imageFile, setImageFiles] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState('');
const onUploadImage = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImageFiles(file);
const reader = new FileReader(); // 변경
reader.readAsDataURL(file); // 변경
reader.onloadend = () => { // 변경
setImagePreview(reader.result);
}
}
};
<img src={imagePreviewUrl} />
<input type="file" accept="image/*" onChange={onUploadImage} />
(1)과 기존 코드는 동일한 상태에서 setImagePreview로 프리뷰 이미지를 구현하는 방식을 FileReader.readAsDataURL()
을 사용해 변경했다.
그렇다면 프리뷰 이미지를 가져오는 두 방식의 차이점은 뭘까?
URL.createObjectURL
은 동기적으로 실행된다.(즉시 실행)FileReader.readAsDataURL
은 비동기적으로 실행된다.(시간이 조금 지체된 후 실행)URL.createObjectURL
은 revokeObjectURL
메서드가 실행되거나 브라우저가 닫히는 이벤트가 트리거 되기 전까지 메모리에 객체를 저장한다.FileReader.readAsDataURL
은 Blob URL(createObjectURL)
에 비해 메모리를 많이 잡아먹지만, 사용하지 않으면 자동으로 가비지 컬렉터에 의해 제거된다.(StackOverflow 답변)
For me, is better to use blob url's (via createObjectURL), it is more efficient and faster, but if you use many object urls, you need to release these urls by revokeObjectURL (to free memory).
URL.createObjectURL
을 사용하는 게 효율적이고 빠르지만, 사용하지 않을 때 메모리 누수 방지를 위해 revokeObjectURL
메서드로 release 시켜줘야 하는 번거로움이 존재한다.
따라서 나는 URL.createObjectURL
를 사용해 진행했다.
formData
는 XMLHttpRequest 전송을 위하여 설계된 key, value 형식의 특수한 객체다.
formData에 데이터를 추가하기 위해서는
formData.append('TextDataKey',
new Blob([JSON.stringify(inputData)], { type: 'application/json' })
);
진행하는 프로젝트에서 formData 형식으로 이미지 파일과 작성 내용을 전달해줘야 했기 때문에 formData에 이미지 파일을 넣어 진행했다.
const formData = new FormData();
imageFile && formData.append('portfolioImage', imageFile);
formData.append()
를 사용해 두 번째 인자에 이미지 파일을 넣어준다. 첫 번째 인자는 formData의 key값이 된다. 여기서 imageFile
은 File
객체기 때문에 Blob
객체를 상속받으니까 그대로 두번째 인자에 넣는다.
formData에 여러 개의 다중 파일을 추가하려면?
반복문을 활용해 넣는다.const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append("files", files[i]); }
그냥 냅다 console.log(formData)
를 찍어버리면 아래와 같은 빈 객체만 반환한다.
FormData {}
FormData는 단순한 객체가 아닌 XMLHttpRequest 전송을 위해 설계된 특수한 객체 형태이기 때문에, 문자열화할 수 없고, 그에 따라 console.log로 프린트도 할 수 없다.
서버에 데이터를 전송하기 전에 FormData의 값을 확인하고 싶다면, 다음과 같은 방법이 있다.
console.log(...formData);
이렇게 하면 formData에 들어있는 모든 데이터를 확인할 수 있다.
formDta.get('key')
메소드를 사용한다.console.log(formData.get('dataKeyName'));
formData.append()
시 작성한 첫 번째 인자 key값을 매개변수에 넣어서 console에 찍어보면 해당 key의 데이터가 잘 찍히는 것을 확인할 수 있다.
formData.keys()
, formData.values()
메소드를 사용한다.// FormData의 key 확인
for (let key of formData.keys()) {
console.log(key);
}
// FormData의 value 확인
for (let value of formData.values()) {
console.log(value);
}
이렇게 하면 key값은 formData.append()
시 작성한 첫 번째 인자 key값이 찍히고, value값은 아래와 같이 어떤 데이터가 들어있는지 잘 찍힌다!
서버에 formData를 전송할 때는 header에 Content-Type
을 multipart/form-data
로 지정하고 body에 FormData를 넣어 보내주면 된다.
axios.post('/path', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
그리고 form 태그의 encType
을 multipart/form-data
로 지정해 인코딩 타입을 명시한다.
<StForm onSubmit={onSubmitFormData} encType="multipart/form-data">
enctype
enctype은 세 가지 속성 값을 이용할 수 있다.
1. multipart/form-data
파일이 포함된 폼을 전송할 때 사용한다.
2. application/x-www-form-urlcencoded
enctype 속성을 따로 지정하지 않았을 때 적용되는 default 속성이다.
파일이 없는 폼, 즉 multipart/form-data가 아닌 모든 경우에 사용된다.
3. text/plain
인코딩 없이 전송하는 속성이다.
보안성이 없기 때문에 디버깅 등 개발 용도 외에는 사용하지 않는다.파일 전송이 있는데(
<input type="file">
)application/x-www-form-urlcencoded
로 인코딩 타입을 지정할 경우 전송한 데이터를 처리하는 페이지에서 파일 정보에 접근할 수 없기 때문에,multipart/form-data
로 인코딩 타입을 표시해야 파일이 바이너리 정보로 올바르게 처리 페이지로 전송된다.
https://curryyou.tistory.com/442