본문으로 건너뛰기
김신건의 로그

[JavaScript] fetch API: 현대 HTTP 클라이언트

· 수정 · 📖 약 1분 · 350자/단어 #javascript #http #network #fetch #web-api
js-fetch, fetch API, fetch, Response, Request, JS fetch, JavaScript fetch, fetch 함수

정의

fetch 는 Promise 기반의 표준 HTTP 클라이언트 Web API. ES2015 이후 XMLHttpRequest (XHR) 를 실질적으로 대체했다. 브라우저와 Node.js 18+ 에서 기본 지원.

언제 쓰나

상황선택
REST API 호출fetch 기본
JSON POSTfetch + JSON.stringify
요청 취소 / 타임아웃AbortController / AbortSignal.timeout
스트리밍 응답res.body (ReadableStream)
복잡한 인터셉터/재시도axios / ky 등 래퍼 라이브러리

요청/응답 흐름

sequenceDiagram
    participant App as "앱 코드"
    participant Fetch as "fetch()"
    participant Net as "네트워크"
    participant Server as "서버"

    App->>Fetch: "fetch(url, options)"
    Fetch->>Net: "HTTP 요청 전송"
    Net->>Server: "패킷 전달"
    Server-->>Net: "HTTP 응답"
    Net-->>Fetch: "Response 객체 생성"
    Fetch-->>App: "Promise<Response> resolve"
    App->>Fetch: "res.json() 또는 res.text()"
    Fetch-->>App: "Promise<데이터> resolve"

IMPORTANT

fetch 는 네트워크 오류 (DNS 실패, 연결 거부 등) 만 reject 한다. HTTP 4xx / 5xx 는 정상 resolve 이므로 반드시 res.ok 로 확인해야 한다.

기본 사용

// GET 요청
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const users = await res.json();

options 객체

fetch(url, {
    method: 'POST',              // 기본값: 'GET'
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify({ name: 'Alice' }),

    // CORS 관련
    mode: 'cors',                // 'cors' | 'no-cors' | 'same-origin'
    credentials: 'include',      // 'omit' | 'same-origin' | 'include'

    // 캐싱
    cache: 'no-cache',           // 'default' | 'no-store' | 'reload' | 'force-cache'

    // 리다이렉트
    redirect: 'follow',          // 'follow' | 'error' | 'manual'

    // 취소 신호
    signal: controller.signal,
});

method / headers / body

// POST + JSON
await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice', age: 30 }),
});

// PUT 업데이트
await fetch(`/api/users/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Bob' }),
});

// DELETE
await fetch(`/api/users/${id}`, { method: 'DELETE' });

// 폼 데이터
const form = new FormData();
form.append('file', fileInput.files[0]);
await fetch('/api/upload', { method: 'POST', body: form });
// Content-Type: multipart/form-data 헤더는 자동 설정됨

mode / credentials

// 크로스 오리진 요청에 쿠키 포함
fetch('https://api.example.com/data', {
    mode: 'cors',
    credentials: 'include',   // 쿠키, Authorization 헤더 전송
});

// 동일 오리진에서만 쿠키 포함 (기본값)
fetch('/api/data', {
    credentials: 'same-origin',
});

Response 객체

const res = await fetch('/api/data');

// 상태 정보
res.status;          // 200, 404, 500 ...
res.ok;              // true if 200-299
res.statusText;      // 'OK', 'Not Found' ...
res.headers.get('Content-Type');   // 'application/json'
res.url;             // 최종 URL (리다이렉트 후)
res.redirected;      // 리다이렉트 여부

// Body 소비 메서드 (한 번만 호출 가능)
await res.json();        // JSON 파싱
await res.text();        // 문자열
await res.blob();        // Blob (이미지 등)
await res.arrayBuffer(); // ArrayBuffer (바이너리)
await res.formData();    // FormData

Body 스트리밍

const res = await fetch('/api/large-file');
const reader = res.body.getReader();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    // value: Uint8Array 청크
    process(value);
}

Request 객체

URL 과 옵션을 미리 구성해 재사용하거나 미들웨어에 전달할 때.

const req = new Request('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice' }),
});

// 복제해서 수정
const reqWithAuth = new Request(req, {
    headers: { ...req.headers, 'Authorization': `Bearer ${token}` },
});

const res = await fetch(reqWithAuth);

에러 처리

async function apiFetch(url, options) {
    let res;
    try {
        res = await fetch(url, options);
    } catch (networkErr) {
        // DNS 실패, 연결 거부, CORS 차단 등
        throw new Error(`네트워크 오류: ${networkErr.message}`);
    }

    if (!res.ok) {
        const body = await res.text().catch(() => '');
        throw new Error(`HTTP ${res.status}: ${body}`);
    }

    return res.json();
}

HTTP 상태 코드별 처리

async function fetchWithStatusHandling(url) {
    const res = await fetch(url);

    switch (true) {
        case res.status === 401:
            throw new Error('인증 필요: 로그인 하세요');
        case res.status === 403:
            throw new Error('권한 없음');
        case res.status === 404:
            return null;
        case res.status >= 500:
            throw new Error(`서버 오류: ${res.status}`);
        case !res.ok:
            throw new Error(`요청 실패: ${res.status}`);
        default:
            return res.json();
    }
}

취소

// AbortController 로 수동 취소
const ctl = new AbortController();

const promise = fetch(url, { signal: ctl.signal });

// 5초 후 취소
setTimeout(() => ctl.abort(), 5000);

try {
    const res = await promise;
} catch (err) {
    if (err.name === 'AbortError') {
        console.log('요청이 취소됐습니다');
    }
}

타임아웃

// AbortSignal.timeout (ES2022, 가장 간단)
const res = await fetch(url, {
    signal: AbortSignal.timeout(5000),
});

// 직접 구성 (지원 안 하는 환경)
async function fetchWithTimeout(url, ms = 5000) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), ms);
    try {
        return await fetch(url, { signal: controller.signal });
    } finally {
        clearTimeout(timer);
    }
}

실전 예시

공통 API 클라이언트

const API_BASE = 'https://api.example.com';

async function apiRequest(path, options = {}) {
    const url = `${API_BASE}${path}`;
    const defaults = {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${getToken()}`,
        },
        credentials: 'include',
        signal: AbortSignal.timeout(10_000),
    };

    const res = await fetch(url, { ...defaults, ...options });
    if (!res.ok) {
        const err = await res.json().catch(() => ({ message: res.statusText }));
        throw Object.assign(new Error(err.message), { status: res.status });
    }
    if (res.status === 204) return null;   // No Content
    return res.json();
}

재시도 로직

async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const res = await fetch(url, options);
            if (res.ok) return res;
            if (res.status < 500) throw new Error(`HTTP ${res.status}`);  // 4xx: 재시도 의미 없음
        } catch (err) {
            if (i === retries - 1) throw err;
            await new Promise(r => setTimeout(r, 300 * 2 ** i));   // exponential backoff
        }
    }
}

Blob 이미지 다운로드

async function downloadImage(url) {
    const res = await fetch(url);
    const blob = await res.blob();
    const objectUrl = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = objectUrl;
    a.download = 'image.jpg';
    a.click();
    URL.revokeObjectURL(objectUrl);
}

함정

4xx/5xx 는 reject 안 함

// ❌ 이렇게 쓰면 오류를 감지 못 함
const res = await fetch('/api/missing');
const data = await res.json();   // 404 응답의 body를 파싱하게 됨

// ✅ res.ok 확인 필수
if (!res.ok) throw new Error(`HTTP ${res.status}`);

Body 는 한 번만 소비 가능

const res = await fetch(url);

// ❌ 두 번 소비하면 TypeError
const json = await res.json();
const text = await res.text();   // TypeError: body already consumed

// ✅ clone 후 각각 소비
const clone = res.clone();
const json = await res.json();
const text = await clone.text();

WARNING

Response.body 는 ReadableStream 이며 한 번만 읽을 수 있다. 두 번 파싱이 필요하면 반드시 res.clone() 을 먼저 호출한다.

credentials 기본값

// 기본값 'same-origin': 크로스 오리진 요청에는 쿠키가 전송되지 않음
fetch('https://other.com/api');                         // 쿠키 없음

// 크로스 오리진에서 쿠키 보내려면 명시
fetch('https://other.com/api', { credentials: 'include' });
// 서버도 CORS 헤더에 Access-Control-Allow-Credentials: true 필요

관련 위키

이 글의 용어 (5개)
[JavaScript] AbortControllerjavascript
정의 는 비동기 작업 (fetch, event listener, 커스텀 Promise 등) 을 취소 하기 위한 표준 Web API 인터페이스. | 인터페이스 | 역할 | |:--…
[Javascript] async/awaitjavascript
정의 / 는 기반 비동기 코드를 마치 동기 코드처럼 쓸 수 있게 해주는 ES2017 의 문법 설탕. 본질은 Promise 그 자체, 문법만 다르다. 전체 동작 메커니즘은 글 참조…
[Javascript] Error / try-catchjavascript
정의 JS 의 에러 처리. 로 던지고 로 잡는다. 내장 Error 타입과 사용자 정의 Error. throw / try / catch 는 에러 유무와 무관하게 실행. 내장 Err…
[Javascript] Promisejavascript
정의 Promise 는 미래의 어떤 시점에 결정될 값을 나타내는 객체다. ECMAScript 2015 (ES6) 에 도입되어 기반 비동기의 가독성과 에러 처리 문제를 해결했다. …
[Network] HTTP/1.1: keep-alive, pipelining, chunked transfernetwork
정의 HTTP/1.1 (1997, RFC 9112 으로 재정리) 는 텍스트 기반 stateless request/response 프로토콜. 2026 시점에도 대부분의 트래픽이 H…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기