"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.
브라우저와의 차이
fetch 가 Web spec 거의 정확히 따라, 몇 실용적 차이:- Same-origin 정책 없음 — Node fetch 가 CORS 없이 어떤 origin 이든 호출 가능. CORS 는 브라우저 보안 모델; Node 는 브라우저 아냐.
- 쿠키 기본 없음 — Node 가 쿠키 jar 출하 안 함.
Cookieheader 수동 또는 라이브러리로 관리. - 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 의 고백
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 써" 는 구체적 이유 아냐.