파일이나 추가 필드 여부와 무관하게 통용되는 HTML form 전송 방식.
폼을 쉽게 보내도록 도와주는 객체로, FormData 객체는 HTML 폼 데이터를 나타낸다.
🖥️ 생성자
let formData = new FormData([form])
// html에 form 요소가 있는 경우 위 코드로 해당 폼 요소의 필드 전체 자동 반영
fetch 등의 네트워크 메서드가 FormData 객체를 받는 것이 FormData의 특징.
이때 브라우저가 보내는 HTTP는 인코딩 되고, Content-Type 속성은 multipart/form-data로 지정된 후 전송된다. 서버 관점에서는 FormData와 일반 폼 전송 방식에 차이 없음.
🖥️ 간단한 폼 전송
<form id="formElem">
<input type="text" name="name" value="Bora">
<input type="text" name="surname" value="Lee">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/article/formdata/post/user', {
method: 'POST,
body: new FormData(formElem)
});
let result = await reponse.json();
alert(result.message);
};
</script>
// 서버는 POST 요청을 받아 '저장 성공'이라는 응답을 보내준다.
FormData에 속하는 필드를 수정하는 메서드들
<input type="file"> 형태의 필드 추가. 세번째 인수 fileName은 (필드 이름이 아니고) 사용자가 해당 이름을 가진 파일을 폼에 추가한 것처럼 설정해줌.폼은 이름(name)이 같은 필드 여러 개를 허용하기 때문에, append 메서드로 여러 번 호출해 이름이 같은 필드를 계속 추가해도 문제가 없다.
append 메서드 외에 필드 추가 시 set 메서드도 사용할 수 있다. set은 append와 달리, name과 동일한한 이름을 가진 필드를 모두 제거하고 새로운 필드 하나를 추가한다. 따라서 set 메서드로 name을 가진 필드의 유일성을 보장할 수 있다. 다른 특징은 append와 동일.
🖥️
// 참고로 폼 데이터 필드에 반복 작업을 할 때는 for...of 루프를 사용할 수 있다.
let formData = new formData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');
// key-value 쌍이 담긴 리스트
for (let [name, value] of formData) {
alert(`${name} = ${value}`);
// key 1= value1, then key2 = value2
}
폼을 전송할 때 HTTP 메시지의 Content-Type 속성은 항상 multipart/form-data이고, 메시지는 인코딩되어 전송된다. 파일이 있는 폼도 이 규칙을 따르므로 <input type="file">로 지정한 필드 역시 일반 폼 전송과 유사하게 전송된다.
🖥️ // 파일이 있는 폼 전송 예시
<form id="formElem">
<input type="text" name="firstName" value="Bora">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await
fetch('/article/formdata/post/user-avatar', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json();
alert(result.message);
};
</script>
fetch 챕터에서 살펴봤듯, 이미지 같이 동적으로 새성된 바이너리 파일은 Blob를 사용해 쉽게 전송할 수 있다. 이때 Blob 객체는 fetch 메서드의 body 매개변수에 바로 넘겨줄 수 있다.
그런데 실제 코딩을 하다 보면 이미지를 별도로 넘겨주는 것보다, 폼에 필드를 추가하고 여기에 이미지 '이름' 등의 메타데이터를 같이 실어 넘겨주는 게 좀 더 편리하다. 서버 입장에서도 원시 바이너리 데이터보다는 multipart-encoded 폼을 받는 것이 더 적합하다.
🖥️ <canvas>로 만든 이미지를 FormData를 사용해 폼 형태로 다른 추가 필드와 함께 전송하는 예시
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<input type="button" value="이미지 전송" onClick="submit()">
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasEle.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
async function submit() {
let imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
let formData = new FromData();
formData.append("firstName", "Bora");
formData.append("image", imageBlob, "image.png");
let reponse = await fetch('/article/formdata/post/image-form', {
method: 'POST',
body: formData
});
let result = await response.json();
alert(result.message);
</script>
</ body>
위 예시에서 이미지 Blob을 추가한 코드를 다시 보면,
formData.append("image", imageBlob, "image.png"); 폼에
<image type="file" name="image"> 태그가 있고, 사용자 기기의 파일 시스템에서 파일명인 "image.png"(3번째 인수 참고)인 imageBlob 데이터(2번째 인수 참고)를 추가한 것과 동일한 효과를 준다.
요청을 받은 서버는 일반 폼과 동일하게 폼 데이터와 파일을 읽고 처리.
🎪 모던 JavaScript 튜토리얼 - FormData 객체
1. 이미지 인풋 추가
🖥️ <Labeled Input
id="image"
name="image"
label="이미지 업로드"
type="file"
ref={imageRef}
required
/>
2. createMeetup 함수 수정
const createMeetup = async (newMeetup: FormData): Promise<void> => {
const response = await fetch("http://localhost:8000/api/v1/meetup", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: newMeetup,
});
if (!response.ok) {
throw new Error("모임 생성 실패");
}
return;
};
3. handleMeetupFormSubmit 수정
🖥️ const handleMeetupFormSubmit = (event: React.FormEvent) => {
event.preventDefault();
const formData = new FormData();
formData.append("name", nameRef.current?.value || "");
formData.append("description", descriptionRef.current?.value || "");
formData.append("place", placeRef.current?.value || "");
formData.append("placeDescription", placeDescription.current?.value || "");
formData.append("startedAt", isStartedAtNull ? null : startedAtRef.current.value || null); // 확인 필요
formData.append("endedAt", isEndedAtNull? null : endedAtRef.current.value || null); // 확인 필요
formData.append("adTitle", adTitleRef.current?.value || null);
formData.append("adEndedAt", adEndedAtRef.current?.value || null);
formData.append("isPublic", isPublicRef.current?.checekd ? "true" : "false");
formData.append("category", categoryRef.current?.value || "");
// 이미지 파일 추가
if (imageRef.current?.files?.[0]) {
formData.append("iamge", imageRef.current.files[0]);
}
createMutation.mutate(formData);
}
➡️ 근데 이거는 Blob 없이 모든 인풋을 formData 형식으로 때려넣고 있잖니..
🖥️ // 모임 생성
const createMeetup = async (newMeetup: FormData): Promise<void> => {
const response = await fetch("http://localhost:8000/api/v1/meetup", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${token}`,
},
body: newMeetup, // FormData 객체 전달
});
if (!response.ok) {
throw new Error("모임 생성 실패");
}
return;
};
➡️ 컨텐트 타입 "multipart/form-data"로 변경
🖥️ const createMutation = useMutation<void, Error, FormData>({
mutationFn: createMeetup,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["meetups"] });
},
});
➡️ Meetup 대신 FormData로 변경
🖥️ // 이미지 파일 추가
if (imageRef.current?.files?.[0]) {
formData.append("iamge", imageRef.current.files[0]);
}
formData.append("newMeetup", new Blob([JSON.stringify(newMeetup)], { type: "application, json" }));
createMutation.mutate(formData);
필요한 경우:
만약 서버가 image 필드(이미지의 파일 경로 또는 URL 등)를 기대하고 있다면, newMeetup 객체에 포함되어야 합니다. 예를 들어, 서버가 JSON 데이터를 처리하고 이미지를 별도로 저장하는 경우입니다. 이때 image는 파일의 경로(또는 이름)를 나타낼 수 있습니다.
필요하지 않은 경우:
서버가 이미지 파일을 multipart/form-data로 처리하고 JSON 데이터에는 이미지 관련 정보가 없거나 필요 없는 경우입니다. 이 경우 image는 newMeetup에 포함될 필요가 없습니다.
결론:
이미지 파일 자체를 FormData로 전송하고 JSON 데이터에 이미지 관련 정보가 필요 없다면, newMeetup에 image 필드는 필요 없습니다.
서버가 이미지를 JSON 데이터로도 받기를 기대한다면 포함해야 합니다.
imageRef.current.files는 FileList 객체이며, 이는 HTML <input type="file" />에서 선택한 파일들의 리스트를 제공합니다.
다중 파일 선택이 가능하기 때문에 FileList는 배열처럼 동작하며, 선택된 첫 번째 파일이 files[0]에 저장됩니다.
파일이 하나일 경우에도 배열처럼 저장:
단일 파일을 업로드하더라도 files는 배열처럼 반환됩니다.
따라서 files[0]를 사용하여 선택한 첫 번째 파일을 참조해야 합니다.
배열처럼 작동하는 이유:
<input type="file" multiple /> 속성을 통해 다중 파일 업로드를 허용할 수 있으므로, 기본적으로 FileList는 여러 파일을 저장할 수 있도록 설계되었습니다.
createMutation.mutate(newMeetup)와createMutation.mutate(formData)의 차이newMeetup:
newMeetup은 일반 JavaScript 객체입니다. 서버가 application/json 형식의 요청 본문을 기대할 때 사용됩니다.
fetch 요청 시 body에 JSON.stringify로 직렬화하여 전송해야 합니다.
formData:
formData는 파일 및 텍스트 데이터를 함께 전송할 때 사용하는 객체입니다.
fetch 요청 시, formData는 직렬화할 필요 없이 body로 바로 전달되며, Content-Type 헤더가 자동으로 설정됩니다.
차이점:
newMeetup은 JSON으로 데이터를 전송하고,
formData는 multipart/form-data 형식으로 전송합니다.
타입 오류 해결 이유:
createMutation은 FormData를 받도록 타입을 변경했으므로, newMeetup 대신 formData를 사용해야 타입 오류가 나지 않습니다.
formData.append("newMeetup", new Blob([JSON.stringify(newMeetup)], { type: "application/json" }));의 의미Blob이란?
Blob은 파일과 유사한 데이터 객체로, 바이너리 데이터와 텍스트 데이터를 포함할 수 있습니다.
의미:
JSON.stringify(newMeetup)로 newMeetup 객체를 JSON 문자열로 변환합니다.
new Blob([...])로 JSON 데이터를 Blob 객체로 감싸고, application/json MIME 타입을 설정합니다.
formData.append("newMeetup", ...)로 FormData 객체에 추가합니다.
왜 필요한가?
FormData는 기본적으로 파일 및 키-값 쌍을 처리합니다. JSON 데이터를 추가하려면 문자열로 변환하거나, Blob 형태로 감싸 MIME 타입을 지정해야 합니다.
결론:
이 코드는 newMeetup 데이터를 JSON으로 직렬화한 후, 이를 multipart/form-data로 서버에 전송하기 위해 Blob 형식으로 추가하는 것입니다.