스크래핑

타겟이 차단 가능한가? 안티봇 스택 조회

스크레이퍼를 만들기 전에 타겟을 보호하는 것이 무엇인지 확인하세요. 상위 1,000개 도메인을 조사해 어떤 안티봇 벤더를 사용하는지, 그리고 일반 클라이언트가 어떤 결과를 보였는지 확인했습니다.

Chris Collins

Chris Collins

2026년 10월 3일 · 9 분 소요

대부분의 스크래핑 프로젝트는 사이트를 보호하는 것이 무엇인지 힘든 방식으로 알게 된다. 월요일에는 프로토타입이 작동하고, 화요일에는 챌린지 페이지가 나타나며, 금요일이 되면 팀은 헤드리스 브라우저 중심으로 재구축하고 있다. 이 중 많은 부분은 첫날에 알 수 있었던 것들이다. 봇 관리 제품은 사이트의 응답 헤더와 쿠키에 눈에 보이는 흔적을 남기며, 평범한 요청 하나만으로도 이를 읽어낼 수 있다.

우리는 정확히 이를 수행하는 짧은 조회 도구를 만들어 상위 1,000개 도메인의 홈페이지에 대해 실행했고, 일반 HTTP 클라이언트가 어떤 응답을 받았는지 기록했다. 이 가이드는 그 결과와 코드, 그리고 프로젝트를 계획할 때 그 답을 어떻게 활용할지를 다룬다.

핵심 요약

  • 요청 한 번으로도 많은 것이 드러난다. 벤더 쿠키와 헤더는 우리가 불러올 수 있었던 653개의 상위 사이트 홈페이지 중 37%에서 봇 보호 또는 엣지 보안 벤더를 식별했으며, 16%에서는 실제로 작동 중인 봇 관리 제품을 확인했다.
  • Cloudflare가 압도적으로 가장 흔했으며, 홈페이지의 4분의 1에서 발견되었고, 그 뒤를 Akamai가 이었다. DataDome, AWS WAF, Imperva, HUMAN은 훨씬 드물게 나타났으며, DataDome과 HUMAN은 우리가 보낸 모든 요청을 챌린지하거나 차단했다.
  • Chrome을 사칭한 일반 HTTP 클라이언트는 홈페이지의 81%에서 응답을 받았고, 10%에서 챌린지되었으며, 9%에서 차단되었다. 라이브러리의 기본 User-Agent를 사용했을 때는 차단 비율이 16%로 상승했다.
  • 브라우저인 척하는 것이 오히려 역효과를 낼 수 있다. 한 대형 소매업체의 10개 국가별 스토어프론트는 정직한 라이브러리 User-Agent에는 전체 홈페이지를 제공했지만, Chrome을 사칭한 요청에는 1~2킬로바이트짜리 챌린지 페이지를 제공했다.
  • 이 조회 도구를 계획 단계의 입력값으로 취급하라. 이는 무엇을 예상해야 하는지, 공식 API나 허가를 받는 것이 더 나은 경로인지, 그리고 프로젝트 예산을 어떻게 책정할지를 알려준다.

요청 한 번으로 드러나는 것

봇 관리 및 엣지 보안 제품은 웹사이트 앞단에 위치해 모든 요청을 검사하는 방식으로 작동한다. 이를 위해 대부분의 제품은 방문자의 브라우저에 쿠키를 설정하거나 응답 헤더를 추가하며, 이 이름들은 안정적이고 종종 문서화되어 있다.

벤더전형적인 증거출처
Cloudflare모든 응답에 포함되는 cf-ray 헤더; Bot Management 또는 Bot Fight Mode가 켜져 있을 때의 __cf_bm 쿠키; 챌린지 페이지의 cf-mitigated: challengeCloudflare 문서
AkamaiAkamai-GRN 요청 번호 헤더; Bot Manager의 _abck, ak_bmsc, bm_sz 쿠키헤더는 Akamai 문서, 쿠키는 사이트 쿠키 정책
DataDomedatadome 쿠키; x-datadome 헤더DataDome 문서
HUMAN_px3, _pxhd, _pxvid 쿠키HUMAN 문서
Impervavisid_incap_, incap_ses_, nlbi_ 쿠키사이트 쿠키 정책
AWS WAF챌린지 시 HTTP 202와 함께 x-amzn-waf-action: challenge; aws-waf-token 쿠키AWS 문서

결과를 읽을 때 중요한 두 가지 구분이 있다. 콘텐츠 전송 네트워크 뒤에 있는 것과 봇 관리를 운영하는 것은 다르다. cf-ray 헤더는 사이트가 Cloudflare를 사용한다는 것을 의미하고, __cf_bm 쿠키는 Cloudflare의 봇 제품이 실제로 방문자를 채점하고 있다는 것을 의미한다. 그리고 특징이 없다고 해서 아무것도 증명하지 못한다. 많은 사이트가 자체 탐지 시스템이나 첫 응답에서 흔적을 남기지 않는 제품을 운영한다.

코드

아래 모듈은 응답 하나를 읽고 탐지된 벤더와 그 증거, 봇 관리 제품의 활성 여부, 그리고 요청이 받은 결과(정상 응답, 챌린지, 차단)를 반환한다.

import re

# 각 벤더가 설정하는 봇 또는 WAF 제품의 쿠키와 추가 헤더. 쿠키가 가장 강력한 증거다.
SIGNATURES = {
    "Cloudflare": {"headers": ["cf-ray"], "cookies": [r"^__cf_bm$", r"^cf_clearance$"]},
    "Akamai":     {"headers": ["akamai-grn"], "cookies": [r"^_abck$", r"^ak_bmsc$", r"^bm_sz$", r"^bm_sv$"]},
    "DataDome":   {"headers": ["x-datadome"], "cookies": [r"^datadome$"]},
    "HUMAN":      {"headers": [], "cookies": [r"^_px(3|hd|vid|cvid|de)$"]},
    "Imperva":    {"headers": [], "cookies": [r"^visid_incap_\d+$", r"^incap_ses_", r"^nlbi_\d+$", r"^reese84$"]},
    "AWS WAF":    {"headers": ["x-amzn-waf-action"], "cookies": [r"^aws-waf-token$"]},
}
# 사이트 앞단의 CDN만이 아니라 실제로 작동 중인 봇 관리 제품의 증거.
BOT_PRODUCT_COOKIES = [r"^__cf_bm$", r"^cf_clearance$", r"^_abck$", r"^ak_bmsc$", r"^bm_sz$", r"^datadome$", r"^_px", r"^reese84$"]


def cookie_names(response):
    """응답이 설정하려 한 모든 쿠키의 이름."""
    return {c.split("=", 1)[0].strip() for c in response.raw.headers.getlist("Set-Cookie")}


def detect_stack(response):
    """응답 하나의 헤더와 쿠키로부터 {벤더: [증거]}를 반환한다."""
    headers = {k.lower() for k in response.headers}
    cookies = cookie_names(response)
    found = {}
    for vendor, sig in SIGNATURES.items():
        evidence = [f"header {h}" for h in sig["headers"] if h in headers]
        evidence += [f"cookie {c}" for c in sorted(cookies) if any(re.match(p, c) for p in sig["cookies"])]
        if evidence:
            found[vendor] = evidence
    server = response.headers.get("server", "").lower()
    if server == "cloudflare":
        found.setdefault("Cloudflare", []).append("server header")
    if "akamaighost" in server:
        found.setdefault("Akamai", []).append("server header")
    return found


def bot_product_active(response):
    """단순 CDN이 아니라 봇 관리 제품이 요청을 처리했음을 쿠키가 보여줄 때 True."""
    return any(re.match(p, c) for c in cookie_names(response) for p in BOT_PRODUCT_COOKIES)


def outcome(response):
    """요청이 받은 결과를 정상 응답, 챌린지, 차단으로 분류한다."""
    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 or "incapsula incident id" in body:
        return "blocked"
    return "served"

이 코드는 requests 라이브러리의 응답에서 작동하며, 원시 Set-Cookie 헤더에서 쿠키를 읽기 때문에 클라이언트가 평소에는 유지하지 않을 쿠키까지 확인한다. 여기서 “정상 응답”이 의미하는 바에 유의하라. 챌린지나 차단 신호가 발견되지 않았다는 뜻일 뿐이다. 페이지에 실제 콘텐츠가 포함되어 있었는지는 증명하지 않으며, 이 공백에 대해서는 아래에서 다시 다룬다.

상위 1,000개 도메인에서 발견한 것

2026년 10월 3일, 우리는 Tranco 순위 상위 1,000개 도메인 각각의 홈페이지에 대해 Chrome User-Agent 문자열로 한 번, requests 라이브러리의 기본 User-Agent로 한 번씩 루마니아의 단일 연결에서 요청을 보냈다. 두 요청 모두 동일한 Python HTTP 클라이언트에서 보냈으며, 이는 JavaScript를 실행하지 않고 브라우저의 네트워크 핑거프린트도 가지고 있지 않다. 653개의 서로 다른 사이트가 HTML 홈페이지를 반환했고, 나머지는 콘텐츠 네트워크, API 호스트, 홈페이지가 없는 기타 도메인이었다.

탐지된 벤더홈페이지 수활성 봇 관리 쿠키가 있는 경우
Cloudflare162 (24.8%)79
Akamai55 (8.4%)16
AWS WAF14 (2.1%)0
DataDome8 (1.2%)8
HUMAN22
Imperva20
위 중 하나 이상241 (36.9%)105 (16.1%)
탐지되지 않음412 (63.1%)0

단 두 개의 홈페이지만 동시에 두 개의 벤더를 보였다. AWS WAF가 발견된 14개 사이트 중 10개는 같은 소매업체의 국가별 스토어프론트였다.

요청이 받은 결과:

요청정상 응답챌린지차단
Chrome User-Agent, 653개 홈페이지530 (81.2%)64 (9.8%)59 (9.0%)
라이브러리 기본 User-Agent, 650개 홈페이지500 (76.9%)49 (7.5%)101 (15.5%)

그리고 사이트를 보호한 것이 무엇인지에 따라 나눈 결과, 두 요청이 모두 완료된 사이트만 집계했다.

탐지된 보호 수단사이트 수Chrome User-Agent: 챌린지 또는 차단라이브러리 기본값: 챌린지 또는 차단
Cloudflare1625463
Akamai542622
DataDome777
탐지되지 않음4111953

결과가 말하는 것

상위 웹사이트 대부분은 평범한 요청에 응답한다. 홈페이지 다섯 중 네 개는 Chrome을 사칭한 기본적인 HTTP 클라이언트에 눈에 띄는 챌린지 없이 응답했다. 다만 홈페이지는 사이트에서 가장 접근하기 쉬운 페이지이며, 검색 결과나 가격, 결제 흐름은 보통 현관보다 더 엄격하게 보호된다.

단순한 필터와 본격적인 제품은 다르게 동작한다. 탐지 가능한 벤더가 없는 사이트에서는 라이브러리의 기본 User-Agent가 Chrome 문자열보다 거의 세 배 더 자주 챌린지되거나 차단되었다(53회 대 19회). 이는 단순한 User-Agent 필터링의 전형적인 특징이다. DataDome을 운영하는 사이트에서는 두 요청 모두 매번 챌린지되거나 차단되었으며, User-Agent는 아무런 차이를 만들지 못했다.

정체를 잘못 표시하는 것이 정직한 것보다 더 나쁠 수 있다. 해당 소매업체의 10개 국가별 스토어프론트는 Chrome을 사칭한 요청에는 1~2킬로바이트짜리 챌린지 또는 플레이스홀더 페이지로, 정직한 라이브러리 User-Agent에는 678KB에서 1.4MB에 이르는 전체 홈페이지로 응답했다. 10개 모두에 대해 확인을 반복했고 패턴은 동일하게 유지되었다. TLS 및 HTTP/2 핑거프린팅에서 설명한 것처럼, Chrome처럼 보이지 않는 연결에서 도착한 Chrome User-Agent 문자열 자체가 하나의 신호다.

성공적인 상태 코드가 성공적인 페이지를 의미하지는 않는다. 그 플레이스홀더 페이지들 중 세 개는 HTTP 200과 함께 챌린지 헤더 없이 응답했으므로, 상태 코드만 확인하는 검사는 이를 정상 응답으로 간주한다. 조용한 실패율과 사이트가 가짜 또는 차단된 콘텐츠를 제공하는지 판단하는 방법에서 다룬 것처럼, 상태 코드뿐 아니라 콘텐츠를 검증해야 한다.

조회 도구로 프로젝트 계획하기

실제로 필요한 페이지에서 조회 도구를 실행하라. 홈페이지만이 아니라. 그리고 그 답이 계획을 결정하도록 하라.

조회 결과가 보여주는 것일반적인 의미합리적인 다음 단계
벤더 없음, 일반 요청이 정상 응답해당 페이지에 봇 관리가 거의 또는 전혀 없음정직한 식별, 적당한 속도, 올바른 헤더를 사용한 일반 HTTP
CDN만 있고 봇 쿠키 없음활성 봇 채점이 없는 엣지 보안일반 HTTP, 하지만 물량이 늘어나면서 챌린지가 나타나는지 주시
봇 관리 쿠키 있음, 요청이 정상 응답이번 요청을 허용한 활성 채점 시스템규모가 커지면 챌린지를 예상하고, 타깃 상태 점수로 모니터링
첫 요청부터 챌린지됨자동화 클라이언트를 걸러내겠다는 의도적 결정먼저 공식 API나 데이터 피드를 찾아보고, 헤드리스 브라우저가 필요한 시점을 참고해 브라우저가 정당화되는지 고려
첫 요청부터 차단됨엄격한 규칙, 대개 네트워크나 지역 기준페이지 뒤에 숨은 API를 포함해 다른 방법으로 데이터를 얻을 수 있는지, 그리고 차단이 지역적인지 확인

조회 도구는 의도에 대해서도 말해준다. 활성 봇 관리 제품을 운영하는 사이트는 자동화된 접근을 통제하기로 결정한 것이며, robots.txt, AI 옵트아웃과 예약 신호에서 논의한 것처럼 이는 그 사이트의 이용약관 및 robots.txt와 함께 저울질할 가치가 있다. 많은 프로젝트에서, 강하게 보호된 타깃에 대한 올바른 대응은 에스컬레이션이 아니라 라이선스, 파트너십, 공식 API다. 수집이 적절한 경우, 보호된 사이트를 위한 에스컬레이션 단계에서 비용 순으로 선택지를 설명한다.

측정의 한계

  • 홈페이지만 대상으로 함. 더 깊은 페이지는 종종 다르게 보호된다.
  • 각각 한 번의 요청, 하나의 네트워크에서. 결과는 국가, 네트워크 평판, 시간대, 요청 이력에 따라 달라질 수 있다. 어느 국가가 지오블로킹을 가장 많이 당하는지에서 지역별 차이를 다뤘다.
  • 브라우저 없음. 많은 챌린지는 JavaScript를 실행하는 실제 브라우저가 통과하도록 설계되어 있으며, 우리의 클라이언트는 설계상 그렇게 할 수 없었다.
  • 특징은 일부를 놓친다. 자체 개발 탐지 시스템과 첫 요청에서 흔적을 남기지 않는 제품은 “탐지되지 않음”으로 집계되므로, 실제로 보호되는 사이트의 비율은 더 높다.

결론

평범한 요청 하나로도 스크래핑 프로젝트를 계획하는 데 필요한 대부분의 정보를 알 수 있다. 누가 사이트를 보호하는지, 봇 채점이 활성화되어 있는지, 일반 클라이언트가 어떻게 취급되는지다. 상위 1,000개 도메인에서 대부분의 홈페이지는 응답했고, 4분의 1은 Cloudflare 뒤에 있었으며, 6개 중 1개는 활성 봇 관리 제품을 운영했고, 가장 엄격한 제품들은 우리가 보낸 모든 요청을 챌린지하거나 차단했다.

스크래퍼를 작성하기 전에, 필요한 페이지에서 조회 도구를 실행하라. 이것이 언제 단순하게 진행해도 되는지, 언제 브라우저를 계획해야 하는지, 언제 데이터를 얻기 위한 공식 경로를 찾아야 하는지를 알려주도록 하라.

출처 및 참고자료

시작할 준비가 되셨나요?

205M개 이상의 IP, 195개 이상의 국가를 지원하는 Shifter의 레지덴셜 프록시를 $0.10/GB부터 이용해보세요.

시작하기