2026년 Cloudflare의 React 기반 봇 탐지 리버스 엔지니어링

발행: (2026년 4월 3일 AM 10:36 GMT+9)
11 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I need the full text of the post. Could you please paste the content you’d like translated (excluding the source line you already provided)? Once I have the article text, I’ll translate it into Korean while preserving the original formatting, markdown, and any code blocks or URLs.

React 기반 Cloudflare 감지 작동 방식

전통적인 Cloudflare 보호는 CDN 수준에서 요청을 가로채고 대상 사이트가 로드되기 전에 챌린지 페이지를 표시합니다.
React 기반 감지는 다릅니다:

  1. CDN은 챌린지 없이 React 앱을 제공합니다.
  2. React 앱이 렌더링되고 JavaScript가 실행됩니다.
  3. React 컴포넌트 내부(보통 useEffect 훅)에서 Cloudflare의 봇 감지 스크립트가 실행됩니다.
  4. 스크립트가 사용자를 봇으로 판단하면, 컴포넌트는 다음 중 하나를 수행합니다
    • 실제 콘텐츠를 언마운트하고 챌린지를 렌더링하거나,
    • 조용히 Cloudflare에 신호를 보냅니다.
  5. 이후 IP/지문에서 오는 요청은 더 강도 높은 챌린지를 받게 됩니다.

React 레이어에서 수행되는 일반적인 검사

기법스크립트가 수행하는 작업
Canvas fingerprint보이지 않는 캔버스를 렌더링하고 픽셀 데이터를 읽음
WebGL fingerprintGPU 렌더러 문자열을 읽음
Font enumeration특정 폰트 목록에 대해 렌더링된 텍스트 크기를 측정함
AudioContext fingerprint오디오 신호를 생성하고 출력값을 해시함
Navigator propertiesnavigator.webdriver, 플러그인 목록, 언어 배열을 확인함
Mouse/keyboard timing컴포넌트가 마운트되기 전에 모든 상호작용을 감지함
Performance timingperformance.now() 정밀도를 검사함 (헤드리스 브라우저에서 감소됨)

여기서 발생하는 문제

표준 curl_cffi 접근 방식이 실패하는 이유는 다음과 같습니다:

  • curl_cffi는 TLS 지문(4계층)을 처리하지만 JavaScript를 실행하지 못합니다.
  • 기본적인 스텔스 패치를 적용한 Playwright조차도 실패할 수 있는데, 이는 탐지가 CDN 계층이 아니라 애플리케이션 계층에 있기 때문입니다.

해결책: JavaScript‑API 수준에서 지문을 교정한 전체 브라우저를 사용합니다.

도구 1 – Camoufox (이 패턴에 가장 적합)

Camoufox는 C++ 수준에서 Firefox를 패치하여, JS API가 실제 사용자의 브라우저와 일치하는 값을 반환하도록 합니다.

pip install camoufox
python -m camoufox fetch
from camoufox.sync_api import Camoufox
import time

def scrape_react_protected_site(url: str) -> str:
    with Camoufox(headless=True) as browser:
        page = browser.new_page()

        # Navigate and wait for React to hydrate
        page.goto(url, wait_until="networkidle")

        # Wait for the React bot‑detection component to run
        # (usually within 2‑3 seconds of page load)
        time.sleep(3)

        # Check if we got past detection
        content = page.content()
        if "cf-challenge" in content or "Checking your browser" in content:
            print("Bot detection triggered — trying interaction pattern")
            # Simulate brief human interaction
            page.mouse.move(400, 300)
            time.sleep(0.5)
            page.mouse.move(402, 305)
            time.sleep(1)

        return page.content()

result = scrape_react_protected_site("https://target-site.com")
print(result[:1000])

Camoufox의 저수준 패치는 React 기반 검사를 안정적으로 무력화합니다.

도구 2 – FingerprintJS 스푸핑을 이용한 Playwright

Camoufox가 옵션이 아니라면, Playwright에서 가장 일반적인 지문을 수동으로 스푸핑할 수 있습니다.

from playwright.sync_api import sync_playwright
import time

# ----------------------------------------------------------------------
# Stealth script – patches typical fingerprinting vectors
# ----------------------------------------------------------------------
STEALTH_SCRIPT = """
// Canvas fingerprinting – add subtle noise
const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function(x, y, w, h) {
    const img = origGetImageData.call(this, x, y, w, h);
    const data = img.data;
    for (let i = 0; i  undefined });

// Fake plugins list
Object.defineProperty(navigator, 'plugins', {
    get: () => [
        { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
        { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
        { name: 'Native Client', filename: 'internal-nacl-plugin' },
    ]
});

// Fake language list
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });

// Reduce performance.now() precision
const origNow = performance.now.bind(performance);
performance.now = () => Math.round(origNow() * 100) / 100;
"""
def scrape_with_stealth_playwright(url: str) -> str:
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox",
                "--disable-setuid-sandbox",
            ],
        )
        context = browser.new_context()
        page = context.new_page()

        # Inject the stealth script before any site code runs
        page.add_init_script(STEALTH_SCRIPT)

        page.goto(url, wait_until="networkidle")
        time.sleep(3)                     # give the React detection component time to execute

        content = page.content()
        if "cf-challenge" in content or "Checking your browser" in content:
            print("Challenge detected – performing human‑like interaction")
            page.mouse.move(400, 300)
            time.sleep(0.4)
            page.mouse.click(400, 300)
            time.sleep(1)

        return page.content()

result = scrape_with_stealth_playwright("https://target-site.com")
print(result[:1000])

성공을 위한 핵심 포인트

  • 스크립트를 가능한 한 빨리 주입하세요 (add_init_script).
  • 초기 로드 후 아주 작은 인간 상호작용을 흉내 내세요; 많은 React 검사에서는 감지 컴포넌트가 마운트되기 전에 마우스/키보드 활동을 확인합니다.
  • 브라우저의 headless 플래그를 유지하세요 (headless=True) 하지만 AutomationControlled 블링크 기능을 비활성화하여 눈에 띄는 navigator.webdriver 플래그를 피합니다.

TL;DR

  • Cloudflare의 새로운 React‑기반 봇 탐지는 페이지의 JavaScript 내부에서 실행되며, 단순한 HTTP‑클라이언트 트릭은 효과가 없습니다.
  • 실제 브라우저를 사용하고 저수준 지문 패치(Camoufox)를 적용하거나 수동 JS‑수준 스푸핑(Playwright + 커스텀 스크립트)을 사용하세요.
  • 페이지가 로드된 후 작은 인간‑같은 인터랙션을 추가해 타이밍 검사를 통과시키세요.

이러한 방법들을 사용하면 2026년에 React에 내장된 Cloudflare 챌린지를 우회할 수 있을 것입니다.

디버깅: 감지가 실제로 확인하는 것은 무엇인가?

브라우저 DevTools 또는 mitmproxy를 사용하여 React 컴포넌트가 어떤 신호를 반환하는지 확인하세요.

방법 1 – mitmproxy를 사용하여 외부 요청 검사하기

pip install mitmproxy
mitmproxy --mode transparent -p 8080 --showhost

그 다음 스크립트에서:

proxy = {
    "http": "http://127.0.0.1:8080",
    "https": "http://127.0.0.1:8080"
}

mitmproxy 출력에서 다음과 같은 Cloudflare 엔드포인트에 대한 POST 요청을 찾아보세요:

  • challenges.cloudflare.com
  • turnstile.cf-analytics.com
  • JSON 페이로드에 cfjskey 또는 cf_chl_opt 필드가 포함된 모든 엔드포인트

요청 본문을 보면 어떤 지문 데이터가 수집되었는지 확인할 수 있습니다.

방법 2 – 페이지 내부 콘솔 로깅

from playwright.sync_api import sync_playwright

def debug_cloudflare_detection(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)  # headless=False to see what happens
        page = browser.new_page()

        # Log all network requests
        page.on(
            "request",
            lambda req: print(f"REQ: {req.method} {req.url[:80]}")
            if "cloudflare" in req.url or "challenges" in req.url else None,
        )
        page.on(
            "response",
            lambda res: print(f"RES: {res.status} {res.url[:80]}")
            if "cloudflare" in res.url else None,
        )

        # Log console messages from the page
        page.on("console", lambda msg: print(f"CONSOLE: {msg.type} - {msg.text[:100]}"))

        page.goto(url)
        import time
        time.sleep(5)  # Watch what happens

        browser.close()

React 기반 탐지를 위한 실용 체크리스트

When you suspect a React‑embedded bot detection:

  • Confirm it’s React – look for __NEXT_DATA__, window.__react_root, or data-reactroot in the page source.
  • Use camoufox first – patched at the C++ level; most reliable.
  • If camoufox fails – add explicit fingerprint patching (canvas, WebGL, AudioContext).
  • If still failing – use mitmproxy to see what data Cloudflare receives; patch specifically what’s leaking.
  • Nuclear option – run a real browser via remote desktop (Browserless.io, BrightData’s Scraping Browser).

포기하고 데이터 서비스를 이용해야 할 때

React 내장 탐지는 유지 비용이 많이 듭니다. Cloudflare가 정기적으로 업데이트하고, 패치가 깨지며, 결국 무기 경쟁에 빠지게 됩니다.

이 정도 수준의 보호를 가진 사이트의 경우, 다음을 고려하세요:

  • 스크래핑‑브라우저 서비스 (BrightData, Oxylabs) – 우회 코드를 유지 관리합니다.
  • 공식 데이터 제공자 – 사이트가 API 또는 데이터 덤프를 제공하는 경우.
  • 캐시/인덱스된 데이터 – Common Crawl, Wayback Machine, Google Cache.

ROI 계산: 우회 작업을 구축하는 데 8 시간이 걸리고 매월 깨진다면, 개발자 시간당 $100 / 시간을 기준으로 연간 $1,200 / 년이 됩니다 — 종종 데이터를 직접 구매하는 것보다 더 비쌀 수 있습니다.

다음 단계로

Setup을 건너뛰세요. Cloudflare 탐지 우회를 위한 프로덕션‑준비 도구:

0 조회
Back to Blog

관련 글

더 보기 »

React에서 배열을 순회하는 방법 (올바른 방법)

React를 배우고 있다면, 어느 순간 다음과 같은 질문을 하게 될 것입니다: > “arrays를 올바르게 순회하려면 어떻게 해야 할까?” 그리고 맞아요… for를 사용할 수는 있지만, 그게 바로 정답은 아닙니다.

NH:STA S01E02 OpenPGP.js

이 게시물은 Sovereign Tech Agency(https://www.sovereign.tech/)를 위한 우리의 작업에 대한 시리즈의 일부입니다. 시리즈의 첫 번째 게시물에서는 우리가 왜 그리고 어떻게 ...

Typescript 6: 리허설

1969년 5월 18일, NASA는 아폴로 10호를 발사했습니다. 세 명의 우주비행사—톰 스태퍼드, 존 영, 그리고 진 서넌—은 달로 향해 비행하고, 달 궤도에 진입했으며, 달에 착륙하기 위해 하강했습니다.