나는 Headless Chrome을 스크래핑 전략으로 취급하는 것을 그만두었다
요약
본 글은 웹 스크래핑 전략에서 Headless Chrome을 기본값으로 사용하는 것에 대한 비판적 시각을 제시합니다. 브라우저 기반 방식은 데모에는 효과적이지만, 프로덕션 환경에서는 높은 비용(시작 시간, 메모리 등)과 복잡성을 초래하며, 모든 페이지에 적용하는 것은 과도한 접근입니다. 진정한 스크래핑 성공은 단순히 '접근 가능성'이나 '렌더링 가능성'을 넘어, 데이터의 실제 품질과 신뢰성에 달려 있습니다. 따라서 HTTP 클라이언트 기반의 효율적인 페치 경로와 응답 분류가 중요하며, 브라우저 사용은 예외적이고 필요한 경우에만 폴백(fallback)으로 고려해야 합니다.
핵심 포인트
- Headless Chrome을 기본 스크래핑 전략으로 사용하는 것은 비용과 복잡성 측면에서 비효율적입니다.
- 브라우저는 앱 셸이나 상호작용이 필수적인 페이지에 한해서만 사용되어야 하며, 대부분의 콘텐츠는 초기 HTTP 응답에 포함되어 있습니다.
- 스크래핑 실패의 가장 큰 위험은 '403 Forbidden' 같은 명확한 오류가 아니라, 상태 코드(200 OK)는 정상이나 내용물이 잘못된 경우입니다 (예: 봇 챌린지 페이지).
- 효율적인 스크래퍼 아키텍처를 위해서는 단순한 렌더링 여부보다 페치 경로 최적화와 응답 본문 분류가 핵심입니다.
- AI 에이전트나 LLM 앱의 경우, 잘못된 데이터를 추출하는 것이 데이터베이스 오염 이상의 심각한 문제를 야기할 수 있습니다.
Headless Chrome은 유용합니다. 하지만 동시에 많은 스크래핑 시스템이 느려지고, 비용이 많이 들며, 논리적으로 파악하기 불가능해지는 지점이기도 합니다. 저는 웹 추출 API, CLI, 그리고 에이전트와 LLM 앱을 위한 MCP 서버인 webclaw를 구축하고 있습니다. 한 가지 아키텍처 결정이 계속해서 제값을 하고 있습니다. 바로 브라우저를 기본값이 아닌 폴백(fallback)으로 사용하는 것입니다. 대상 페이지가 까다로워지기 전까지는 이 말이 당연하게 들릴 것입니다. 그러면 반사적으로 다음과 같은 행동이 나옵니다: Playwright를 사용한다. Chrome을 실행한다. 네트워크 유휴 상태(network idle)를 기다린다. DOM을 추출한다. 전송한다. 이것이 충분히 자주 작동하기 때문에 사람들은 이를 전략과 혼동하곤 합니다. 이것은 전략이 아닙니다. 그것은 값비싼 망치일 뿐입니다.
브라우저 우선 방식은 시연하기에 좋습니다
브라우저 우선 파이프라인은 간단합니다: URL -> 브라우저 -> 렌더링된 DOM -> 추출. 데모를 보여줄 때는 이것이 훌륭합니다. Puppeteer나 Playwright를 페이지에 지정합니다. JavaScript가 실행됩니다. DOM이 나타납니다. 텍스트를 가져옵니다. 모두가 박수를 칩니다. 어떤 페이지들의 경우, 그것이 정확히 당신이 필요한 것입니다. 만약 페이지가 앱 셸(app shell)이라면, 콘텐츠가 클라이언트 측 요청 후에만 나타난다면, 상호작용이 중요하거나 시각적 상태가 중요하다면 브라우저를 사용하십시오. 그 점에는 이견이 없습니다. 문제는 모든 페이지를 그런 식으로 취급할 때 시작됩니다. 실제 추출 작업의 대부분의 URL은 상호작용형 앱이 아닙니다. 그것들은 문서 페이지, 기사, 제품 페이지, 가격 페이지, 변경 로그(changelog), 지원 페이지, 마켓플레이스 목록, 블로그 포스트 또는 랜딩 페이지입니다. 유용한 콘텐츠는 종종 초기 응답에 이미 포함되어 있습니다. 이 모든 것에 Chrome을 실행하는 것은 우편함을 열기 위해 대저택의 월세를 내는 것과 같습니다.
비용은 나중에 나타납니다
Headless 브라우저 스크래핑은 적은 양에서는 괜찮아 보입니다. 하지만 프로덕션 규모에서는 다음과 같은 세금이 매우 현실적으로 다가옵니다: 시작 시간(startup time), 페이지당 메모리, 브라우저 풀 관리(browser pool management), Docker 이미지 크기, 누락된 시스템 종속성, 충돌, 좀비 프로세스, 네트워크 유휴 타임아웃(network idle timeouts), 낮은 동시성(concurrency). 이 중 흥미로운 엔지니어링은 하나도 없습니다. 그저 짐일 뿐입니다. 그리고 만약 당신이 AI 에이전트를 구축하고 있다면 상황은 더 악화됩니다. 사용자는 기다리고 있습니다. 5초의 브라우저 오버헤드는 야간 작업(nightly job) 속에 숨겨질 수 있는 것이 아닙니다. 그것은 제품 경험의 일부입니다.
만약 당신이 추출 (extraction)을 중심으로 한 SaaS를 구축하고 있다면, 이는 마진 (margins)에도 영향을 미칩니다. 불필요한 모든 브라우저 세션은 피할 수 있었던 비용입니다. 안티 봇 (Anti-bot)은 렌더링 (rendering)과 동일하지 않습니다. 이것이 수많은 잘못된 스크래퍼 아키텍처 (scraper architecture)를 만드는 실수입니다. 사람들은 이 두 가지 질문을 혼동합니다: '내가 페이지에 접근할 수 있는가?'와 '내가 페이지를 렌더링할 수 있는가?' 이들은 겹치기도 하지만, 동일한 문제는 아닙니다. 대상 (target)은 JavaScript가 중요해지기도 전에 당신의 기본 HTTP 클라이언트 (HTTP client)를 차단할 수 있습니다. 대상은 200 OK와 함께 챌린지 페이지 (challenge page)를 반환할 수 있습니다. 대상은 브라우저에서 로드될 수 있지만, 세션 (session), 요청 동작 (request behavior), 또는 응답 본문 (response body)이 잘못되어 여전히 쓸모없을 수 있습니다. 반대로, 많은 페이지는 렌더링이 전혀 필요하지 않습니다. 이들은 더 나은 페치 경로 (fetch path), 응답 분류 (response classification), 그리고 콘텐츠 추출 (content extraction)이 필요합니다. 이것이 바로 "그냥 Playwright를 사용하세요"라는 조언이 불완전한 이유입니다. 그것은 한 종류의 렌더링 문제만을 해결합니다. 그것이 신뢰성 (trust), 가짜 성공 (fake success), 응답 품질 (response quality), 비용 (cost), 또는 추출 출력 (extraction output)을 자동으로 해결해주지는 않습니다.
실제로 가장 큰 타격을 주는 실패
가장 위험한 스크래핑 실패는 '403 Forbidden'이 아닙니다. 그것은 정직합니다. 위험한 실패는 다음과 같은 모습입니다:
HTTP 200
본문 다운로드됨
추출기 (extractor)가 파이프라인 (pipeline) 실행
계속됨
데이터가 잘못됨
페이지가 실패한 것이 아닙니다. 페이지가 거짓말을 한 것입니다. 본문이 봇 챌린지 (bot challenge)였을 수도 있습니다. 로그인 벽 (login wall)이었을 수도 있습니다. 동의 화면 (consent screen)이었을 수도 있습니다. 빈 JavaScript 셸 (JavaScript shell)이었을 수도 있습니다. 혹은 당신이 필요로 하는 것이 누락된 특정 지역 버전이었을 수도 있습니다. 상태 코드 (status code)가 정상(green)이었기 때문에 당신의 코드는 성공이라고 말합니다. 하지만 당신의 다운스트림 시스템 (downstream system)은 쓰레기를 받게 됩니다. 일반적인 스크래퍼의 경우, 이는 데이터베이스를 오염시킵니다. LLM 앱의 경우, 상황은 더 심각합니다. 에이전트 (agent)가 챌린지 페이지를 요약할 수도 있습니다. 당신의 RAG 인덱스 (RAG index)가 내비게이션 텍스트를 임베딩 (embed)할 수도 있습니다. 당신의 연구 워크플로 (research workflow)가 로그인 벽을 인용할 수도 있습니다. 모델은 페치 레이어 (fetch layer)가 자신에게 헛소리를 전달했다는 사실을 알지 못합니다.
그렇기 때문에 저는 "페이지를 열 수 있는가?"보다 응답 분류 (response classification)에 더 신경을 씁니다. 제가 지금 신뢰하는 아키텍처, 즉 제가 선호하는 파이프라인은 다음과 같습니다: URL -> 브라우저와 유사한 페치 (browser-like fetch) -> 응답 분류 (classify the response) -> 유용한 콘텐츠 추출 (extract useful content) -> 콘텐츠 존재 여부 확인 (verify the content exists) -> 마크다운 (markdown) / JSON / 메타데이터 (metadata) 반환 -> 필요한 경우에만 에스컬레이션 (escalate)
중요한 부분은 결정 계층 (decision layer)입니다. 첫 번째 응답이 깨끗하다면 그것을 사용하십시오. 만약 그것이 챌린지 (challenge)라면, 에스컬레이션하십시오. 만약 그것이 빈 앱 셸 (empty app shell)이라면, 렌더링 (render)하십시오. 만약 그것이 로그인 벽 (login wall)이라면, 명확하게 실패를 알리십시오. 만약 추출 신뢰도 (extraction confidence)가 낮다면, 스크래핑이 성공한 척하는 대신 그 사실을 드러내십시오. 브라우저는 여전히 존재합니다. 다만 그것이 종교(절대적인 원칙)는 아닐 뿐입니다.
브라우저 우선보다는 브라우저 폴백 (Browser fallback beats browser-first)
다음과 같은 경우에는 브라우저를 사용하십시오:
- JavaScript 이후에만 콘텐츠가 나타나는 경우
- 페이지에 상호작용 (interaction)이 필요한 경우
- 초기 HTML이 비어 있는 경우
- 챌린지 (challenge)를 위해 브라우저 실행이 필요한 경우
- 작업이 렌더링된 상태 (rendered state)에 의존하는 경우
단순히 다음과 같은 이유로 브라우저를 사용하지 마십시오:
- 사이트가 현대적인 경우
- 사이트가 React를 사용하는 경우
- 첫 번째 단순 요청 (naive request)이 실패한 경우
- 응답을 분류하고 싶지 않은 경우
- 튜토리얼에서 Puppeteer를 사용한 경우
이러한 구분은 모든 것을 바꿉니다. 이는 지연 시간 (latency)을 바꿉니다. 이는 동시성 (concurrency)을 바꿉니다. 이는 인프라 비용 (infra cost)을 바꿉니다. 이는 시스템을 디버깅하는 난이도를 바꿉니다.
Webclaw이 위치하는 곳
이것이 제가 webclaw에 구축하고 있는 형태입니다. 공개 인터페이스 (public interface)는 지루해야 합니다: URL 전송 -> 깨끗한 콘텐츠 획득 -> 다음 단계로 이동
API 사용 시:
curl -X POST https://api.webclaw.io/v1/scrape \ -H "Authorization: Bearer $WEBCLAW_API_KEY " \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "formats": ["markdown"], "only_main_content": true }'
TypeScript 사용 시:
import { Webclaw } from "@webclaw/sdk"; const client = new Webclaw({ apiKey: process.env.WEBCLAW_API_KEY!, }); const page = await client.scrape({ url: "https://example.com", formats: ["markdown"], only_main_content: true, }); console.log(page.markdown);
핵심은 사용자가 모든 계층을 직접 설정해야 한다는 것이 아닙니다.
핵심은 API가 유용한 컨텍스트 (context)를 반환하거나 명확하게 실패를 알려야 한다는 것입니다. "200 OK, 여기 챌린지 페이지가 있습니다. 행운을 빕니다."와 같은 방식이 되어서는 안 됩니다. 이는 에이전트 (agents), 즉 AI 에이전트들에게 더욱 중요합니다. AI 에이전트는 대부분의 경우 가공되지 않은 HTML (raw HTML)을 필요로 하지 않습니다. 그들에게 필요한 것은 깨끗한 컨텍스트입니다: 제목, 본문 내용, 링크, 표, 메타데이터 (metadata), 소스 URL (source URL), 그리고 요청 시의 구조화된 필드 (structured fields)와 같은 것들입니다. 또한 도구 계층 (tool layer)도 정직해야 합니다. 페이지를 가져올 수 없었다면 그렇다고 말해야 합니다. 콘텐츠가 누락되었다면 그렇다고 말해야 합니다. 브라우저 렌더링 (browser rendering)이 필요하다면 상위 단계로 에스컬레이션 (escalate)해야 합니다. 하지만 반환된 HTML이 무엇이든 모델에 쏟아붓고 LLM이 알아서 해결하기를 바라서는 안 됩니다. 그것은 에이전트 전략이 아닙니다. 그것은 토큰 (tokens)이 첨부된 부정 (denial)일 뿐입니다. 제가 현재 사용하는 규칙은 다음과 같습니다: 먼저 가져오기 (Fetch). 응답 분류하기 (Classify). 유용한 콘텐츠 추출하기 (Extract). 페이지가 필요함을 증명할 때만 에스컬레이션하기 (Escalate). Headless Chrome은 여전히 유용합니다. 다만 모든 스크래핑 문제에 대한 첫 번째 해답이 되기에는 비용이 너무 많이 들 뿐입니다. 더 자세한 분석은 여기서 작성했습니다: Anti-bot scraping API: browser fallback beats browser-first. 그리고 이 시리즈의 이전 포스트는 여기 있습니다: Raw HTML is where LLM context goes to die. Webclaw: https://webclaw.io
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기