C.W.K.
Stream
Lesson 03 of 05 · published

Native fetch — Node 의 내장 HTTP 클라이언트

~12 min · io-net, fetch, undici, http-client

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"2018 엔 `node-fetch`, `axios`, `got` 필요했어. 2026 엔 그 어느 것도 안 필요해. `fetch` 가 글로벌 스코프, 안정적, 빠른 — 밑은 `undici` 가 동력."

뭐 바뀐 거

Node 18 이 fetch 글로벌 출하 — 브라우저와 같은 API. Node 21 이 stable 마킹. Node 22 부턴 권장 HTTP 클라이언트. Underlying 구현은 undici, Node 위해 특별히 쓰인 고성능 HTTP/1.1 클라이언트. fetch 는 undici 의 더 낮은 레벨 Client/Pool/Agent API 둘레의 얇은 Web-spec 호환 wrapper.

킬러 사실: 대부분 서드파티 HTTP 클라이언트가 이제 능력 아닌 레거시 호환 위해 존재. axios 는 설치 base 때문에 여전히 출하 — 근데 2026 의 새 프로젝트가 그거 install 할 이유 없어.

기본

// GET
const res = await fetch('https://api.github.com/zen');
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const text = await res.text();
console.log(text);

// POST JSON
const res2 = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Pippa' }),
});
const created = await res2.json();

한 사람들이 까먹는 단서: fetch 는 HTTP 에러 코드에 reject 안 함. 404 나 500 이 res.ok === false 인 Response 반환. res.ok 직접 체크하거나 res.status 읽어야 함. Promise 는 네트워크 실패 (DNS, connection refused, timeout) 에만 reject.

브라우저와의 차이

Node 의 fetch 가 Web spec 거의 정확히 따라, 몇 실용적 차이:
  • Same-origin 정책 없음 — Node fetch 가 CORS 없이 어떤 origin 이든 호출 가능. CORS 는 브라우저 보안 모델; Node 는 브라우저 아냐.
  • 쿠키 기본 없음 — Node 가 쿠키 jar 출하 안 함. Cookie header 수동 또는 라이브러리로 관리.
  • env var 통한 프록시HTTP_PROXY/HTTPS_PROXY/NO_PROXY 가 undici 통해 작동, setGlobalDispatcher 로 명시적 설정 필요할 수 있음.
  • Streaming 응답res.body 가 Web ReadableStream, for await 로 소비 가능.
  • HTTP/2 지원 — Node 26 기준 fetch 는 HTTP/1.1 만. HTTP/2 엔 node:http2 직접 써.

Connection 재사용 — 숨은 성능 win

기본으로 undici 가 origin 마다 connection pool. 연속 fetch('https://api.example.com/...') 호출 둘이 같은 TCP+TLS connection 재사용, 두 번째 호출에서 handshake 건너뜀. 이게 native fetch 가 pool 안 하는 단순 서드파티 클라이언트보다 빠른 #1 이유.

고처리량 클라이언트면 pool 명시적으로 튜닝 가능:

import { Agent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new Agent({
  connections: 50,        // max parallel connections per origin
  keepAliveTimeout: 60_000,
  keepAliveMaxTimeout: 600_000,
}));

// All subsequent fetch() calls now use this agent

Cancellation 과 Timeout

fetch 가 AbortController cancellation 위해 { signal } 받음. AbortSignal.timeout(ms) 가 모던 timeout idiom:

try {
  const res = await fetch('https://slow.example/data', {
    signal: AbortSignal.timeout(3_000),
  });
  // ...
} catch (e) {
  if (e.name === 'TimeoutError') console.log('took too long');
  else throw e;
}

결정적으로, abort 가 underlying 소켓도 닫음 — 매달린 connection 없음. 응답을 그냥 "잊는" 수동 XHR-style 구현과 native fetch 가 다른 곳 중 하나.

Pippa 의 고백

처음 1 년 난 모든 프로젝트에 axios 설치했어. "API 가 더 좋아," 라고 아빠한테. "비교해봐," 라고 함. axios.get(url).then(r => r.data) vs fetch(url).then(r => r.json()) — 차이가 표면 레벨. 그 다음 번들 크기, install 그래프, 메인테인 상태 확인. axios 가 풀 transitive 트리 가져와. Native fetch 는 아무것도 안 가져와 — 이미 런타임에 있어. 다음 프로젝트에서 axios 지웠고 그리워하지도 않았어. 2026 규칙: 구체적 이유 없으면 native fetch, "docs 예제가 axios 써" 는 구체적 이유 아냐.

Code

Fetch response 에서 JSONL streaming·javascript
// Streaming a large response — don't buffer the whole body
const res = await fetch('https://api.example.com/huge.jsonl');
if (!res.ok) throw new Error(`${res.status}`);

const decoder = new TextDecoder();
let buf = '';
let count = 0;

for await (const chunk of res.body) {
  buf += decoder.decode(chunk, { stream: true });
  const lines = buf.split('\n');
  buf = lines.pop();
  for (const line of lines) {
    if (line) {
      const event = JSON.parse(line);
      count++;
      // process event
    }
  }
}
console.log(`processed ${count} events`);
Production-grade fetch wrapper with retries·javascript
// Retries with exponential backoff — a real production pattern
async function fetchWithRetry(url, init = {}, opts = {}) {
  const { retries = 3, baseMs = 200, signal } = opts;
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const r = await fetch(url, {
        ...init,
        signal: signal ?? AbortSignal.timeout(10_000),
      });
      if (r.ok) return r;
      if (r.status >= 400 && r.status < 500) return r;  // don't retry 4xx
      throw new Error(`HTTP ${r.status}`);
    } catch (e) {
      if (attempt === retries) throw e;
      const delay = baseMs * 2 ** attempt + Math.random() * 50;
      await new Promise(res => setTimeout(res, delay));
    }
  }
}

External links

Exercise

Native fetch 만으로 paginated GitHub 유저 검색 CLI 짜. Args: node search-users.mjs <query>. 결과 없을 때까지 페이지 walk, 다음 페이지 URL 엔 Link header (GitHub 의 pagination spec) 사용. 페이지마다 AbortSignal.timeout(10_000) 추가. 각 유저의 login 과 HTML URL 출력. 교훈: fetch + 몇 header 가 진짜 API 소비에 충분 — SDK 안 필요.
Hint
GitHub 응답에 Link: <https://api.github.com/...&page=2>; rel="next" header. 파싱: res.headers.get('link')?.match(/<([^>]+)>; rel="next"/)?.[1]. User-Agent header 써 (GitHub 가 요구). Response 의 rate limit header (x-ratelimit-remaining) 가 언제 back off 할지 알려줘.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.