May 24, 2025
“왜 파일 업로드가 계속 실패하지?” - 많은 개발자들이 겪는 흔한 실수
파일 업로드 기능을 구현했는데 이유없이 실패하는 문제를 맞닥뜨렸다.
// 이렇게 했는데 왜 안 될까? 🤷♂️
const formData = new FormData()
formData.append('file', file)
axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data', // 이게 문제!
},
})
서버에서는 400 Bad Request나 파일 파싱 오류가 발생하고, 몇 시간을 디버깅해도 원인을 찾기 어려웠다.
이것저것 찾아본 결과 내가 찾은 해결책은 정말 파격적이었다. formData를 보낼 때마다 당연히 헤더를 설정했는데 말이다.
// ✅ 올바른 방법
const formData = new FormData()
formData.append('file', file)
formData.append('description', 'My awesome file')
axios.post('/upload', formData)
// 끝! 이게 전부입니다.
multipart/form-data
로 데이터를 전송할 때, 각 필드를 구분하기 위해 boundary라는 특별한 구분자가 필요하다.
아래와 같이 boundary가 없는 경우 서버는 어떤 문자열로 필드를 구분해야 하는지 모르게 된다.
const formData = new FormData()
formData.append('username', 'john_doe')
formData.append('email', 'john@example.com')
formData.append('bio', 'I love coding!')
Content-Disposition: form-data; name="username"
john_doe
Content-Disposition: form-data; name="email"
john@example.com
Content-Disposition: form-data; name="bio"
I love coding!
아래와 같이 boundary가 있는 경우 서버는 어떤 문자열로 필드를 구분해야 하는지 알 수 있다.
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
[파일 바이너리 데이터]
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
My awesome file
------WebKitFormBoundary7MA4YWxkTrZu0gW--
여기서 ------WebKitFormBoundary7MA4YWxkTrZu0gW
가 바로 boundary다.
// ❌ 이렇게 하면...
headers: {
'Content-Type': 'multipart/form-data' // boundary가 없음!
}
하지만 내가 헤더를 수동으로 설정하면 boundary가 포함되지 않는다. 그래서 서버는 “어떤 문자열로 필드를 구분해야 하는지” 모르게 된다. 마치 책에서 페이지 번호가 없는 것과 같다.
----formdata-axios-0.27.2-1234567890
)Content-Type: multipart/form-data; boundary=----formdata-axios-0.27.2-1234567890
const formData = new FormData()
formData.append('file', file)
axios.post('/upload', formData)
Network 탭에서 Request Headers를 보면:
Content-Type: multipart/form-data; boundary=----formdata-axios-0.27.2-16789012345
이는 axios만의 특별한 기능이 아니다. 모든 HTTP 클라이언트에서 동일하다.
// ✅ 올바른 방법
fetch('/upload', {
method: 'POST',
body: formData, // Content-Type 자동 설정
})
# ✅ 자동으로 boundary 설정
curl -X POST -F "file=@photo.jpg" http://example.com/upload
// Node.js에서 form-data 라이브러리 사용 시
const FormData = require('form-data')
const form = new FormData()
form.append('file', fs.createReadStream('file.txt'))
axios.post('/upload', form, {
headers: {
...form.getHeaders(), // 올바른 헤더 자동 생성
},
})
HTTP 표준 (RFC 7578)에서 정의: