nodriver로 스크래핑하기는 더 이상 Selenium WebDriver에 의존하지 않고, Chrome DevTools Protocol(CDP)을 직접 WebSocket으로 통신하는 차세대 Python 브라우저 자동화 방식입니다. undetected-chromedriver(uc)의 작성자가 새로 만든 nodriver는 navigator.webdriver 플래그와 CDP 런타임 누수를 원천적으로 제거하여 Cloudflare, Imperva, DataDome 같은 봇 탐지 시스템을 우회하는 데 훨씬 유리합니다.
⚠️ 법적 고지: 이 글은 공개 데이터에 대한 승인된 자동화만을 다룹니다. 미국 CFAA(Computer Fraud and Abuse Act)와 EU GDPR은 무단 데이터 수집에 민사·형사적 책임을 부과할 수 있습니다. 대상 사이트의 robots.txt와 이용약관(ToS)을 반드시 확인하고, 인증이 필요한 페이지는 사전 허가를 받으십시오.
nodriver의 아키텍처: 왜 WebDriver를 버렸는가
기존 Selenium 기반 스크래핑은 ChromeDriver라는 중간 프로세스를 거칩니다. ChromeDriver는 WebDriver BiDi 프로토콜을 사용하여 브라우저를 제어하는데, 이 과정에서 브라우저에 navigator.webdriver = true라는 자바스크립트 플래그가 설정됩니다. Cloudflare의 Turnstile이나 Imperva의 봇 탐지 엔진은 이 플래그를 즉시 감지하여 요청을 차단합니다.
nodriver는 이 중간 계층을 완전히 제거합니다. Chrome DevTools Protocol을 통해 브라우저의 디버그 포트(기본 9222)에 직접 WebSocket 연결을 맺고, Runtime, Page, Network 도메인 명령을 전송합니다. ChromeDriver 프로세스가 없으므로 navigator.webdriver가 undefined로 남고, window.cdc_ 같은 ChromeDriver 주입 변수도 나타나지 않습니다.
이러한 설계 철학은 nodriver의 PyPI 패키지 설명에서도 명시적으로 강조됩니다: "Selenium/WebDriver를 사용하지 않으며, chromedriver 바이너리가 필요 없다"는 점이 핵심 차별점입니다.
CDP 직접 통신의 장점
- 제로 오버헤드: ChromeDriver 프로세스(약 30-50MB 메모리)가 없어 리소스 소비가 감소합니다.
- 스텔스: WebDriver 흔적(
navigator.webdriver,cdc_변수,--enable-automation플래그)이 자동으로 제거됩니다. - 비동기 네이티브: asyncio 기반이라 여러 탭을 동시에 제어할 때 스레드 오버헤드가 없습니다.
- 이벤트 기반:
tab.add_handler(CDP_EVENT_TYPE, callback)로 DOM 이벤트와 네트워크 이벤트를 리스닝합니다.
nodriver의 비동기 API: uc.start()부터 이벤트 훅까지
nodriver의 API는 undetected-chromedriver와 유사하지만 완전히 async/await 기반입니다. 핵심 객체는 Browser와 Tab이며, 모든 상호작용은 코루틴으로 이루어집니다.
기본 구조
import nodriver as uc
import asyncio
async def main():
browser = await uc.start(
headless=False,
browser_args=["--disable-blink-features=AutomationControlled"],
)
tab = await browser.get("https://example.com")
# 요소 선택 — WebDriver의 find_element 대신
element = await tab.select("#content", timeout=10)
if element:
text = await element.get_text()
print(text)
await browser.stop()
asyncio.run(main())
uc.start()는 Chrome을 시작하고 첫 번째 탭을 반환합니다. browser.get(url)은 새 탭을 열고 Tab 객체를 반환합니다. tab.select()는 CSS 셀렉터로 요소를 찾으며, tab.find()는 더 유연한 검색(텍스트, 속성 등)을 지원합니다.
WebDriver waits 대신 이벤트 훅
Selenium에서는 WebDriverWait(driver, 10).until(EC.presence_of_element_located(...))를 썼지만, nodriver에서는 이벤트 핸들러나 tab.wait_for()를 사용합니다.
async def on_request(event):
req = event.body
if req.get("method") == "GET" and "/api/data" in req.get("url", ""):
print(f"인터셉트: {req['url']}")
tab.add_handler(uc.cdp.network.RequestWillBeSent, on_request)
await tab.get("https://target-site.com")
await asyncio.sleep(5) # 이벤트 수신 대기
이 방식은 폴링 기반 대기보다 효율적이며, 네트워크 응답을 가로채서 JSON API 응답을 직접 추출하는 데도 사용할 수 있습니다.
nodriver에 프록시 연동하기: browser_args와 ProxyHat
nodriver에는 내장된 프록시 회전 기능이 없습니다. 대신 Chrome의 --proxy-server= 커맨드라인 인수를 browser_args에 전달하여 프록시를 설정합니다. 하지만 Chrome의 --proxy-server는 인증을 지원하지 않습니다 — 사용자명/비밀번호가 필요한 프록시는 별도의 인증 헤더 주입이나 확장 프로그램이 필요합니다.
ProxyHat의 residential 프록시는 게이트웨이 기반 인증을 사용하므로, --proxy-server와 함께 인증 정보를 URL에 포함할 수 없습니다. 대신 Chrome 확장 기반 프록시 인증 또는 Page.authenticate CDP 명령을 사용합니다. nodriver에서는 CDP의 Fetch.authRequired 이벤트를 처리하는 것이 가장 깔끔한 방법입니다.
ProxyHat residential 프록시 URL 구성
def build_proxyhat_url(country="US", session="abc123"):
"""ProxyHat residential HTTP 프록시 URL 생성"""
username = f"user-country-{country}-session-{session}"
password = "YOUR_PASSWORD"
return f"http://{username}:{password}@gate.proxyhat.com:8080"
# SOCKS5를 사용하는 경우:
# socks5://user-country-US-session-abc123:pass@gate.proxyhat.com:1080
ProxyHat 게이트웨이는 gate.proxyhat.com:8080(HTTP) 또는 gate.proxyhat.com:1080(SOCKS5)에서 수신합니다. 사용자명에 country-{국가코드}와 session-{세션ID} 플래그를 포함하면 지정된 국가의 residential IP가 할당되며, 동일한 세션 ID를 사용하면 sticky session이 유지됩니다. 자세한 위치 옵션은 ProxyHat 위치 페이지를 참조하세요.
왜 residential IP가 필수인가
datacenter IP 대역(예: AWS, DigitalOcean, Hetzner)은 Cloudflare와 Imperva의 IP 평판 데이터베이스에서 거의 항상 플래그됩니다. nodriver로 브라우저를 완벽하게 스텔스 모드로 실행해도, 요청이 datacenter IP에서 오면 차단됩니다. residential IP는 실제 ISP에서 할당된 주소이므로 봇 탐지 엔진이 정상 사용자 트래픽과 구별하기 어렵습니다.
실행 가능한 예제: 보호된 페이지에서 JSON 추출
다음은 nodriver를 ProxyHat residential 프록시와 함께 사용하여 Cloudflare로 보호된 페이지에서 API 응답 JSON을 가로채는 완전한 예제입니다.
import nodriver as uc
import asyncio
import json
def build_proxyhat_url(country="US", session="abc123"):
username = f"user-country-{country}-session-{session}"
password = "YOUR_PASSWORD"
# Chrome --proxy-server에는 인증 URL을 직접 넣을 수 없으므로
# 호스트:포트만 전달하고, CDP Fetch.authRequired로 인증
return "gate.proxyhat.com:8080"
async def main():
proxy_host = build_proxyhat_url(country="US", session="abc123")
proxy_user = "user-country-US-session-abc123"
proxy_pass = "YOUR_PASSWORD"
browser = await uc.start(
headless=False,
browser_args=[
f"--proxy-server=http://{proxy_host}",
"--disable-blink-features=AutomationControlled",
"--no-first-run",
"--no-default-browser-check",
],
)
# 프록시 인증을 위한 Fetch 도메인 활성화
tab = await browser.get("about:blank")
await tab.send(uc.cdp.fetch.enable(
handle_auth_requests=True,
patterns=[{"urlPattern": "*"}],
))
captured_data = []
async def on_auth_required(event):
await tab.send(uc.cdp.fetch.continue_with_auth(
request_id=event.request_id,
auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
response="ProvideCredentials",
username=proxy_user,
password=proxy_pass,
),
))
async def on_response(event):
url = event.response.url
if "/api/products" in url and event.response.status == 200:
body = await tab.send(uc.cdp.network.get_response_body(
request_id=event.request_id
))
captured_data.append(json.loads(body[0]))
tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth_required)
tab.add_handler(uc.cdp.network.ResponseReceived, on_response)
await tab.get("https://protected-site.com/catalog")
await asyncio.sleep(8)
print(f"캡처된 API 응답: {len(captured_data)}개")
for item in captured_data:
print(json.dumps(item, indent=2)[:500])
await browser.stop()
asyncio.run(main())
이 예제에서 핵심은 Fetch.authRequired 이벤트 핸들러로 프록시 인증을 처리하는 것입니다. Chrome의 --proxy-server는 인증을 자체 처리하지 않으므로, CDP Fetch 도메인을 활성화하고 continue_with_auth로 사용자명과 비밀번호를 전달합니다.
스케일링: 동시 탭, Docker 헤드리스 플릿, 컨텍스트별 프록시
동시 탭 관리
nodriver는 asyncio 기반이므로 단일 브라우저 프로세스에서 여러 탭을 동시에 제어할 수 있습니다. 하지만 모든 탭이 동일한 --proxy-server를 공유합니다. 서로 다른 IP가 필요하면 브라우저 인스턴스를 여러 개 실행해야 합니다.
async def scrape_with_proxy(country, session, url):
proxy_host = "gate.proxyhat.com:8080"
proxy_user = f"user-country-{country}-session-{session}"
proxy_pass = "YOUR_PASSWORD"
browser = await uc.start(
headless=True,
browser_args=[
f"--proxy-server=http://{proxy_host}",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
],
)
tab = await browser.get(url)
# ... 스크래핑 로직 ...
await browser.stop()
async def run_fleet():
tasks = [
scrape_with_proxy("US", "s1", "https://example.com/page1"),
scrape_with_proxy("DE", "s2", "https://example.com/page2"),
scrape_with_proxy("JP", "s3", "https://example.com/page3"),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
asyncio.run(run_fleet())
Docker에서 헤드리스 플릿 운영
컨테이너 환경에서 nodriver를 실행할 때 주의할 점은 Chrome이 /dev/shm을 기본적으로 사용한다는 것입니다. Docker 컨테이너의 기본 /dev/shm 크기는 64MB로, Chrome이 메모리 부족으로 크래시할 수 있습니다. --disable-dev-shm-usage 플래그를 추가하거나 shm_size: 2g를 Docker Compose에 설정하세요.
# docker-compose.yml
version: "3.8"
services:
scraper:
build: .
shm_size: "2gb"
environment:
- PROXYHAT_USER=YOUR_USERNAME
- PROXYHAT_PASS=YOUR_PASSWORD
- MAX_CONCURRENT=10
deploy:
replicas: 3
restart: unless-stopped
컨텍스트별 프록시 할당
Chrome의 Browser Context(CDP Target.createBrowserContext)를 사용하면 동일한 브라우저 내에서 격리된 쿠키/세션 컨텍스트를 만들 수 있습니다. 하지만 --proxy-server는 브라우저 수준에서 설정되므로 컨텍스트별로 다른 프록시를 지정하려면 별도 브라우저 인스턴스가 필요합니다. ProxyHat의 sticky session(session-{ID})을 사용하면 동일한 IP를 유지하면서 쿠키를 분리할 수 있습니다.
Graceful shutdown
스크래핑 작업이 중단될 때 브라우저 프로세스가 좀비로 남지 않도록 finally 블록에서 browser.stop()을 호출해야 합니다. SIGTERM 시그널 핸들러를 등록하면 Docker가 컨테이너를 종료할 때 정상적으로 브라우저를 닫을 수 있습니다.
import signal
async def main():
browser = await uc.start(headless=True, browser_args=[...])
# ... 작업 ...
async def graceful_shutdown(browser):
await browser.stop()
print("브라우저 정상 종료")
# signal 핸들러 등록
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(graceful_shutdown(browser)))
nodriver vs HTTP 스크래핑: 언제 전체 브라우저가 필요한가
nodriver는 강력하지만 모든 상황에 적합하지 않습니다. 대상 페이지가 서버 사이드 렌더링이고 API 엔드포인트가 발견 가능하다면, curl이나 curl_cffi(TLS 지문 모방 라이브러리)와 HTTP 프록시를 사용하는 것이 훨씬 저렴하고 빠릅니다.
| 기준 | nodriver + 브라우저 | curl_cffi + HTTP 프록시 |
|---|---|---|
| 메모리 사용량 | 200-500MB per Chrome 인스턴스 | ~20MB per 요청 |
| 요청 속도 | 2-5초 (페이지 로드 + 렌더링) | 200-800ms |
| JavaScript 실행 | ✅ 완전 지원 | ❌ 불가능 |
| Cloudflare Turnstile 통과 | 높은 성공률 (residential IP 필요) | 제한적 (TLS 지문만 모방) |
| 동시성 | 10-50 인스턴스 (서버 사양에 따라) | 500+ 동시 요청 가능 |
| 비용 (ProxyHat 기준) | 트래픽 기반, 브라우저는 더 많은 데이터 소비 | 동일 트래픽 기반이지만 소비량 적음 |
결론: 페이지가 JavaScript 챌린지(Cloudflare JS 챌린지, Imperva 봇 퍼즐)를 요구하거나 동적 렌더링이 필요하면 nodriver를 사용하고, 정적 HTML이나 API JSON만 필요하면 curl_cffi + ProxyHat HTTP 프록시를 사용하세요.
ProxyHat 설정 및 내부 링크
ProxyHat은 residential, mobile, datacenter 프록시를 제공합니다. nodriver와 함께 사용할 때는 residential 프록시를 권장합니다 — datacenter IP는 봇 탐지 시스템에서 차단될 확률이 높고, mobile IP는 신뢰도는 가장 높지만 비용이 더 듭니다. 가격 정보는 ProxyHat 가격 페이지에서 확인할 수 있으며, 웹 스크래핑 사용 사례는 웹 스크래핑 페이지를, SERP 추적은 SERP 추적 페이지를 참조하세요.
ProxyHat API 문서는 docs.proxyhat.com에서 확인할 수 있으며, 인증 토큰 발급, 세션 관리, 국가별 IP 할당 등의 상세 가이드가 제공됩니다.
주요 요점 (Key Takeaways)
- nodriver는 WebDriver를 사용하지 않습니다: CDP를 직접 WebSocket으로 통신하여
navigator.webdriver와 ChromeDriver 흔적을 제거합니다. - 비동기 네이티브: 모든 API가 asyncio 기반이며,
tab.select(),tab.find(), 이벤트 핸들러로 WebDriver wait를 대체합니다. - 프록시는
--proxy-server+ CDP 인증: nodriver에 내장 회전 기능이 없으므로browser_args로 게이트웨이를 지정하고Fetch.authRequired로 인증합니다. - residential IP는 필수: datacenter IP는 Cloudflare/Imperva에서 차단됩니다. ProxyHat residential 프록시로
gate.proxyhat.com:8080을 사용하세요. - 스케일링은 asyncio.gather + Docker: 여러 브라우저 인스턴스를 동시에 실행하고,
shm_size: 2gb로 Docker 메모리를 확보하세요. - HTTP 스크래핑이 더 저렴할 수 있습니다: JavaScript가 필요 없으면
curl_cffi+ HTTP 프록시를 먼저 고려하세요.
FAQ
nodriver로 스크래핑하기란 무엇인가?
nodriver로 스크래핑하기는 Python의 nodriver 라이브러리를 사용하여 Chrome DevTools Protocol(CDP)을 직접 통신하며 웹 페이지를 자동화하고 데이터를 추출하는 방식입니다. Selenium WebDriver나 ChromeDriver를 거치지 않아 봇 탐지 시스템에 걸리지 않으며, asyncio 기반으로 여러 탭을 동시에 제어할 수 있습니다.
왜 nodriver 스크래핑에 프록시가 중요한가?
nodriver가 브라우저 지문을 완벽하게 숨기더라도, 요청이 datacenter IP에서 오면 Cloudflare나 Imperva가 IP 평판으로 차단합니다. Residential 프록시는 실제 ISP 주소를 사용하므로 정상 사용자 트래픽과 구별되지 않아 차단 우회율이 크게 높아집니다. ProxyHat의 gate.proxyhat.com:8080 게이트웨이로 국가별 sticky session을 설정할 수 있습니다.
nodriver에 어떤 프록시 유형이 가장 적합한가?
Cloudflare, Imperva, DataDome 같은 고급 봇 탐지 시스템을 우회하려면 residential 프록시가 가장 적합합니다. Mobile 프록시는 신뢰도가 더 높지만 비용이 더 들고, datacenter 프록시는 차단될 확률이 높습니다. ProxyHat residential 프록시는 user-country-US-session-abc123 형식의 사용자명으로 geo-targeting과 sticky session을 지원합니다.
nodriver 스크래핑 시 차단을 어떻게 피하나요?
차단을 피하려면 세 가지를 결합해야 합니다: (1) nodriver로 WebDriver 흔적 제거, (2) residential 프록시로 IP 평판 확보, (3) 적절한 요청 간격과 sticky session으로 자연스러운 브라우징 패턴 시뮬레이션. 추가로 --disable-blink-features=AutomationControlled 플래그를 사용하고, 각 세션마다 고유한 session ID를 할당하여 IP를 분리하세요.






