Playwright에서 파싱 스크립트를 실행하거나 멀티 계정을 사용하는 경우 각 프록시마다 별도의 브라우저 인스턴스를 실행하면 메모리와 CPU를 낭비하게 됩니다. Playwright는 전체 브라우저가 아닌 BrowserContext 수준에서 프록시를 지정할 수 있으며, 이는 스크립트의 아키텍처를 근본적으로 변화시킵니다: 하나의 브라우저 프로세스, 서로 다른 IP, 쿠키 및 지문을 가진 수십 개의 격리된 컨텍스트. 이 기사에서는 이것이 어떻게 작동하는지, Python 및 Node.js 코드, 그리고 거주형 및 모바일 프록시를 사용할 때 특히 중요한 이유를 설명합니다.
문제: browser.launch()에서 프록시 사용 — 비용이 많이 들고 불편함
많은 Playwright 스크립트의 전통적인 접근 방식은 다음과 같습니다: 각 프록시마다 chromium.launch({ proxy: {...} })를 호출하여 별도의 브라우저 프로세스를 생성하고, 이 프로세스가 작동한 후 닫히고, 새 프록시로 다음 프로세스가 시작됩니다. Wildberries 또는 Ozon을 위한 50개의 IP 주소를 동시에 처리해야 한다면, 이는 50개의 완전한 Chromium 프로세스를 동시에 실행해야 함을 의미합니다 — 각 프로세스는 시작할 때만 150MB에서 300MB의 RAM을 소모하며, 탭 및 렌더링은 제외됩니다.
실제로 이는 일반 VPS에서 8GB 메모리로 15-20개의 병렬 브라우저에서 한계에 부딪힙니다. 서버는 스왑을 시작하고, 스크립트는 타임아웃으로 실패하며, 일부 프록시는 작업을 완료하지 못합니다. 문제는 프록시나 Playwright에 있는 것이 아닙니다 — 문제는 아키텍처에 있습니다: 각 브라우저의 시작은 전체 렌더링 엔진, GPU 프로세스, 네트워크 및 수십 개의 보조 스레드를 끌어오지만, 실제로 필요한 것은 단지 새로운 IP와 깨끗한 세션입니다.
Playwright는 이 문제를 우아하게 해결합니다: 프록시는 브라우저를 시작할 때가 아니라 BrowserContext를 생성할 때 지정할 수 있습니다 — 동일한 브라우저 프로세스 내에서의 가벼운 격리된 "샌드박스". 하나의 Chromium 프로세스는 각기 다른 프록시, 쿠키 및 localStorage를 가진 수십 개의 컨텍스트를 유지할 수 있으며 서로 간섭하지 않습니다.
브라우저 vs BrowserContext: 차이점
Browser는 엔진 프로세스(Chromium, Firefox 또는 WebKit) 자체로, 한 번 실행되고 오랫동안 유지됩니다. BrowserContext는 이 프로세스 내의 격리된 환경으로, 개인 창(incognito)과 유사하며, 고유한 쿠키, 캐시, 권한 및 중요한 네트워크 설정(프록시 포함)을 가집니다.
Dolphin Anty 또는 AdsPower와 같은 안티탐지 브라우저와 비교하면, 각 "프로필"도 본질적으로 쿠키와 지문을 가진 별도의 격리된 컨테이너입니다. Playwright는 프로그래밍적으로 유사한 논리를 구축할 수 있게 해줍니다: 하나의 브라우저 — 여러 개의 컨텍스트, 각 컨텍스트는 고유한 IP를 가진 별도의 "프로필"처럼 작동합니다.
| 매개변수 | 브라우저에 대한 프록시 | BrowserContext에 대한 프록시 |
|---|---|---|
| 하나의 IP에 대한 메모리 | 150-300MB (새 프로세스) | 15-40MB (새 컨텍스트) |
| IP 변경 속도 | 2-5초 (브라우저 재시작) | 0.1-0.3초 (새 컨텍스트) |
| 쿠키 격리 | 완전한 격리 (다른 프로세스) | 완전한 격리 (컨텍스트는 쿠키를 공유하지 않음) |
| 8GB RAM에서 최대 병렬 세션 수 | ~20-25 | ~150-200 |
컨텍스트에 대한 프록시: Python 코드
아래는 기본 패턴입니다: 하나의 브라우저가 한 번 실행되고, 각 프록시마다 별도의 컨텍스트가 생성됩니다. 이는 Playwright의 동기 API이지만, 논리는 비동기 버전에서도 동일합니다.
from playwright.sync_api import sync_playwright
proxies = [
{"server": "http://proxy1.example.com:8000", "username": "user1", "password": "pass1"},
{"server": "http://proxy2.example.com:8000", "username": "user2", "password": "pass2"},
{"server": "http://proxy3.example.com:8000", "username": "user3", "password": "pass3"},
]
with sync_playwright() as p:
# 브라우저를 한 번만 시작하고, 프록시 없이
browser = p.chromium.launch(headless=True)
for proxy in proxies:
# 프록시는 브라우저가 아닌 컨텍스트에 지정됩니다
context = browser.new_context(
proxy={
"server": proxy["server"],
"username": proxy["username"],
"password": proxy["password"],
},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
)
page = context.new_page()
page.goto("https://httpbin.org/ip", timeout=30000)
print(page.content())
page.close()
context.close() # 컨텍스트만 닫고, 브라우저는 계속 실행됨
browser.close()
핵심 포인트는 browser.launch()가 루프 외부에서 한 번 호출되고, browser.new_context()가 프록시를 위한 루프 내부에서 호출된다는 것입니다. 로그인 및 비밀번호로 인증된 프록시의 경우, Playwright는 네트워크 수준에서 Basic Auth 헤더를 자동으로 추가하므로 추가 설정이 필요 없습니다.
컨텍스트에 대한 프록시: Node.js 코드
JavaScript에서 async/await를 사용하는 동일한 논리입니다 — Node.js의 파싱 프레임워크와 함께 자주 사용됩니다:
const { chromium } = require('playwright');
const proxies = [
{ server: 'http://proxy1.example.com:8000', username: 'user1', password: 'pass1' },
{ server: 'http://proxy2.example.com:8000', username: 'user2', password: 'pass2' },
];
(async () => {
const browser = await chromium.launch({ headless: true });
for (const proxy of proxies) {
const context = await browser.newContext({
proxy: {
server: proxy.server,
username: proxy.username,
password: proxy.password,
},
});
const page = await context.newPage();
await page.goto('https://httpbin.org/ip', { timeout: 30000 });
console.log(await page.content());
await page.close();
await context.close();
}
await browser.close();
})();
수십 개의 프록시를 병렬로 처리하기 위해서는 Promise.all을 사용하여 이전 것을 기다리지 않고 컨텍스트를 생성할 수 있습니다 — 이것이 "하나의 브라우저, 많은 컨텍스트" 접근 방식의 주요 이점입니다.
브라우저를 재시작하지 않고 프록시를 깨끗하게 회전하기
"깨끗한 회전"은 IP 변경 시 이전 세션의 흔적이 남지 않음을 의미합니다 — 쿠키, 캐시, 서비스 워커가 없습니다. 전체 브라우저를 재시작할 때는 자동으로 보장되지만, 자원 소모가 큽니다. 컨텍스트 작업 시에는 이전 컨텍스트를 닫고 새 컨텍스트를 생성하여 동일한 결과를 얻을 수 있으며, 이 작업은 몇 초의 시간만 소요됩니다.
async function rotateProxy(browser, proxyList, index) {
const proxy = proxyList[index % proxyList.length];
const context = await browser.newContext({ proxy });
const page = await context.newPage();
return { context, page };
}
// 이렇게 사용합니다: N번째 반복마다 IP를 변경합니다
for (let i = 0; i < 100; i++) {
if (i % 10 === 0) {
const { context, page } = await rotateProxy(browser, proxies, i);
// 새로운 세션으로 10개의 요청을 처리합니다
}
}
이 패턴은 마켓플레이스 파싱에 특히 유용합니다: 예를 들어, Wildberries 또는 Ozon에서 가격을 수집할 때, 봇 방지 시스템은 단일 IP에서의 요청 수를 시간 단위로 추적합니다. 컨텍스트 수준에서의 회전은 IP를 5-10개의 요청마다 변경할 수 있게 하여 브라우저 재시작으로 인한 성능 손실 없이 작업할 수 있습니다.
이러한 시나리오에는 거주형 프록시가 적합합니다 — 이들은 실제 사용자의 IP를 제공하여 마켓플레이스에서 의심스럽다고 표시되는 경우가 적습니다, 명백한 서버 범위와는 달리.
메모리 및 속도 비교
8GB RAM 및 4 vCPU를 가진 테스트 VPS에서 100페이지를 다양한 IP로 우회하는 작업에 대해 두 가지 접근 방식을 검토했습니다. 첫 번째 경우는 각 IP마다 별도의 chromium.launch()를 실행하고, 두 번째 경우는 하나의 브라우저에서 100개의 컨텍스트를 순차적으로 생성합니다.
| 메트릭 | 100번의 browser.launch() 실행 | 1 브라우저에서 100개의 컨텍스트 |
|---|---|---|
| 최대 메모리 | ~6.8GB | ~1.9GB |
| 총 실행 시간 | 182초 | 97초 |
| 타임아웃으로 실패한 세션 수 | 7 | 0 |
이 테스트에서 메모리 절약은 약 70%였으며, 전체 실행 시간은 Chromium 프로세스를 매번 시작하고 종료하는 오버헤드가 없어서 거의 절반으로 줄어들었습니다. 대량의 작업 — 하루에 수천 개의 IP —에서는 차이가 더욱 두드러지며, 이는 서버에서 하드웨어 업그레이드 없이 사용할 수 있는 프록시 수에 직접적인 영향을 미칩니다.
실용적인 사례: 파싱 및 멀티 계정
마켓플레이스 파싱. Wildberries, Ozon 또는 Avito에서 가격을 수집할 때, 동일한 브라우저는 고유한 IP와 쿠키 저장소를 가진 수십 개의 컨텍스트를 열어 둡니다. 이는 요청 패턴으로 인한 차단 위험을 줄이고, IP 간의 프로세스 재시작을 기다리지 않고 데이터를 더 빠르게 수집할 수 있게 합니다.
멀티 계정 및 계정 농사. Facebook Ads 또는 TikTok Ads에서 광고 캠페인을 테스트하기 위해 Playwright 스크립트를 통해 각 계정을 고유한 프록시와 사용자 에이전트를 가진 별도의 컨텍스트에 유지하는 것이 논리적입니다 — 이는 Multilogin 또는 GoLogin과 같은 안티탐지 브라우저의 행동을 모방하며, 안티탐지 라이센스 없이 완전히 프로그래밍적으로 이루어집니다.
광고의 지리적 테스트. 다양한 국가에서 크리에이티브가 어떻게 표시되는지 확인해야 하는 마케팅 담당자에게는, 다양한 프록시를 가진 컨텍스트가 하나의 브라우저 세션에서 필요한 지역을 순차적으로 "방문"할 수 있게 해 주며, 수동으로 여러 개의 개별 창을 열 필요가 없습니다.
모바일 트래픽을 모방하는 것이 중요한 시나리오 — 예를 들어, 웹사이트의 모바일 버전이나 TikTok Ads의 광고를 확인하는 경우 — 모바일 프록시를 고려해야 합니다. 이들은 실제 이동통신사로부터 IP를 제공하며, 안티프로드 시스템에서 의심을 덜 받습니다.
반면, 높은 익명성이 필요 없는 빠르고 안정적인 파싱이 목표라면 (예: 엄격한 안티봇 보호가 없는 웹사이트에서 공개 데이터를 수집하는 경우), 데이터 센터 프록시가 대량 요청에서 속도와 비용의 최적 비율을 제공합니다.
자주 발생하는 오류 및 피하는 방법
1. launch()에서 프록시를 지정하고 new_context()에서 지정하지 않음. 이는 초보자들이 가장 자주 범하는 실수입니다 — 브라우저 시작 시 지정된 프록시는 모든 컨텍스트에 동시에 적용되며 개별적으로 재정의할 수 없습니다. IP에 대한 유연성이 필요하다면 항상 new_context()에 프록시를 전달하세요.
2. 이전 컨텍스트를 닫지 않음. 수백 개의 컨텍스트를 생성하고 context.close()를 호출하지 않으면, 브라우저의 메모리는 선형적으로 증가하며 프로세스가 OOM으로 실패할 때까지 계속됩니다. 해당 IP에 대한 작업이 완료된 후 항상 컨텍스트를 닫으세요.
3. 모든 컨텍스트에 동일한 사용자 에이전트 사용. 프록시가 변경되더라도 브라우저의 지문이 동일하게 유지된다면 (사용자 에이전트, 언어, 시간대), 마켓플레이스 및 소셜 미디어의 안티봇 시스템은 여전히 세션을 서로 연결할 수 있습니다. 각 컨텍스트에 대해 프록시와 함께 사용자 에이전트 및 뷰포트를 변경하세요.
4. 프록시의 타임아웃을 무시함. 거주형 및 모바일 프록시는 실제 네트워크 인프라로 인해 데이터 센터 프록시보다 응답이 느릴 수 있습니다. page.goto()를 호출할 때 합리적인 타임아웃(20-40초)을 설정하세요, 그렇지 않으면 스크립트가 작동하는 프록시를 "죽은" 것으로 간주할 수 있습니다.
5. 하나의 브라우저에서 너무 많은 컨텍스트를 동시에 사용. 메모리 절약에도 불구하고, 500개 이상의 활성 컨텍스트를 무제한으로 생성하면 프로세스의 한계에 부딪힙니다. 합리적인 균형은 서버의 RAM 용량에 따라 하나의 브라우저 프로세스에서 30-80개의 활성 컨텍스트입니다.
결론
전체 브라우저가 아닌 BrowserContext 수준에서 프록시를 지정하는 것은 최대 70%의 메모리를 절약하고, IP 회전을 수십 배 빠르게 하며, 세션 간 쿠키의 깨끗한 격리를 제공하여 Chromium을 지속적으로 재시작할 필요가 없습니다. 마켓플레이스 파싱, 다양한 지역에서의 광고 테스트 및 멀티 계정 작업을 위해 이는 동일한 서버에서 병렬 세션 수를 증가시키는 직접적인 방법입니다.
Wildberries, Ozon에서 데이터를 수집하거나 Facebook Ads 및 TikTok Ads 광고 계정 작업을 위한 Playwright 스크립트를 구축하는 경우, 프록시 풀의 품질은 코드 아키텍처만큼이나 중요합니다. IP의 신뢰성과 안티봇 시스템에 대한 저항력이 중요한 작업의 경우, 거주형 프록시로 시작하는 것이 좋습니다 — 이는 위에서 설명한 "하나의 브라우저, 많은 컨텍스트" 접근 방식과 잘 결합됩니다.