Deno와 Bun에서 프록시 fetch 사용하기: 실전 가이드

Deno와 Bun의 최신 JavaScript 런타임에서 fetch()에 프록시를 적용하는 방법을 코드 중심으로 설명합니다. Deno.createHttpClient, Bun의 proxy 옵션, 지역 타겟팅, sticky 세션, SOCKS5, 재시도 로직까지 포함합니다.

Using Proxies in Deno and Bun: A Code-First Guide for Modern JavaScript

Deno와 Bun에서 프록시 fetch로 시작하기

Deno와 Bun은 Node.js보다 빠르고 표준 친화적인 최신 JavaScript 런타임이지만, 두 환경 모두 기본 fetch()는 프록시를 무시합니다. 이건 버그가 아니라 WHATWG Fetch 표준의 의도된 동작입니다 — Fetch 표준 자체에 프록시 구성 스펙이 없기 때문입니다. 따라서 Deno 프록시, Bun fetch 프록시, Deno.createHttpClient, JavaScript 프록시 fetch를 다루는 개발자는 런타임별 API를 이해하고 있어야 합니다. 이 가이드는 두 런타임에서 ProxyHat의 gate.proxyhat.com:8080 게이트웨이를 사용해 레지덴셜 프록시를 적용하는 방법을 코드 중심으로 설명합니다.

실무에서 프록시가 필요한 순간은 명확합니다. SERP 추적, 이커머스 가격 모니터링, 소셜 미디어 리서스, AI 학습 데이터 수집 — 이 모든 작업에서 단일 IP로 수백 번 요청을 보내면 곧 차단됩니다. 레지덴셜 프록시는 실제 ISP IP를 사용하므로 데이터센터 대역보다 차단률이 현저히 낮습니다. 웹 스크래핑SERP 추적 사용 사례에서 특히 효과적입니다.

왜 기본 fetch()는 프록시를 무시하는가

WHATWG Fetch 표준은 브라우저 컨텍스트에서 설계되었고, 브라우저는 OS 수준의 프록시 설정을 따르지만 JavaScript 코드에서 프록시 URL을 직접 지정하는 인터페이스를 노출하지 않습니다. Node.js는 undiciProxyAgent를 통해 이를 해결하고, Deno와 Bun은 각자의 방식으로 같은 문제를 풉니다.

핵심 차이는 세 가지입니다:

  • DenoDeno.createHttpClient({ proxy: { url, basicAuth } })로 커스텀 HTTP 클라이언트를 만들고 fetch(url, { client })로 전달합니다. 클라이언트 재사용이 가능해 연결 풀 효율이 좋습니다.
  • Bunfetch(url, { proxy: 'http://user:pass@host:port' }) 한 줄로 적용합니다. 더 간결하지만 클라이언트 재사용 추상화는 개발자가 직접 관리해야 합니다.
  • 환경 변수HTTP_PROXY/HTTPS_PROXY를 설정하면 두 런타임 모두 이를 인식하지만, 세밀한 제어가 어렵고 전역 상태가 됩니다.

Deno에서 Deno.createHttpClient로 프록시 적용하기

Deno의 접근 방식은 명시적이고 타입 안전합니다. Deno.createHttpClientproxy 객체와 caCerts, poolMaxIdlePerHost 등의 옵션을 받아 재사용 가능한 클라이언트를 반환합니다.

// deno run --allow-net --allow-env proxy_fetch.ts

const PROXY_URL = "http://gate.proxyhat.com:8080";
const USERNAME = "user-country-US-session-abc123";
const PASSWORD = "your_password";

// 프록시 URL에 인증 정보를 포함하거나 basicAuth 필드를 사용합니다.
const client = Deno.createHttpClient({
  proxy: {
    url: PROXY_URL,
    basicAuth: { username: USERNAME, password: PASSWORD },
  },
  // 커스텀 CA 인증서가 필요한 경우 (예: MITM 프록시 환경)
  // caCerts: [Deno.readTextFileSync("ca.pem")],
  poolMaxIdlePerHost: 10,
  http1: true,
});

try {
  const res = await fetch("https://httpbin.org/ip", { client });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  console.log("프록시 IP:", body.origin);
} catch (err) {
  console.error("요청 실패:", err);
} finally {
  client.close(); // 클라이언트 명시적 종료
}

이 패턴의 장점은 클라이언트 재사용입니다. 동일한 client 인스턴스로 여러 fetch 호출을 수행하면 TCP 연결이 풀링되어 핸드셰이크 오버헤드가 줄어듭니다. 100개의 동시 요청을 보낼 때 클라이언트당 약 200ms의 레이턴시를 절약할 수 있습니다.

지역 타겟팅과 sticky 세션 인코딩

ProxyHat은 사용자 이름 필드에 플래그를 인코딩해 지역과 세션을 제어합니다. 예를 들어 user-country-US-session-abc123은 미국 IP를 할당하고 세션 ID abc123으로 동일 IP를 유지합니다.

// 여러 국가를 순환하며 지역별 IP 확인
const countries = ["US", "DE", "JP", "BR", "GB"];

async function checkCountry(country: string) {
  const client = Deno.createHttpClient({
    proxy: {
      url: "http://gate.proxyhat.com:8080",
      basicAuth: {
        username: `user-country-${country}-session-${country}-001`,
        password: "your_password",
      },
    },
  });
  try {
    const res = await fetch("https://httpbin.org/ip", { client });
    const data = await res.json();
    console.log(`${country}: ${data.origin}`);
  } finally {
    client.close();
  }
}

await Promise.all(countries.map(checkCountry));

Bun에서 fetch proxy 한 줄 적용

Bun은 더 직관적인 API를 제공합니다. fetch의 두 번째 인자에 proxy 문자열을 전달하면 됩니다.

// bun run proxy_fetch.ts

const proxyUrl =
  "http://user-country-DE-session-bun-42:your_password@gate.proxyhat.com:8080";

try {
  const res = await fetch("https://httpbin.org/ip", { proxy: proxyUrl });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  console.log("독일 프록시 IP:", data.origin);
} catch (err) {
  console.error("요청 실패:", err);
}

Bun의 proxy 옵션은 HTTP와 HTTPS 프록시를 모두 지원합니다. SOCKS5는 Bun 1.1.x 이상에서 socks5:// 스킴으로 사용할 수 있으며, ProxyHat의 SOCKS5 포트는 1080입니다.

// Bun에서 SOCKS5 사용
const socksProxy =
  "socks5://user-country-JP-session-sock-1:your_password@gate.proxyhat.com:1080";

const res = await fetch("https://httpbin.org/ip", { proxy: socksProxy });
console.log(await res.json());

HTTP_PROXY / HTTPS_PROXY 환경 변수 경로

두 런타임 모두 환경 변수를 인식합니다. 이 방식은 CLI 도구나 컨테이너 환경에서 편리하지만, 전역 상태가 된다는 점에 주의해야 합니다.

# 환경 변수로 프록시 설정 (Deno와 Bun 모두 인식)
export HTTP_PROXY="http://user-country-US:your_password@gate.proxyhat.com:8080"
export HTTPS_PROXY="http://user-country-US:your_password@gate.proxyhat.com:8080"

# 이후 fetch는 자동으로 프록시를 사용
deno run --allow-net --allow-env script.ts
bun run script.ts

환경 변수 방식은 다음 상황에서 적합합니다:

  • 단일 프록시 설정으로 충분한 경우
  • CI/CD 파이프라인에서 프록시를 주입하는 경우
  • 서드파티 라이브러리가 내부적으로 fetch를 호출하는 경우

하지만 요청마다 다른 국가나 세션을 사용해야 한다면 per-client 구성(Deno)이나 per-request proxy(Bun)가 필요합니다. 환경 변수는 런타임 중에 변경해도 이미 생성된 클라이언트에는 반영되지 않을 수 있습니다.

레지덴셜 프록시로 차단 회피: 동시 요청과 sticky 세션 풀

실제 시나리오를 가정해 봅시다. 이커머스 사이트의 상품 페이지를 50개 동시에 수집해야 하고, 각 요청은 서로 다른 IP에서 나가야 합니다. 이때 sticky 세션 풀을 만들어 Promise.all로 동시 실행하고 AbortController로 타임아웃을 적용합니다.

// deno run --allow-net concurrent_scrape.ts

const TARGETS = Array.from({ length: 50 }, (_, i) =>
  `https://httpbin.org/anything?item=${i}`
);

const PROXY = "http://gate.proxyhat.com:8080";
const PASS = "your_password";

function makeClient(sessionId: string) {
  return Deno.createHttpClient({
    proxy: {
      url: PROXY,
      basicAuth: {
        username: `user-country-US-session-${sessionId}`,
        password: PASS,
      },
    },
    poolMaxIdlePerHost: 5,
  });
}

async function fetchWithTimeout(url: string, sessionId: string, ms = 10000) {
  const client = makeClient(sessionId);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    const res = await fetch(url, { client, signal: controller.signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    if (err.name === "AbortError") {
      console.warn(`타임아웃: ${url}`);
    }
    throw err;
  } finally {
    clearTimeout(timer);
    client.close();
  }
}

// 세션 ID를 무작위로 생성해 각 요청이 다른 IP를 사용하도록 함
const results = await Promise.allSettled(
  TARGETS.map((url, i) =>
    fetchWithTimeout(url, `pool-${i}-${Date.now()}`)
  )
);

const ok = results.filter((r) => r.status === "fulfilled").length;
console.log(`성공: ${ok}/${TARGETS.length}`);

이 패턴의 핵심은 각 요청마다 고유한 세션 ID를 사용해 ProxyHat이 서로 다른 레지덴셜 IP를 할당하도록 하는 것입니다. 세션 ID가 같으면 같은 IP가 유지되고(sticky), 다르면 새 IP가 할당됩니다. Promise.allSettled를 사용하면 일부 요청이 실패해도 전체가 중단되지 않습니다.

차단 위험이 높은 대상에서는 동시성을 제한하는 것이 중요합니다. 50개를 한 번에 보내지 않고 10개씩 배치로 처리하면 차단률을 낮추면서도 처리량을 유지할 수 있습니다.

프로덕션 팁: 재시도, 백오프, 커스텀 CA, 연결 재사용

지수 백오프 재시도

// Deno와 Bun 모두에서 동작하는 재시도 유틸리티

async function fetchWithRetry(
  url: string,
  proxyConfig: unknown,
  opts: { retries?: number; baseDelay?: number } = {}
): Promise<Response> {
  const { retries = 3, baseDelay = 500 } = opts;
  let lastErr: unknown;

  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      // Deno: { client } 사용, Bun: { proxy } 사용
      // 여기서는 Bun 스타일 예시
      const res = await fetch(url, {
        proxy: proxyConfig as string,
        signal: AbortSignal.timeout(15000),
      });

      // 429 또는 5xx는 재시도 대상
      if (res.status === 429 || res.status >= 500) {
        throw new Error(`재시도 가능: HTTP ${res.status}`);
      }
      return res;
    } catch (err) {
      lastErr = err;
      if (attempt < retries) {
        const delay = baseDelay * Math.pow(2, attempt);
        // 지터 추가로 동시 재시도 폭주 방지
        const jitter = Math.random() * 200;
        await new Promise((r) => setTimeout(r, delay + jitter));
        console.warn(
          `재시도 ${attempt + 1}/${retries} — ${delay}ms 후`
        );
      }
    }
  }
  throw lastErr;
}

커스텀 CA 인증서 (Deno)

기업 환경에서 MITM 프록시나 자체 CA를 사용하는 경우, Deno의 caCerts 옵션으로 신뢰할 인증서를 명시적으로 추가할 수 있습니다.

const client = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: { username: "user-country-DE", password: "pass" },
  },
  caCerts: [
    Deno.readTextFileSync("/etc/ssl/certs/company-ca.pem"),
  ],
});

const res = await fetch("https://internal-api.example.com/data", { client });

연결 재사용으로 레이턴시 절감

Deno에서는 동일한 client 인스턴스를 재사용하면 TCP 연결이 풀에 유지됩니다. 100개 요청을 새 클라이언트로 보내면 매번 TLS 핸드셰이크가 발생하지만, 재사용하면 첫 요청 이후 약 200ms씩 절약됩니다.

// 좋은 예: 클라이언트 재사용
const sharedClient = Deno.createHttpClient({
  proxy: {
    url: "http://gate.proxyhat.com:8080",
    basicAuth: { username: "user-country-US", password: "pass" },
  },
  poolMaxIdlePerHost: 20,
});

const urls = [
  "https://httpbin.org/ip",
  "https://httpbin.org/headers",
  "https://httpbin.org/user-agent",
];

const results = await Promise.all(
  urls.map((u) => fetch(u, { client: sharedClient }).then((r) => r.json()))
);
console.log(results);
sharedClient.close();

ProxyHat Node SDK와 병행 사용하기

ProxyHat은 Node.js SDK를 제공하며, 이 SDK는 Deno와 Bun에서도 호환됩니다. ProxyHat 문서에서 최신 SDK 사용법을 확인할 수 있습니다. SDK를 사용하면 인증, 세션 관리, 로테이션 로직을 직접 구현할 필요가 없습니다.

// Node SDK를 Bun에서 사용 (npm 호환)
// bun add @proxyhat/node-sdk

import { ProxyHat } from "@proxyhat/node-sdk";

const ph = new ProxyHat({
  apiKey: process.env.PROXYHAT_API_KEY!,
});

// SDK가 자동으로 세션과 로테이션을 관리
const session = ph.createSession({
  country: "US",
  type: "residential",
});

const proxyUrl = session.getProxyUrl();
// → http://user-country-US-session-xyz:pass@gate.proxyhat.com:8080

// Bun에서 직접 fetch에 적용
const res = await fetch("https://httpbin.org/ip", { proxy: proxyUrl });
console.log(await res.json());

SDK와 원시 프록시 URL의 주요 차이:

특징원시 프록시 URLProxyHat SDK
인증 관리수동 (사용자명에 플래그 인코딩)자동 (API 키 기반)
세션 로테이션개발자가 세션 ID 생성SDK가 자동 관리
지역 타겟팅수동 (user-country-US)파라미터로 전달
재시도/백오프직접 구현 필요내장 옵션
런타임 호환Deno, Bun, Node 모두Deno, Bun, Node 모두
의존성없음SDK 패키지 필요

소규모 스크립트나 빠른 프로토타이핑에서는 원시 URL이 충분하지만, 프로덕션 시스템에서는 SDK가 더 안정적입니다. ProxyHat 요금제지원 위치 목록을 참고해 적절한 플랜을 선택하세요.

윤리적 스크래핑: 공개 데이터만, 공식 API 우선

프록시는 강력한 도구지만, 사용에 책임이 따릅니다. 미국에서는 컴퓨터 사기 및 남용법(CFAA)이 무단 접근을 제한하며, EU에서는 GDPR이 개인정보 보호를 규율합니다. 다음 원칙을 지키세요:

  • 공식 API를 먼저 확인하세요. 많은 플랫폼이 공식 API를 제공하며, 스크래핑보다 안정적이고 법적 위험이 적습니다.
  • 공개 데이터만 수집하세요. 로그인 뒤에 있는 데이터는 ToS 위반일 가능성이 높습니다.
  • robots.txt를 존중하세요. 크롤링 정책을 따르는 것이 장기적으로 더 안전합니다.
  • 요청 속도를 제한하세요. 대상 서버에 부담을 주는 속도는 피해야 합니다.
  • 개인정보를 수집하지 마세요. GDPR과 CCPA 하에서 위험할 수 있습니다.

ProxyHat은 윤리적 사용을 권장하며, 프록시 인프라는 합법적인 데이터 수집 작업을 지원하기 위해 제공됩니다.

핵심 요약

Key Takeaways

  • Deno는 Deno.createHttpClient({ proxy })로 재사용 가능한 클라이언트를 만들고 fetch(url, { client })로 전달합니다.
  • Bun은 fetch(url, { proxy: 'http://...' }) 한 줄로 프록시를 적용합니다.
  • 지역 타겟팅과 sticky 세션은 사용자 이름에 인코딩합니다: user-country-US-session-abc123
  • SOCKS5는 포트 1080을 사용하고, HTTP는 포트 8080을 사용합니다.
  • 동시 요청에는 Promise.allSettledAbortController 타임아웃을 조합하세요.
  • 재시도는 지수 백오프와 지터를 사용하고, 429와 5xx만 재시도 대상으로 삼으세요.
  • 클라이언트 재사용으로 연결 풀을 유지하면 요청당 약 200ms를 절약할 수 있습니다.
  • ProxyHat Node SDK는 Deno와 Bun 모두에서 작동하며, 세션 관리를 자동화합니다.
  • 공식 API를 우선하고, 공개 데이터만 수집하며, CFAA와 GDPR을 준수하세요.

FAQ

Deno와 Bun에서 프록시 fetch란 무엇인가요?

Deno와 Bun의 fetch() 호출에 HTTP 또는 SOCKS5 프록시를 적용해 요청이 프록시 서버를 경유하도록 만드는 기법입니다. Deno는 Deno.createHttpClient({ proxy: { url, basicAuth } })로 클라이언트를 만들어 fetch(url, { client })에 전달하고, Bun은 fetch(url, { proxy: 'http://user:pass@host:port' }) 한 줄로 적용합니다. 덕분에 IP 로테이션, 지역 타겟팅, 차단 회피를 런타임 수준에서 구현할 수 있습니다.

왜 프록시 fetch가 Deno·Bun 사용자에게 중요한가요?

최신 JavaScript 런타임의 기본 fetch()는 WHATWG Fetch 표준을 따르며 프록시 설정을 직접 다루지 않기 때문에, 별도 구성 없이는 모든 요청이 원본 IP로 나갑니다. SERP 스크래핑, 가격 모니터링, 소셜 리서스처럼 차단 위험이 높은 작업에서는 프록시 적용이 필수입니다. Deno와 Bun은 각각의 방식으로 이 문제를 해결하며, 개발자는 런타임 API만으로 IP 로테이션과 지역 타겟팅을 구현할 수 있습니다.

Deno·Bun 프록시에 어떤 프록시 유형이 가장 적합한가요?

차단 회피가 목적이라면 레지덴셜 프록시가 가장 안전합니다. 실제 ISP IP를 사용하므로 데이터센터 대역보다 훨씬 낮은 차단률을 보입니다. 모바일 프록시는 더 강력하지만 비용이 높고 속도가 느릴 수 있으며, 데이터센터 프록시는 속도가 빠르지만 Cloudflare나 Akamai 같은 WAF에 쉽게 차단됩니다. ProxyHat은 세 가지 유형을 모두 제공하므로 작업별로 선택할 수 있습니다.

Deno·Bun 프록시 사용 시 차단을 어떻게 피하나요?

세 가지를 조합하세요. 첫째, 요청마다 다른 sticky 세션을 사용해 IP를 로테이션합니다. 둘째, 지역 타겟팅(user-country-US)으로 타겟 지역에 맞는 IP를 사용합니다. 셋째, 재시도와 지수 백오프, AbortController 타임아웃을 적용해 429와 503을 우아하게 처리합니다. 또한 User-Agent와 Accept-Language 헤더를 일관되게 유지하고, robots.txt와 대상 서비스 ToS를 준수해야 합니다.

시작할 준비가 되셨나요?

AI 필터링으로 148개국 이상에서 5천만 개 이상의 레지덴셜 IP에 액세스하세요.

가격 보기레지덴셜 프록시
← 블로그로 돌아가기