Joonas' Note

Joonas' Note

FormData 전송할 때 fetch vs. axios 본문

개발/Javascript

FormData 전송할 때 fetch vs. axios

2025. 4. 16. 00:47 joonas

    서버에 파일을 업로드하는 함수를 구현하던 중, 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))

    안타깝게도 세상은 그렇게 호락호락하지 않다.

    multipart/form-data payload (fake)

     

    내용물을 동일하게 만들었다하더라도, 파라미터로 넘기는 객체가 FormData 타입이 아니기때문에 Content-Length 도 다르고 fetch 함수 내부에서 다르게 처리되는 것인지 올바르지 않은 포맷으로 전송되고 있었다.

    axiosXMLHttpRequest 객체를 사용하는 라이브러리이다. 개인적으로 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

     

    Comments