Joonas' Note
FormData 전송할 때 fetch vs. axios 본문
- 참고
서버에 파일을 업로드하는 함수를 구현하던 중, axios를 fetch 로 변경하였는데 서버쪽에서 500 에러가 발생했다.
문제는 서버에 도달한 요청 데이터에 RequestBody가 사라진 것이다.
아래의 두 함수를 비교하면, 전혀 문제될 것이 없어보인다.
먼저, axios를 사용하고 있던 기존의 함수 로직이다.
axios
.post(ENDPOINT, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((data) => console.log(data))
다음으로 fetch로 변경한 함수 로직이다.
fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
body: formData,
})
.then((data) => console.log(data))
결론부터 말하자면, 아래와 같이 고치면 정상적으로 동작한다.
fetch(ENDPOINT, {
method: 'POST',
body: formData,
})
.then((data) => console.log(data))
이유를 알 수 없어서 여러 방법으로 분석해보았다.
서버쪽 로그도 찍어보고, Postman으로도 전송해보았는데 서버 문제는 아니었다.
그렇다면 axios와 fetch의 동작에 차이가 있다는 것으로 보여서 네트워크로 전송되는 데이터를 비교해보았다.
먼저 axios를 통해 전송되는 데이터이다.
POST /upload HTTP/1.1
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 235
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary1x8F0uD1L2QhdmuW
DNT: 1
Host: localhost:3000
Origin: http://localhost:3001
Pragma: no-cache
Referer: http://localhost:3001/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
다음으로 fetch를 통해 전송되는 데이터이다.
POST /upload HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: ko
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 235
Content-Type: multipart/form-data
DNT: 1
Host: localhost:3000
Origin: http://localhost:3001
Pragma: no-cache
Referer: http://localhost:3001/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
두 데이터에서 유일하게 다른 부분은 Content-Type 이다.
도대체 왜 fetch 에서는 boundary 가 사라진 것일까?
그렇다면 boundary 를 붙이고 내용물을 동일하게 가공하여 전송하면 제대로 동작할까?
(일단 되는지 확인하기 위해서) 아래처럼 FormData 를 쪼개서 전송 폼에 맞게 만들어보았다.
const boundary = '----WebKitFormBoundary' + 'caKRudE3BYlaVvsk';
const fields = Array.from(formData).map(([key, value], index) => {
// 먼저 string 타입만 전송 테스트
return `Content-Disposition: form-data; name="${key}"\n\n${value}\n${boundary}`;
});
const encodedBody = `--${boundary}\n${fields.join('\n')}--`;
console.log(encodedBody);
fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
},
body: encodedBody,
})
.then((data) => console.log(data))
안타깝게도 세상은 그렇게 호락호락하지 않다.

내용물을 동일하게 만들었다하더라도, 파라미터로 넘기는 객체가 FormData 타입이 아니기때문에 Content-Length 도 다르고 fetch 함수 내부에서 다르게 처리되는 것인지 올바르지 않은 포맷으로 전송되고 있었다.
axios는 XMLHttpRequest 객체를 사용하는 라이브러리이다. 개인적으로 XMLHttpRequest 의 사용 방법이 너무나도 불편했고 axios는 물론 과거 ajax 역시 초기 세팅 등 사용하기에 편하지는 않았다. 그래서 Fetch API가 등장한 이후로는 너무 유용하게 사용하고 있었다.
아무튼 이런 차이때문인지는 몰라도, fetch에서는 multipart/form-data의 경우에는 Content-Type을 지정하지 않아야 boundary가 자동으로 붙어서 전송되어 정상적으로 동작한다.
스택오버플로우의 답변으로부터 약 10년이 지난 지금까지도 유효한 방법이다 (...)
참고
fetch - Missing boundary in multipart/form-data POST
I want to send a new FormData() as the body of a POST request using the fetch api The operation looks something like this: var formData = new FormData() formData.append('myfile', file, 'someFileNam...
stackoverflow.com
What is the boundary in multipart/form-data?
I want to ask a question about the multipart/form-data. In the HTTP header, I find that the Content-Type: multipart/form-data; boundary=???. Is the ??? free to be defined by the user? Or is it
stackoverflow.com
'개발 > Javascript' 카테고리의 다른 글
[React] useDeepMemo (0) | 2025.04.15 |
---|---|
사과 게임 헬퍼 만들어보기 (0) | 2025.03.09 |
[Browser] pageX/screenX/clientX/offsetX 비교 (0) | 2024.08.26 |
블로그 포스트 읽는 시간 예측하기 (0) | 2024.02.07 |
눈코입 맞추기 게임 만들기 3편 (3) | 2024.01.20 |