Node.js 18 LTS에서 내장 fetch API 사용하기

배경

10월에 Node.js 18이 LTS로 전환되면서 실험적 기능이었던 내장 fetch API를 프로덕션에서 사용할 수 있게 되었다. 기존 프로젝트에서 axios와 node-fetch를 혼용하고 있었는데, 이 기회에 네이티브 fetch로 통일하기로 결정했다.

마이그레이션 과정

기존에는 다음과 같이 axios를 사용했다.

const axios = require('axios');

const response = await axios.get('https://api.example.com/data', {
  headers: { 'Authorization': `Bearer ${token}` },
  timeout: 5000
});
const data = response.data;

네이티브 fetch로 변경하면서 몇 가지 차이점을 발견했다.

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

const response = await fetch('https://api.example.com/data', {
  headers: { 'Authorization': `Bearer ${token}` },
  signal: controller.signal
});

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
clearTimeout(timeoutId);

주의사항

  1. 에러 처리: axios는 4xx, 5xx 응답에서 자동으로 throw하지만, fetch는 네트워크 실패시에만 reject된다. 수동으로 response.ok 체크가 필요하다.

  2. 타임아웃: axios의 timeout 옵션과 달리 AbortController를 직접 구현해야 한다.

  3. 인터셉터: axios의 interceptor 기능이 없어서 공통 헤더나 에러 처리를 위한 wrapper 함수를 만들었다.

async function apiRequest(url, options = {}) {
  const defaultHeaders = {
    'Content-Type': 'application/json',
    ...(token && { 'Authorization': `Bearer ${token}` })
  };

  const response = await fetch(url, {
    ...options,
    headers: { ...defaultHeaders, ...options.headers }
  });

  if (!response.ok) {
    throw new Error(`API Error: ${response.status}`);
  }

  return response.json();
}

결과

package.json에서 axios와 node-fetch 의존성을 제거하면서 번들 사이즈가 약 300KB 줄었다. 네이티브 API를 사용하니 보일러플레이트는 늘었지만, 외부 의존성 관리 부담이 줄어든 것은 확실한 이점이다.

간단한 API 호출이 대부분이라면 네이티브 fetch로도 충분하다고 판단된다.