자바스크립트 교재 · 17편 / 26편

fetch API — API 호출의 표준

GET·POST·JSON·headers·AbortController, 그리고 CORS 한 줄.

중급읽는 시간 7분2026-05-17
fetch 가 URL 에 GET/POST 요청을 보내고 Response 로 받는 흐름 도식

외부 데이터를 가져오는 표준은 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

© 2026 주나이테크(주) @JUNAITECH