스크래퍼가 User-Agent 헤더를 설정할 때는 브라우저 버전을 하나 선택하는데, 그 버전을 나중에 바꾸는 경우는 거의 없다. 프로젝트가 시작되는 날 그 문자열이 설정 파일에 들어가고, 프로젝트는 몇 년 동안 계속 그 브라우전인 척한다. 그동안 실제 브라우저는 계속 발전한다. Chrome은 2025년 9월 말부터 2026년 9월 말 사이에 13개의 메이저 버전을 출시했다.
이 차이가 실제로 문제가 될까? 안티봇 시스템은 실제 방문자가 어떤 브라우저 버전을 쓰는지 알고 있으므로, 몇 년째 아무도 쓰지 않는 버전은 눈에 띈다. 우리는 397개 상위 사이트의 홈페이지에 다섯 가지 다른 Chrome 버전을 자처하며 요청을 보내, 그 영향이 어느 정도인지 측정했다. 최신 릴리스부터 6년 반 된 버전까지를 대상으로 했다.
핵심 요약
- 오래된 버전을 자처할수록 매번 더 자주 거부당했다. 홈페이지의 17.1%가 최신 Chrome 버전에 대해 챌린지나 차단을 가했고, 1년 된 버전에는 19.9%, 6년 반 된 버전에는 22.7%가 거부했다.
- 이 효과는 한쪽 방향으로만 작동했다. 최신 버전을 제공받은 22개 사이트가 가장 오래된 버전은 거부했다. 반대로 최신 버전을 거부하고 오래된 버전을 제공받은 사이트는 하나도 없었다.
- 최신 버전에 대한 두 번의 동일한 요청은 397개 사이트 모두에서 동일한 결과를 냈으므로, 이 차이는 잡음이 아니다.
- Akamai 뒤에 있는 사이트가 가장 먼저 반응했다. 불과 1년 된 버전을 거부한 11개 사이트 중 9개가 Akamai로 보호되고 있었다. Cloudflare로 보호되는 사이트는 대체로 가장 오래된 버전에만 반응했다.
- 버전을 최신으로 유지하고, 요청의 나머지 부분과 일관되게 맞추고, 다른 의존성과 마찬가지로 일정에 따라 점검하라.
측정 방법
우리는 이전 안티봇 스택 조회에서 사용한, Tranco 순위 상위 1,000개 도메인의 홈페이지 중에서 400개 사이트를 무작위로 선택했다. 2026년 10월 5일에 각 홈페이지에 대해 1초 간격으로, 사이트별로 무작위 순서를 적용하여 여섯 번씩 요청을 보냈으며, 표준 Windows Chrome User-Agent 문자열로 다음 버전들을 자처했다.
| 자처한 버전 | 안정 버전 출시일 | 테스트 시점의 연령 |
|---|---|---|
| Chrome 154, 두 번 요청 | 2026년 9월 22일 | 최신 |
| Chrome 141 | 2025년 9월 30일 | 약 1년 |
| Chrome 129 | 2024년 9월 17일 | 약 2년 |
| Chrome 110 | 2023년 2월 7일 | 약 3.5년 |
| Chrome 80 | 2020년 2월 4일 | 약 6.5년 |
최신 버전에 대한 반복 요청은 대조군이다. 사이트가 자체적으로 결과를 다르게 낸다면 그 두 요청 사이의 차이로 드러날 것이다. 모든 요청은 동일한 Python HTTP 클라이언트와 동일한 연결에서 이루어졌으며, User-Agent의 버전 번호만 바뀌었다. 세 개 사이트는 요청 중 적어도 하나에 응답하지 않아 제외되었고, 397개가 남았다.
결과
| 자처한 버전 | 제공됨 | 챌린지 | 차단 | 챌린지 또는 차단 |
|---|---|---|---|---|
| Chrome 154, 첫 번째 요청 | 329 | 42 | 26 | 17.1% |
| Chrome 154, 반복 | 329 | 42 | 26 | 17.1% |
| Chrome 141 | 318 | 43 | 36 | 19.9% |
| Chrome 129 | 315 | 43 | 39 | 20.7% |
| Chrome 110 | 314 | 43 | 40 | 20.9% |
| Chrome 80 | 307 | 52 | 38 | 22.7% |
최신 버전에 대한 두 요청은 모든 사이트에서 일치했으며, 이 덕분에 표의 나머지 부분을 쉽게 해석할 수 있다. 모든 차이는 버전 번호에서 비롯된 것이다.
두 번 모두 최신 버전을 제공받은 사이트만 집계하면, 오래된 버전을 거부한 사이트 수는 연령이 늘어날수록 꾸준히 증가했다.
| 자처한 버전 | 최신 버전은 제공받았지만 이 버전은 거부한 사이트 수 |
|---|---|
| Chrome 141, 1년 전 | 11 |
| Chrome 129, 2년 전 | 14 |
| Chrome 110, 3.5년 전 | 15 |
| Chrome 80, 6.5년 전 | 22 |
반대 방향으로 움직인 사이트는 하나도 없었다. 오래된 버전이 도움이 된 적은 없었다.
누가, 어떻게 반응하는가
사이트들을 이전 조회에서 감지된 보호 체계와 교차 대조해보면, 벤더마다 기준선을 다른 지점에 긋는다는 것을 알 수 있다.
- Akamai에게는 1년이면 충분했다. Chrome 141을 거부한 11개 사이트 중 9개가 Akamai로 보호되고 있었다. 11개 모두 HTTP 403으로 응답했고, 그중 10개는 챌린지가 아니라 단순 차단이었다.
- Cloudflare는 아주 오래된 버전에 반응했다. Chrome 80을 거부한 22개 사이트 중 10개가 Cloudflare로 보호되고 있었으며 챌린지를 반환했다. 9개는 Akamai였다.
- 페이지가 방문자에게 알리는 경우는 드물었다. 최신 버전에는 보여주지 않으면서 Chrome 80에만 “브라우저를 업데이트하세요” 식의 메시지를 보여준 사이트는 두 곳뿐이었다. 대부분은 그냥 거부했다.
이는 봇 관리가 작동하는 방식과 일치한다. 실제 방문자가 1년 된 브라우저를 쓰는 일은 드물고, 6년 된 브라우저를 쓰는 일은 더 드물다. 따라서 오래된 버전은 그 자체로는 약한 신호지만, 요청에서 그 외에 이상한 점과 결합되면 더 강한 신호가 된다.
코드
아래 함수는 어떤 URL에 대해서도 동일한 비교를 실행한다. 각 버전을 자처하며 무작위 순서로 페이지를 요청하고, 각각에 대해 상태, 결과, 크기를 반환하며, 페이지가 방문자에게 브라우저가 오래되었다고 알리는지도 반환한다. 버전을 두 번 나열하면 잡음 대조군을 얻을 수 있다.
import random
import re
import time
import requests
CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{v}.0.0.0 Safari/537.36"
OUTDATED = re.compile(r"(browser (is|you are using is) (not supported|out of date|outdated|no longer supported)"
r"|update your browser|upgrade your browser|unsupported browser|outdated browser)", re.I)
def classify(response):
"""served, challenged or blocked, from the status and well-known challenge markers."""
body = response.text[:20000].lower()
if (response.headers.get("cf-mitigated", "").lower() == "challenge"
or response.headers.get("x-amzn-waf-action", "").lower() == "challenge"
or "captcha-delivery.com" in body):
return "challenged"
if response.status_code in (401, 403, 405, 429) or response.status_code >= 500:
return "blocked"
return "served"
def ua_drift_check(url, versions, pause=1.0, seed=None):
"""Request the same URL claiming each Chrome version, in random order, and compare what comes back.
A version may be listed twice, as a control for how much the site varies on its own."""
order = list(enumerate(versions))
random.Random(seed).shuffle(order)
results = [None] * len(versions)
for i, v in order:
headers = {"User-Agent": CHROME.format(v=v), "Accept": "text/html,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9"}
try:
r = requests.get(url, headers=headers, timeout=20)
results[i] = {"version": v, "status": r.status_code, "outcome": classify(r), "bytes": len(r.content),
"outdated_notice": bool(OUTDATED.search(r.text))}
except requests.RequestException as e:
results[i] = {"version": v, "error": type(e).__name__}
time.sleep(pause)
return results
User-Agent를 변경하기 전후로 의존하는 사이트에 대해 이를 실행하고 결과를 보관하라. 대상이 보내는 버전을 거부하기 시작하면, 그 버전이 원인인지 아닌지 알 수 있을 것이다.
측정의 한계
- 홈페이지, 단일 네트워크, 단일 시점. 더 깊은 페이지, 다른 국가, 다른 시간대에서는 다르게 동작할 수 있다.
- 브라우저가 아니다. 우리 클라이언트는 Python HTTP 라이브러리에서 Chrome User-Agent를 보냈으므로, TLS와 HTTP/2 핑거프린팅에서 설명했듯 모든 요청에는 이미 Chrome과 일치하지 않는 네트워크 핑거프린트가 있었다. 이 비교는 버전 번호의 효과만 분리해서 보여줄 뿐, 각 버전의 실제 브라우저가 어떻게 동작할지는 보여주지 않는다.
- 기준선이 0이 아니다. 이 클라이언트로 최신 버전을 보냈을 때도 홈페이지의 17%가 거부했다. 이는 올바른 User-Agent가 필요조건이지 충분조건은 아니라는 점을 상기시켜 준다.
우리 자신에 관한 발견도 짚어야 한다. 이번 주 초에 진행한 안티봇 조회는 Chrome 129 User-Agent를 사용했다. 이는 2년 된 문자열로, 오래된 스크립트에서 그대로 복사한 것이었다. 이 측정 결과에 따르면, 이는 홈페이지 성공률 몇 퍼센트포인트의 손실을 의미한다.
User-Agent를 최신으로 유지하기
- 의존성으로 취급하라. 브라우저 버전을 한 곳에 두고 일정에 따라 업데이트하라. Chrome은 대략 4주마다 새로운 메이저 버전을 출시한다.
- 최신 버전에서 몇 버전 이내로 유지하라. 우리 데이터에서는 1년 된 버전도 이미 최신 버전보다 더 자주 거부당했다.
- 모든 것을 일관되게 유지하라. 최신 Chrome 버전이라도 헤더 순서가 오래되었거나, 클라이언트 힌트가 빠졌거나, 브라우저가 아닌 네트워크 핑거프린트를 가지고 있다면 여전히 일관성이 없는 것이다. 올바른 헤더와 User-Agent 설정에서 전체 내용을 다룬다.
- 세션마다 하나의 아이덴티티를 사용하라. 세션 내에서 버전을 로테이션하는 것은 단일 버전보다 더 의심스럽다.
- CI에서 테스트하라. 주요 대상에 대해 현재와 이전 설정으로 정기적으로 점검하면 드리프트가 데이터에 나타나기 전에 잡아낼 수 있다. CI/CD에서 스크래퍼 실행하기에서 그 구성 방법을 설명한다.
- 거부율을 주시하라. 403과 챌린지가 서서히 증가하는 것은 드리프트의 전형적인 증상이며, 대상 상태 점수에 포함되어야 한다.
결론
브라우저 버전은 한 번 선택하고 끝나는 설정이 아니다. 397개 상위 사이트를 대상으로 한 측정에서, 자처하는 Chrome 버전이 1년씩 더 오래될수록 거부율이 늘어났고, 이 효과가 반대 방향으로 작동한 적은 없었으며, 일부 보호 서비스는 불과 1년 된 버전에도 반응했다.
버전을 최신으로 유지하고, 요청의 나머지 부분을 그와 일관되게 맞추며, 자신에게 중요한 대상에서 그 효과를 직접 측정하라. 이는 스크래핑 프로젝트가 할 수 있는 가장 저렴한 안정성 개선 중 하나다.