Fetch API는 단순히 fetch(url).then(r => r.json())만 하는 것이 아닙니다. Request/Response 객체를 깊이 이해하면 스트리밍 다운로드, 인터셉터, 캐시 제어까지 가능합니다.

Fetch 기본 복습

JS
// GET 요청
const response = await fetch("/api/users");
const data = await response.json();

// POST 요청
const response = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "정훈", age: 25 }),
});

fetch가 에러를 던지지 않는 함정

JS
// 404, 500 등 HTTP 에러에서도 fetch는 reject하지 않음!
const response = await fetch("/api/not-found");
console.log(response.ok);     // false
console.log(response.status); // 404
// 에러가 아님 — response는 정상적으로 반환됨

// 네트워크 에러(서버 다운, CORS 등)만 reject
try {
  await fetch("https://unreachable.example.com");
} catch (err) {
  console.log("네트워크 에러:", err);
}

Request 객체

JS
// Request를 직접 생성
const request = new Request("/api/data", {
  method: "POST",
  headers: new Headers({
    "Content-Type": "application/json",
    Authorization: "Bearer token123",
  }),
  body: JSON.stringify({ key: "value" }),
  mode: "cors",
  credentials: "include", // 쿠키 포함
  cache: "no-cache",
});

// fetch에 전달
const response = await fetch(request);

// Request 복제 (한번 사용한 Request는 재사용 불가)
const cloned = request.clone();

Response 객체

JS
const response = await fetch("/api/data");

// 상태 정보
console.log(response.status);     // 200
console.log(response.statusText); // "OK"
console.log(response.ok);         // true (200-299)
console.log(response.url);        // 최종 URL (리다이렉트 후)
console.log(response.redirected); // 리다이렉트 여부

// 헤더 접근
console.log(response.headers.get("Content-Type"));
response.headers.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

// 본문 읽기 (한 번만 가능!)
const json = await response.json();
// await response.text(); // 이미 읽었으므로 TypeError

본문 읽기 메서드

JS
const response = await fetch(url);

// 텍스트
const text = await response.text();

// JSON
const json = await response.json();

// Blob (이미지, 파일)
const blob = await response.blob();

// ArrayBuffer (바이너리)
const buffer = await response.arrayBuffer();

// FormData
const formData = await response.formData();

스트리밍 응답 — 다운로드 진행률

JS
async function downloadWithProgress(url) {
  const response = await fetch(url);
  const contentLength = response.headers.get("Content-Length");
  const total = parseInt(contentLength, 10);

  const reader = response.body.getReader();
  let received = 0;
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    chunks.push(value);
    received += value.length;

    const progress = ((received / total) * 100).toFixed(1);
    console.log(`다운로드 진행: ${progress}%`);
  }

  // 청크 합치기
  const blob = new Blob(chunks);
  return blob;
}

스트리밍 텍스트 — ChatGPT 스타일

JS
async function streamText(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  let result = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    result += chunk;

    // 실시간으로 UI에 추가
    updateUI(chunk);
  }

  return result;
}

인터셉터 패턴 — fetch 래퍼

Axios의 인터셉터와 비슷한 기능을 구현할 수 있습니다.

JS
class HttpClient {
  constructor(baseURL = "") {
    this.baseURL = baseURL;
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }

  // 요청 인터셉터 등록
  onRequest(fn) {
    this.requestInterceptors.push(fn);
  }

  // 응답 인터셉터 등록
  onResponse(fn) {
    this.responseInterceptors.push(fn);
  }

  async fetch(url, options = {}) {
    let config = { url: this.baseURL + url, ...options };

    // 요청 인터셉터 실행
    for (const interceptor of this.requestInterceptors) {
      config = await interceptor(config);
    }

    let response = await fetch(config.url, config);

    // 응답 인터셉터 실행
    for (const interceptor of this.responseInterceptors) {
      response = await interceptor(response);
    }

    return response;
  }
}

// 사용
const api = new HttpClient("https://api.example.com");

// 인증 토큰 자동 추가
api.onRequest((config) => {
  config.headers = {
    ...config.headers,
    Authorization: `Bearer ${getToken()}`,
  };
  return config;
});

// 401 에러 시 토큰 갱신
api.onResponse(async (response) => {
  if (response.status === 401) {
    await refreshToken();
    // 재요청 로직
  }
  return response;
});

// 에러 응답 자동 처리
api.onResponse(async (response) => {
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message);
  }
  return response;
});

재시도 패턴

JS
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) return response;

      // 5xx 에러는 재시도
      if (response.status >= 500 && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000; // 지수 백오프
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }

      return response; // 4xx 에러는 재시도하지 않음
    } catch (err) {
      if (attempt === maxRetries) throw err;
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

캐시 제어

JS
// 캐시 모드 옵션
await fetch(url, { cache: "default" });     // 기본 브라우저 캐시
await fetch(url, { cache: "no-store" });    // 캐시 완전 무시
await fetch(url, { cache: "no-cache" });    // 서버에 재검증 요청
await fetch(url, { cache: "force-cache" }); // 캐시 우선
await fetch(url, { cache: "reload" });      // 캐시 무시, 새로 요청

// 조건부 요청
await fetch(url, {
  headers: {
    "If-None-Match": etag,            // ETag 기반
    "If-Modified-Since": lastModified, // 날짜 기반
  },
});

fetch vs Axios

기능fetchAxios
브라우저 내장OX (설치 필요)
HTTP 에러 자동 rejectXO
요청/응답 인터셉터직접 구현내장
타임아웃AbortSignaltimeout 옵션
진행률ReadableStreamonUploadProgress
JSON 자동 변환X (.json() 호출)O

**기억하기 **: fetch는 HTTP 에러(4xx, 5xx)에서 reject하지 않습니다. response.ok를 반드시 확인해야 합니다. 스트리밍이 필요하면 response.body.getReader()를, 인터셉터가 필요하면 fetch 래퍼를 만들어 사용하면 됩니다.

댓글 로딩 중...