Crawlee for Python에서 프록시 로테이션 완벽 가이드

Crawlee for Python의 ProxyConfiguration과 SessionPool을 활용해 residential proxy를 세션별로 로테이션하는 방법을 실전 코드와 함께 다룹니다. Cloudflare, DataDome 차단 우회 패턴과 프로덕션 운영 노하우까지.

Proxy Rotation in Crawlee for Python: A Developer's Guide to Residential IPs

Crawlee for Python에서 프록시 로테이션의 기본

웹 스크래핑을 프로덕션으로 옮기면 가장 먼저 마주하는 벽은 IP 차단입니다. Crawlee for Python에서 프록시 로테이션은 이 문제를 해결하는 핵심 메커니즘입니다. Crawlee는 ProxyConfiguration 클래스와 SessionPool을 통해 프록시를 관리하며, 이 둘을 어떻게 연결하느냐에 따라 스크래퍼의 성공률이 99% 수준으로 올라가거나, 반대로 첫 100개 요청에서 차단될 수 있습니다.

이 글에서는 Crawlee 파이썬 프록시 설정의 모든 측면을 다룹니다: ProxyConfigurationnew_url(session_id=...) 메서드로 residential IP를 세션에 바인딩하는 방법, Crawlee 세션 풀이 쿠키와 브라우저 지문을 IP에 연결하는 원리, Cloudflare와 DataDome 같은 안티봇 시스템에서 residential proxy가 datacenter IP보다 왜 우월한지, 그리고 실제 프로덕션에서 사용할 수 있는 완전한 코드 예제까지. Crawlee 공식 문서는 crawlee.dev/python에서 확인할 수 있습니다.

Crawlee 아키텍처 — 통합 큐, 자동 스케일링, SessionPool

Crawlee for Python은 두 가지 주요 크롤러를 제공합니다. BeautifulSoupCrawler는 HTTP 요청 기반으로 동작하며 가볍고 빠릅니다 — 초당 수십 개의 페이지를 처리할 수 있습니다. PlaywrightCrawler는 헤드리스 브라우저를 사용해 JavaScript 렌더링이 필요한 페이지를 처리합니다. 두 크롤러 모두 동일한 통합 요청 큐(unified request queue) 위에서 동작하므로, 한 크롤러에서 다른 크롤러로 전환해도 프록시 설정과 세션 관리 코드를 그대로 재사용할 수 있습니다.

자동 스케일링은 Crawlee의 핵심 기능 중 하나입니다. AutoscaledPool은 시스템 리소스와 네트워크 상태를 모니터링하여 동시 요청 수를 자동으로 조절합니다. CPU 사용량이 70%를 넘으면 동시성을 줄이고, 응답 시간이 느려지면 요청 속도를 낮춥니다. 이는 Crawlee 프록시 로테이션 전략에 직접적인 영향을 미칩니다 — 동시 세션 수가 변하면 프록시 풀의 소비 속도도 변하기 때문입니다.

Crawlee 세션 풀이 IP와 쿠키를 연결하는 방식

Crawlee 세션 풀(SessionPool)은 각 세션에 고유 ID를 할당하고, 그 세션에 쿠키, 브라우저 지문, 그리고 프록시 URL을 바인딩합니다. 세션이 활성 상태인 동안 같은 IP와 쿠키가 유지되므로, 로그인 상태를 유지하거나 페이지네이션을 따라가는 크롤링에 필수적입니다. 세션이 만료되거나 차단되면 session.retire()를 호출하여 풀에서 제거하고, 새 세션이 새 프록시 IP와 함께 생성됩니다.

이 설계가 중요한 이유는 단순히 IP만 바꾸는 것이 아니라 IP + 쿠키 + 지문의 일관성을 유지하기 때문입니다. Cloudflare 같은 안티봇 시스템은 IP만 바뀌고 쿠키가 유지되는 패턴을 감지합니다. Crawlee SessionPool은 이 문제를 구조적으로 해결합니다 — 세션이 은퇴하면 IP, 쿠키, 지문이 함께 교체됩니다.

Crawlee ProxyConfiguration 클래스로 프록시 로테이션 구현하기

ProxyConfiguration은 Crawlee에서 프록시를 관리하는 중심 클래스입니다. Crawlee ProxyConfiguration은 두 가지 모드로 동작합니다:

  • 라운드 로빈(round-robin): proxy_urls 리스트에 여러 프록시 URL을 등록하면, 각 요청마다 순차적으로 다음 URL을 반환합니다. IP가 계속 바뀌어야 하는 SERP 스크래핑에 적합합니다.
  • 스티키 세션(sticky session): new_url(session_id="abc123")을 호출하면, 동일한 session_id에 대해 항상 같은 프록시 URL을 반환합니다. 로그인 세션이나 페이지네이션을 따라가는 크롤링에 필수적입니다.

ProxyHat과 연동할 때, 스티키 세션은 username에 session 플래그를 포함하는 것으로 구현합니다. 예를 들어 user-country-US-session-abc123이라는 username은 항상 같은 미국 residential IP를 반환합니다. 세션 ID를 바꾸면 새 IP가 할당됩니다.

ProxyConfiguration 기본 설정

from crawlee.proxy_configuration import ProxyConfiguration

# 라운드 로빈용 프록시 URL 리스트
proxy_configuration = ProxyConfiguration(
    proxy_urls=[
        'http://user-country-US:pass@gate.proxyhat.com:8080',
        'http://user-country-DE:pass@gate.proxyhat.com:8080',
        'http://user-country-GB:pass@gate.proxyhat.com:8080',
    ]
)

# 또는 단일 엔드포인트로 자동 로테이션
proxy_configuration_simple = ProxyConfiguration(
    proxy_urls=['http://user-country-US:pass@gate.proxyhat.com:8080']
)

proxy_urls에 단일 URL만 전달하면 Crawlee는 모든 요청에 같은 프록시를 사용합니다. ProxyHat 게이트웨이는 백엔드에서 자체적으로 IP를 로테이션하므로, 단일 엔드포인트만으로도 기본적인 로테이션이 가능합니다. 하지만 세션 단위로 IP를 고정하려면 new_url(session_id=...)을 사용해야 합니다.

new_url(session_id=...)로 세션별 IP 고정하기

import uuid
from crawlee.proxy_configuration import ProxyConfiguration

PROXYHAT_USER = 'your_username'
PROXYHAT_PASS = 'your_password'

def make_proxy_url(session_id: str, country: str = 'US') -> str:
    """세션 ID별로 고유한 residential IP를 반환하는 프록시 URL 생성"""
    username = f'{PROXYHAT_USER}-country-{country}-session-{session_id}'
    return f'http://{username}:{PROXYHAT_PASS}@gate.proxyhat.com:8080'

# 10개의 스티키 세션 프록시 URL 생성
session_ids = [str(uuid.uuid4())[:8] for _ in range(10)]
proxy_urls = [make_proxy_url(sid) for sid in session_ids]

proxy_configuration = ProxyConfiguration(proxy_urls=proxy_urls)

이 패턴에서 각 session_id는 ProxyHat 게이트웨이에서 고유한 residential IP에 매핑됩니다. Crawlee의 SessionPool이 세션을 생성할 때 자동으로 proxy_configuration.new_url(session_id=...)을 호출하여, 해당 세션에 프록시 URL을 바인딩합니다. 세션이 은퇴하면 새 세션이 생성되고, 새 session_id로 새 IP가 할당됩니다.

Residential vs Datacenter: Cloudflare와 DataDome에서의 차이

프록시 유형 선택은 안티봇 시스템을 얼마나 효과적으로 우회하느냐를 결정합니다. 다음 표는 세 가지 프록시 유형의 주요 차이점을 보여줍니다:

특성 Residential Datacenter Mobile
IP 출처 ISP 실사용자 IP 데이터센터 IP 대역 이동통신사 IP
Cloudflare 우회율 90% 이상 30~50% 95% 이상
평균 응답 속도 200~500ms 50~100ms 300~800ms
동시 세션 수 100+ concurrent sessions 1000+ sessions 50~100 sessions
비용 중간 낮음 높음

Cloudflare와 DataDome 같은 안티봇 시스템은 IP 평판 데이터베이스를 사용하여 요청을 평가합니다. 데이터센터 IP 대역(AWS, DigitalOcean, OVH 등)은 기본적으로 높은 봇 위험도로 분류됩니다. 반면 residential IP는 실제 ISP 고객에게 할당된 IP이므로, 안티봇 시스템이 합법적인 사용자 트래픽과 구분하기 어렵습니다. Cloudflare의 봇 관리 문서에 따르면, IP 평판은 봇 탐지 신호의 핵심 요소 중 하나입니다.

티어드 프록시 전략

프로덕션에서는 단일 프록시 유형에 의존하지 않는 것이 좋습니다. 티어드 프록시(tiered proxy) 전략은 다음과 같이 동작합니다:

  1. 1단계: Datacenter 프록시로 빠른 요청 시도 (비용 효율적, 50~100ms 응답)
  2. 403이나 CAPTCHA를 만나면 해당 세션을 은퇴하고 2단계로 전환
  3. 2단계: Residential 프록시로 재시도 (90% 이상 우회율)
  4. 여전히 차단되면 3단계: Mobile 프록시로 최후의 재시도 (95% 이상 우회율)

이 전략은 Crawlee의 max_request_retriessession.retire()를 조합하여 구현할 수 있습니다. 각 재시도마다 다른 프록시 티어를 사용하도록 ProxyConfiguration을 동적으로 교체하거나, 여러 ProxyConfiguration 인스턴스를 준비해두고 차단 유형에 따라 선택합니다.

실전 예제: BeautifulSoupCrawler + ProxyConfiguration

이제 모든 개념을 결합한 완전한 실행 가능한 예제를 살펴보겠습니다. 이 코드는 BeautifulSoupCrawler를 사용하여 ProxyHat residential 프록시로 웹 페이지를 스크랩하며, 세션별로 IP를 고정하고 차단 시 자동으로 세션을 교체합니다.

import asyncio
import uuid
from crawlee.beautifulsoup_crawler import BeautifulSoupCrawler
from crawlee.proxy_configuration import ProxyConfiguration

PROXYHAT_USER = 'your_username'
PROXYHAT_PASS = 'your_password'

def make_proxy_url(session_id: str, country: str = 'US') -> str:
    """ProxyHat 세션별 residential 프록시 URL 생성"""
    username = f'{PROXYHAT_USER}-country-{country}-session-{session_id}'
    return f'http://{username}:{PROXYHAT_PASS}@gate.proxyhat.com:8080'

async def main():
    # 20개의 스티키 세션 프록시 URL 생성
    session_ids = [str(uuid.uuid4())[:8] for _ in range(20)]
    proxy_urls = [make_proxy_url(sid, country='US') for sid in session_ids]

    proxy_configuration = ProxyConfiguration(proxy_urls=proxy_urls)

    crawler = BeautifulSoupCrawler(
        proxy_configuration=proxy_configuration,
        max_request_retries=3,
        max_session_retries=3,
    )

    @crawler.router.default_handler
    async def handler(context):
        session_id = context.session.id if context.session else 'N/A'
        context.log.info(f'Processing {context.request.url} | session={session_id}')

        title = context.soup.find('title')
        if title:
            context.log.info(f'Title: {title.text.strip()}')

        # 차단 감지: 403 상태 코드 확인
        if context.http_response and context.http_response.status_code == 403:
            context.log.warning('Blocked! Retiring session...')
            if context.session:
                context.session.retire()
            raise RuntimeError('Blocked by target site')

        # 결과 저장
        await context.push_data({
            'url': context.request.url,
            'title': title.text.strip() if title else None,
        })

    await crawler.run(['https://example.com'])

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

이 예제의 핵심 포인트는 다음과 같습니다:

  • 20개의 스티키 세션: 각 세션은 고유한 residential IP에 바인딩됩니다. Crawlee의 SessionPool이 자동으로 세션을 선택하고, 해당 세션에 매핑된 프록시 URL을 사용합니다.
  • max_request_retries=3: 요청당 최대 3회 재시도. 각 재시도마다 새 세션(따라서 새 IP)이 사용될 수 있습니다.
  • max_session_retries=3: 세션당 최대 3회 재시도 후 세션을 폐기합니다.
  • 403 감지 시 session.retire(): 차단을 감지하면 즉시 세션을 은퇴시켜, 다음 요청이 새 IP로 시도되도록 합니다.

프로덕션 패턴 — 세션 은퇴, 재시도, 동시성 관리

프로덕션 크롤러에서는 단순히 프록시를 설정하는 것을 넘어, 차단에 대응하는 복원력을 설계해야 합니다. 다음은 Crawlee for Python에서 검증된 프로덕션 패턴들입니다.

1. session.retire()로 우아한 세션 교체

차단을 감지했을 때 가장 중요한 것은 즉시 세션을 은퇴시키는 것입니다. session.retire()를 호출하면 해당 세션이 SessionPool에서 제거되고, 동일한 IP와 쿠키 조합이 더 이상 사용되지 않습니다. 다음 요청은 자동으로 새 세션과 새 프록시 IP를 받습니다.

@crawler.router.default_handler
async def handler(context):
    response = context.http_response

    # 차단 패턴 감지
    blocked = False
    if response and response.status_code in (403, 429):
        blocked = True
    elif context.soup:
        title = context.soup.find('title')
        if title and ('captcha' in title.text.lower() or 'blocked' in title.text.lower()):
            blocked = True

    if blocked:
        sid = context.session.id if context.session else 'N/A'
        context.log.warning(f'Blocked on {context.request.url}. Retiring session {sid}.')
        if context.session:
            context.session.retire()
        raise RuntimeError('Blocked, retiring session')

    # 정상 처리 로직
    await context.push_data({'url': context.request.url})

여기서 RuntimeError를 발생시키면 Crawlee가 max_request_retries 범위 내에서 자동으로 재시도합니다. 재시도 시 새 세션이 선택되고, 새 프록시 IP로 요청이 전송됩니다.

2. 동시성 관리와 AutoscaledPool

Crawlee의 AutoscaledPool은 기본적으로 활성화되어 있으며, 시스템 리소스에 따라 동시 요청 수를 조절합니다. 하지만 프록시를 사용할 때는 프록시 서버의 용량도 고려해야 합니다. ProxyHat residential 프록시는 100개 이상의 동시 세션을 지원하지만, 한 번에 너무 많은 요청을 보내면 게이트웨이에서 속도 제한이 걸릴 수 있습니다.

동시성을 명시적으로 제한하려면 크롤러 생성 시 옵션을 전달합니다:

from datetime import timedelta

crawler = BeautifulSoupCrawler(
    proxy_configuration=proxy_configuration,
    max_request_retries=3,
    max_session_retries=3,
    # 요청 핸들러 타임아웃 설정
    request_handler_timeout=timedelta(seconds=30),
    # 크롤링할 최대 요청 수
    max_requests_per_crawl=5000,
)

프로덕션에서는 동시성을 10~20개 정도로 유지하면서, 각 세션당 초당 1~2개 요청 이하로 보내는 것이 안전한 기준입니다. 이렇게 하면 대상 서버에 부하를 주지 않으면서도 안정적으로 데이터를 수집할 수 있습니다.

3. 에러 핸들링과 재시도 전략

모든 재시도가 실패하면 요청은 실패 로그에 기록됩니다. 프로덕션에서는 이 실패 로그를 모니터링하여 특정 도메인이나 프록시 세션의 실패율을 추적해야 합니다. 일반적으로 성공률이 95% 미만으로 떨어지면 프록시 풀을 점검하거나 국가 설정을 변경해야 합니다.

재시도 간 대기 시간을 조절하려면 커스텀 로직을 핸들러에 추가합니다. Crawlee는 지수 백오프를 기본적으로 제공하지 않으므로, 필요하다면 asyncio.sleep()을 사용하여 구현합니다:

import asyncio
import random

@crawler.router.default_handler
async def handler(context):
    try:
        # 메인 크롤링 로직
        await context.push_data({'url': context.request.url})
    except RuntimeError as e:
        # 재시도 전 랜덤 대기 (지수 백오프 + 지터)
        retry_count = context.request.retry_count or 0
        wait_time = (2 ** retry_count) + random.uniform(0, 1)
        context.log.info(f'Retry {retry_count}: waiting {wait_time:.1f}s')
        await asyncio.sleep(wait_time)
        raise

브라우저 크롤링을 피해야 할 때와 윤리적 고려사항

PlaywrightCrawler는 언제 필요한가?

모든 사이트가 브라우저 렌더링을 필요로 하는 것은 아닙니다. BeautifulSoupCrawler로 충분한 경우:

  • 서버 사이드 렌더링(SSR) 페이지 — HTML에 이미 데이터가 포함됨
  • 공개 API 엔드포인트가 존재하는 경우 — HTML 파싱보다 API 호출이 효율적
  • 단순한 정적 페이지 — JavaScript 없이도 콘텐츠 접근 가능

반면 PlaywrightCrawler가 필요한 경우:

  • SPA(단일 페이지 애플리케이션) — JavaScript 실행 후 콘텐츠가 로드됨
  • Cloudflare Turnstile이나 reCAPTCHA v3 등 클라이언트 사이드 챌린지
  • WebSocket이나 무한 스크롤로 데이터를 로드하는 페이지

브라우저 크롤링은 리소스 소모가 큽니다 — 각 Playwright 세션은 약 200~300MB의 메모리를 사용합니다. 100개의 동시 세션을 실행하면 약 20~30GB의 RAM이 필요합니다. BeautifulSoupCrawler는 세션당 10~20MB 수준이므로, 가능하면 HTTP 기반 크롤링을 우선하는 것이 비용 효율적입니다. 컨테이너 환경에서는 PlaywrightCrawler를 별도 컨테이너로 분리하여 스케일아웃하는 것이 좋습니다.

윤리적 스크래핑 가이드라인

주의: 이 글의 기술은 공개적으로 접근 가능한 데이터에만 적용하세요. 비밀번호로 보호된 페이지, 유료 콘텐츠, 또는 서비스 약관이 명시적으로 금지하는 스크래핑에 이 기술을 사용하는 것은 미국 Computer Fraud and Abuse Act(CFAA) 및 유럽 GDPR 위반이 될 수 있습니다. 항상 robots.txt를 확인하고, 공식 API가 존재하면 그것을 우선 사용하세요.

구체적인 권장 사항:

  • robots.txt 준수: robots.txt가 크롤링을 금지하는 경로는 스크랩하지 마세요. Crawlee는 기본적으로 robots.txt를 확인하지 않으므로, 별도로 로직을 추가해야 합니다.
  • 속도 제한: 대상 서버에 과부하를 주지 않도록 요청 속도를 제한하세요. 일반적으로 초당 1~2개 요청이 안전한 기준입니다.
  • 공식 API 우선: 많은 플랫폼이 공식 API를 제공합니다. API를 사용하면 차단 위험 없이 안정적으로 데이터를 수집할 수 있습니다.
  • 개인정보 보호: 개인 식별 정보(PII)를 수집하는 경우 GDPR과 CCPA 요구사항을 준수해야 합니다. 수집 목적을 명시하고, 데이터 보관 기간을 설정하세요.

ProxyHat은 다양한 프록시 위치를 제공하며, 프록시 가격 페이지에서 residential, datacenter, mobile 프록시의 비용을 확인할 수 있습니다. 웹 스크래핑SERP 추적 사용 사례에 대한 자세한 내용은 각 페이지를 참조하세요. ProxyHat 연동에 대한 기술 문서는 ProxyHat 문서에서 확인할 수 있습니다.

핵심 요약

  • Crawlee ProxyConfigurationnew_url(session_id=...)을 통해 세션별 프록시 IP 바인딩을 지원합니다. 라운드 로빈과 스티키 세션을 상황에 맞게 선택하세요.
  • Crawlee 세션 풀(SessionPool)은 IP, 쿠키, 브라우저 지문을 하나의 세션으로 묶어, 안티봇 시스템이 IP 변경 + 쿠키 유지 패턴을 감지하지 못하게 합니다.
  • Residential 프록시는 Cloudflare와 DataDome에서 datacenter IP 대비 2~3배 높은 우회율(90% 이상)을 보입니다. 비용이 중요하면 datacenter로 시작하고 차단 시 residential로 폴백하는 티어드 전략을 사용하세요.
  • session.retire()로 차단 시 즉시 세션을 교체하고, max_request_retries=3으로 자동 재시도를 설정하세요. 재시도 시 새 세션과 새 IP가 자동 할당됩니다.
  • 윤리적 스크래핑: 공개 데이터만 수집하고, robots.txt를 준수하며, 공식 API를 우선하세요. CFAA와 GDPR을 항상 염두에 두세요.

시작할 준비가 되셨나요?

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

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