Scrapy에서 커스텀 프록시 설정하는 방법 (프로덕션 대응)

최종 업데이트: August 11, 2026
Hand-drawn Scrapy crawler routing through a proxy pool, retrying a 503 route and completing through a 200 route
AI 요약
- 승인된 경로를 할당하고, 인증을 안전하게 추가하며, 실패 사유를 기록하고, 재시도 시 요청 메타데이터를 보존하는 커스텀 Scrapy 프록시 미들웨어를 구축하세요. - downloader middleware 순서를 이해하고, 특히 커스텀 프록시 로직이 HttpProxyMiddleware, RetryMiddleware, 리디렉션, 예외 처리와 어떻게 상호작용하는지 파악하세요. - 무작위 선택만 하지 말고, 제한된 재시도, 쿨다운, 프록시 헬스 상태, 대상별 정책을 적용해 로테이션을 구성하세요. - 프록시 인증 실패, 연결 오류, DNS 문제, 대상의 403 응답, 레이트 리밋을 구분하여 각 상황에 맞는 대응을 적용하세요. - 비밀값 저장, 동시성, 관측 가능성, 세션 일관성, 그리고 승인된 프록시가 없을 때의 fail-closed 동작을 위한 프로덕션 안전장치를 사용하세요.

Your Scrapy spider crushes it on 100 test pages, then falls apart at 10,000. That's not really a scraping bug — it's a networking problem wearing a scraping bug's costume. The fix everyone reaches for is a proxy, and slapping one into request.meta takes about thirty seconds.

The problem is that the thirty-second fix is where many basic tutorials stop. They show you meta={"proxy": "http://IP:PORT"}, maybe a middleware class, and call it a day. They often omit what happens when that proxy dies mid-crawl, how credentials leak into Git history, or why a retry may reuse the same failed proxy. Those are the production concerns this guide covers: fail-closed configuration, secret management, deliberate proxy selection on retry, protocol limits, and measurable cost trade-offs.

Scrapy에서 프록시 미들웨어란?

프록시 미들웨어는 Scrapy의 downloader pipeline 안에서 동작하며, 요청이 실제로 전송되기 전에 어떤 IP로 보이게 할지 결정하는 코드입니다. Scrapy에는 기본 제공 미들웨어인 HttpProxyMiddleware가 있습니다. 이 미들웨어는 request.metaproxy 값을 읽고, 필요할 경우 프록시 인증 정보를 붙인 뒤, 실제로 연결을 여는 download handler에 요청을 넘깁니다. 커스텀 프록시 미들웨어는 이 전송 단계를 대체하는 것이 아니라, 기본 메커니즘이 작동하기 전에 어떤 프록시를 넣을지 고르는 선택기 역할을 합니다. 이 차이는 생각보다 훨씬 중요하며, 대부분의 “커스텀 미들웨어가 안 먹어요” 류의 문제 보고가 여기서 나옵니다.

프로덕션 Scrapy 프로젝트에서 프록시가 중요한 이유

프록시는 네트워크 경로와 외부에 보이는 출발 IP를 바꿉니다. 덕분에 지역별 테스트를 합법적으로 진행하거나, 승인된 요청 트래픽을 분산하거나, 네트워크 장애를 격리하는 데 도움이 됩니다. 하지만 그 자체가 권한을 부여하거나, 레이트 리밋을 무시하게 해주거나, 접속을 보장하는 것은 아닙니다. 크롤링에 프록시가 필요한지는 대상 사이트, 이용 약관, 요청 속도, 그리고 작업의 신뢰성 요구사항에 따라 달라집니다.

프록시 품질은 제각각이며, 잘못 고르면 그 자체가 운영 사고가 됩니다:

프록시 유형안정성속도탐지 위험일반적인 비용
무료 공개 프록시편차가 매우 큼편차가 큼대체로 높음비용은 없지만 보안/운영 리스크가 큼
데이터센터 프록시제공업체와 대상에 따라 다름보통 빠름대상에 따라 다름일반적으로 GB 또는 IP 단위 과금
Residential 프록시제공업체와 대상에 따라 다름편차가 큼대상에 따라 다름일반적으로 GB 단위 과금
ISP 프록시제공업체와 대상에 따라 다름편차가 큼대상에 따라 다름제공업체별 상이

무료 프록시는 특히 따로 짚고 넘어갈 가치가 있습니다. 실제 측정된 위험이 상당하기 때문입니다. 30개월간 진행된 Free Proxies Unmasked 연구는 11개 제공업체의 64만 개가 넘는 주소를 추적했는데, 그중 34.5%만 최소 한 번이라도 활성 상태였고, 16,923개는 콘텐츠를 조작했습니다. 이 결과는 공개 프록시 목록에 대한 강한 보안 경고로는 유효하지만, 현재의 모든 목록·유료 풀·대상·워크로드에 그대로 적용되는 보편적 실패율은 아닙니다.

프록시가 최신 봇 탐지를 마법처럼 무력화해 주는 것도 아닙니다. Cloudflare 같은 서비스는 TLS 지문, 헤더 일관성, JavaScript 실행, 행동 패턴 등 수십 가지 신호를 종합해 판단하며, IP는 그중 하나일 뿐입니다. 깔끔한 Residential 프록시를 쓰더라도 헤더가 들쭉날쭉하고 쿠키 저장소가 없는 요청은 여전히 차단될 수 있습니다. “IP만 바꾸면 된다”는 식으로 전체 아키텍처를 짜기 전에 기대치를 현실적으로 잡아두세요.

시작하기 전에

  • 난이도: 중급
  • 소요 시간: 전체 프로덕션 구성 약 30~45분, 간단 테스트 약 5분
  • 준비물: Python 3.9 이상, Scrapy 설치 완료(이 가이드는 2026년 7월 출시된 Scrapy 2.17.0 기준으로 검증됨), scrapy startproject로 만든 동작 가능한 spider, 그리고 최소 1개의 프록시 엔드포인트(테스트용으로는 데이터센터 프록시 제공업체의 무료 체험도 충분)

1단계: request meta 파라미터로 프록시 테스트하기

프록시가 동작하는지 가장 빨리 확인하는 방법은 미들웨어 구조를 아예 건너뛰고 직접 넣어보는 것입니다.

Scrapy의 기본 HttpProxyMiddlewarerequest.meta에서 proxy 키를 읽어 해당 경로로 요청을 보냅니다. 설정 변경도 없고, 미들웨어 클래스도 필요 없습니다. 키워드 인자 하나면 됩니다.

import scrapy

class ProxyTestSpider(scrapy.Spider):
    name = "proxy_test"
    start_urls = ["https://httpbin.org/ip"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url,
                meta={"proxy": "http://203.0.113.10:8080"},
                callback=self.parse,
            )

    def parse(self, response):
        self.logger.info(response.text)

scrapy runspider proxy_test.py로 실행해 보세요. 프록시가 정상이라면 httpbin.org/ip가 여러분의 IP가 아니라 프록시 IP를 돌려줍니다. 그게 확인 신호입니다. 응답이 멈추고 결국 TCP connection timed out 오류가 나면, 프록시가 죽었거나 접근 불가한 상태입니다. 솔직히 말해, 프록시 업체들이 인정하고 싶어 하는 것보다 이런 일은 훨씬 자주 일어납니다.

이 방식은 일회성 spider나 빠른 테스트에는 괜찮습니다. 하지만 spider가 둘 이상이 되는 순간 문제가 생깁니다. 동일한 프록시 문자열을 다섯 개 파일에 하드코딩하게 되기 때문입니다.

2단계: 커스텀 프록시 미들웨어 만들기

단일 spider를 넘어서면 프록시 로직을 한 곳으로 모아두는 것이 좋습니다. 프로젝트의 middlewares.pyProxyMiddleware 클래스를 만드세요:

from scrapy.exceptions import NotConfigured

class ProxyMiddleware:
    def __init__(self, proxy_url):
        self.proxy_url = proxy_url

    @classmethod
    def from_crawler(cls, crawler):
        proxy_url = crawler.settings.get("PROXY_URL")
        if not proxy_url:
            raise NotConfigured("PROXY_URL is required; refusing silent direct fallback")
        return cls(proxy_url=proxy_url)

    def process_request(self, request, spider):
        if "proxy" not in request.meta:
            request.meta["proxy"] = self.proxy_url

그리고 settings.py에 등록합니다:

import os

PROXY_URL = os.environ.get("PROXY_URL")

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.ProxyMiddleware": 350,
}

여기서 __init__ 대신 from_crawler 클래스메서드를 쓰는 점에 주목하세요. Scrapy가 설정을 읽을 때 실제로 권장하는 패턴이며, 4단계에서 비밀값 관리 이야기를 할 때도 다시 사용할 핵심 훅입니다. 장점은 단순합니다. 설정 하나만 바꾸면 프로젝트 내 모든 spider가 새 프록시를 자동으로 사용합니다. 더 이상 죽은 IP를 바꾸려고 파일 다섯 개를 뒤질 필요가 없습니다.

3단계: 미들웨어 실행 순서 이해하기 (그래야 프록시가 조용히 무시되지 않습니다)

대부분의 다른 튜토리얼이 “그냥 우선순위를 350으로 두면 됩니다, 믿으세요” 정도로 넘어가는 부분입니다. Scrapy의 downloader middleware는 정해진 순서대로 실행되며, 이를 이해하지 못하면 프록시 설정 때문에 생긴 문제를 전혀 다른 문제로 오해하게 됩니다.

요청 쪽 훅(process_request)은 오름차순으로 실행됩니다. 숫자가 낮을수록 먼저입니다. 응답 쪽 훅(process_response, process_exception)은 내림차순으로 실행됩니다. 즉, 숫자가 높은 쪽부터 다시 되돌아옵니다.

Request flow (ascending):
  Spider → [100] RobotsTxt → [300] HttpAuth → [350] YOUR PROXY MIDDLEWARE
         → [500] UserAgent → [550] Retry → [700] Cookies → [750] HttpProxy
         → Downloader

Response flow (descending):
  Downloader → [750] HttpProxy → [700] Cookies → [550] Retry
             → [500] UserAgent → [350] YOUR PROXY MIDDLEWARE → [300] HttpAuth
             → [100] RobotsTxt → Spider

Scrapy 내장 미들웨어의 현재 기본 우선순위 표는 다음과 같습니다(2.17.0 기준 확인):

미들웨어기본 우선순위
RobotsTxtMiddleware100
HttpAuthMiddleware300
DownloadTimeoutMiddleware350
DefaultHeadersMiddleware400
UserAgentMiddleware500
RetryMiddleware550
RedirectMiddleware600
CookiesMiddleware700
HttpProxyMiddleware750
DownloaderStats850
HttpCacheMiddleware900

RetryMiddleware가 재시도를 예약하면 실패한 요청을 복사하는데, 여기에는 메타데이터도 포함됩니다. 이렇게 복사된 새 요청은 다시 downloader middleware 체인에 들어갑니다. 선택기의 숫자가 550과 어떤 관계가 있느냐는 로테이션을 보장하지 않습니다. 로테이션은 선택기 코드가 재시도를 인식하고 복사된 meta["proxy"] 값을 의도적으로 덮어쓸 때만 발생합니다. 350은 기본 전송 단계인 750보다 먼저 실행되는 편리한 위치일 뿐, 그 자체로 로테이션 메커니즘은 아닙니다.

Scrapy request and response paths through priorities 350, 550, and 750, with a 503 retry re-entering the middleware chain

흔한 미들웨어 순서 실수

  • 미들웨어를 HttpProxyMiddleware와 같은 우선순위(750)로 설정하는 경우: Scrapy의 딕셔너리 순서(로직이 아니라)가 어떤 미들웨어가 먼저 요청을 처리할지 결정하는 경쟁 상태가 생깁니다. 증상: 프록시 동작이 간헐적으로, 설명 불가능하게 달라짐.
  • 회전 선택기에서 setdefault() 또는 if "proxy" not in request.meta를 사용하는 경우: 재시도로 복사된 요청이 이전 프록시를 그대로 유지합니다. 증상: 모든 재시도가 같은 실패 경로를 반복함. 해결: 재시도 여부(retry_times > 0 등)를 감지하고, 선택기 소유의 프록시 값을 명시적으로 교체하세요.
  • HttpProxyMiddleware를 완전히 비활성화하는 경우: 일부 튜토리얼은 “커스텀 미들웨어가 다 처리한다”는 이유로 "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": None으로 설정하라고 합니다. 하지만 그렇지 않습니다. 커스텀 미들웨어는 선택기일 뿐 전송 계층이 아닙니다. 기본 미들웨어를 끄면 프록시 인증 헤더가 절대 붙지 않고, 요청은 인증 없이 나가거나 아예 나가지 않습니다.

4단계: 프록시 인증 정보를 하드코딩하지 않기

제가 본 상위 랭킹 Scrapy 프록시 튜토리얼 대부분은 http://username:password@proxy.example.com:8080을 Python 파일에 그대로 씁니다. 그건 Git 히스토리에 영구히 남는 자격 증명입니다. 누군가 print()를 대충 찍어 로그를 남기면, 모든 clone과 fork, 그리고 각종 로그 덤프에까지 따라다닙니다.

이 문제를 다루는 방법은 크게 세 단계입니다:

방식보안성유연성적합한 용도
spider/settings.py에 하드코딩나쁨 — 비밀값이 저장소에 남음낮음빠른 로컬 테스트만
http_proxy 환경 변수(Scrapy 기본)더 좋음 — 코드 밖으로 분리낮음(프록시 1개)CI/CD, Docker
.env 파일 + python-dotenv + from_crawler가장 좋음 — 코드 밖, 환경별 분리높음(다중 프록시, 로테이션 가능)프로덕션 스크래퍼

세 번째 방법은 제대로 구축할 가치가 있습니다. python-dotenv를 설치하고 .env 파일을 만든 뒤, 즉시 .gitignore에 추가하세요. 정말로 지금 하세요:

PROXY_USER=myuser
PROXY_PASSWORD=my$ecret!Pass
PROXY_HOST=proxy.example.com
PROXY_PORT=8080

그다음 settings.py 상단에서 로드합니다:

from dotenv import load_dotenv
import os

load_dotenv()

PROXY_USER = os.getenv("PROXY_USER")
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")
PROXY_HOST = os.getenv("PROXY_HOST")
PROXY_PORT = os.getenv("PROXY_PORT")

그리고 middleware 안에서는 from_crawler를 통해 안전하게 읽습니다. 이 방식은 “scrapy crawl 전에 어떻게 값을 넣죠? 실행할 때마다 자격 증명이 달라질 수 있는데요”라는 질문에 대한 정확한 해결책입니다:

import os
from urllib.parse import quote

class SecureProxyMiddleware:
    def __init__(self, user, password, host, port):
        self.user = quote(user, safe="")
        self.password = quote(password, safe="")
        self.host = host
        self.port = port

    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings
        return cls(
            user=settings.get("PROXY_USER"),
            password=settings.get("PROXY_PASSWORD"),
            host=settings.get("PROXY_HOST"),
            port=settings.get("PROXY_PORT"),
        )

    def process_request(self, request, spider):
        proxy_url = f"http://{self.user}:{self.password}@{self.host}:{self.port}"
        request.meta["proxy"] = proxy_url

여기서 자격 증명에 urllib.parse.quote()를 적용한 점을 보세요. 비밀번호에 @, :, / 같은 문자가 들어가면(비밀번호 생성기는 이런 문자를 자주 넣습니다), 먼저 퍼센트 인코딩하지 않으면 URL 파싱이 깨집니다. 이 한 줄이면 “invalid proxy URL” 오류를 잡느라 허비하는 민망한 한 시간을 아낄 수 있습니다. 문제는 프록시 자체가 아니라 URL 형식일 가능성이 높습니다.

자격 증명은 로그에 남기지 말고, 대표적이지만 가짜 값으로 퍼센트 인코딩을 테스트하세요. HTTPS 터널링, SOCKS5, 비라틴 문자 자격 증명은 핸들러별 경계가 다릅니다. 테스트로 검증되지 않은 인증 패턴이 모든 환경에서 동일하게 동작한다고 가정하면 안 됩니다.

Secure flow from a locked environment file through percent-encoded proxy settings into Scrapy request metadata

5단계: 프록시 로테이션 추가하기

좋은 프록시 하나라도 같은 사이트에 5,000번 요청하면 결국 탐지될 가능성이 큽니다. 풀(pool)이 필요합니다.

옵션 A — 직접 구현하기. 정말 간단합니다:

import random

class RotatingProxyMiddleware:
    def __init__(self, proxy_pool):
        self.proxy_pool = proxy_pool

    @classmethod
    def from_crawler(cls, crawler):
        return cls(proxy_pool=crawler.settings.getlist("PROXY_POOL"))

    def process_request(self, request, spider):
        request.meta["proxy"] = random.choice(self.proxy_pool)

기본 사용에는 충분하지만, 어떤 프록시가 실제로 살아 있는지에 대한 정보가 전혀 없습니다. 매 요청마다 그냥 주사위를 던지는 셈입니다.

옵션 B — scrapy-rotating-proxies 사용하기. 이 서드파티 패키지는 기본으로 차단 감지와 자동 백오프를 제공합니다:

pip install scrapy-rotating-proxies

다만 솔직히 말하면, PyPI의 최신 릴리스는 2019년 배포된 0.6.2 버전이고, 프로젝트 태그도 Alpha입니다. 최신 Scrapy에서 반드시 깨진다는 뜻은 아니지만, “2019년 이후 적극적으로 유지보수되지 않았다”는 사실은 “2026년 프로덕션 트래픽에 대해 검증됐다”와 전혀 다릅니다. 버전을 고정하고, 실제 대상 사이트에서 테스트하고, 인증 프록시 엔드포인트를 잘 처리할 거라고 가정하지 마세요. 대체로 그렇지 않습니다.

6단계: 장애 허용 프록시 미들웨어 만들기(죽은 프록시 감지)

이 부분은 경쟁 튜토리얼이 거의 다 건너뛰는 영역입니다. 데모와 6시간짜리 무인 크롤링을 버텨내는 시스템의 차이는 바로 여기서 납니다.

시나리오대부분의 튜토리얼이 보여주는 것이 미들웨어가 추가하는 것
프록시가 407 반환다루지 않음프록시 인증 실패로 처리
대상이 403/429 반환대개 한데 묶음정책/레이트 피드백과 프록시 상태를 분리
프록시 타임아웃다루지 않음설정 가능한 타임아웃 기준, 헬스 점수 감소
모든 프록시가 죽음다루지 않음부드러운 폴백 또는 로그 경고와 함께 크롤 일시 중지
프록시가 들쭉날쭉함(간헐적 실패)다루지 않음풀에 다시 넣기 전 쿨다운 기간 적용
import time
import random
from scrapy.exceptions import IgnoreRequest

class FaultTolerantProxyMiddleware:
    MAX_FAILURES = 3
    COOLDOWN_SECONDS = 300

    def __init__(self, proxy_pool):
        self.pool = {p: {"failures": 0, "banned_until": 0} for p in proxy_pool}

    @classmethod
    def from_crawler(cls, crawler):
        return cls(proxy_pool=crawler.settings.getlist("PROXY_POOL"))

    def _healthy_proxies(self):
        now = time.time()
        return [p for p, s in self.pool.items() if s["banned_until"] < now]

    def process_request(self, request, spider):
        healthy = self._healthy_proxies()
        if not healthy:
            spider.logger.warning("All proxies unhealthy — pausing crawl")
            raise IgnoreRequest("No healthy proxies available")
        request.meta["proxy"] = random.choice(healthy)

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy")
        if proxy and response.status == 407:
            self._mark_failure(proxy)
        return response

    def process_exception(self, request, exception, spider):
        proxy = request.meta.get("proxy")
        if proxy:
            self._mark_failure(proxy)

    def _mark_failure(self, proxy):
        state = self.pool[proxy]
        state["failures"] += 1
        if state["failures"] >= self.MAX_FAILURES:
            state["banned_until"] = time.time() + self.COOLDOWN_SECONDS
            state["failures"] = 0

실제로 만들어보면 몇 가지를 꼭 기억해야 합니다. 모든 403을 프록시 자체의 죽음으로 해석하면 안 됩니다. RFC 9110에 따르면 403은 “서버가 이해했지만 거부했다”는 뜻이므로, 프록시 문제가 아니라 헤더나 세션이 수상해서 그런 것일 수도 있습니다. 반면 407은 프록시가 인증을 거부했다는 뜻이라 훨씬 더 직접적이고 구체적인 신호입니다. 이 두 신호를 같은 바구니에 넣으면 멀쩡한 프록시를 이유 없이 소진하게 됩니다.

프로덕션에서는 Scrapy의 일시적 재시도 기본값을 명시적으로 적어두어 정책을 리뷰할 수 있게 하세요:

RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]
RETRY_TIMES = 2

이 값들은 Scrapy 2.17의 기본값입니다. 403이나 407을 무작정 추가하지 마세요. 403은 여러 원인으로 발생할 수 있는 대상 거부이고, 407은 일반적인 요청 재시도로는 해결되지 않는 프록시 인증 문제입니다. 특정 대상 계약상 다른 상태 코드를 재시도해야 한다면, 그 이유를 문서화하고 별도로 테스트하세요.

Distinct handling for 407 authentication failure, 429 rate limiting, 503 retry, 200 success, and a closed gate when proxies are unavailable

7단계: 비용을 고려한 프록시-on-retry 패턴

일부 승인된 대상은 정상 콘텐츠를 직접 반환하지만, 문서화된 일시적 응답 이후에만 프록시가 필요할 수 있습니다. 이렇게 하면 프록시 사용량을 줄일 수는 있지만, 보편적으로 말할 수 있는 절감률이나 직접 요청 성공률은 없습니다. 이 패턴을 쓰기 전에 여러분의 실제 워크로드로 측정해야 합니다.

Scrapy의 공개 get_retry_request() 헬퍼를 사용하고, 에스컬레이션은 명시적으로 분류한 대상 전용 상태 코드로 제한하세요. 아래 예시는 429와 503을 재시도 가능한 압력 신호로 취급하며, 403과 407은 의도적으로 제외합니다.

from scrapy.downloadermiddlewares.retry import get_retry_request

class CostAwareEscalationMiddleware:
    def process_response(self, request, response, spider):
        if response.status not in {429, 503}:
            return response

        retry = get_retry_request(
            request,
            spider=spider,
            reason=f"proxy_escalation_{response.status}",
            max_retry_times=2,
        )
        if retry is None:
            return response

        current_tier = request.meta.get("proxy_tier", "direct")
        if current_tier == "direct":
            retry.meta["proxy"] = spider.settings["DATACENTER_PROXY"]
            retry.meta["proxy_tier"] = "datacenter"
        elif current_tier == "datacenter":
            retry.meta["proxy"] = spider.settings["RESIDENTIAL_PROXY"]
            retry.meta["proxy_tier"] = "residential"
        else:
            return response
        return retry

직접 요청의 유효 콘텐츠 비율, 프록시별 유효 콘텐츠 비율, 성공 레코드당 전송 바이트, 성공당 재시도 횟수, 추가 지연 시간을 기록하세요. direct-first 설계는 직접 접속이 승인된 경우에만 허용되며, 프록시가 필요할 때는 반드시 fail-closed로 동작해야 합니다. 절감액은 “가정한 비율”이 아니라 프록시 트래픽이 실제로 얼마나 줄었는지로 측정해야 합니다.

직접 프록시 관리가 그럴 가치가 없을 때

이 글의 나머지가 무의미하다는 뜻으로 들리지 않게 분명히 말하자면, 위 내용은 모두 실제로 유용한 엔지니어링입니다. 특히 대용량 크롤링, 커스텀 파이프라인, 요청 스케줄링을 완전히 제어해야 하는 프로젝트라면 올바른 선택입니다.

하지만 진짜 목표가 “프록시 인프라를 운영하는 것”이 아니라 “이 페이지에서 구조화된 데이터를 얻는 것”이라면, 추출 API가 더 나은 경계일 수 있습니다. Thunderbit의 현재 POST /extract 문서는 페이지 URL과 선택적 JSON Schema를 받습니다. 스키마를 생략하면 서비스가 페이지 콘텐츠를 기반으로 스키마를 생성할 수도 있습니다. 이 엔드포인트는 none, basic, full 렌더 모드와 timeout 및 로드 이후 대기 제어도 제공합니다. 현재 지원되는 요청 표면에는 prompt-only 추출이 포함되지 않으므로, 프로덕션 연동은 문서화된 스키마 계약을 기준으로 설계해야 합니다. 이 방식은 추출 인터페이스를 한 번의 요청 뒤로 숨겨줄 뿐, 모든 anti-bot이나 CAPTCHA 대상에 대한 보편적 해결책을 보장하지는 않습니다.

| 요소 | 직접 구현한 Scrapy + 프록시 | API 기반 추출(예: Thunderbit) | |---|---|---|---| | 설정 난이도 | 높음 — 미들웨어, 로테이션, 재시도 로직 필요 | 낮음 — 스키마가 있는 API 호출 1회 | | 네트워크/렌더링 동작 | 핸들러, 프록시, 헤더, 지연을 직접 구성 | 문서화된 API 옵션으로 제어 | | 유지보수 | 셀렉터, 풀 상태, 대상 변경을 직접 관리 | 스키마 품질, 검증, 통합 동작을 관리 | | 비용 모델 | 프록시 비용 + 컴퓨팅 + 엔지니어링 시간 | 현재 문서 기준 페이지당 20 유닛(2026-08-10 확인) | | 제어 수준 | 높음 — 커스텀 파이프라인, 미들웨어 체인 전체 | API 기능 범위 내로 제한 | | 적합한 경우 | 복잡한 크롤링, 대용량, 커스텀 로직 | 타겟 추출, 프로토타이핑, 데이터 보강 |

리드 리스트, 상품 데이터, 리서치용으로 구조화된 페이지를 주로 추출한다면, Thunderbit Chrome Extension이나 API로 대표 샘플을 먼저 돌려보고, 유효 레코드 수·지연 시간·소모 유닛·유지보수 시간을 비교해 보세요. 프로젝트에 커스텀 크롤 그래프와 파이프라인 제어가 필요하다면, 여전히 Scrapy가 더 강력한 선택입니다.

더 알아보기

팁 및 흔한 실수

  • 팁: 실제 대상에 넣기 전에 httpbin.org/ip로 프록시를 먼저 테스트하세요. 복잡한 구성을 얹기 전에 라우팅이 제대로 되는지 확인하는 가장 빠른 방법입니다.
  • 실수: request.meta가 아니라 request.headers에 프록시를 넣는 경우. 정말 흔한 오타성 버그입니다. HttpProxyMiddlewaremeta["proxy"]만 읽기 때문에, 헤더에 넣으면 눈에 띄는 오류 없이 조용히 실패합니다.
  • 실수: HTTP와 HTTPS 프록시를 동일하게 설정한다고 가정하는 경우. HTTP 프록시 URL이 HTTPS 대상에 라우팅되는 것은 호환되는 handler에서는 보통 CONNECT 터널링으로 동작하지만, 프록시 엔드포인트 자체를 https:// 스킴으로 두는 것은 다른 이야기이며 지원 수준도 다릅니다. 두 개념을 혼동하지 마세요.
  • 팁: SOCKS5가 필요하다면 먼저 사용 중인 Scrapy 버전의 download handler 기능을 확인하세요. Scrapy 2.17의 실험적 Httpx handler는 httpx[socks]를 통해 SOCKS5를 지원하지만, 여전히 experimental 표시입니다. 자체 테스트 없이 프로덕션 의존성으로 삼지 마세요.

대안 방법

scrapy-rotating-proxies 외에도, 일부 팀은 프록시 로직 전부를 provider gateway로 넘깁니다. 즉, 단일 프록시 URL만 두고, 벤더가 내부적으로 로테이션, 세션 고정, 지역 타게팅을 처리하는 방식입니다. 이 방식은 제어권을 조금 줄이는 대신 미들웨어 코드를 크게 줄여주므로, 직접 풀을 구축하기 전에 비용 비교를 해볼 가치가 있습니다.

결론

Scrapy에서 프록시를 설정하는 건 한 줄이면 끝납니다. 하지만 실제 프로덕션 크롤링을 버티게 하려면 명시적인 실패 정책, 안전한 자격 증명 관리, 검증된 핸들러 경계, 그리고 재시도 시 복사된 프록시 메타데이터를 의도적으로 덮어쓰는 선택기 코드가 필요합니다. 이 글에서 두 가지 습관만 가져가세요. 프록시가 필요할 때는 반드시 fail-closed로 동작할 것, 그리고 프록시 비밀번호를 Python 파일에 절대 커밋하지 말 것.

자주 묻는 질문

인증이 필요한 커스텀 프록시를 Scrapy에서 어떻게 설정하나요? protocol://username:password@host:port 형식을 사용하면 됩니다. 다만 username과 password에 특수 문자가 있다면 urllib.parse.quote()로 먼저 퍼센트 인코딩하세요. 프로덕션에서는 비밀값을 하드코딩하지 말고, 환경 변수에서 읽어오는 from_crawler 클래스메서드를 통해 가져오는 것이 좋습니다.

Scrapy에서 커스텀 프록시 미들웨어의 우선순위는 몇 번이 적당한가요? 350이 흔한 선택자 우선순위입니다. HttpProxyMiddleware의 750보다 먼저 실행되기 때문입니다. 하지만 이 숫자가 로테이션을 보장하지는 않습니다. 재시도 요청이 복사되었을 때 선택기가 이를 인식하고 이전 meta["proxy"] 값을 덮어써야만 새 프록시가 적용됩니다.

Scrapy에서 죽은 프록시는 자동으로 어떻게 처리하나요? process_responseprocess_exception에서 프록시별 실패 횟수를 추적하고, 실패 임계값을 넘으면 활성 풀에서 제외했다가 쿨다운 이후 다시 넣는 미들웨어를 만드세요. 영구 차단보다 이런 방식이 운영에 더 적합합니다.

Scrapy에서 SOCKS5 프록시를 사용할 수 있나요? Scrapy 2.17의 실험적 HttpxDownloadHandlerhttpx[socks]가 설치되어 있으면 SOCKS5를 지원합니다. 하지만 기본 HTTP/1.1 handler는 SOCKS 프록시를 지원하지 않습니다. 프로덕션용으로 간주하기 전에 handler와 버전을 고정하고 통합 테스트를 진행하세요.

proxy-on-retry 패턴으로 실제 얼마나 프록시 비용을 아낄 수 있나요? 보편적인 비율은 없습니다. 직접 요청으로 유효 콘텐츠가 반환되는 승인된 요청의 비중, 각 프록시 계층별 전송 바이트, 성공 레코드당 재시도 횟수, 추가 지연 시간을 측정하세요. 관측된 프록시 트래픽 감소량이 곧 절감액입니다. direct-first 접속이 승인되지 않았거나 유효하지 않다면 이 패턴을 사용하지 마세요.

Ke
Ke
Thunderbit CTO | 시니어 데이터 사이언티스트 & ML 전문가 머신러닝과 데이터 과학 분야에서 약 10년에 가까운 경험을 쌓아온 Ke Shen은 컬럼비아 대학교 출신이며, 전 Walmart Labs의 시니어 데이터 사이언티스트였습니다. Python, R, Java, 통계 분야에서 동료들에게도 인정받는 깊은 전문성을 바탕으로, 복잡한 AI 알고리즘을 이론에서 실제 운영 수준의 아키텍처로 전환하는 데 필요한 실전 인사이트를 공유합니다.
Topics
Scrapy 프록시 미들웨어Python 웹 스크래핑프록시 로테이션
목차
Thunderbit · AI 웹 데이터 에이전트

원클릭 내 모든 페이지에서 데이터 추출

25만 명 이상의 사용자에게 신뢰받는
무료 플랜 이용 가능
AI를 사용하여 데이터 추출
Google Sheets, Airtable 또는 Notion으로 데이터를 쉽게 전송하세요
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week