Python에서 Cloudflare "Just a Moment": 왜 requests가 실패하는가

Cloudflare의 'Just a moment...' 페이지는 JavaScript 챌린지이며, Python requests는 JavaScript를 실행할 수 없습니다. 이를 감지하는 방법, 서버가 노트북보다 더 많은 부하를 받는 이유, cloudscraper 스타일의 트릭이 더 이상 충분하지 않은 이유, 그리고 2026년 9월 29일 테스트에서 통과한 방법을 소개합니다.

'Just a moment...'은 Cloudflare의 챌린지 페이지입니다. 사이트가 응답하기 전에 클라이언트에게 JavaScript를 실행하고 브라우저임을 증명하도록 요구합니다. Python requests는 JavaScript를 실행할 수 없으며, 데이터센터 서버에서는 IP와 TLS 핸드셰이크가 이미 자동화된 것으로 보입니다. 통과하는 것은 깨끗한 IP의 실제 브라우저이거나 이를 실행하는 웹 언락커입니다.

이 가이드는 Python에서 챌린지를 감지하는 방법, 동일한 스크립트가 노트북에서는 통과하고 서버에서는 실패하는 이유, cloudscraper 시대의 트릭이 더 이상 충분하지 않은 이유, 그리고 2026년 9월 29일 테스트에서 실제로 통과한 것을 보여줍니다. 우리가 시도한 것 중 'Just a moment'를 넘지 못한 사이트도 포함됩니다.

"Just a moment..." 페이지의 실제 의미

Cloudflare는 이를 인터스티셜 챌린지 페이지라고 부릅니다: Cloudflare가 방문자가 자동화된 것인지 결정하는 동안 방문자와 사이트 사이의 전체 페이지 게이트입니다. 챌린지 페이지에 대한 문서는 세 가지 유형을 설명합니다. 비대화형 챌린지는 브라우저가 주입된 JavaScript를 실행하기만 하면 됩니다. 관리형 챌린지는 요청의 신호에서 Cloudflare가 챌린지 유형을 선택하게 합니다. 대화형 챌린지는 방문자에게 무언가를 하도록 요청합니다. 검사가 통과되면 원래 요청이 사이트로 계속되고 브라우저는 일정 기간 동안 게이트를 건너뛸 수 있는 cf_clearance 쿠키를 받습니다.

Python HTTP 클라이언트는 그 단계까지 도달하지 못합니다. 챌린지 페이지를 다운로드하고, 스크립트를 실행할 엔진이 없으며, 요청한 페이지 대신 게이트의 HTML을 제공합니다.

챌린지가 403으로 돌아오는 이유와 이를 감지하는 방법

우리의 테스트에서 모든 챌린지 페이지는 상태 403으로 도착했으며, 이는 raise_for_status()가 403 Client Error: Forbidden을 던지는 이유입니다. 상태만으로는 다른 거부와 챌린지를 구별할 수 없습니다. 신뢰할 수 있는 신호는 헤더입니다: Cloudflare는 챌린지 응답이 요청한 리소스에 관계없이 cf-mitigated: challenge를 포함한다고 문서화합니다. 먼저 이를 확인하고, 그 다음에 제목을 백업으로 사용하십시오:

import re
import requests

URL = "https://www.glassdoor.com/Reviews/Google-Reviews-E9079.htm"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}


def title_of(html: str) -> str:
    m = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.S)
    return m.group(1).strip() if m else ""


def is_cloudflare_challenge(r: requests.Response) -> bool:
    if r.headers.get("cf-mitigated") == "challenge":      # documented by Cloudflare
        return True
    if "just a moment" in title_of(r.text).lower():
        return True
    return "enable javascript and cookies to continue" in r.text[:20000].lower()


r = requests.get(URL, headers=HEADERS, timeout=30)
print(r.status_code, is_cloudflare_challenge(r), title_of(r.text))
# 29 Sep 2026, plain request through a residential proxy (US exit):
# 403, body asking to 'Enable JavaScript and cookies to continue'

같은 클라이언트로 챌린지된 요청을 반복해서 시도하지 마십시오. 각 재시도는 동일한 게이트를 가져오며, 일부 사이트는 반복된 실패에 더 엄격한 규칙으로 응답합니다. 이렇게 해서 스크래퍼가 'Just a moment' 루프에 갇히게 됩니다.

"Enable JavaScript and cookies to continue" 및 "your browser does not support JavaScript"

이 문자열은 챌린지 페이지의 백업 텍스트로, 스크립트를 실행하지 않는 클라이언트에게 표시됩니다. Python requests가 이 중 하나를 보여줄 때, 사이트는 헤더에 대해 불평하는 것이 아닙니다. 문자 그대로 JavaScript 엔진과 스크립트가 설정한 것을 유지하는 쿠키 저장소가 필요하다는 것을 말하고 있습니다.

이는 Indeed와 Glassdoor에서 우리의 일반 주거용 실행이 받은 정확한 것입니다: 깨끗한 가정용 IP에서 'Enable JavaScript and cookies to continue'와 함께 403을 받았습니다. Chrome User-Agent, Accept-Language 헤더 또는 Referer를 추가해도 아무런 변화가 없습니다. 페이지는 이를 결정하기 위해 읽고 있는 것이 아니라 스크립트가 실행되기를 기다리고 있습니다.

왜 스크래퍼가 로컬에서는 작동하지만 서버에서 Cloudflare에 의해 차단되는가

이것은 가장 일반적인 불만의 버전입니다: 스크립트가 노트북에서는 작동하지만, 클라우드 VM에서 실행되자마자 'Just a moment'를 받습니다. 이 두 기계 사이에는 두 가지가 바뀝니다.

우리 테스트의 주거용 열은 IP만 수정하는 것의 한계를 보여줍니다. Zillow와 Leboncoin은 주거용 프록시를 통한 일반 요청에 실제 페이지를 제공했습니다. Indeed와 Glassdoor는 여전히 JavaScript 챌린지로 응답했습니다. 노트북은 더 엄격한 사이트에서 같은 벽에 부딪힐 수 있습니다. 단지 덜 자주 부딪힐 뿐입니다.

왜 Python 스크래퍼가 노트북에서는 Cloudflare를 통과하고 클라우드 서버에서는 Just a moment 챌린지를 받는가: 가정용 IP 대 호스팅 IP, 동일한 Python TLS 지문, 두 경우 모두 JavaScript 엔진 없음
동일한 스크립트, 두 기계. IP는 바뀌고, Python TLS 핸드셰이크와 누락된 JavaScript 엔진은 바뀌지 않습니다.

왜 cloudscraper 시대의 트릭이 작동하지 않는가

cloudscraper는 Cloudflare의 이전 'I'm Under Attack' 페이지를 위해 만들어졌습니다. 그 페이지는 주로 클라이언트가 JavaScript 계산을 실행할 수 있는지를 확인했기 때문에, 라이브러리는 챌린지를 가져와 내장된 인터프리터에서 스크립트를 해결하고, 답변을 보내고 쿠키를 유지했습니다. 오랫동안 그것으로 충분했으며, 'cloudscraper 대안'이 현재 사람들이 검색하는 이유입니다.

그 이후로 여러 가지가 바뀌었으며, 이는 특정 라이브러리 버전에 의존하지 않습니다:

2026년 9월 29일 기준으로, cloudscraper 저장소의 마지막 푸시는 2025년 6월이었습니다. 이는 특정 사이트에 대한 판결이 아니며, 이 기사에서는 벤치마크를 하지 않았습니다. 이는 작동한다고 가정하기보다는 자신의 목표에 대해 테스트해야 한다는 의미입니다. 각 대안은 문제의 일부를 해결합니다. curl_cffi는 브라우저 TLS 핸드셰이크를 재현하지만 JavaScript를 실행하지 않습니다. Playwright 또는 다른 실제 브라우저는 챌린지를 실행하지만 여전히 깨끗한 IP가 필요하며, 대량으로 운영하기에는 무겁습니다. 언락커는 두 가지를 결합하고, 둘 다 작동하지 않았을 때 알려줍니다.

통과하는 방법: 웹 언락커 경로

QuantumProxies.io Web Unlocker는 GET을 단계적으로 처리합니다. 먼저, 각기 다른 브라우저 지문으로 새 주거용 출구에서 최대 세 번의 TLS 시도를 합니다. 모든 시도가 챌린지로 돌아오면, 챌린지를 실행하는 헤드리스 브라우저로 에스컬레이션합니다. 그 후에도 페이지가 여전히 차단되면, x-qp-unlocker-blocked 및 차단 클래스를 포함한 오류 상태를 받으며, 200으로 꾸며진 챌린지 페이지는 절대 받지 않습니다. Python에서는 프록시 설정입니다:

import os
import time
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

user = os.environ["QP_UNLOCK_USER"] + "-country-us"          # unlock-XXXXXXXX-country-us
PROXY = f"http://{user}:{os.environ['QP_UNLOCK_PASSWORD']}@unlock.quantumproxies.io:9000"

s = requests.Session()
s.proxies = {"http": PROXY, "https": PROXY}
s.verify = False                     # or the unlocker CA file from /api/v1/scraper/unlock/ca
s.headers["x-qp-session"] = "glassdoor-01"   # same value on every call: same exit, reusable clearance

for attempt in (1, 2):               # same page twice here; in practice, the next page
    t0 = time.monotonic()
    r = s.get(URL, timeout=120)
    print(attempt, r.status_code, f"{time.monotonic() - t0:.1f}s",
          "rendered:", r.headers.get("x-qp-unlocker-rendered"),
          "clearance:", r.headers.get("x-qp-clearance"),
          "block:", r.headers.get("x-qp-block-class"))

x-qp-* 헤더는 언락커가 읽고 사이트로 전달되지 않습니다. 인증서를 전혀 다루고 싶지 않다면, REST 엔드포인트 POST https://app.quantumproxies.io/api/v1/scraper/unlock는 API 키와 함께 JSON 본문에 URL을 받아 응답으로 blocked, blockClass, vendor를 반환합니다. 우리의 웹 언락커 API 튜토리얼은 두 가지를 모두 안내합니다.

측정됨: 통과한 두 가지 Cloudflare 스타일의 벽

2026년 9월 29일에 측정되었습니다. 일반 요청은 Chrome User-Agent를 사용한 curl로, 프랑크푸르트 데이터센터 서버와 미국 출구를 가진 주거용 프록시를 통해 이루어졌습니다. 언락커는 미국 출구를 가진 주거용 티어를 사용했습니다. 시간은 한 요청에 대한 벽 시계 시간입니다.

사이트일반, 데이터센터 서버일반, 주거용 프록시웹 언락커
Glassdoor (회사 리뷰)403, '보안' 페이지403, 'Enable JavaScript and cookies to continue'200, 2번 시도 + 헤드리스 브라우저, 17.2초
Indeed (구직 검색)403, '보안 검사' 페이지403, 'Enable JavaScript and cookies to continue'200, 1번 시도, 브라우저 없음, 2.6초

Glassdoor는 전체 경로입니다. 두 번의 TLS 시도가 챌린지를 받았고, 그 후 헤드리스 브라우저가 챌린지를 실행하고 리뷰 페이지를 반환했습니다. Indeed는 흥미로운 사례입니다: 일반 주거용 요청이 받은 챌린지는 언락커의 첫 번째 시도에서는 전혀 나타나지 않았습니다. 브라우저 TLS 지문이 있는 새 주거용 출구가 충분했으며, 브라우저가 필요하지 않았습니다.

JavaScript 챌린지를 웹 언락커로 통과하기

클리어런스 재사용: 두 번째 요청이 빠른 이유

브라우저 에스컬레이션은 느린 부분이며, 모든 페이지에서 이를 지불하고 싶지 않습니다. 헤드리스 브라우저가 챌린지를 통과하면 클리어런스를 획득합니다. 동일한 x-qp-session 값을 보내거나, 프록시 사용자 이름에 동일한 -session-<id>를 유지하면, 후속 요청은 동일한 출구에서 출발하여 브라우저를 다시 실행하는 대신 클리어런스를 재사용합니다. x-qp-clearance 헤더는 각 호출에서 무슨 일이 일어났는지를 알려줍니다: 새 클리어런스가 획득되었을 때 minted, 저장된 것이 재사용되었을 때 hit, 재사용할 것이 없을 때 miss.

2026년 9월 29일 Glassdoor 테스트에서, 세션이 프록시 사용자 이름에 고정되고 두 개의 별도 연결이 있었을 때, 첫 번째 요청은 14.0초 동안 브라우저를 통해 진행되었고(minted), 두 번째는 1.4초 만에 돌아왔습니다(hit). 고정이 중요한 이유는 Cloudflare가 클리어런스를 묶는 방식 때문입니다: 이는 획득한 방문자와 장치에 묶여 있으며, 사이트가 구성한 기간 동안 유효하게 유지되며, Cloudflare가 세션이 의심스럽다고 판단하지 않는 한 그렇습니다. 출구나 지문을 변경하면 다시 새로운 방문자가 됩니다.

하나의 고정된 언락커 세션에서 두 요청의 타임라인: 첫 번째는 헤드리스 브라우저로 에스컬레이션하여 14.0초 만에 Cloudflare 클리어런스를 획득하고, 두 번째는 이를 재사용하여 1.4초 만에 반환됩니다.
첫 번째 요청은 브라우저에 대한 비용을 지불하고, 동일한 고정된 세션의 후속 요청은 클리어런스를 재사용합니다.

일반 요청이 여전히 Cloudflare 사이트에서 괜찮을 때

많은 사이트가 Cloudflare 뒤에서 CDN으로만 실행되거나, 일반 트래픽을 허용하는 봇 규칙을 가지고 있으며, 일반 요청이 그들에서 작동합니다. 'Cloudflare에 있다'는 브라우저를 찾을 이유가 아닙니다. 신호는 응답입니다. cf-mitigated: challenge 헤더, 'Just a moment' 제목, 403이 없으면 일반 요청을 유지하고 실제로 챌린지를 받는 URL만 에스컬레이션하십시오. 우리의 튜토리얼의 plain-first 도우미는 정확히 그렇게 합니다.

두 가지 인접한 문제에는 자체 가이드가 있습니다. 'Error 1020 Access denied'라는 페이지는 챌린지가 아닌 방화벽 규칙이 거부하는 것이며, JavaScript로 해결할 수 없습니다; Cloudflare 오류 1020을 유발하는 것과 해결하는 것을 참조하십시오. Python을 넘어선 Cloudflare의 방어에 대한 더 넓은 그림을 위해, 2026년 Cloudflare 챌린지를 우회하는 방법을 읽어보십시오. 벽 뒤의 페이지가 Glassdoor라면, 우리의 Glassdoor 급여 및 리뷰 스크래핑 가이드는 데이터 측면을 다룹니다.

자주 묻는 질문

왜 내 스크래퍼는 로컬에서는 작동하지만 클라우드에서 Cloudflare에 의해 차단되는가?

IP가 변경되기 때문입니다. 집에서는 스크립트가 주거용 ISP 주소에서 출발합니다; 클라우드 VM에서는 봇 방어가 즉시 불신하는 호스팅 범위에서 출발합니다. Python TLS 핸드셰이크는 두 경우 모두 동일하며, 더 엄격한 사이트에서는 집에서도 챌린지를 받습니다. 우리의 테스트에서, 주거용 IP만으로는 Indeed나 Glassdoor를 통과하지 못했습니다.

왜 Cloudflare의 "Just a moment" 페이지가 403을 반환하는가?

챌린지 페이지는 요청한 페이지 대신 제공되며, 우리의 테스트에서는 상태 403과 함께 제공되었기 때문에 requests는 '403 Client Error: Forbidden'을 발생시킵니다. 상태는 챌린지에 특정하지 않습니다. Cloudflare가 모든 챌린지 페이지 응답에 대해 challenge로 설정한다고 문서화한 cf-mitigated 응답 헤더를 확인하거나, 'Just a moment' 제목을 확인하십시오.

cloudscraper가 여전히 작동하는가, 좋은 대안은 무엇인가?

설정이 관대한 사이트에서는 여전히 작동할 수 있으므로, 목표에 대해 테스트하십시오. 이는 Python의 TLS 스택에서 실행되며, 인터프리터에서 스크립트를 해결하는 반면, 현재 챌린지는 핸드셰이크와 브라우저 환경도 봅니다. 대안: 브라우저 핸드셰이크를 위한 curl_cffi, JavaScript를 위한 실제 브라우저, 또는 둘 다 수행하는 웹 언락커.

Python requests가 Cloudflare 챌린지를 자체적으로 통과할 수 있는가?

아니요. requests는 JavaScript를 실행하지 않으므로, 페이지의 스크립트를 실행하는 데 의존하는 챌린지를 완료할 수 없습니다. 챌린지를 받지 않는 Cloudflare 사이트에서 페이지를 가져올 수 있습니다. cf-mitigated: challenge가 나타나면, 실제 브라우저가 필요하며, 직접 또는 이를 실행하는 서비스를 통해, 이상적으로는 깨끗한 주거용 IP가 필요합니다.

Cloudflare 클리어런스는 얼마나 오래 지속되는가?

사이트가 설정한 기간 동안 지속됩니다. Cloudflare는 설정을 Challenge Passage라고 부르며, 클리어런스는 Cloudflare가 세션이 의심스럽다고 판단하면 일찍 종료될 수 있습니다. cf_clearance 쿠키는 획득한 방문자와 장치에 묶여 있으므로, 후속 요청이 동일한 출구와 지문을 유지할 때만 도움이 됩니다. 이는 x-qp-session을 고정하는 이유입니다.

짧은 버전: 'Just a moment...'는 브라우저를 요구하므로, requests에서 헤더 트릭으로 이를 만족시킬 수 없습니다. cf-mitigated로 이를 감지하고, 챌린지를 받지 않는 모든 것에 대해 일반 요청을 유지하며, 챌린지를 받은 URL을 깨끗한 IP에서 실제 브라우저를 실행하는 것으로 보내고, 세션을 고정하여 그 브라우저에 대해 한 번만 비용을 지불하십시오.

주거용 출구에서 웹 언락커를 시도해보십시오