당신은 레지던트 프록시를 구매하고, 새로운 Chrome User-Agent를 설정했지만, 사이트가 첫 번째 요청에서 여전히 403을 반환합니다. 익숙하신가요? 문제는 IP나 헤더에 있지 않습니다. 서버가 HTTP 헤더를 읽기 전에 TLS 핸드셰이크를 통해 이미 당신을 감지했습니다. 2026년에는 이것이 탐지 벡터 №1이며, 일반적인 requests는 자동으로 이를 실패합니다. 이것이 어떻게 작동하는지, 그리고 curl_cffi를 통해 몇 줄의 코드로 어떻게 수정할 수 있는지 알아보겠습니다.
무슨 일이 일어나고 있는가: TLS 핸드셰이크가 당신을 감지합니다
클라이언트가 HTTPS 연결을 설정할 때, 가장 먼저 ClientHello 패킷을 전송합니다 — 모든 HTTP 이전에. 이 패킷에는 TLS 버전, 지원되는 암호 모음(cipher suites) 목록, TLS 확장(SNI, ALPN, supported_groups), 타원 곡선 및 점 형식이 나열됩니다. 이러한 필드의 순서와 구성은 서로 다른 클라이언트마다 다릅니다 — 이로 인해 클라이언트는 한 마디도 말하기 전에 식별될 수 있습니다.
이 필드들로부터 지문이 생성됩니다. JA3 (2017년 표준)는 TLSVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats 형식의 문자열을 가져와 MD5로 해시하여 32자리 서명을 생성합니다. JA3의 문제는 2023년 1월부터 Chrome이 확장 필드의 순서를 무작위화하기 시작했다는 것입니다 — 16개의 확장은 16! (20조 개 이상의) 조합을 제공하며, 동일한 브라우저가 서로 다른 JA3를 생성합니다.
그래서 산업계는 JA4 (FoxIO, 2024–2025년 대규모 도입)로 전환했습니다. JA4는 해시하기 전에 확장 코드들을 헥스 값에 따라 정렬합니다 — Chrome의 무작위화가 더 이상 이를 깨뜨리지 않습니다. 해시는 잘린 SHA-256이며, 사람 친화적인 세 부분 형식(a_b_c)으로 ALPN과 QUIC/HTTP3 지원이 포함됩니다. 예를 들어, Chrome 124는 t13d1516h2를 제공하고, 일반 Python requests는 t13d1715h2를 생성합니다. 안티-봇의 경우 두 번째 서명은 '이것은 스크립트입니다'라는 직접적인 마커입니다.
왜 2026년에 이것이 필수적인가
JA4 탐지는 모든 주요 공급업체에 내장되어 있습니다: Cloudflare는 지문을 allowlist와 대조하고, Akamai는 HTTP/2 SETTINGS 프레임에 대한 별도의 해시를 추가하며, DataDome은 알려진 봇 데이터베이스와 비교합니다. 논리는 간단하고 치명적입니다: 만약 당신이 User-Agent: Chrome 131을 보내고, TLS 지문이 'urllib3/OpenSSL'이라고 외친다면 — 이것은 비동기이며, 당신은 즉시 차단됩니다. 어떤 프록시도 구제하지 못합니다: 완벽한 레지던트 IP가 Python requests의 지문을 가지고 있더라도 여전히 실패합니다.
정확히 그래서 '프록시 + 지문 위조' 조합이 2026년에 스크래핑의 기본 위생이 되었고, 더 이상 고급 옵션이 아닙니다.
해결책: curl_cffi로 5분 만에
curl_cffi는 curl-impersonate (BoringSSL을 사용하여 Chrome에서 빌드된 수정된 curl) 위에 구축된 Python 래퍼입니다. 이 라이브러리는 진짜 브라우저 핸드셰이크를 재현하며, API는 거의 기존의 requests와 동일합니다.
1단계. 설치. Windows/macOS/Linux용 curl-impersonate 바이너리는 자동으로 다운로드됩니다:
pip install curl-cffi
2단계. 기본 요청. 임포트를 변경하고 하나의 매개변수를 추가합니다:
from curl_cffi import requests
resp = requests.get("https://target.com/", impersonate="chrome")
print(resp.status_code)
print(resp.http_version) # HTTP/2 — 실제 브라우저와 동일
한 줄의 impersonate="chrome"는 네 가지 레이어를 동시에 위조합니다: TLS 지문 (JA3/JA4), HTTP 버전 (HTTP/2 대신 HTTP/1.1), 헤더 순서 및 ALPN 협상.
3단계. 항상 generic 별칭을 사용하고 버전을 고정하지 마십시오. impersonate="chrome" (또는 "safari", "safari_ios")를 작성하십시오 — 별칭은 자동으로 최신 프로필로 해결됩니다. 하드코딩된 impersonate="chrome124"는 구식이 됩니다: Chrome은 약 4주마다 업데이트되며, 이전 프로필은 스스로 이상이 됩니다. 신뢰할 수 있는 타겟은 Chrome, Edge 및 Safari/iOS입니다 (chrome99에서 chrome131까지, safari15–18 프로필).
4단계. 프록시 및 세션. 실제 스크래핑을 위해 세션에서 상태를 유지하고 프록시를 연결하십시오. 레지던트 또는 모바일 IP가 필수입니다 — 데이터 센터는 TLS와 별도로 ASN에 의해 감지됩니다:
from curl_cffi import requests
session = requests.Session(impersonate="chrome")
headers = {
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.google.com/",
}
proxies = {
"http": "http://user:pass@proxy-host:port",
"https": "http://user:pass@proxy-host:port",
}
resp = session.get("https://target.com", headers=headers, proxies=proxies)
5단계. 대량 처리를 위한 비동기성. requests와 달리 curl_cffi는 기본적으로 async 및 HTTP/2를 지원합니다:
import asyncio
from curl_cffi.requests import AsyncSession
async def fetch(session, url):
r = await session.get(url, impersonate="chrome")
return r.status_code
async def main(urls):
async with AsyncSession() as session:
return await asyncio.gather(*[fetch(session, u) for u in urls])
asyncio.run(main(["https://target.com"] * 20))
당신의 지문을 확인하십시오 — 추측하지 마십시오
전투 트래픽을 보내기 전에, 위조가 실제로 작동하는지 확인하십시오. 공개 검증기에 요청을 보내고 JA4를 기준 브라우저와 비교하십시오:
- tls.peet.ws — JA3, JA4, Akamai 지문 및 HTTP/2 프레임을 JSON으로 반환합니다.
curl_cffi와 실제 Chrome을 통해 요청하고 해시를 비교하십시오. - ja4db.com — 알려진 JA4 데이터베이스로, 당신이 누구를 닮았는지 이해하는 데 도움이 됩니다.
- browserleaks.com/tls 및 Scrapfly의 JA3/JA4 도구 — 필드의 상세한 분석.
스테이징에서는 스크래퍼와 타겟 사이에 mitmproxy를 설치하고 각 요청의 실제 JA4 해시를 모니터링하는 것이 편리합니다.
진단: 여전히 403/429를 잡고 있습니까?
지문이 정확하지만 차단이 계속된다면, 자주 발생하는 것부터 드물게 체크리스트를 따라가십시오:
- 데이터 센터 IP. 이유 №1. 레지던트 프록시 또는 모바일로 전환하십시오 — 단 하나의 지문으로는 부족한 이유는 IP 인텔리전스를 통한 레지던트 프록시 탐지 자료에서 자세히 설명되어 있습니다.
- 구식 프로필.
pip install -U curl-cffi및 generic 별칭"chrome". - 너무 높은 비율. 요청 사이에 1–3초의 무작위 지연을 추가하십시오.
- 빈 헤더. 반드시
Accept-Language,Accept-Encoding,Referer를 보내십시오 — 이들의 부재도 이상입니다. - 세션과 IP의 비동기. 규칙: 하나의 세션 — 그 생애 동안 하나의 IP.
- 상태 200 ≠ 성공. 응답 본문을 확인하십시오: 200 코드 아래에 CAPTCHA 페이지가 있을 수 있습니다.
curl_cffi가 벽에 부딪히는 곳
curl_cffi는 네트워크 레이어를 닫습니다 — 그게 전부입니다. JavaScript를 실행하지 않습니다. 따라서 JS 챌린지에 대해서는 무력합니다: Cloudflare Turnstile, '브라우저 확인 중...' 페이지 (IUAM), 검증 후 스크립트가 설정하는 쿠키 cf_clearance — 이 모든 것은 실제 브라우저 환경을 요구합니다. 2026년에 솔버들이 이러한 예방 시스템에 대해 거의 작동하지 않게 된 이유는 별도의 분석에서 CAPTCHA 우회에 대해 다루었습니다.
JS 벽에 부딪혔을 때 무엇을 해야 할까요:
- 하이브리드. Playwright 또는 Nodriver가 챌린지를 통과하고
cf_clearance를 얻은 다음, 쿠키를 빠른curl_cffi로 전달하여 대부분의 요청을 처리합니다 — 이렇게 하면 무거운 브라우저에 대해 한 번만 비용을 지불합니다. - 솔버 서비스 (CapSolver, 2Captcha)로 자동으로 토큰을 발급받습니다.
- 관리형 스크래핑 API, 인프라를 유지하고 싶지 않다면.
그리고 스레드 안전성을 기억하십시오: 각 스레드에 대해 고유한 세션을 사용하십시오. requirements.txt에서 curl-cffi의 버전을 고정하고, 브라우저가 업데이트될 때마다 6–12주마다 프로필을 검토하십시오.
curl_cffi의 대안
- tls-client — uTLS 기반 Go 라이브러리의 래퍼로, 프로필(
chrome_124,safari_ios_17)과random_tls_extension_order=True플래그를 제공합니다. 지문을 유연하게 조정할 수 있습니다. - primp — Rust로 작성된 클라이언트로,
impersonate_os를 독립적으로 설정할 수 있으며, 더 높은 대역폭을 제공합니다; 단점은 API가requests와 완전히 일치하지 않으며 라이브러리가 더 젊습니다.
어떤 프록시가 필요하고 그 이유
지문 위조와 프록시는 동일한 문제의 서로 다른 절반을 해결합니다: curl_cffi는 '연결이 어떻게 보이는가'에 대한 질문을 해결하고, 프록시는 '어디서 오는가'에 대한 질문을 해결합니다. 안티-봇은 두 신호를 독립적으로 확인하므로, 완벽한 JA4가 검은 데이터 센터 ASN과 함께 있다면 쓸모가 없습니다. 보호가 필요한 목표(마켓플레이스, 소셜 미디어, 여행 집계기)에는 레지던트 또는 모바일 프록시를 사용하십시오: 이들은 깨끗한 운영자 출처를 가지고 있으며, 모바일은 CGNAT '군중 효과' 뒤에 숨습니다. 데이터 센터는 민감하지 않은 목표와 높은 볼륨을 위해 남겨두십시오.
결론
2026년의 스크래핑은 단순한 IP가 아닌 정체성의 게임입니다. 일반 requests는 TLS 핸드셰이크 수준에서 스크립트로 간주되며, 첫 번째 헤더를 읽기 전에 실패합니다. curl_cffi로의 임포트 교체와 impersonate="chrome"는 이 실패를 5분 만에 제거하지만, 깨끗한 레지던트 또는 모바일 IP와 함께 사용해야 하며, 경계를 이해해야 합니다: 네트워크 레이어 — 그렇고, JavaScript 챌린지 — 그렇지 않습니다. 올바른 지문, 올바른 프록시, JS 벽이 있는 곳에서 브라우저와의 하이브리드 스택을 구축하십시오 — 그러면 첫 번째 요청에서 403은 과거의 일이 될 것입니다.
