모든 것을 제대로 했다. 깨끗한 레지덴셜 IP, 실제 Chrome User-Agent, 믿을 만한 헤더, 합리적인 요청 속도 제한까지. 그런데도 강력하게 보호된 타겟이 즉시 차단하며, 때로는 실제 페이지의 바이트 하나조차 보내기 전에 막아버린다. IP를 탓하며 로테이션을 하고 싶어지지만, 로테이션은 도움이 되지 않으며, 그 사실 자체가 신호다. 대부분의 스크래퍼가 생각조차 하지 않는 계층에서 지문이 채취되고 있는 것이다: HTTP 클라이언트 자체, 즉 TLS 핸드셰이크와 HTTP/2 연결 단계에서, 요청이 읽히기도 전에 말이다.
이것은 IP와도 다르고, antidetect browser가 관리하는 브라우저와 디바이스 핑거프린트와도 다른, 신원의 세 번째 계층이다. 이를 이해하는 것이 쉬운 사이트에서만 작동하는 스크래퍼와 어려운 사이트에서도 작동하는 스크래퍼를 가르는 기준이 된다.
하나가 아니라 세 개의 계층
사이트가 당신이 실제 사용자인지 판단할 때, 세 가지 독립적인 요소를 확인한다:
- 네트워크 계층: IP 주소와 그 평판. 데이터센터 IP나 플래그가 붙은 IP는 여기서 실패한다.
- 클라이언트 계층: HTTP 클라이언트가 연결을 여는 방식, TLS 핸드셰이크와 HTTP/2 동작. 스크립팅 라이브러리는 완벽한 IP에서도 여기서 실패한다.
- 디바이스 계층: 브라우저 핑거프린트, 캔버스, 폰트,
navigator등. 자동화된 브라우저는 여기서 실패할 수 있다.
깨끗한 레지덴셜 IP는 첫 번째 계층만 해결할 뿐, 그 외에는 아무것도 해결하지 못한다. TLS 핸드셰이크가 “나는 Python입니다”라고 알리고 있다면, 세상의 어떤 IP도 당신을 사람처럼 보이게 만들 수 없다. 이것이 가장 방어가 강한 타겟들이 페이지가 로드되기 전에 당신을 차단하는 이유다: 클라이언트 계층에서 연결을 거부하기 때문에, IP는 아예 영향력을 발휘할 기회조차 얻지 못한다.
TLS 핑거프린팅, 즉 JA3
모든 HTTPS 연결은 TLS 핸드셰이크로 시작되며, 첫 번째 메시지인 ClientHello가 놀랄 만큼 많은 정보를 드러낸다. 여기에는 클라이언트가 지원하는 정확한 암호 스위트가 특정 순서로 나열되며, TLS 확장, 지원되는 타원 곡선, 서명 알고리즘 등도 함께 포함된다. 이 조합은 연결을 만드는 소프트웨어의 특징을 그대로 드러내며, 놀랍도록 안정적이다.
Chrome은 하나의 독특한 ClientHello를 만든다. Firefox는 다른 것을 만든다. 시스템 OpenSSL을 사용하는 Python의 requests는 둘 중 어느 것과도 닮지 않은 것을 만든다. Go의 net/http는 자체적인 것을 만든다. 안티봇 벤더들은 이 값들을 해시화하여 압축된 핑거프린트로 만드는데, 잘 알려진 형식이 JA3와 그 후속인 JA4이며, 실제 브라우저가 어떤 모습인지에 대한 그림을 유지한다. ClientHello가 해시화되어 명백히 스크립팅 라이브러리이고 브라우저가 아닌 것으로 나타나면, 요청 라인이나 헤더, 쿠키를 보내기도 전에 핸드셰이크 단계에서 이미 플래그가 붙는다.
가혹한 부분은 이것이 드러내는 모순이다. User-Agent: Chrome을 설정했지만, TLS 핑거프린트는 Python을 통한 OpenSSL이라고 말한다. 실제 Chrome은 절대 그런 조합을 만들지 않으므로, 이 불일치는 단순히 위장 실패가 아니라 무언가가 위장하고 있다는 긍정적인 신호가 된다. User-Agent를 아예 보내지 않았을 때보다 오히려 상황이 더 나빠지는 것이다.
HTTP/2 핑거프린팅
TLS 핑거프린트가 그럴듯하다고 가정해보자. 바로 뒤에 또 다른 계층이 있다. 실제 브라우저는 HTTP/2를 사용하며, 어떻게 사용하는지도 하나의 핑거프린트다. HTTP/2 연결이 열릴 때, 클라이언트는 특정 값(헤더 테이블 크기, 최대 동시 스트림 수, 초기 윈도우 크기)을 가진 SETTINGS 프레임을 보내고, 특정한 윈도우 업데이트를 알리며, 유사 헤더(:method, :authority, :scheme, :path)를 특징적인 순서로 정렬한다. 브라우저는 스트림 우선순위 정보조차 알아볼 수 있는 패턴으로 보낸다.
HTTP 클라이언트 라이브러리는 이러한 세부 사항을 눈에 띄는 방식으로 잘못 처리하거나, 아예 HTTP/1.1로 폴백하는데, 이는 현대적인 사이트에서 그 자체로 의심스러운 신호다. 그래서 스크래퍼가 TLS 검사를 통과하더라도 한 단계 위에서 여전히 잡힐 수 있는데, HTTP/2 SETTINGS와 헤더 순서가 실제 브라우저와 일치하지 않기 때문이다. 그리고 일반 HTTP/1.1에서는 헤더의 순서와 대소문자 표기도 또 하나의 핑거프린트다: 브라우저는 대부분의 라이브러리가 재현하지 못하는 일관된 순서로 헤더를 보낸다.
헤더 설정으로는 왜 고쳐지지 않는가
이것이 이렇게 지속적인 이유는, 이 모든 것이 당신이 설정할 수 있는 값에 있지 않기 때문이다. User-Agent를 위조하고, Accept-Language를 추가하고, 브라우저의 헤더 목록을 정확히 복사해도 여전히 걸릴 수 있는데, 핑거프린트는 요청에 무엇을 넣었는지가 아니라 연결이 어떻게 만들어지는지의 속성이기 때문이다. ClientHello의 암호 순서, 라이브러리가 방출하는 HTTP/2 SETTINGS, 헤더가 네트워크에 도달하는 순서, 이것들은 당신의 코드가 아니라 TLS와 HTTP 스택이 결정한다. 헤더로는 JA3 불일치에서 벗어날 수 없다.
네트워크 계층에서 실제 브라우저처럼 보이는 방법
브라우저급 클라이언트 핑거프린트를 제시하는 정직한 방법은 두 가지다.
실제 브라우저를 사용한다. Playwright, Puppeteer, 또는 Selenium은 실제 Chromium을 구동하므로, 그들의 TLS와 HTTP/2 핑거프린트는 정의상 실제 브라우저의 것이다. 가장 강력하게 보호된 타겟에는 이것이 가장 견고한 방법이며, 대신 워커당 브라우저를 실행해야 하는 비용이 든다.
TLS를 위장하는 HTTP 클라이언트를 사용한다. 가볍게 유지하고 싶을 때는, 단순한 HTTP 호출을 유지하면서도 브라우저의 ClientHello와 HTTP/2 설정을 모방하기 위해 특별히 만들어진 클라이언트 종류가 있다. curl-impersonate와 그 Python 바인딩인 curl_cffi, tls-client, Go의 utls 같은 도구들은 브라우저를 실행하지 않고도 Chrome이나 Firefox로 지문 채취되는 요청을 보낼 수 있게 해준다.
from curl_cffi import requests
# Presents a real Chrome TLS + HTTP/2 fingerprint, not python-requests'.r = requests.get( "https://example.com", impersonate="chrome", proxies={"https": "http://customer-USER-country-us:PASS@p.shifter.io:443"},)print(r.status_code)이 단일 impersonate 인자가, ClientHello가 Chrome이라고 말하는 것과 OpenSSL이라고 말하는 것의 차이를 만든다. 깨끗한 레지덴셜 IP와 짝을 이루면, 네트워크 계층과 클라이언트 계층을 동시에 통과하게 되며, 이것이 바로 어려운 타겟들이 확인하는 것이다.
모든 계층을 일관되게 유지하라
핵심은 일관성이다. 당신의 이야기는 세 계층 모두에서 일치해야 한다: Chrome User-Agent는 Chrome TLS 핑거프린트, Chrome HTTP/2 설정, Chrome 헤더 순서가 필요하며, 주장하는 지역과 지리적으로 일치하는 IP를 통해 나가야 한다. 안티봇 시스템은 각 신호를 개별적으로 평가하기보다는 이러한 신호들 간의 일치성을 점점 더 많이 평가하고 있으므로, 하나의 모순, 즉 라이브러리 핸드셰이크에 브라우저 UA를 붙이거나 외국 IP에 미국 페르소나를 붙이는 것이 어느 한 계층이 약간 부족한 것보다 훨씬 더 큰 신호가 된다. 이것은 탐지를 유발하는 실수들 뒤에 있는 것과 동일한 일관성 원칙이다: 모든 계층이 같은 이야기를 해야 한다.
클라이언트 핑거프린트 차단 진단하기
클라이언트 계층 차단과 IP 계층 차단은 보통 그 동작 방식으로 구별할 수 있다. 강력한 보호로 알려진 사이트에서 즉시 차단되고, IP를 로테이션해도 아무것도 바뀌지 않지만 실제 브라우저로 전환하면 갑자기 작동한다면, 이는 클라이언트 핑거프린트 차단이며 IP 문제가 아니다. 반대로, 일반 HTTP 클라이언트가 대부분 잘 작동하고 일부 IP만 챌린지를 받는다면, 이는 다시 IP 평판 문제를 가리킨다. 어느 계층이 실패했는지 아는 것이 어느 레버를 당겨야 하는지 알려주며, IP와 아무 관련이 없는 문제에 대해 IP를 로테이션하는 대신 말이다. 이는 요청이 타임아웃되는 이유를 진단하고 조용한 차단을 탐지하는 것과 동일한 “실패를 올바르게 읽는” 원칙이다.
결론
강력하게 보호된 사이트들은 당신을 세 계층에서 평가하며, 깨끗한 IP는 그중 하나에만 답할 뿐이다. 클라이언트 계층, 즉 TLS 핸드셰이크와 HTTP/2 동작은 스크립팅 라이브러리를 요청이 읽히기도 전에 차단당하게 만들며, 어떤 양의 헤더 위조로도 이를 고칠 수 없는데, 핑거프린트는 연결이 어떻게 만들어지는지에 있기 때문이다. 성공률을 높여 어려운 타겟을 스크래핑하려면, 실제 브라우저를 구동하거나 TLS를 위장하는 클라이언트를 사용하여 브라우저급 클라이언트 핑거프린트를 제시하고, 이를 헤더 및 IP의 지리적 위치와 일관되게 유지해야 한다.
깨끗한 레지덴셜 IP는 여전히 필수적이며, 평판을 통과하게 해주는 계층이지만, 가장 까다로운 사이트에서는 그것만으로는 충분하지 않다. 네트워크 계층에서 실제 브라우저 핑거프린트를 짝지어 모든 신호를 일치시키면, 예전에는 페이지가 로드되기도 전에 발생했던 차단이 더 이상 일어나지 않는다. GB당 가격 정책을 통해 요청당 계량기와 씨름하지 않고도 이 조합을 가장 어려운 타겟들에서 직접 테스트해볼 수 있다.