
웹 확장 프로그램을 이용한 예측 가능한 웹 스크래핑: 보다 로컬화된 + Apify 접근 방식
요약
기존의 헤드리스 브라우저와 프록시 기반 웹 스크래핑 방식이 겪는 차단 문제와 낮은 성공률의 한계를 분석합니다. 이를 해결하기 위해 Apify를 활용하여 더 예측 가능하고 로컬화된 새로운 스크래핑 아키텍처를 제안합니다.
핵심 포인트
- 기존 스크래핑 방식은 IP 평판 및 CAPTCHA 대응 등 보안 기술과의 끊임없는 군비 경쟁에 직면함
- 헤드리스 브라우저와 프록시 풀 방식은 사이트 보안 업데이트에 따라 성공률이 급격히 저하됨
- 단순한 ETL 문제가 아닌, 데이터 확보 단계에서의 차단 방지가 핵심적인 경제적 요소임
- Apify 액터를 활용한 보다 안정적이고 예측 가능한 새로운 아키텍처로의 전환 필요성
병목 현상은 결코 ETL이 아니었습니다
몇 년 전, 저는 이전 직장을 위해 데이터 파이프라인을 구축하고 운영했습니다. 스크래핑을 원치 않는 사이트들로부터 대규모의 공개 부동산 매물 목록과 인물 검색 기록을 집계하는 작업이었습니다. 일단 HTML을 확보하고 나면, 나머지는 일반적인 소프트웨어 엔지니어링(software engineering)이었습니다. 매물을 정규화(normalize)하고, 기록의 중복을 제거(dedupe)하며, 데이터 웨어하우스(warehouse)에 로드하는 과정이죠. 업서트(upsert) 작업 때문에 밤잠을 설치는 사람은 아무도 없습니다.
문제는 애초에 HTML을 가져오는 것이 실제 업무였다는 점입니다. "응답을 파싱(parse)한다"는 단계 이전의 모든 결정은 사실 한 가지, 즉 "차단당하지 않는 것"에 관한 결정이었습니다. IP 평판(IP reputation), CAPTCHA 해결률, 핑거프린트 타당성(fingerprint plausibility) 등은 추상적인 보안 문제가 아니라, 우리의 단위 경제성(unit economics)을 직접적으로 결정짓는 항목들이었습니다.
이를 해결하기 위해 우리가 구축한 것은 두 단계로 구성된 Docker 컨테이너 함대였습니다. 저렴한 단계(cheap tier)는 위험도가 낮은 엔드포인트(endpoints)에 대해 일반적인 HTTP 페치(fetches)를 수행했습니다. 비용이 많이 드는 단계(expensive tier)는 전체 Playwright, 실제 Chrome, Xvfb, 그리고 VNC 서버를 실행하여, 자동화가 실패했을 때 사람이 원격으로 접속해 수동으로 CAPTCHA를 해결할 수 있도록 했습니다. stealth 플러그인이 포함된 playwright-extra, 순환하는 유저 에이전트(user agents), 지속적인 쿠키 저장소(cookie jars), 인간과 유사한 타이핑 지연 시간, 회전 수명(rotation lifetime)에 따라 과금되는 주거용 프록시(residential proxies)까지. 모두 표준적인 방식들이었습니다.
그럼에도 여전히 신뢰할 수 없었습니다. 저희 로그에서 직접 가져온 수치에 따르면, 한 부동산 사이트에 대해서는 약 63%의 성공률을, 다른 사이트에 대해서는 **8%**를 기록했습니다. 한때는 X11 세션을 통해 문자 그대로 스페이스바를 누르고 있도록 xdotool을 호출하여 "누르고 있기" 챌린지를 자동으로 해결하는 코드 경로를 만들기도 했습니다. 한동안은 작동했습니다. 그러다 업체 측에서 신호를 약간 수정하자 성공률은 조용히 떨어졌고, 우리는 다시 원점으로 돌아가야 했습니다.
그것이 업계의 영구적인 상태입니다. Apify가 최근 분석한 웹 스크래핑의 향후 방향에서는 이를 끊임없는 군비 경쟁 (arms race)으로 묘사합니다. Cloudflare는 Turnstile 페이로드 (payloads)를 정기적으로 업데이트하며, 지난주에 작동했던 우회 (bypass) 방법이 오늘 실패할 수도 있습니다. 그 싸움에서 영원히 승리하는 사람은 아무도 없습니다. 여러분은 그저 싸울 필요 자체를 없애기 위해 노력할 뿐이며, 이것이 바로 이 포스트의 핵심 논지입니다.
이것은 Playwright나 stealth-plugin 생태계에 대한 비판이 아닙니다. 그들은 구조적으로 어려운 위치에서 진정으로 어려운 문제를 해결하고 있으며, 왜 그런지는 잠시 후에 자세히 설명하겠습니다. 하지만 이것이 제가 매일, 몇 달 동안 예측 가능하게 실행되어야 하는 작업에 대해 헤드리스 브라우저 (headless-browser) + 프록시 풀 (proxy-pool) 방식을 기본값으로 사용하는 것을 중단한 이유입니다. 이 포스트는 제가 대신 사용하는 아키텍처 (architecture)에 관한 것입니다. 현재 Zillow를 대상으로 하고 있으며, Apify 액터 (actor)로 구축되고 있습니다.
표준 플레이북의 불일치
대부분의 스크래핑 조언은 네 가지 움직임으로 귀결됩니다: 브라우저를 더 인간처럼 보이게 만들기, 마우스를 인간처럼 움직이게 만들기, IP를 주거용 (residential)처럼 보이게 만들기, 또는 이 세 가지를 모두 대신 해줄 사람에게 비용을 지불하기입니다. 각각의 방법은 광고된 것보다 효과가 떨어지는 구체적인 이유가 있습니다.
스텔스 플러그인(Stealth plugins)은 잘못된 계층을 공격합니다. 현대적인 행동 기반 안티봇(anti-bot) 벤더들은 Code Defender라고 불리는 것을 제공합니다 (PerimeterX는 현재 HUMAN 브랜드로 판매되고 있으며, 제가 가장 많은 시간을 들여 싸워온 대상입니다). 이는 페이지 자체의 런타임(runtime)을 감시하여 변조, 주입된 스크립트(injected scripts), 수정된 프로토타입(modified prototypes), 그리고 해당 서비스가 중요하게 여기는 API를 감싸는 함수 래퍼(function wrappers)를 찾아내는 JS 센서입니다. 제가 직접 테스트해 본 결과, 여기에는 navigator.webdriver, Canvas, WebGL, AudioContext와 같은 민감한 액세서(accessors)에 .toString()을 호출하여 그 결과를 수정되지 않은 네이티브 브라우저 코드가 반환해야 하는 값과 비교하는 과정이 포함됩니다. 스텔스 플러그인은 Object.defineProperty()를 사용하여 동일한 액세서들을 패치하며, 이는 .toString()이 보고하는 내용을 변경합니다. 즉, 자동화를 숨기기 위해 의도된 바로 그 행위가 Code Defender가 잡아내도록 설계된 바로 그 행위가 됩니다. JS 계층 아래, 즉 브라우저 엔진 자체의 소스 코드 내부에서 적용된 패치만이 살아남을 수 있습니다.
합성된 마우스 움직임(Synthetic mouse movement)은 상황을 개선하는 것이 아니라 악화시킵니다. 이는 제가 가장 강하게 반대하는 조언이자, 동시에 가장 자신 있게 반복해서 들리는 조언이기도 합니다. 프로그래밍 방식으로 디스패치(dispatch)하는 모든 이벤트, 즉 el.dispatchEvent(new MouseEvent(...))는 isTrusted: false를 포함합니다. 실제 사용자의 입력은 isTrusted: true를 가집니다. 이는 사실 DOM 이벤트 스펙 자체의 일부이며, 2016년 이후 모든 주요 브라우저에서 안정적으로 지원됩니다. JS로 디스패치된
주거용 프록시 (Residential proxies)는 평판 문제를 해결하는 동시에 그 평판을 망가뜨리기도 합니다. 풀링된 주거용 IP는 깨끗한 상태가 아닙니다. 그것은 공유된 정체성(shared identity)이며, 해당 IP를 통해 라우팅되는 다른 모든 고객으로부터 발생하는 평판 손상을 조용히 축적하며, 고객 간 교차 시스템(cross-customer systems)에 의해 점수가 매겨집니다 (PerimeterX는 이를 "집단 지성 (Collective Intelligence)"이라 부릅니다: 단일 IP의 평판은 당신이 접속 중인 사이트뿐만 아니라 PerimeterX를 실행하는 모든 사이트에서의 행동을 통해 결정됩니다). 그리고 여기에는 실제 비용이 발생합니다. 유의미한 수준의 풀을 유지하는 데 월 50달러에서 500달러가 들며, 이는 엔터프라이즈 계약에서는 미미한 금액(rounding error)일 수 있지만, 월 40달러짜리 구독형 제품에서는 비즈니스 모델을 파괴하는 비용 항목이 됩니다.
상용 스크래핑 API (Commercial scraping APIs)는 정신 건강을 위한 점검 도구일 뿐, 정답은 아닙니다. agenthustler가 작성한, Zillow를 대상으로 7개의 스크래핑 API 벤더를 비교한 ScrapeOps의 벤치마크가 이 점을 잘 보여줍니다. 가장 성능이 좋았던 ScraperAPI는 요청당 평균 6.1초에 약 98%의 성공률을 기록했습니다. Scrapfly는 성공률 면에서는 근접했으나 시간이 거의 세 배(18.2초) 더 걸렸습니다. 가장 성능이 낮았던 Scrapingdog는 요청당 19.4초에 61%의 성공률을 기록했습니다. 해당 시장의 최상위권이라 할지라도, 지속적인 지연 시간(latency)은 요청당 6초에서 20초 사이입니다. 한계점은 프록시의 품질이나 핑거프린트(fingerprint)의 정교함이 아닙니다. 바로 속도 조절(pacing)입니다. 세계에서 가장 자본력이 풍부한 스크래퍼들은 빠르게 움직이지 않습니다. 그들은 신중하게 움직입니다.
마지막 지점이 바로 이 아키텍처의 나머지 부분이 끌어당기는 핵심 실마리입니다.
통찰: 인간을 모방하지 말고, 인간이 되십시오
위의 모든 기술은 외부에서 정당한 세션(session)을 _시뮬레이션(simulate)_하려고 시도합니다. 핑거프린트를 속이거나(spoofing), 이벤트를 조작하거나(faking), IP를 세탁(laundering)하는 방식입니다. 더 간단한 방법이 있습니다. 세션을 시뮬레이션하지 마십시오. 세션을 사용하십시오.
브라우저 확장 프로그램의 콘텐츠 스크립트 (content script)는 실제 탭 내부, 즉 페이지의 실제 오리진 (origin)에서 실행됩니다. 이 스크립트가 fetch()를 호출하면, 해당 요청은 탭의 실제 쿠키 (cookies), 실제 TLS 핑거프린트 (TLS fingerprint), 실제 canvas/WebGL/AudioContext 시그니처 (signatures), 그리고 navigator.webdriver === false 상태를 그대로 포함합니다. 이 중 어느 것도 에뮬레이션(emulated)되지 않습니다. CDP 자동화 플래그도 없고, Code Defender가 잡아낼 수 있는 패치된 액세서 (patched accessor)도 없으며, 평판 검사 (reputation-check)를 위한 프록시 IP도 필요 없습니다.
저는 올해 초 이미 이와 무관한 프로젝트에서 이 패턴을 검증한 바 있습니다. 제3자 사이트의 이미 인증된 세션으로부터 실시간에 가까운 라이브 데이터를 읽어오는 확장 프로그램이었습니다. (이 패턴은 부분적으로 Matt Frisbie의 Building Browser Extensions에서 영감을 얻었습니다. 이는 제가 발견한 콘텐츠 스크립트 (content-script) 메커니즘에 관한 최고의 단일 참고 자료입니다.)
// 대상 페이지의 자체 오리진에 주입된 콘텐츠 스크립트 (content script)
async function fetchInPageContext(apiUrl, headers) {
const res = await fetch(apiUrl, {
...
위 코드는 실제 패턴을 단순화한 버전입니다. 실제 코드는 특정 제3자 API를 대상으로 하며, 이 스니펫 (snippet)에서 생략된 실제 요청 구축 (request-building) 작업을 수행합니다. 하지만 핵심적인 트릭은 바로 이것입니다: credentials: 'include' 옵션을 사용한 콘텐츠 스크립트의 fetch()입니다. 스크립트가 페이지 자체의 실행 컨텍스트 (execution context)에서 실행되기 때문에, 이 옵션 하나만으로 실제 세션이 연결됩니다. 봇이 아니기 때문에 CORS 문제도, 별도의 인증 흐름 (auth flow)도 발생하지 않습니다. 이는 실제 상호작용이 트리거할 것과 동일한 fetch를 수행하는 사용자의 실제 로그인 세션이기 때문입니다.
Zillow와 같이 WAF로 보호되고 로그아웃된 상태인 사이트에 동일한 패턴을 적용하려면 한 가지 요소가 더 필요했습니다. Zillow는 브라우징을 위해 로그인을 요구하지 않지만, CloudFront 뒤에서 PerimeterX와 AWS WAF를 실행하며 세션 동작을 적극적으로 점수화 (scoring)합니다. 이 아키텍처를 사용하면 핑거프린트 (fingerprint) 문제는 사라집니다. 하지만 페이싱 (pacing, 속도 조절) 문제는 사라지지 않으며, 그것이 남은 싸움의 전부가 되었습니다.
아키텍처: 수집 계층으로서의 확장 프로그램, 브로커 (broker)로서의 Apify
제가 구축하고 있는 시스템(가칭: zillow-leads-property-data actor)은 세 가지 구성 요소와 그 사이에서 작동하는 Apify 자체 브로커(broker)로 이루어져 있습니다:

이 다이어그램이 압축하여 보여주는 한 가지 사실은, 확장 프로그램(extension) 자체의 데이터베이스는 인메모리(in-memory) 방식뿐이라서 수집된 데이터가 자체적으로는 새로고침 시 유지되지 않는다는 점입니다. 모든 행(row)은 버퍼링된 후 /ingest를 통해 FastAPI 러너(runner)로 플러시(flush)되며(100개 행 단위 또는 5분 주기 중 먼저 도달하는 기준에 따라 배치 처리), 이 플러시 작업이 러너의 캐시를 영구적으로 유지(durable)하게 만듭니다. "dispatch shortfall / ingest" 화살표는 양방향 통로입니다. 작업(work)은 아래로 내려가고, 수집된 행(rows)은 위로 올라옵니다.
확장 프로그램 (Extension): 실제 쿠키를 보유한 탭을 통해 검색 및 상세 정보 수집을 수행하며, 매 매물마다 페이지를 새로 탐색하지 않습니다. Zillow 서버는 전체 부동산 객체(property object), 즉 resoFacts 등을 포함한 데이터를 상세 페이지의 초기 HTML 내 __NEXT_DATA__/gdpClientCache 안에 직접 서버 사이드 렌더링(server-renders)합니다. 매물당 한 번의 fetch()를 수행하고, 정규 표현식(regex)을 사용하여 임베디드된 JSON을 추출합니다.
러너 (Runner): 확장 프로그램에는 없는 영구적인 SQLite 캐시를 소유합니다. 구매자의 주문이 들어오면, 러너는 먼저 해당 캐시에서 요청에 즉시 응답할 수 있는지 확인합니다. 요청된 모든 내용이 이미 캐시에 있다면, 실시간 수집 없이 몇 초 만에 주문이 완료되는 캐시 히트(cache hit)가 발생합니다. 그렇지 않은 경우, 러너는 정확히 무엇이 누락되었는지 계산하여 그 차이(gap)만큼만 확장 프로그램으로 전달(dispatch)합니다.
액터 (Actor): 구매자 대면 계층(buyer-facing layer)입니다. 공유 KV 저장소(KV store)에 주문을 기록하고, 결과 파트가 도착할 때마다 폴링(poll)하며, 이벤트당 비용을 청구하고(매물 기본 요율 vs. 데이터 보강 요율), 주문이 완료될 때까지 기다렸다가 한꺼번에 덤프(dump)하는 방식이 아니라 주문이 이행되는 동안 행(rows)을 구매자의 데이터셋으로 스트리밍(stream)합니다.
두 경로를 동시에 보여주는 전체 주문 수명 주기(order lifecycle)는 다음과 같습니다. 왼쪽은 빠른 캐시 히트(cache-hit) 경로이며, 오른쪽은 확장 프로그램을 실행하는 캐시 미스(cache-miss) 경로입니다. 단일 주문에서 두 방식이 모두 사용될 수 있으며, 일부 행(rows)은 캐시에서 즉시 제공되는 동안 나머지 행은 실시간으로 수집됩니다.

인프라 비용 관점에서 제가 가장 좋아하는 부분은 다음과 같습니다. 소규모 규모에서는 클라우드에서 스크래핑 인프라를 전혀 실행할 필요가 없습니다. Apify가 호스팅하는 것은 액터(actor)와 KV 브로커(KV broker)뿐입니다. 실제 페칭(fetching)을 수행하는 것은 일반적인 연결을 가진 일반 컴퓨터의 브라우저 확장 프로그램이며, 이는 신뢰할 수 있는 IP와 실제 핑거프린트(fingerprint)를 얻을 수 있는 가장 저렴한 소스이기도 합니다. 프록시 풀(proxy pool)도 필요 없고, 헤드리스 브라우저(headless-browser) 팜(farm)도 필요 없으며, 따라잡기 위한 핑거프린트 위조(fingerprint-spoofing) 군비 경쟁도 필요 없습니다.
행동 페이싱(Behavioral pacing): 실제로 남은 싸움
핑거프린트 문제를 해결한다고 해서 안티 봇(anti-bot) 문제가 해결되는 것은 아닙니다. 단지 그 문제를 하나의 계층, 즉 PerimeterX의 실제 주요 신호인 행동 생체 인식(behavioral biometrics)으로 격리할 뿐입니다. 이는 브라우저가 무엇인지(what your browser is)를 보는 것이 아니라, 브라우저를 어떻게 사용하는지(how you use it)를 관찰합니다. 즉, 요청 타이밍(request timing), 세션 일관성(session coherence), 그리고 당신의 행동이 사람처럼 보이는지 아니면 스크립트처럼 보이는지를 감시합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기