← 블로그로 돌아가기

Node.js에서 Undici ProxyAgent: HTTP/SOCKS5 프록시 설정 및 불필요한 의존성 없이 IP 회전

Node.js에서 HTTP 및 SOCKS5 프록시와 함께 내장된 undici ProxyAgent를 사용하는 방법, IP 회전을 구성하는 방법, https-proxy-agent 및 socks-proxy-agent 없이 사용하는 방법을 설명합니다.

📅2026년 10월 18일

대부분의 Node.js 프록시 가이드는 프로젝트에 https-proxy-agent, socks-proxy-agent 및 몇 가지 패키지를 추가하라고 권장합니다. 그러나 Node.js 18+ 버전부터 내장된 HTTP 클라이언트 undici는 이미 ProxyAgent를 포함하고 있어 프록시를 통한 API 파싱 및 테스트의 대부분의 작업을 처리합니다. 이 기사에서는 undici를 통해 HTTP/HTTPS 터널링을 설정하고 요청 간 IP 회전을 구성하는 방법과 SOCKS5가 필요할 때 추가 종속성을 가져오지 않고 처리하는 방법을 살펴보겠습니다.

axios 및 request 대신 undici를 사용하는 이유

undici는 HTTP/1.1 및 HTTP/2 클라이언트로, Node.js 팀이 작성하였으며 18 버전부터는 전역 fetch의 기초가 됩니다. 전이 종속성이 없으며, connection pooling에 대한 대부분의 벤치마크에서 axios보다 빠르며, 우리 주제와 관련하여 중요한 점은 HTTP CONNECT 터널링을 통해 프록시와 작업하기 위한 네이티브 모듈이 있다는 것입니다.

Wildberries, Ozon을 파싱하거나 다양한 지역에서 광고 크리에이티브를 테스트할 때, node_modules에 있는 모든 추가 패키지는 Node.js 업데이트 시 잠재적인 오류 지점이 됩니다. undici는 런타임과 함께 제공되므로 최신 TLS, HTTP/2 및 keep-alive 구현을 얻을 수 있으며, 버전 업데이트 시 중단된 라이브러리로 인해 문제가 발생할 위험이 없습니다.

설치는 매우 간단합니다:

npm install undici

Node.js 20+가 있는 경우 일부 API는 가져오지 않고도 전역적으로 사용할 수 있지만, ProxyAgent는 기본적으로 전역 fetch에 내보내지 않기 때문에 여전히 가져와야 합니다.

ProxyAgent 기본 설정

프록시를 통한 최소 작업 예제는 다음과 같습니다:

import { ProxyAgent, fetch } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

const data = await response.json();
console.log(data);

핵심 포인트는 dispatcher 매개변수입니다. 이는 이전 http 모듈의 agent 옵션이 아니라, undici의 디스패처로, TLS 핸드셰이크 전에 연결을 가로채고 CONNECT 메소드를 통해 대상 호스트에 대한 터널을 설정합니다. HTTPS 요청을 프록시를 통해 처리할 때 이는 매우 중요합니다: 일반 HTTP 프록시는 암호화된 트래픽의 내용을 볼 수 없으며, TCP 터널 내에서 바이트를 단순히 전달합니다.

fetch가 아닌 저수준의 undici.request를 사용하는 경우 구문은 유사합니다:

import { request, ProxyAgent } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const { statusCode, body } = await request('https://api.example.com/data', {
  dispatcher: proxyAgent,
});

console.log(statusCode);
for await (const chunk of body) {
  console.log(chunk.toString());
}

분당 수백 개의 요청을 수행하는 프로덕션 파서의 경우, 매 요청마다 새로운 ProxyAgent를 생성하지 않는 것이 중요합니다 — 이는 connection pooling을 방해합니다. 연결 풀에 대해 한 번 에이전트 인스턴스를 생성하고 호출 간에 재사용하십시오.

로그인 및 비밀번호로 프록시 인증

대부분의 유료 프록시 제공업체는 IP 화이트리스트가 아닌 로그인/비밀번호 쌍으로 접근을 제공합니다. undici에서는 자격 증명이 프록시 URL에 직접 전달됩니다:

import { ProxyAgent, fetch } from 'undici';

const login = 'user123';
const password = 'pass456';
const proxyUrl = `http://${login}:${password}@gate.proxycove.com:9000`;

const proxyAgent = new ProxyAgent(proxyUrl);

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

console.log(await response.json());

주의: 로그인이나 비밀번호에 특수 문자가 포함되어 있는 경우 (@, :, /) encodeURIComponent를 사용하여 인코딩해야 합니다. 그렇지 않으면 undici 내부의 URL 파서가 연결 문자열을 올바르게 해석하지 못하고 이해하기 쉬운 메시지 대신 407 인증 오류가 발생합니다.

세션의 안정성이 중요한 작업 — 예를 들어, 세션 동안 하나의 IP에 바인딩된 마켓플레이스에서의 인증 — 에는 레지던트 프록시를 사용하는 것이 더 편리합니다: IP는 정해진 시간 동안 변경되지 않으므로 서비스는 지역 간의 "점프"를 감지하지 않으며 각 요청마다 CAPTCHA를 반복적으로 요구하지 않습니다.

요청 간 프록시 회전

N개의 프록시 목록이 있고 요청을 분산해야 하는 경우, 가장 간단한 방법은 타사 큐 라이브러리 없이 라운드 로빈 방식입니다:

import { ProxyAgent, fetch } from 'undici';

const proxyList = [
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
];

let currentIndex = 0;

function getNextAgent() {
  const proxyUrl = proxyList[currentIndex];
  currentIndex = (currentIndex + 1) % proxyList.length;
  return new ProxyAgent(proxyUrl);
}

async function fetchWithRotation(url) {
  const agent = getNextAgent();
  return fetch(url, { dispatcher: agent });
}

for (let i = 0; i < 10; i++) {
  const res = await fetchWithRotation('https://httpbin.org/ip');
  console.log(await res.json());
}

이러한 접근 방식은 각 요청이 이전 요청과 논리적으로 독립적인 경우, 예를 들어 상품 카드 파싱이나 가격 모니터링에 잘 작동합니다. 각 요청마다 IP를 빠르게 변경해야 하는 작업에는 제공업체 측에서 자동 회전하는 프록시를 사용하는 것이 더 간단합니다 — 그러면 위의 모든 코드는 하나의 정적 URL 게이트웨이로 축소되고, 회전은 데이터 센터 프록시 또는 타이머에 따라 회전하는 레지던트 풀에서 처리합니다.

단순한 원형 회전이 아니라 가중치를 두고 회전해야 하는 경우 — 예를 들어 더 빠른 프록시에 우선권을 주려면 데이터 구조에 가중치를 추가하십시오:

const weightedProxies = [
  { url: 'http://user:[email protected]:8000', weight: 5 },
  { url: 'http://user:[email protected]:8000', weight: 1 },
];

function pickWeighted(list) {
  const total = list.reduce((sum, p) => sum + p.weight, 0);
  let rnd = Math.random() * total;
  for (const p of list) {
    if (rnd < p.weight) return p.url;
    rnd -= p.weight;
  }
  return list[0].url;
}

프록시 생존 확인 및 "죽은" IP 제외

프록시 풀은 시간이 지남에 따라 "썩어" 갑니다: 일부 IP는 대상 사이트에 의해 차단되고, 일부는 응답하지 않게 됩니다. 회전이 죽은 주소를 통해 요청을 보내지 않도록 하려면, 내장된 AbortController를 사용하여 헬스 체크를 추가하십시오:

import { ProxyAgent, fetch } from 'undici';

async function isProxyAlive(proxyUrl, timeoutMs = 5000) {
  const agent = new ProxyAgent(proxyUrl);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch('https://httpbin.org/ip', {
      dispatcher: agent,
      signal: controller.signal,
    });
    return res.status === 200;
  } catch {
    return false;
  } finally {
    clearTimeout(timer);
  }
}

async function filterAliveProxies(proxyList) {
  const checks = await Promise.all(
    proxyList.map(async (url) => ({ url, alive: await isProxyAlive(url) }))
  );
  return checks.filter((c) => c.alive).map((c) => c.url);
}

몇 분마다 백그라운드 타이머로 이러한 검사를 실행하고 전체 정적 목록이 아닌 활성 풀을 업데이트하십시오. 이는 저렴한 데이터 센터 IP에서 자체 프록시를 유지하는 경우 특히 중요합니다 — 그곳의 고장 비율은 관리되는 제공업체 풀보다 높습니다.

타사 라이브러리 없이 SOCKS5 사용하기

중요한 점: undici의 내장 ProxyAgent는 HTTP CONNECT 터널링을 통해 작동하며 "기본적으로" HTTP/HTTPS 프록시만 이해합니다. SOCKS5 프로토콜은 다르게 구성되어 있으며, TCP 레벨에서의 이진 핸드셰이크로, undici는 이를 네이티브로 해석하지 않습니다. SOCKS5가 꼭 필요하고 socks-proxy-agent를 설치하고 싶지 않다면, net 모듈과 사용자 정의 Agent의 connect 옵션을 통해 최소한의 커넥터를 수동으로 구현할 수 있습니다:

import net from 'node:net';
import { Agent, fetch } from 'undici';

function socksHandshake(socket, targetHost, targetPort) {
  return new Promise((resolve, reject) => {
    // 단계 1: 인사, 인증 없이 방법
    socket.write(Buffer.from([0x05, 0x01, 0x00]));

    socket.once('data', (greeting) => {
      if (greeting[1] !== 0x00) {
        return reject(new Error('SOCKS5: 인증 방법이 지원되지 않습니다.'));
      }

      const hostBuf = Buffer.from(targetHost, 'utf8');
      const request = Buffer.concat([
        Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
        hostBuf,
        Buffer.from([targetPort >> 8, targetPort & 0xff]),
      ]);

      socket.write(request);

      socket.once('data', (reply) => {
        if (reply[1] !== 0x00) {
          return reject(new Error('SOCKS5: 연결이 거부되었습니다.'));
        }
        resolve(socket);
      });
    });
  });
}

const socksAgent = new Agent({
  connect: (options, callback) => {
    const socket = net.connect(1080, 'socks-proxy.example.com', async () => {
      try {
        await socksHandshake(socket, options.hostname, 443);
        callback(null, socket);
      } catch (err) {
        callback(err, null);
      }
    });
    socket.on('error', (err) => callback(err, null));
  },
});

const response = await fetch('https://httpbin.org/ip', { dispatcher: socksAgent });
console.log(await response.json());

이는 로그인 및 비밀번호에 의한 인증(방법 0x02) 없이 간소화된 핸드셰이크이며, IPv6 지원이 없습니다 — 프로덕션에서는 인증 및 다양한 주소 유형 처리에 대한 분기를 추가하는 것이 좋습니다. 그러나 메커니즘을 이해하는 데는 충분합니다: SOCKS5는 핸드셰이크 단계에서 터널 내 TLS를 요구하지 않으므로 HTTP CONNECT와는 달리 코드가 처음 보기보다 짧습니다.

실제로 SOCKS5는 웹 페이지를 파싱하는 데보다 모바일 애플리케이션 및 Dolphin Anty 또는 AdsPower와 같은 안티 디텍트 브라우저의 트래픽을 전달하는 데 더 자주 사용됩니다. Node.js에서 HTTP/HTTPS 트래픽이 필요한 경우 내장된 ProxyAgent가 일반적으로 충분하며, 수동 SOCKS5 커넥터를 다룰 필요는 없습니다.

타임아웃 처리 및 재시도 로직

프록시는 직접 연결보다 연결을 끊는 경우가 더 많습니다 — 이는 주거 네트워크에서 노드가 언제든지 오프라인으로 전환될 수 있기 때문에 정상입니다. 지수 지연이 있는 재시도 래퍼가 필요합니다:

import { ProxyAgent, fetch } from 'undici';

async function fetchWithRetry(url, proxyList, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const proxyUrl = proxyList[attempt % proxyList.length];
    const agent = new ProxyAgent(proxyUrl);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 8000);

    try {
      const res = await fetch(url, { dispatcher: agent, signal: controller.signal });
      clearTimeout(timer);
      if (res.status >= 500) throw new Error(`서버 오류: ${res.status}`);
      return res;
    } catch (err) {
      clearTimeout(timer);
      lastError = err;
      const delay = 500 * 2 ** attempt;
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error(`모든 시도가 소진되었습니다: ${lastError?.message}`);
}

이 래퍼는 오류 발생 시 회전 문제도 해결합니다: 현재 프록시가 실패하면 다음 시도는 목록에서 다른 IP로 자동으로 전환되며, 동일한 죽은 노드에 계속 연결되지 않습니다.

undici ProxyAgent, https-proxy-agent 및 socks-proxy-agent 비교

도구 종속성 SOCKS5 지원 HTTP/2 속도
undici ProxyAgent 없음 (Node 18+에 내장) 기본적으로 없음 예 높음
https-proxy-agent 1-2 패키지 아니오 아니오 중간
socks-proxy-agent 3-4 패키지 예 아니오 중간

최소한의 종속성으로 순수 HTTP/HTTPS 파싱을 위해 undici ProxyAgent는 내장성과 HTTP/2 호환성 덕분에 이점을 가집니다. 프로덕션에서 SOCKS5를 사용할 경우, 수동 핸드셰이크를 유지하는 것보다 검증된 socks-proxy-agent를 사용하는 것이 더 쉽습니다 — 위의 섹션에서의 수동 구현은 학습 및 드문 엣지 케이스에 적합하지만, 시간당 수천 개의 연결이 필요한 프로덕션에는 적합하지 않습니다.

실용적인 사례: 파싱 및 지리 테스트

마켓플레이스 가격 모니터링. 위의 섹션에서 설명한 프록시 회전 및 재시도 로직을 갖춘 undici 스크립트는 Wildberries 및 Ozon의 비율 제한을 우회하는 데 이상적입니다: 다음 상품 카드 요청은 새로운 IP로 전송되며, 헬스 체크는 자동으로 차단된 주소를 풀에서 제외합니다.

지리별 광고 크리에이티브 테스트. Google Ads 또는 Facebook Ads를 위해 특정 국가의 사용자에게 콘텐츠가 어떻게 제공되는지 확인해야 하는 경우, ProxyAgent의 프록시 URL을 해당 지역의 노드로 변경하는 것만으로 충분합니다 — API 또는 서버의 HTML 응답을 통해 확인하는 경우 브라우저를 실행할 필요가 없습니다.

다양한 국가에서 콘텐츠 접근성 확인. 사이트가 IP에 따라 다른 콘텐츠를 제공하는 경우, 레지던트 연결은 대상 서버에 일반 사용자와 구별되지 않으므로 데이터 센터 범위에 비해 차단 위험이 줄어듭니다. 이러한 시나리오는 모바일 프록시에 특히 중요합니다. 대상 서비스가 네트워크 유형을 확인하고 명백한 데이터 센터 서브넷을 차단하는 경우.

대량 API 엔드포인트 확인. 외부 프록시 풀을 통해 자체 API를 부하 테스트할 때, connection pooling을 주의 깊게 살펴보는 것이 중요합니다 — undici는 기본적으로 하나의 ProxyAgent 내에서 TCP 연결을 재사용하므로 동일한 호스트에 대한 수백 개의 연속 요청 시 TLS 핸드셰이크에 소요되는 시간을 절약할 수 있습니다.

결론

내장된 undici ProxyAgent는 Node.js에서 HTTP/HTTPS 프록시와 관련된 대부분의 작업을 수행할 수 있으며, 타사 패키지를 가져올 필요가 없습니다: URL을 통한 인증, CONNECT를 통한 터널링, HTTP/2와의 호환성 및 connection pooling이 즉시 작동합니다. 프록시 회전 및 헬스 체크는 순수한 프로미스와 타이머로 쉽게 구성할 수 있으며, 필요할 경우 SOCKS5는 사용자 정의 connect 커넥터를 통해 수동으로 구현할 수 있지만, 프로덕션에서는 SOCKS 전용 라이브러리를 사용하는 것이 더 합리적입니다.

undici 스크립트가 마켓플레이스 파싱이나 가격 모니터링을 위해 설계된 경우, 레지던트 프록시에 주목하십시오 — 이들은 전통적인 데이터 센터 IP보다 안티봇 시스템에 더 강하고, Wildberries, Ozon 또는 Avito에 대한 대량 요청 시 추가 CAPTCHA 해결을 덜 요구합니다.