Node에서 프록시를 통해 요청 하나를 통과시키는 것은 열 줄짜리 문제다. 모든 튜토리얼은 거기서 끝나지만, 실제로는 프로젝트가 커진다. 두 번째 HTTP 클라이언트가 등장하고, 자격 증명이 세 개의 파일에 하드코딩되고, 누군가 미국 대신 독일이 필요하다고 하고, 요청이 실제로 어디에서 나가고 있는지 아무도 답하지 못하게 된다.
이 글은 문법 가이드가 아니라 설정 가이드다. 각 클라이언트가 요구하는 정확한 에이전트 설정, 예를 들어 Axios가 에이전트와 함께 proxy: false를 필요로 하는 이유 같은 내용이 필요하다면 Node.js에서 Axios와 Got으로 레지덴셜 프록시 사용하기에서 자세히 다룬다. 여기서 다루는 것은 프로젝트가 커져도 연결 구성이 올바르게 유지되도록 프로젝트를 정리하는 방법이다.
실제로 지원할 클라이언트를 정하라
Node에는 흔히 쓰이는 HTTP 클라이언트가 세 가지 있고, 각각 다른 방식으로 프록시를 받아들인다. 어느 정도 된 코드베이스에서 셋 다 지원하는 것은 흔한 일이지만, 그것은 우연이 아니라 결정이어야 한다.
Axios는 명시적인 에이전트가 필요한데, 자체 proxy 옵션이 사람들이 기대하는 방식으로 HTTPS 터널링을 처리하지 않기 때문이다. https-proxy-agent를 설치해야 한다.
Got은 최상위 옵션이 아니라 agent.https 슬롯으로 에이전트를 받는다.
네이티브 fetch는 undici를 통하므로, 디스패처로 ProxyAgent가 필요하다. undici는 Node에 기본 포함되어 있지만, 직접 디스패처를 구성한다면 명시적인 의존성으로 두는 편이 좋다.
실질적인 결과는 의존성 목록이 유지하는 클라이언트에 따라 결정된다는 것이며, 클라이언트 두 개를 유지한다는 것은 프록시 경로 두 개를 유지한다는 뜻이다. 대부분의 코드베이스는 하나로 표준화하고 나머지를 전환하는 편이 나으며, 정말로 두 개가 필요한 곳이라면 최소한 그 이유를 알고 있어야 한다.
자격 증명은 소스가 아니라 환경에 있어야 한다
게이트웨이는 사용자명을 통해 타겟팅을 받아들이는데, 이는 자격 증명 문자열과 라우팅 설정이 같은 문자열이라는 뜻이다. 이는 편리하지만, 동시에 프록시 자격 증명이 커밋되는 방식이기도 하다.
설정에서 이 요소들을 분리해서 유지하라.
PROXY_HOST=p.shifter.ioPROXY_PORT=443PROXY_USER=customer-USERNAMEPROXY_PASS=your-passwordPROXY_COUNTRY=us그런 다음 런타임에 사용자명을 조합한다. 나중에 도움이 되는 규칙은 프록시 모듈 외부의 어떤 파일도 프록시 URL 리터럴을 포함해서는 안 된다는 것이다. 타겟팅이 내장된 자격 증명 문자열이 한 번이라도 스크레이퍼 안에 인라인으로 등장하면, 그것은 복사되고, 복사본들은 서로 어긋나기 시작한다.
로그로 남기는 URL에 비밀번호를 넣지 마라. 프록시 URL은 자격 증명이 로그 수집기에 도달하는 가장 흔한 경로인데, 자연스러운 디버그 로그 줄이 URL 전체이기 때문이다.
모듈 하나가 에이전트를 만든다
가장 중요한 구조적 결정은 설정을 에이전트로 변환하는 곳이 정확히 한 곳만 있어야 한다는 것이다. 이는 작은 모듈이지만 광범위한 문제를 예방한다.
import { HttpsProxyAgent } from "https-proxy-agent";
const { PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS } = process.env;
export function proxyUrl({ country, session, ttl } = {}) { const parts = [PROXY_USER]; if (country) parts.push("country", country); if (session) parts.push("sid", session); if (session && ttl) parts.push("ttl", String(ttl)); const user = parts.join("-"); return `https://${user}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`;}
export function agentFor(opts) { return new HttpsProxyAgent(proxyUrl(opts));}이 코드에서 명시적으로 언급할 만한 두 가지 세부사항이 있다. 타겟팅 플래그는 각 호출 지점에서 문자열을 이어 붙이는 대신 이름 있는 옵션들로부터 만들어지는데, 이것이 플래그 이름의 오타가 자격 증명 문제처럼 보이는 407 오류가 되는 것을 막아준다. 그리고 ttl은 세션이 있을 때만 추가되는데, 유효 시간(time to live)은 세션 식별자 없이는 유지할 대상이 없기 때문이다.
이후 호출하는 쪽에서는 URL을 전혀 보지 않는다.
const agent = agentFor({ country: "de", session: "job-141", ttl: 600 });
// axiosawait axios.get(url, { httpsAgent: agent, proxy: false });
// gotawait got(url, { agent: { https: agent } });네이티브 fetch는 에이전트를 받지 않는 유일한 경우인데, undici는 대신 디스패처를 원하기 때문이다.
import { ProxyAgent, fetch } from "undici";import { proxyUrl } from "./proxy.js";
const dispatcher = new ProxyAgent(proxyUrl({ country: "de" }));await fetch(url, { dispatcher });proxyUrl을 별도로 내보내는 것이 문자열 조합을 중복하지 않고 fetch 경로가 다른 둘과 설정을 공유하게 해주는 요소다.
에이전트를 재사용하고, 세션은 미리 결정하라
에이전트는 연결 풀을 유지한다. 요청마다 에이전트를 새로 만들면 매번 그 풀을 버리는 것이며, 이는 네트워크 문제로 오진하기 쉬운 지연 시간으로 나타난다.
사용하는 설정마다 에이전트를 한 번씩 만들어 캐시하라. 다섯 개 국가를 로테이션한다면, 요청마다 하나가 아니라 프로세스 수명 동안 다섯 개의 에이전트를 유지하는 것이다.
세션은 관련된 결정이다. 세션 식별자가 없으면 게이트웨이는 로테이션하는데, 이는 독립적인 요청들에 원하는 동작이다. sid를 사용하면 지정한 ttl 동안 유지되는 고정 IP를 얻는데, 이는 연속성이 있는 작업, 즉 페이지네이션된 결과 집합, 다단계 흐름, 두 번째 요청이 첫 번째 요청과 같은 곳에서 나가야 하는 모든 경우에 원하는 동작이다. 별도로 설정하지 않으면 기본 고정 시간은 120초다.
호출 지점마다가 아니라 설정 시점에 이를 선택하는 것이 일관된 로테이션 정책과, 요청의 절반이 우연히 고정되어 있는 코드베이스의 차이를 만든다. 이 트레이드오프는 고정 프록시 대 로테이팅 프록시에 정리되어 있다.
구축하기 전에 검증하라
스크레이퍼보다 검증 스크립트를 먼저 작성하라. 5분이면 되고, 향후 발생할 혼란의 전체 범주를 즉각적인 답으로 바꿔준다.
import { agentFor } from "./proxy.js";import axios from "axios";
const agent = agentFor({ country: "de" });const { data } = await axios.get("https://api.ipify.org?format=json", { httpsAgent: agent, proxy: false,});console.log(data);이것이 자신의 주소를 출력한다면, 에이전트가 연결되지 않은 것이고 프로젝트의 모든 요청이 직접 나가고 있는 것이다. 이는 실제로 며칠 동안 지속될 수 있는 흔한 상태인데, 아무것도 실패하지 않기 때문이다. 코드는 작동하지만 그저 프록시를 사용하지 않고 있을 뿐이다. 국가가 실제로 요청한 것과 일치하는지 확인하는 것이 점검의 나머지 절반이며, 이를 제대로 수행하는 방법은 프록시 속도, 성공률, 위치 정확도 테스트에 있다.
무언가 이상해 보일 때 누구나 실행할 수 있도록 package.json에 스크립트로 등록하라.
오류는 한 번만 매핑하라
프록시 오류는 자동으로 진단할 수 있을 만큼 구체적이며, 이를 한 곳에서 처리하는 것이 새벽 세 시에 매번 반복해서 해석하는 것보다 낫다.
- 407은 자격 증명이 잘못되었거나 타겟팅 플래그의 형식이 잘못되었다는 뜻이다. 오타가 있는 플래그가 여기 걸리므로, 비밀번호보다 플래그 이름을 먼저 확인할 가치가 있다.
- 502는 필터에 맞는 출구가 없다는 뜻이다. 네트워크가 다운된 것이 아니라 필터가 너무 좁은 것이다.
- 509는 대역폭 할당량이 소진되었다는 뜻이다.
- **연결 거부(Connection refused)**는 대개 오래된 설정에서 남은 레거시 호스트나 포트를 의미한다.
일시적인 실패만 재시도할 가치가 있다. 407을 반복 재시도하는 것은 결코 저절로 해결되지 않을 문제에 대해 요청 한도(rate limit)를 소모하는 것이며, 509를 재시도하는 것은 아무 효과가 없다. 고정 지연이 아니라 실제 백오프로 재시도를 감싸라. 이는 요청 한도 제한과 스로틀링에서 다룬다.
동시성을 의도적으로 제한하라
Node는 만 개의 요청을 기꺼이 시작할 것이고, 세 클라이언트 중 어느 것도 이를 막지 않는다. 프록시 아래에서는 이것이 평소보다 더 나쁜데, 그 요청 하나하나가 새로 수립해야 하는 터널이기 때문이다.
첫 사고가 발생한 후 추가하는 대신 처음부터 동시성 제한기를 사용하라. 방어 체계를 촉발한 뒤 재시도에 시간을 쓰는 제한 없는 폭주보다, 꾸준한 처리량을 가진 적당한 제한이 더 나은 성과를 낸다.
개발, CI, 프로덕션은 다르다
프록시가 도입된 이후 모든 Node 프로젝트에서 나오는 실질적인 세 가지 참고 사항이다.
레지덴셜 트래픽은 대역폭 기준으로 과금되므로, 프록시를 통해 실제 대상에 접속하는 테스트 스위트는 아무 이득 없이 반복되는 청구서일 뿐이다. 단위 테스트에서는 HTTP 계층을 모킹하고, 실제 요청은 별도의 수동으로 실행하는 점검에 소수만 남겨두라.
로컬 개발은 프로덕션과 같은 모듈, 같은 환경 변수를 사용하되 값만 다르게 해야 한다. 프로덕션에만 존재하는 설정은 아무도 테스트하지 않은 설정이다.
그리고 컨테이너 이미지에 자격 증명을 굽지 마라. 다른 비밀 값과 마찬가지로 런타임에 전달하라.
FAQ
fetch만 사용한다면 https-proxy-agent가 필요한가?
아니다. undici의 ProxyAgent가 그 경로를 처리한다. Axios와 Got에는 별도의 에이전트 패키지가 필요하다.
이미 에이전트를 주었는데 왜 Axios는 proxy: false가 필요한가?
그렇지 않으면 Axios가 에이전트 위에 자체 프록시 처리를 적용하려고 시도하는데, 둘은 함께 작동하지 않기 때문이다. 이를 false로 설정하면 작업이 온전히 에이전트에게 넘어간다.
요청마다 타겟팅을 설정할 수 있나, 아니면 에이전트마다 해야 하나?
가능하지만, 각기 다른 설정은 각기 다른 에이전트이므로 요청마다 하나씩 만드는 것은 연결 풀을 희생시킨다. 설정 키로 캐시하라.
국가는 설정 값이어야 하나, 아니면 호출마다 넘기는 인자여야 하나?
실제로는 둘 다다. 설정에 기본값을 두고, 특정 지역이 필요한 작업에 대해서는 호출마다 재정의할 수 있게 하라. 피해야 할 것은 국가가 개별 스크레이퍼 안에 리터럴로 등장하는 것이다.
결론
Node의 프록시 연결 구성 자체는 작고 잘 알려져 있다. 프로젝트가 유지보수 가능하게 남을지를 결정하는 것은 그 주변의 구성이다. 설정으로부터 에이전트를 만드는 모듈 하나, 환경에 존재하는 자격 증명, 재구축이 아니라 재사용되는 에이전트, 의도적으로 선택된 세션 정책, 스크레이퍼보다 먼저 존재하는 검증 스크립트, 그리고 너무 좁은 필터와 잘못된 비밀번호의 차이를 아는 오류 처리.
이것을 한 번 제대로 설정해두면 클라이언트 라이브러리나 새 지역을 추가하는 것은 설정 변경에 불과하다. 이를 건너뛰면 새로운 요구사항 하나하나가 코드베이스 안에서 하드코딩된 문자열을 찾는 일이 된다. 게이트웨이 세부사항은 레지덴셜 프록시 페이지에, 대역폭 요금은 가격 페이지에 있다.