핵심 정리
import { readFile } from "node:fs/promises";
// 파일을 Blob으로 만들어 FormData에 첨부
const fileBuffer = await readFile("./photo.jpg");
const blob = new Blob([fileBuffer], { type: "image/jpeg" });
const form = new FormData();
form.append("name", "kero");
form.append("avatar", blob, "photo.jpg"); // 파일명도 함께 전달
// Content-Type은 직접 설정하지 않는다 — boundary가 자동 생성됨
const response = await fetch("https://api.example.com/users", {
method: "POST",
body: form,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();웹 API 호출
먼저 눈에 들어와야 하는 기본형은 아래입니다.
- 업로드 본문:
FormData - 버퍼 -> 업로드 객체:
new Blob([buffer], { type }) - 파일명 포함 첨부:
form.append(name, blob, filename) - 중복 필드 추가:
append - 기존 필드 교체:
set
업로드 본문은 FormData가 만들고 헤더도 같이 만든다
파일 업로드 요청에서 가장 흔한 실수는 Content-Type: multipart/form-data를 직접 넣는 것입니다. fetch가 boundary를 같이 만들어야 하므로, FormData를 body로 넘길 때는 이 헤더를 비워 둬야 합니다.
파일 소스가 Buffer면 먼저 Blob으로 감싼다
Node에서 파일을 읽으면 보통 Buffer가 나옵니다. Web API 쪽으로 넘길 때는 Blob이나 File로 감싸는 편이 자연스럽습니다. 파일명을 명시하고 싶으면 form.append(name, blob, filename) 패턴이면 충분합니다.
append와 set은 용도가 다르다
같은 필드를 여러 번 보내야 하면 append, 기존 값을 교체하려면 set입니다. 멀티 파일 업로드나 태그 배열처럼 같은 이름을 반복하는 폼이라면 append가 기본입니다.
form.append("tag", "red");
form.append("tag", "blue");
form.set("name", "kero");응답이 JSON이 아닐 수도 있다
업로드 결과가 이미지, PDF, 바이너리라면 response.json()이 아니라 arrayBuffer()나 blob()으로 읽어야 합니다. 여기서는 업로드 요청과 바이너리 응답 경계를 같이 봅니다.
const response = await fetch(url);
const image = Buffer.from(await response.arrayBuffer());Node Web API와 전통 Node API를 섞는 지점을 의식한다
업로드 준비는 Blob, FormData, fetch 같은 Web API 감각으로 가지만, 실제 파일 읽기와 저장은 fs나 Buffer 쪽으로 다시 돌아옵니다.
실전에서 헷갈리는 지점도 대체로 이 경계입니다.
언제 어떤 본문을 쓰나
체크포인트
- 텍스트 + 파일 업로드:
FormData - Buffer를 업로드용 객체로:
new Blob([buffer], { type }) - 파일명 포함 첨부:
form.append("file", blob, "name.ext") - 같은 이름 필드를 누적한다:
append - 기존 필드를 교체한다:
set - 바이너리 응답 저장:
response.arrayBuffer()+Buffer.from() - 타입 보존된 바이너리:
response.blob()
주의할 점
FormData에 Content-Type을 직접 넣으면 boundary가 깨진다. 업로드 실패가 생기면 먼저 서버보다 이 헤더를 의심하는 편이 빠르다.
await fetch(url, {
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
body: form,
});이 패턴이 바로 대표적인 실패 예시입니다.
참고 링크
1 sources