Node.js 내장 Fetch와 Undici 기반 분석
Ethan Miller
Product Engineer · Leapcell

소개
수년 동안 Node.js에서 HTTP 요청을 만드는 것은 종종 axios 또는 node-fetch와 같은 외부 라이브러리를 사용하는 것을 포함했습니다. 이러한 라이브러리들은 커뮤니티에 잘 봉사해 왔지만, 추가적인 종속성을 도입했으며 때로는 브라우저의 네이티브 fetch API에 익숙한 개발자에게 약간의 학습 곡선을 안겨주었습니다. Node.js 18의 출시와 함께 중요한 이정표가 달성되었습니다. 별도의 설치 없이 브라우저의 것과 유사한 전역 내장 fetch API를 사용할 수 있게 된 것입니다. 이 통합은 Node.js 웹 개발을 크게 간소화하여 데이터 가져오기를 위한 친숙한 인터페이스를 제공하고 성능 및 안정성 향상을 약속합니다. 이 문서는 Node.js 네이티브 fetch를 철저히 탐색하고, 그 근본적인 아키텍처를 밝히며, 특히 이를 지원하는 최첨단 HTTP/1.1 클라이언트인 undici와의 깊은 관계를 구체적으로 살펴볼 것입니다.
핵심 개념 설명
fetch와 undici의 세부 사항을 살펴보기 전에, 우리의 논의에서 중심이 될 몇 가지 기본 용어를 명확히 하겠습니다.
fetchAPI: 네트워크 요청을 만드는 현대적이고 promise 기반 API로, 종종 네트워크를 통해 리소스를 검색하는 데 사용됩니다.XMLHttpRequest보다 더 유연하고 강력하도록 설계되었습니다.undici: Node.js를 위한 고성능 WHATWGfetchAPI 호환 HTTP/1.1 클라이언트로, 빠르고 안정적으로 만들기 위해 처음부터 구축되었습니다. Node.js의 표준 HTTP 클라이언트를 목표로 합니다.- WHATWG 
fetch표준: Node.js의 내장fetch가 이 표준과 긴밀하게 일치하도록 목표하는 웹 하이퍼텍스트 애플리케이션 기술 그룹(WHATWG)의fetchAPI를 정의하는 사양입니다. - 스트림(Streams): Node.js에서 데이터를 청크 단위로 처리하기 위한 기본 개념입니다. 
fetch는 요청 본문과 응답 본문 모두에 스트림을 활용하여 대량의 데이터를 효율적으로 처리할 수 있도록 합니다. - 요청/응답 객체(Request/Response Objects): 
fetchAPI에서 사용되는 핵심 객체입니다.Request객체는 아웃바운드 네트워크 요청을 나타내고,Response객체는 인바운드 네트워크 응답을 나타냅니다. 
Node.js Fetch의 내부 작동 방식
Node.js의 내장 fetch API는 완전히 독립적인 구현이 아닙니다. 대신, undici 라이브러리를 직접 재내보내고 약간 수정된 것입니다. Node.js 핵심 팀의 이러한 전략적 선택은 여러 가지 이점을 제공합니다:
- 표준 준수: 
undici는 WHATWGfetch표준을 준수하도록 세심하게 설계되어, Node.js 개발자가 브라우저 환경과 일관되게 동작하는 API를 얻도록 합니다. 이는 인지 부하를 줄이고 동형 코드(isomorphic code)를 촉진합니다. - 성능 및 효율성: 
undici는 탁월한 성능으로 유명합니다. 맞춤형 요청/응답 파서, 커넥션 풀링, 파이프라이닝 및 기타 최적화를 통해 오버헤드를 크게 줄이고 이전 HTTP 클라이언트에 비해 처리량을 향상시킵니다.undici를 활용함으로써 Node.jsfetch는 이러한 성능 이점을 상속받습니다. - 활발한 개발: 
undici는 활발하게 개발되는 프로젝트로, 지속적인 개선, 버그 수정 및 기능 향상을 보장합니다. 이를 통합함으로써 Node.js는 이러한 발전을 신속하게 채택할 수 있습니다. 
기본 사용 예제
Node.js 18+에서 fetch를 사용하는 것은 브라우저에서처럼 간단합니다.
// my-data-fetcher.js async function fetchData(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // 또는 .text(), .blob(), .arrayBuffer(), .formData() console.log('Fetched data:', data); return data; } catch (error) { console.error('Error fetching data:', error); throw error; } } // 예제 사용: fetchData('https://jsonplaceholder.typicode.com/todos/1') .then(data => console.log('Successfully retrieved:', data)) .catch(error => console.error('Failed to retrieve:', error.message)); // 사용자 정의 헤더가 있는 POST 요청 예제 async function postData(url, data) { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('POST successful:', result); return result; } catch (error) { console.error('Error posting data:', error); throw error; } } postData('https://jsonplaceholder.typicode.com/posts', { title: 'foo', body: 'bar', userId: 1, }) .then(data => console.log('New post created:', data)) .catch(error => console.error('Failed to create post:', error.message));
이 코드를 실행하려면 my-data-fetcher.js로 저장하고 터미널에서 node my-data-fetcher.js를 실행하면 됩니다. 데이터가 가져와져 콘솔에 로깅되는 것을 볼 수 있습니다.
undici가 fetch를 지원하는 방법
Node.js 18+에서 global.fetch()를 호출할 때, 본질적으로 undici.fetch()를 호출하는 것입니다. 통합은 원활합니다. undici는 연결 관리, 요청 직렬화, 응답 파싱 및 오류 처리를 처리하는 핵심 HTTP 클라이언트 기능을 제공합니다.
간략한 개념적 흐름을 살펴보겠습니다.
fetch호출:fetch(url, options)가 호출되면, 전역fetch함수(undici.fetch)가 이러한 인수를 받습니다.Request객체 생성:undici는 WHATWGfetch사양에 따라 제공된 URL 및 옵션을 기반으로 내부적으로Request객체를 구성합니다.- 연결 관리: 
undici는 정교한 연결 풀링 메커니즘을 사용합니다. 기존 HTTP/1.1 연결을 재사용하거나 새 연결을 효율적으로 설정합니다. - 요청 전송: 
Request객체는 HTTP 메시지로 직렬화되어 설정된 연결을 통해 전송됩니다.undici는 헤더, 본문 인코딩 및 리디렉션과 같은 문제를 처리합니다. - 응답 수신 및 파싱: 서버 응답을 받은 후, 
undici는 수신된 HTTP 메시지를 신속하게 파싱하고Response객체를 구성하며, 응답 본문을ReadableStream으로 사용할 수 있도록 합니다. Response객체 반환:fetch호출은Response객체로 해결되어,status,headers와 같은 속성에 액세스하거나json()또는text()와 같은 메서드를 사용하여 본문을 소비할 수 있습니다. 이러한 본문 메서드는 종종 기본undici스트림을 소비하는 것을 포함합니다.
고급 fetch 기능 및 undici의 역할
undici는 fetch 외에도 더 낮은 수준의 API를 노출하며, HTTP 클라이언트 동작에 대한 직접적인 제어가 필요한 경우 액세스할 수 있습니다. 예를 들어, undici.Agent는 연결 풀링, 시간 초과 및 리디렉션에 대한 세분화된 제어를 제공합니다.
global.fetch는 종종 충분하지만, 사용자 정의 연결 에이전트 또는 고급 풀링 전략이 필요한 시나리오에서는 직접 undici 사용을 고려할 수 있습니다. 그러나 fetch API 자체는 init 객체 내에서 일반적인 사용 사례 대부분을 다루는 강력한 옵션 세트를 제공합니다.
signal:AbortController를 사용하여 요청을 중단합니다.const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5초 후 중단 fetch('https://slow-api.example.com/data', { signal: controller.signal }) .then(response => { clearTimeout(timeoutId); return response.json(); }) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.error('Fetch aborted by user or timeout'); } else { console.error('Fetch error:', err); } });redirect: 리디렉션 동작을 제어합니다 (follow,error,manual).body와 스트림: 전체 페이로드를 메모리에 버퍼링하지 않고 효율적으로 대량의 데이터를 보냅니다.const { Readable } = require('stream'); async function uploadStreamData() { const readableStream = new Readable({ read() { this.push('Hello '); this.push('World!'); this.push(null); // 더 이상 데이터 없음 } }); try { const response = await fetch('https://httpbin.org/post', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: readableStream // Node.js fetch는 Readable 스트림을 직접 수락할 수 있습니다. }); const data = await response.json(); console.log('Stream upload response:', data); } catch (error) { console.error('Stream upload error:', error); } } uploadStreamData();
undici 덕분에 Node.js의 fetch API는 이러한 고급 메커니즘을 지원하여 서버 측 애플리케이션에서 강력하고 다재다능한 네트워크 통신 도구가 됩니다.
결론
Node.js 18+에 fetch API가 통합된 것은 플랫폼의 중요한 발전으로, 개발자에게 HTTP 요청을 만드는 익숙하고 강력하며 성능이 뛰어난 방법을 제공합니다. 이 원활한 경험은 Node.js 네이티브 fetch의 백본 역할을 하는 최첨단 HTTP/1.1 클라이언트인 undici 덕분입니다. undici를 활용함으로써 Node.js fetch는 고성능과 WHATWG fetch 표준에 대한 엄격한 준수를 달성할 뿐만 아니라 현대 웹 개발을 위한 강력한 기반을 제공합니다. Node.js fetch는 HTTP 상호 작용을 단순화하여 개발자가 클라이언트 측 HTTP 복잡성보다는 애플리케이션 로직에 더 집중할 수 있도록 합니다.