24.12.02 Formdata 제출

강연주·2024년 12월 2일

📚 TIL

목록 보기
106/186

FormData 객체

파일이나 추가 필드 여부와 무관하게 통용되는 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 메서드

FormData에 속하는 필드를 수정하는 메서드들

  • formData.append(name, value) : name과 value를 가진 폼 필드 추가
  • formData.append(name, blob, fileName) - <input type="file"> 형태의 필드 추가. 세번째 인수 fileName은 (필드 이름이 아니고) 사용자가 해당 이름을 가진 파일을 폼에 추가한 것처럼 설정해줌.
  • formData.delete(name) - name에 해당하는 필드 삭제
  • formData.get(name) - name에 해당하는 필드의 값 가져옴
  • formData.has(name) - name에 해당하는 필드가 있으면 true, 아니면 false 반환

폼은 이름(name)이 같은 필드 여러 개를 허용하기 때문에, append 메서드로 여러 번 호출해 이름이 같은 필드를 계속 추가해도 문제가 없다.

append 메서드 외에 필드 추가 시 set 메서드도 사용할 수 있다. set은 append와 달리, name과 동일한한 이름을 가진 필드를 모두 제거하고 새로운 필드 하나를 추가한다. 따라서 set 메서드로 name을 가진 필드의 유일성을 보장할 수 있다. 다른 특징은 append와 동일.

  • formData.set(name, value)
  • formData.set(name, blob, fileName)
🖥️ 
// 참고로 폼 데이터 필드에 반복 작업을 할 때는 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>

Blob 데이터가 있는 폼 전송

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);

  1. const newMeetup: Meetup 안에 image가 필요해?
    필요 여부:

필요한 경우:
만약 서버가 image 필드(이미지의 파일 경로 또는 URL 등)를 기대하고 있다면, newMeetup 객체에 포함되어야 합니다. 예를 들어, 서버가 JSON 데이터를 처리하고 이미지를 별도로 저장하는 경우입니다. 이때 image는 파일의 경로(또는 이름)를 나타낼 수 있습니다.
필요하지 않은 경우:
서버가 이미지 파일을 multipart/form-data로 처리하고 JSON 데이터에는 이미지 관련 정보가 없거나 필요 없는 경우입니다. 이 경우 image는 newMeetup에 포함될 필요가 없습니다.
결론:

이미지 파일 자체를 FormData로 전송하고 JSON 데이터에 이미지 관련 정보가 필요 없다면, newMeetup에 image 필드는 필요 없습니다.
서버가 이미지를 JSON 데이터로도 받기를 기대한다면 포함해야 합니다.


  1. imageRef.current?.files?.[0]에서 files[0]를 사용하는 이유는?
    files는 배열처럼 동작:

imageRef.current.files는 FileList 객체이며, 이는 HTML <input type="file" />에서 선택한 파일들의 리스트를 제공합니다.
다중 파일 선택이 가능하기 때문에 FileList는 배열처럼 동작하며, 선택된 첫 번째 파일이 files[0]에 저장됩니다.

파일이 하나일 경우에도 배열처럼 저장:

단일 파일을 업로드하더라도 files는 배열처럼 반환됩니다.
따라서 files[0]를 사용하여 선택한 첫 번째 파일을 참조해야 합니다.

배열처럼 작동하는 이유:

<input type="file" multiple /> 속성을 통해 다중 파일 업로드를 허용할 수 있으므로, 기본적으로 FileList는 여러 파일을 저장할 수 있도록 설계되었습니다.


  1. 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를 사용해야 타입 오류가 나지 않습니다.


  1. 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 형식으로 추가하는 것입니다.

profile
아무튼, 개발자

0개의 댓글