fetch API — API 호출의 표준
GET·POST·JSON·headers·AbortController, 그리고 CORS 한 줄.
외부 데이터를 가져오는 표준은 fetch API. 옛 XMLHttpRequest 의 후속이자, Promise 기반의 깔끔한 API. 17편은 GET·POST 기본부터 헤더·바디·취소·CORS까지 — 거의 모든 API 호출이 이 한 함수로 끝납니다.
가장 기본 — GET 한 줄
// 단순 호출
const res = await fetch("https://api.example.com/users");
const users = await res.json();
console.log(users);
// 전체 흐름 명시
async function loadUsers() {
const res = await fetch("https://api.example.com/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
fetch 의 가장 큰 함정. 4xx·5xx 응답도 reject 되지 않습니다 — res.ok(2xx 이면 true) 직접 검사. 네트워크 자체가 끊겼을 때만 reject. 모든 fetch 코드에 if (!res.ok) 가 필요한 이유.
Response 객체 — 다양한 형태로 받기
const res = await fetch(url);
// res 의 속성
res.ok; // 2xx 여부
res.status; // 200, 404, 500 등
res.statusText; // "OK", "Not Found"
res.headers; // Headers 객체
res.url; // 최종 URL (리다이렉트 후)
// 바디 읽기 — 한 번만 가능
await res.json(); // JSON 파싱
await res.text(); // 텍스트
await res.blob(); // 바이너리 (이미지·파일)
await res.arrayBuffer(); // ArrayBuffer
await res.formData(); // multipart form
POST · JSON 보내기
const res = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "준성", age: 39 }),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`HTTP ${res.status}: ${err}`);
}
const created = await res.json();
console.log(created);
HTTP 메서드별 헬퍼 — 깔끔한 패턴
async function api(method, path, body) {
const res = await fetch(`/api${path}`, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
let detail;
try { detail = await res.json(); } catch { detail = await res.text(); }
throw new Error(`HTTP ${res.status}: ${JSON.stringify(detail)}`);
}
// 204 No Content 처리
if (res.status === 204) return null;
return res.json();
}
// 사용
await api("GET", "/users");
await api("POST", "/users", { name: "준성" });
await api("PATCH", "/users/1", { name: "준석" });
await api("DELETE", "/users/1");
쿼리 스트링 — URLSearchParams
// ❌ 직접 문자열
const url = `/api/users?page=${page}&q=${q}`; // 인코딩 누락
// ✅ URLSearchParams
const params = new URLSearchParams({ page: "1", q: "준성 김", active: "true" });
const url = `/api/users?${params}`;
// "/api/users?page=1&q=%EC%A4%80%EC%84%B1+%EA%B9%80&active=true"
// 동적 추가
const params = new URLSearchParams();
if (q) params.set("q", q);
if (page > 1) params.set("page", String(page));
headers — Authorization · 커스텀
// 객체로
fetch(url, {
headers: {
"Authorization": `Bearer ${token}`,
"Accept-Language": "ko",
"X-Request-Id": crypto.randomUUID(),
},
});
// Headers 객체로 (더 안전)
const headers = new Headers();
headers.set("Authorization", `Bearer ${token}`);
headers.append("X-Tag", "first");
headers.append("X-Tag", "second"); // 같은 키 여러 값
// 자주 같이 보내는 헤더는 인터셉터 패턴
function authFetch(url, options = {}) {
return fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${getToken()}`,
...options.headers,
},
});
}
AbortController — 요청 취소·타임아웃
const ctrl = new AbortController();
const res = await fetch(url, { signal: ctrl.signal });
// 다른 곳에서 취소
ctrl.abort(); // fetch 가 즉시 reject (AbortError)
// 타임아웃 (ES2024+)
const res = await fetch(url, {
signal: AbortSignal.timeout(5000), // 5초 뒤 자동 취소
});
// 옛 방식
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 5000);
try {
const res = await fetch(url, { signal: ctrl.signal });
} finally {
clearTimeout(timer);
}
// React 컴포넌트 unmount 시 취소
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal });
return () => ctrl.abort();
}, []);
FormData — 파일 업로드
const form = new FormData();
form.append("name", "준성");
form.append("avatar", fileInput.files[0]); // File 객체
const res = await fetch("/api/upload", {
method: "POST",
body: form,
// Content-Type 직접 지정 X — 브라우저가 boundary 자동 설정
});
FormData + Content-Type 함정. FormData 보낼 때는 Content-Type 헤더 절대 직접 지정 X. 브라우저가 multipart/form-data; boundary=... 를 자동 생성하니까요. 직접 적으면 boundary 가 빠져 서버가 못 읽음.
credentials — 쿠키 보내기
fetch(url, {
credentials: "include", // 다른 origin 도 쿠키 포함
// "same-origin" — 같은 origin 만 (기본값)
// "omit" — 쿠키 안 보냄
});
CORS — 한 줄 정리
CORS 가 뭔지. 브라우저는 보안상 다른 origin(다른 도메인·포트·스킴) 의 응답을 막습니다. 서버가 Access-Control-Allow-Origin 헤더로 명시적 허용 표시를 해야 풀림.
해결법은 서버 쪽. 클라이언트 fetch 옵션으로는 못 풀어요. 백엔드에서 Access-Control-Allow-Origin: *(또는 특정 origin) 설정하거나, 같은 origin 으로 프록시.
스트리밍 응답 — ReadableStream
const res = await fetch("/api/big-data");
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(decoder.decode(value, { stream: true }));
}
// LLM 스트리밍 응답 받기 등에 사용
실전 — 에러 처리 + 재시도
async function apiWithRetry(url, options, max = 3) {
let lastErr;
for (let i = 0; i < max; i++) {
try {
const res = await fetch(url, {
...options,
signal: AbortSignal.timeout(10000),
});
if (res.ok) return res;
// 5xx 만 재시도 (4xx 는 클라이언트 에러라 의미 없음)
if (res.status < 500) throw new Error(`HTTP ${res.status}`);
lastErr = new Error(`HTTP ${res.status}`);
} catch (err) {
lastErr = err;
}
// 지수 백오프
await new Promise(r => setTimeout(r, 1000 * 2 ** i));
}
throw lastErr;
}
18편 — 클래스와 객체지향
class·extends·this·getter/setter — 모던 JS 의 OOP 표준.
이전: 16편 Promise · 현재: 17편 (중급) · 다음 → 18편 클래스 · 진행: 17/26