스크래핑 작업을 시작할 때 반사적으로 헤드리스 브라우저를 찾게 된다. Playwright, Puppeteer, Selenium은 실제 브라우저가 로드할 수 있는 모든 것을 로드할 수 있으므로 안전한 기본 선택처럼 느껴진다. 하지만 헤드리스 브라우저는 페이지를 가져오는 가장 비용이 많이 드는 방법이다. CPU와 메모리를 소모하고, 페이지 전체와 거기에 딸린 모든 자산을 내려받으며, 속도도 느려서 워커당 수집할 수 있는 페이지 수에 상한을 만든다. 많은 경우 순수 텍스트로 이미 존재하던 몇 개의 필드를 추출하기 위해 이 모든 비용을 지불하는 셈이다. 진짜 질문은 습관적으로 “브라우저를 쓸 것인가 말 것인가”가 아니라, 특정 페이지가 실제로 무엇을 요구하는가이다. 이를 판단하는 방법을 살펴본다.
헤드리스 브라우저가 실제로 제공하는 것
헤드리스 브라우저는 눈에 보이는 창이 없는 실제 브라우저 엔진이다. 페이지의 JavaScript를 실행하고, DOM을 구성하며, 최신 페이지가 만드는 백그라운드 fetch와 XHR 호출을 수행하고, 클라이언트 측 콘텐츠를 렌더링하며, 클릭, 스크롤, 타이핑으로 결과와 상호작용할 수 있게 해준다. 또한 진짜 브라우저이기 때문에 완전하고 사실적인 브라우저 핑거프린트와 TLS 핸드셰이크도 제공한다. 이는 상당한 능력이다. 문제는 이 모든 것 하나하나가 비용을 수반한다는 점이며, 대부분의 페이지는 그 대부분을 필요로 하지 않는다.
일반 HTTP로 충분한 경우, 생각보다 훨씬 흔하다
브라우저를 자동화하기 전에 페이지가 실제로 무엇으로 구성되어 있는지 살펴봐야 한다. 상당수의 경우 Python의 requests나 httpx 같은 일반 HTTP 클라이언트만으로 충분하다.
첫 번째 경우는 서버에서 렌더링된 HTML이다. 원하는 데이터가 초기 HTML 응답에 존재한다면, 페이지 소스를 보거나 URL에 curl을 실행하면 그 자리에 있는 것을 볼 수 있으며, 이 경우 브라우저는 오버헤드 외에 아무것도 더해주지 않는다. 두 번째, 그리고 사람들이 가장 자주 놓치는 경우는 배후의 JSON API이다. 최신 페이지는 페이지 자체가 호출하는 백엔드 엔드포인트에서 렌더링되는 경우가 매우 흔하며, 네트워크 패널을 열어 XHR 요청을 관찰하면 원하는 데이터를 정확히 담은 깔끔한 JSON 응답을 자주 발견하게 되는데, 이 경우 HTML 파싱이 전혀 필요 없다. 그 엔드포인트를 HTTP로 직접 호출하는 것이 그것을 소비하는 페이지를 렌더링하는 것보다 더 빠르고, 더 안정적이며, 파싱하기도 더 쉽다. 세 번째 경우는 정적이거나 가볍게 동적인 페이지로, 중요한 것이 클라이언트 측 스크립팅에 의존하지 않는 경우다.
이 경로는 단순할 뿐 아니라 비용도 극적으로 저렴하다. HTTP 요청 하나로 요청한 바이트만 가져올 뿐 그 외에는 아무것도 가져오지 않으므로 빠르고, 병렬화가 잘 되며, 프록시를 통해 이동하는 데이터의 양도 훨씬 적다. 이 마지막 지점은 비용과 직결되는데, 일반적인 클라이언트 패턴은 Python에서 레지덴셜 프록시 사용하기 가이드에 나와 있으며, HTTP 경로를 유지하는 것은 프록시 대역폭 절감과 지연 시간 낮추기를 위한 가장 큰 지렛대 중 하나다.
진짜로 브라우저가 필요한 경우
일부 페이지는 정말로 브라우저를 필요로 하며, 이런 페이지에 HTTP를 강요하는 것도 그 자체로 시간 낭비다. 데이터가 JavaScript 실행 후에만 존재하는 경우, 즉 접근 가능한 API가 뒤에 없이 클라이언트 측에서 콘텐츠를 렌더링하는 싱글 페이지 앱이라면 헤드리스 브라우저를 사용해야 한다. 데이터가 상호작용을 통해 조립되는 경우, 즉 스크롤하거나 클릭할 때만 더 가져오는 무한 스크롤과 지연 로딩 페이지네이션의 경우에도 브라우저를 사용해야 한다. 로그인이나 다단계 흐름이 사용 가능한 세션을 확립하기 위해 클라이언트 측 스크립팅에 의존하는 경우에도 브라우저를 사용해야 한다. 그리고 사이트가 실제 브라우저 컨텍스트인지 적극적으로 확인하며, 순수 HTTP 클라이언트로는 충족할 수 없는 JavaScript 챌린지를 실행하거나 브라우저 전용 신호를 검사하는 경우에도 브라우저를 사용해야 한다. 이런 경우 브라우저는 오버헤드가 아니라 데이터를 얻을 수 있는 유일한 도구다.
브라우저 경로의 비용을 명확히 짚어보기
브라우저를 사용하기로 했다면 무엇에 비용을 지불하는지 알아야 한다. 가장 큰 숨은 비용은 대역폭이다. 브라우저는 사람이 그러하듯 페이지 전체, 즉 HTML에 더해 모든 이미지, 스타일시트, 폰트, 트래커, 서드파티 스크립트를 로드하며, 이는 실제로 원했던 하나의 HTML 문서나 JSON 덩어리보다 여러 배 클 수 있다. 이 바이트 하나하나가 프록시를 거쳐 이동한다. 해결책은 필요 없는 리소스 타입, 즉 이미지, 미디어, 폰트, 애널리틱스를 차단하여 브라우저가 페이지 전체를 다운로드하지 않고도 데이터를 만들어낼 만큼만 렌더링하게 하는 것이며, 이는 브라우저 경로에서 프록시 대역폭을 상당히 줄일 수 있다. 두 번째 비용은 속도다. 렌더링은 느리므로 브라우저 워커는 HTTP 워커보다 분당 훨씬 적은 페이지를 수집하며, 브라우저는 메모리를 많이 소모하므로 여러 대를 확장하는 것은 HTTP 요청을 쏘는 것보다 훨씬 무거운 인프라다. 세 번째 비용은 많은 팀에게 뜻밖일 텐데, 헤드리스 브라우저가 자동으로 더 은밀해지지는 않는다는 점이다. 그대로 사용하면 자체적으로 탐지 가능한 흔적을 남기므로, 허술하게 설정된 헤드리스 셋업은 잘 구성된 HTTP 요청보다 오히려 더 쉽게 걸릴 수 있다.
대부분이 건너뛰는 중간 경로
“브라우저가 필요하다”고 느끼는 상황의 상당수는 사실 “진짜처럼 보이는 요청이 필요하다”는 것이다. 완전한 렌더링으로 확대하기 전에, 정확하고 일관된 핑거프린트를 갖춘 일반 HTTP를 시도해보라. 완전하고 일관된 헤더, 적절한 쿠키 처리, 그리고 일치하는 TLS 및 HTTP/2 핑거프린트를 갖추면, 브라우저 형태의 핑거프린트 집합을 지닌 요청은 순수 클라이언트가 차단되는 곳에서도 종종 통과한다. 여기에 차단 회피라는 일반적인 원칙을 결합하면, 브라우저를 요구하는 것처럼 보였던 많은 페이지가 실제로는 훨씬 적은 비용으로 잘 구성된 HTTP 요청에 순순히 응답한다는 것을 알게 된다. 데이터가 정말로 다른 방법으로는 얻어지지 않을 때만 브라우저로 확대하라.
어느 쪽이든 프록시는 필요하다
브라우저 대신 HTTP를 선택한다고 해서 깨끗한 네트워크 아이덴티티에 대한 필요성이 사라지는 것은 아니며, 두 경로 모두 평범한 방문자처럼 보이기 위해 레지덴셜 IP를 거친다. 달라지는 것은 그것을 위해 지불하는 비용이다. 브라우저는 페이지당 프록시를 통해 훨씬 더 많은 데이터를 밀어 넣으므로, 종량제 레지덴셜 대역폭에서는 선택하는 도구가 비용에 직접적인 영향을 미친다. 레지덴셜 프록시와 GB당 요금제는 어느 접근 방식을 쓰든 내부적으로 동일하며, 바로 이 때문에 가능할 때 더 가벼운 도구를 선택하는 원칙이 그만한 가치가 있는 것이다.
짧은 의사결정 흐름
자동화 코드를 작성하기 전에 각 대상에 대해 동일한 빠른 점검을 실행하라.
- 데이터가 초기 HTML에 있는가? HTTP를 사용하라.
- 페이지가 데이터를 반환하는 JSON 또는 XHR 엔드포인트를 호출하는가? 그 엔드포인트를 HTTP로 호출하라.
- 콘텐츠가 JavaScript 실행 후에만 나타나며 접근 가능한 API가 없는가? 헤드리스 브라우저를 사용하라.
- 데이터가 스크롤이나 클릭을 통해서만 로드되는가? 헤드리스 브라우저를 사용하라.
- 올바른 데이터 경로를 갖고도 차단당하는가? 먼저 핑거프린트와 IP를 고친 뒤, 최후의 수단으로 브라우저를 고려하라.
실전에서는 view-source와 네트워크 패널을 빠르게 살펴보는 것만으로 무엇에 손을 대기도 전에 처음 두 질문에 답할 수 있으며, 이 처음 두 가지가 대부분의 사람들이 예상하는 것보다 더 많은 사이트를 커버한다. 렌더링이 꼭 필요할 때는 가볍게 유지하라.
# HTTP first: hit the underlying JSON endpoint the page already callsimport requests
PROXY = "http://customer-USERNAME-country-us:PASSWORD@p.shifter.io:443"proxies = {"http": PROXY, "https": PROXY}
r = requests.get("https://shop.example.com/api/products?page=1", proxies=proxies, timeout=15)r.raise_for_status()data = r.json() # structured data, no rendering, minimal bytes
# Only if rendering is truly required, run a browser through the same# proxy and block heavy resource types so it does not pull the whole page:# route.abort() on image / media / font / stylesheet requests# before reading the rendered content.결론
헤드리스 브라우저는 클라이언트 측 렌더링과 진정한 상호작용에는 옳은 도구이지만, 그 외 나머지, 즉 웹의 대부분에서는 틀린 도구다. 무언가를 자동화하기 전에 데이터가 HTML에 있는지 아니면 JSON 엔드포인트 뒤에 있는지 확인하고, 브라우저를 띄우기보다는 진짜 핑거프린트를 갖춘 잘 구성된 HTTP 요청을 선호하며, 렌더링을 해야 할 때는 필요 없는 것을 차단해서 몇 개의 필드를 위해 페이지 전체를 다운로드하는 비용을 치르지 않도록 하라. 두 경로 모두 레지덴셜 IP가 필요하지만, 레코드마다 페이지 전체를 프록시로 보내는 것은 그중 하나뿐이다. 브라우저는 맨 먼저가 아니라 맨 마지막에 손을 뻗어야 하며, 데이터를 안정적으로 얻을 수 있는 가장 저렴한 도구가 바로 써야 할 도구다.
그 네트워크 계층을 제공하는 것이 레지덴셜 프록시이며, 순수 HTTP로 가져오든 완전한 브라우저를 구동하든 동일하게 작동하는 방대한 실제 가정용 IP 풀이다. GB당 요금제가 더 가벼운 경로가 이득이 되는 이유이며, 작업이 요구하는 만큼 많은 대상을 상대로 실제로 필요한 데이터만 이동시키게 된다.