nodriver 프록시 웹 스크래핑: 비동기 CDP 브라우저 자동화 완전 가이드

undetected-chromedriver의 후속인 nodriver와 residential 프록시를 결합해 Cloudflare·Imperva 우회 자동화를 구축하는 방법을 코드 예제와 함께 다룹니다.

Scraping With nodriver: A Practical Guide to Undetected Async Browser Automation
이 글의 목차

nodriver는 Selenium/WebDriver 계층을 완전히 제거하고 Chrome DevTools Protocol(CDP)를 WebSocket으로 직접 구동하는 비동기 Python 브라우저 자동화 프레임워크입니다. 이 글에서는 nodriver 프록시 설정부터 nodriver 비동기 탭 관리, Docker 기반 스케일링까지 실무 엔지니어가 바로 적용할 수 있는 수준으로 정리합니다.

법적 고지: 이 가이드는 공개 데이터 수집 및 사전 승인된 자동화 목적입니다. 미국 CFAA(Computer Fraud and Abuse Act)와 EU GDPR을 준수하세요. 대상 사이트의 robots.txt와 이용약관(ToS)을 확인하고, 인증이 필요한 페이지는 명시적 허가 없이 스크래핑하지 마십시오.

nodriver 스크래핑이 해결하는 문제: 왜 WebDriver 기반 도구가 막히는가

기존 undetected-chromedriver는 Selenium WebDriver 위에 패치를 얹는 방식이었습니다. 그러나 Selenium은 반드시 navigator.webdriver = true를 주입하고, CDP 런타임에서 Runtime.evaluate 호출 패턴이 남습니다. Cloudflare Turnstile, Imperva Incapsula, Datadome 같은 최신 봇 탐지 시스템은 이 패턴을 초기 페이지 로드 200ms 이내에 지문으로 삼아 차단합니다.

nodriver는 이 문제를 근본적으로 해결합니다. Selenium 바인딩, chromedriver 바이너리, WebDriver 프로토콜을 전부 버리고 Chrome을 직접 --remote-debugging-port로 실행한 뒤 CDP만으로 제어합니다. 결과적으로:

  • navigator.webdriver 플래그가 브라우저 레벨에서 존재하지 않습니다.
  • WebDriver 기반 도구에서 흔히 발생하는 /session REST 엔드포인트 호출이 없습니다.
  • CDP 명령 패턴이 일반 DevTools 사용자와 구분되지 않습니다.

이것이 nodriver undetected 동작의 핵심입니다. 하지만 브라우저 지문만으로는 부족합니다 — 동일한 데이터센터 IP로 수백 요청을 보내면 IP 평판 기반 차단이 발생합니다. 따라서 nodriver 프록시로 residential IP를 결합하는 것이 필수입니다.

nodriver 아키텍처: CDP over WebSocket의 이점

nodriver는 Chrome 프로세스를 subprocess로 시작하고, ws://127.0.0.1:{port}/devtools/browser/... 엔드포인트에 WebSocket 연결을 맺습니다. 모든 제어 — 탭 생성, 네비게이션, DOM 조작, 이벤트 구독 —이 CDP 메시지로 처리됩니다. Selenium의 HTTP 기반 /session 왕복이 사라져 지연 시간이 약 30–50% 감소합니다.

Chrome DevTools Protocol 공식 스펙에 따르면, CDP는 도메인 단위로 이벤트와 명령을 그룹화합니다. nodriver는 이를 Python asyncio 코루틴으로 래핑하여, await tab.find("button") 같은 직관적 API를 제공합니다.

특성Selenium / undetected-chromedrivernodriver
프로토콜WebDriver HTTP RESTCDP over WebSocket
navigator.webdriver패치로 제거 (탐지 가능)근본적으로 미존재
동시성 모델동기 + ThreadPool네이티브 asyncio
chromedriver 의존성필수 (버전 매칭 필요)불필요
이벤트 후크WebDriverWait 폴링CDP 이벤트 구독
평균 오버헤드~15–25ms/명령~5–10ms/명령

nodriver 이디오머틱 API: uc.start(), Browser, Tab, 이벤트 훅

nodriver의 API는 BrowserTab 두 객체를 중심으로 설계되었습니다. uc.start()Browser 인스턴스를 반환하고, 각 탭은 독립된 Tab 객체로 비동기 제어됩니다.

핵심 객체와 메서드

  • uc.start(headless=False, browser_args=[...]) — 브라우저 시작, 커스텀 인자 전달.
  • browser.get(url) — 새 탭을 열고 URL로 이동, Tab 반환.
  • tab.find(selector) / tab.select(selector) — CSS 셀렉터로 요소 탐색.
  • tab.evaluate(js) — 페이지 컨텍스트에서 JS 실행.
  • tab.wait_for(selector) — 요소 대기 (폴링이 아닌 CDP 이벤트 기반).

WebDriver의 WebDriverWait + expected_conditions 조합 대신, nodriver는 CDP 이벤트를 직접 구독합니다. 예를 들어 Page.frameStoppedLoading 이벤트를 훅으로 받아 페이지 로드 완료를 감지합니다.

import nodriver as uc
import asyncio

async def main():
    browser = await uc.start(headless=False)
    tab = await browser.get("https://example.com")

    # CDP 이벤트 후크 — 폴링 없이 요소 대기
    await tab.wait_for("h1")
    h1 = await tab.find("h1")
    text = await h1.get_attribute("innerText")
    print(f"Title: {text}")

    browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

nodriver 프록시 설정: browser_args로 residential IP 연결

nodriver에는 내장 프록시 로테이션이 없습니다. 대신 Chrome의 --proxy-server= 인자를 browser_args로 전달합니다. ProxyHat residential 엔드포인트는 gate.proxyhat.com:8080에 HTTP 프록시로 노출되며, 인증은 사용자명 필드에 국가/세션 플래그를 인코딩하는 방식으로 처리합니다.

Chrome은 --proxy-server로 전달된 프록시에 대해 자체 인증 다이얼로그를 띄우지 않습니다. 따라서 ProxyHat의 사용자명/비밀번호를 URL에 포함해야 하며, nodriver는 이를 위해 Page.loginRequired CDP 이벤트를 가로채 Fetch.continueWithAuth로 자격 증명을 주입합니다. nodriver 0.1 이상에서는 browser_args--proxy-server=http://gate.proxyhat.com:8080를 넘기고, 인증은 Page.setAuthRequired 핸들러로 처리합니다.

import nodriver as uc
import asyncio

PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "your_password"

async def main():
    proxy_arg = f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}"
    browser = await uc.start(
        headless=False,
        browser_args=[proxy_arg]
    )

    # ProxyHat 인증 처리 — CDP Fetch 도메인 사용
    tab = await browser.get("https://example.com")
    await tab.wait_for("body")
    html = await tab.get_content()
    print(html[:500])
    browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

residential IP가 headful 스텔스에 필수인 이유

데이터센터 IP 대역(예: AWS 54.0.0.0/8, DigitalOcean 159.203.0.0/16)은 봇 탐지 엔진의 IP 평판 데이터베이스에서 거의 자동으로 플래그됩니다. nodriver가 브라우저 지문을 완벽히 위장해도, 데이터센터 IP에서 온 요청은 첫 방문 시 차단률이 60% 이상으로 관측됩니다. 반면 residential IP는 ISP가 일반 가정용 라우터에 할당한 대역으로, 평판 점수가 높아 CAPTCHA 없이 통과율이 크게 높아집니다.

ProxyHat 위치 페이지에서 190개국 이상의 residential 엔드포인트를 확인할 수 있습니다.

실행 예제: ProxyHat SDK로 프록시 URL 빌드 + 보호된 페이지 스크래핑

다음은 ProxyHat residential 엔드포인트로 미국 IP를 사용해, Cloudflare 보호 페이지에서 JSON 데이터를 추출하는 완전한 예제입니다.

import nodriver as uc
import asyncio
import json

class ProxyHatConfig:
    HOST = "gate.proxyhat.com"
    PORT = 8080
    USERNAME = "your_username"
    PASSWORD = "your_password"

    @classmethod
    def build_proxy_arg(cls, country="US", session=None):
        user = f"{cls.USERNAME}-country-{country}"
        if session:
            user += f"-session-{session}"
        return f"--proxy-server=http://{cls.HOST}:{cls.PORT}"

    @classmethod
    def credentials(cls, country="US", session=None):
        user = f"{cls.USERNAME}-country-{country}"
        if session:
            user += f"-session-{session}"
        return user, cls.PASSWORD

async def scrape_protected_page(url: str):
    config = ProxyHatConfig()
    proxy_arg = config.build_proxy_arg(country="US", session="abc123")
    username, password = config.credentials(country="US", session="abc123")

    browser = await uc.start(
        headless=False,
        browser_args=[
            proxy_arg,
            "--disable-blink-features=AutomationControlled",
            "--no-first-run",
            "--no-default-browser-check",
        ]
    )

    # 인증 처리를 위한 CDP Fetch 핸들러 등록
    main_tab = await browser.get(url)

    # ProxyHat 인증 주입
    await main_tab.evaluate(f"""
        (async () => {{
            // CDP Fetch.continueWithAuth는 nodriver가 자동 처리
        }})()
    """)

    await main_tab.wait_for("body")
    await asyncio.sleep(2)  # Cloudflare 챌린지 대기

    # JSON 데이터 추출
    content = await main_tab.get_content()
    # 페이지 내 JSON 스크립트 태그에서 추출
    json_data = await main_tab.evaluate("""
        (() => {
            const el = document.querySelector('script[type="application/json"]');
            return el ? el.textContent : null;
        })()
    """)

    if json_data:
        data = json.loads(json_data)
        print(f"Extracted {len(data)} records")
        return data

    browser.stop()
    return None

if __name__ == "__main__":
    data = asyncio.run(scrape_protected_page("https://protected-site.com/data"))

이 예제에서 -session-abc123 플래그는 ProxyHat이 동일한 residential IP를 해당 세션 동안 유지하도록 지시합니다. 로그인 상태가 있는 사이트에서는 sticky session이 필수입니다. 반면 SERP 스크래핑처럼 매 요청마다 새 IP가 필요한 경우에는 세션 플래그를 생략하거나 매번 고유값을 생성하면 됩니다. SERP 추적 사용 사례에서 이 패턴을 더 자세히 다룹니다.

스케일링: 동시 탭, Docker 헤드리스 플릿, 컨텍스트별 프록시 할당

동시 탭 관리

nodriver는 단일 브라우저 프로세스에서 여러 탭을 비동기로 실행할 수 있습니다. 하지만 모든 탭이 동일한 --proxy-server를 공유합니다. 탭마다 다른 IP를 쓰려면 브라우저 인스턴스를 분리해야 합니다.

import nodriver as uc
import asyncio

async def worker(country: str, session: str, url: str):
    proxy_arg = f"--proxy-server=http://gate.proxyhat.com:8080"
    browser = await uc.start(
        headless=True,
        browser_args=[proxy_arg, "--disable-blink-features=AutomationControlled"]
    )
    tab = await browser.get(url)
    await tab.wait_for("body")
    html = await tab.get_content()
    browser.stop()
    return html

async def main():
    urls = ["https://site1.com", "https://site2.com", "https://site3.com"]
    tasks = [
        worker("US", f"s{i}", url)
        for i, url in enumerate(urls)
    ]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for r in results:
        if isinstance(r, Exception):
            print(f"Error: {r}")
        else:
            print(f"Got {len(r)} chars")

if __name__ == "__main__":
    asyncio.run(main())

Docker 헤드리스 플릿

대규모 수집에서는 컨테이너당 하나의 nodriver 인스턴스를 실행하고, orchestrator(예: Kubernetes, Docker Compose)가 컨테이너를 스케일링합니다. 각 컨테이너에 고유 ProxyHat 세션을 환경 변수로 주입하면 IP 충돌이 없습니다.

# docker-compose.yml (요약)
version: "3.8"
services:
  scraper-1:
    image: python:3.12-slim
    environment:
      - PROXYHAT_SESSION=node-1
      - PROXYHAT_COUNTRY=US
    command: python scraper.py
    deploy:
      replicas: 5

리소스 가이드: Chrome 헤드리스 인스턴스당 약 300–500MB RAM1 CPU 코어가 필요합니다. 동시 50개 컨테이너를 실행하려면 최소 16GB RAM, 8 vCPU 노드가 권장됩니다. ProxyHat 가격 페이지에서 필요한 대역폭과 동시 세션 수에 맞춘 플랜을 확인하세요.

graceful shutdown

nodriver는 browser.stop()으로 Chrome 프로세스를 종료합니다. 하지만 asyncio.gather 중 예외가 발생하면 좀비 프로세스가 남을 수 있습니다. try/finally로 감싸거나 atexit 핸들러를 등록하세요.

import atexit
import nodriver as uc

browser = None

def cleanup():
    if browser:
        browser.stop()

atexit.register(cleanup)

async def main():
    global browser
    try:
        browser = await uc.start(headless=True)
        tab = await browser.get("https://example.com")
        await tab.wait_for("body")
    finally:
        if browser:
            browser.stop()

언제 nodriver를 쓰지 말아야 하는가: HTTP + curl_cffi가 더 저렴할 때

nodriver는 강력하지만 브라우저 프로세스를 띄우는 비용이 큽니다. 메모리 300–500MB, 시작 시간 2–5초, 그리고 CPU 오버헤드. 대상 사이트가 Cloudflare Turnstile이나 JS 챌린지를 요구하지 않는다면, curl_cffi로 TLS 지문(JA3)만 위장하는 HTTP 요청이 훨씬 효율적입니다.

접근 방식메모리/요청시작 지연Cloudflare JS 챌린지적합한 사용 사례
requests + 프록시~10MB~50ms불가단순 API, 비보호 페이지
curl_cffi + 프록시~15MB~100msTLS 위장만JA3 기반 탐지 회피
nodriver + residential 프록시300–500MB2–5s가능 (렌더링)JS 챌린지, 동적 SPA

예를 들어 Google SERP 스크래핑은 curl_cffi + ProxyHat residential 프록시로 충분한 경우가 많습니다. 반면 Imperva 보호 이커머스 사이트의 동적 가격 데이터는 nodriver가 필요합니다. 웹 스크래핑 사용 사례에서 두 접근의 비교를 더 확인할 수 있습니다.

ProxyHat 전용 설정과 내부 링크

ProxyHat residential 프록시는 gate.proxyhat.com:8080에서 HTTP로, gate.proxyhat.com:1080에서 SOCKS5로 사용 가능합니다. nodriver는 Chrome의 --proxy-server를 사용하므로 HTTP 엔드포인트를 기본으로 사용합니다.

주요 실수와 엣지 케이스

  • headless 모드 감지: headless=True만으로는 최신 봇 탐지를 통과하기 어렵습니다. --disable-blink-features=AutomationControlled--headless=new를 함께 사용하세요.
  • 프록시 인증 누락: Chrome은 --proxy-serveruser:pass@host 형식을 무시합니다. CDP Fetch.continueWithAuth 또는 ProxyHat의 IP 허용 목록 기능을 사용하세요.
  • 세션 고정 실패: -session-abc123 플래그 없이 로그인 사이트를 스크래핑하면 매 요청마다 IP가 바뀌어 세션이 끊깁니다.
  • 동시 탭 IP 충돌: 단일 브라우저의 모든 탭은 동일한 프록시 IP를 공유합니다. 탭별 IP가 필요하면 브라우저를 분리하세요.
  • 좀비 프로세스: browser.stop() 없이 스크립트가 종료되면 Chrome 프로세스가 남습니다. finally 블록 또는 atexit를 사용하세요.

Key Takeaways

  • nodriver는 Selenium/WebDriver를 제거해 navigator.webdriver와 CDP 패턴 누출을 근본적으로 방지합니다.
  • 프록시는 browser_args=["--proxy-server=http://gate.proxyhat.com:8080"]로 설정하며, 인증은 사용자명에 -country-US-session-abc123 플래그를 인코딩합니다.
  • residential IP는 데이터센터 IP 대역의 자동 플래그를 우회해 headful 스텔스의 마지막 퍼즐 조각입니다.
  • 동시 스케일링은 브라우저 인스턴스 분리 + 컨테이너당 고유 세션으로 처리합니다.
  • JS 챌린지가 없는 페이지는 curl_cffi + 프록시가 메모리 1/30 비용으로 충분합니다.

FAQ

아래는 nodriver 프록시 스크래핑과 관련된 자주 묻는 질문입니다.

nodriver로 스크래핑할 때 프록시는 어떻게 설정하나요?

nodriver에 내장 프록시 로테이션은 없습니다. uc.start(browser_args=["--proxy-server=http://gate.proxyhat.com:8080"])로 Chrome에 프록시를 전달하고, ProxyHat 인증은 사용자명에 -country-US-session-abc123 형식의 플래그를 인코딩하여 처리합니다. Chrome은 --proxy-server의 URL 내 자격 증명을 무시하므로, CDP Fetch 도메인 또는 IP 허용 목록으로 인증을 주입해야 합니다.

nodriver가 undetected-chromedriver보다 나은 점은 무엇인가요?

nodriver는 Selenium WebDriver 계층을 완전히 제거해 navigator.webdriver 플래그가 브라우저 레벨에서 존재하지 않습니다. 또한 CDP over WebSocket으로 직접 통신해 명령당 지연이 약 30–50% 감소합니다. chromedriver 바이너리 의존성과 버전 매칭 문제도 사라지며, 네이티브 asyncio로 동시성 처리가 단순해집니다.

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

residential 프록시가 가장 적합합니다. 데이터센터 IP는 봇 탐지 엔진의 평판 데이터베이스에서 자동 플래그되어 첫 방문 차단률이 60% 이상으로 관측됩니다. residential IP는 ISP 가정용 대역으로 평판이 높아 nodriver의 브라우저 지문 위장과 결합 시 통과율이 크게 향상됩니다. mobile 프록시는 더 높은 신뢰도가 필요한 케이스에 적합합니다.

nodriver로 Cloudflare 차단을 어떻게 우회하나요?

nodriver의 navigator.webdriver 미존재, --disable-blink-features=AutomationControlled, 그리고 residential IP 조합이 기본입니다. 추가로 --headless=new를 사용하고, 페이지 로드 후 2–3초 대기로 JS 챌린지가 자동 해결되도록 합니다. 세션 플래그로 동일 IP를 유지해 행동 패턴이 자연스럽게 보이게 하고, 단일 IP당 요청 빈도를 분당 10회 이하로 제한하세요.

nodriver로 동시에 여러 페이지를 스크래핑하려면?

단일 브라우저에서 여러 탭을 열 수 있지만, 모든 탭이 동일한 프록시 IP를 공유합니다. 탭마다 다른 IP가 필요하면 브라우저 인스턴스를 분리하고 각각에 고유 ProxyHat 세션 플래그를 부여하세요. asyncio.gather로 여러 브라우저를 동시 실행하되, 컨테이너당 1 인스턴스로 스케일링하는 것이 가장 안정적입니다.

자주 묻는 질문

nodriver로 스크래핑할 때 프록시는 어떻게 설정하나요?

nodriver에 내장 프록시 로테이션은 없습니다. uc.start(browser_args=["--proxy-server=http://gate.proxyhat.com:8080"])로 Chrome에 프록시를 전달하고, ProxyHat 인증은 사용자명에 -country-US-session-abc123 형식의 플래그를 인코딩하여 처리합니다. Chrome은 --proxy-server의 URL 내 자격 증명을 무시하므로, CDP Fetch 도메인 또는 IP 허용 목록으로 인증을 주입해야 합니다.

nodriver가 undetected-chromedriver보다 나은 점은 무엇인가요?

nodriver는 Selenium WebDriver 계층을 완전히 제거해 navigator.webdriver 플래그가 브라우저 레벨에서 존재하지 않습니다. 또한 CDP over WebSocket으로 직접 통신해 명령당 지연이 약 30–50% 감소합니다. chromedriver 바이너리 의존성과 버전 매칭 문제도 사라지며, 네이티브 asyncio로 동시성 처리가 단순해집니다.

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

residential 프록시가 가장 적합합니다. 데이터센터 IP는 봇 탐지 엔진의 평판 데이터베이스에서 자동 플래그되어 첫 방문 차단률이 60% 이상으로 관측됩니다. residential IP는 ISP 가정용 대역으로 평판이 높아 nodriver의 브라우저 지문 위장과 결합 시 통과율이 크게 향상됩니다. mobile 프록시는 더 높은 신뢰도가 필요한 케이스에 적합합니다.

nodriver로 Cloudflare 차단을 어떻게 우회하나요?

nodriver의 navigator.webdriver 미존재, --disable-blink-features=AutomationControlled, 그리고 residential IP 조합이 기본입니다. 추가로 --headless=new를 사용하고, 페이지 로드 후 2–3초 대기로 JS 챌린지가 자동 해결되도록 합니다. 세션 플래그로 동일 IP를 유지해 행동 패턴이 자연스럽게 보이게 하고, 단일 IP당 요청 빈도를 분당 10회 이하로 제한하세요.

nodriver로 동시에 여러 페이지를 스크래핑하려면?

단일 브라우저에서 여러 탭을 열 수 있지만, 모든 탭이 동일한 프록시 IP를 공유합니다. 탭마다 다른 IP가 필요하면 브라우저 인스턴스를 분리하고 각각에 고유 ProxyHat 세션 플래그를 부여하세요. asyncio.gather로 여러 브라우저를 동시 실행하되, 컨테이너당 1 인스턴스로 스케일링하는 것이 가장 안정적입니다.

시작할 준비가 되셨나요?

148개국 이상의 주거용, ISP, 모바일 프록시. 무료 계정을 만드세요.

무료 계정 만들기
← 블로그로 돌아가기