- Published on
크롤링 차단 우회 기법 총정리 2026 — Cloudflare, IP 차단, 캡차까지
- Authors

- Name
- 매크로 마스터

안녕하세요, 매크로마스터입니다.
크롤링 의뢰가 들어오면 절반 이상은 비슷한 패턴으로 출발합니다. "Python으로 한 페이지 긁어보긴 했는데, 100건 넘어가니까 403이 뜨더라구요." "처음엔 잘 되다가 갑자기 캡차가 뜨기 시작했어요." "Cloudflare 페이지가 막아서 진도가 안 나갑니다." 차단의 패턴은 늘 비슷하고, 우회의 원리도 비슷합니다. 다만 단계마다 다루는 도구가 달라지고 비용 구조가 달라질 뿐입니다.
이 글에서는 크롤링이 차단되는 일곱 가지 대표 패턴과 각각의 우회 기법을, 가장 가벼운 헤더 위장부터 캡차 자동 풀이까지 단계별로 정리합니다. 실전 코드 네 개, 차단 패턴 × 우회 기법 매트릭스, 합법성 검토까지 한 번에 다룹니다. 공공 API처럼 차단 없는 정공법이 더 나은 케이스도 짚어드리니, 무리하게 우회하기 전에 한 번씩 생각해 보시길 권합니다. 공공 데이터 쪽 정공법은 DART API 완벽 가이드에서 별도로 다뤘습니다.
크롤링이 차단되는 7가지 패턴
차단은 우연이 아니라 설계입니다. 사이트 운영자 입장에서 봇 트래픽은 인프라 비용, 데이터 유출, 가격 비교 침해 같은 명확한 리스크입니다. 그래서 상용 사이트일수록 여러 층의 탐지 시스템을 겹쳐 둡니다. 우회를 하려면 먼저 무엇을 보고 차단하는지를 알아야 합니다.
가장 먼저 마주치는 건 요청 헤더 검사입니다. User-Agent가 비어 있거나 python-requests/2.31.0 같은 기본값으로 들어오면 첫 줄에서 걸러집니다. Accept-Language, Accept-Encoding, Referer가 누락되거나 일관성이 깨져도 마찬가지입니다.
두 번째는 요청 빈도입니다. 동일 IP에서 초당 수십 건씩 일정 간격으로 호출이 오면 사람이 아니라는 게 너무 명확합니다. 트래픽 분석 기준치는 사이트마다 다르지만, 분당 30회를 넘기는 순간 노란불이 켜진다고 보면 안전합니다.
세 번째는 IP 평판입니다. 데이터센터 IP(AWS, GCP, Azure 등)는 이미 알려진 봇 트래픽 출처라서 사이트마다 IP 대역을 통째로 차단합니다. 같은 IP에서 비정상 패턴이 한 번이라도 잡히면 그 IP는 한동안 블랙리스트에 들어갑니다.
네 번째는 세션과 쿠키 일관성입니다. 정상 사용자는 로그인 후 세션 쿠키, 추적 쿠키, 광고 쿠키를 모두 들고 다닙니다. 크롤러가 매 요청마다 쿠키를 비우거나 새 세션으로 들어오면 행동 패턴이 뚜렷합니다.
다섯 번째는 JavaScript 챌린지입니다. Cloudflare의 IUAM(I'm Under Attack Mode), Bot Fight Mode, Turnstile이 대표적입니다. 페이지가 로딩되기 전에 자바스크립트가 일정한 연산을 수행해야 본 페이지로 넘어갈 수 있는 구조라, requests 같은 라이브러리는 본문 자체를 못 받습니다.
여섯 번째는 **캡차(CAPTCHA)**입니다. 의심 점수가 일정 임계치를 넘으면 reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile이 노출됩니다. 이걸 자동으로 푸는 건 기술적으로 가능하지만 비용과 합법성 문제가 함께 따라옵니다.
일곱 번째는 브라우저 핑거프린팅입니다. navigator.webdriver 속성, Canvas 렌더링 지문, WebGL 벤더 정보, 폰트 목록, 화면 해상도, 터치 이벤트 지원 여부를 종합해서 자동화 브라우저인지 판별합니다. 최신 봇 디텍션은 이 7번째 층에서 대부분 잡습니다.

1단계 — Basic: User-Agent와 헤더 위장
가장 흔한 차단은 가장 가벼운 우회로 풀립니다. 정상 브라우저처럼 보이게 헤더를 맞추는 것만으로도 차단 비율이 절반 이하로 떨어지는 경우가 많습니다.
먼저 User-Agent입니다. 빈 값이거나 python-requests/2.31.0처럼 라이브러리 기본값을 그대로 보내는 건 자살 행위입니다. 실제 크롬, 사파리, 파이어폭스의 최신 UA 문자열을 가져와서 풀에 넣고 회전시키는 게 기본입니다. UA 문자열은 useragentstring.com 같은 사이트에서 최신 목록을 받거나, 직접 본인 브라우저에서 navigator.userAgent를 확인해서 씁니다.
UA 하나만 바꾼다고 끝나는 게 아닙니다. 정상 브라우저가 보내는 헤더 세트는 보통 다음과 같습니다.
User-Agent: 브라우저와 OS 식별자Accept: 받을 수 있는 MIME 타입Accept-Language: 언어 우선순위 (한국어 사이트면ko-KR,ko;q=0.9,en;q=0.8정도)Accept-Encoding: 압축 방식 (gzip, deflate, br)Referer: 직전 페이지Sec-Fetch-Site,Sec-Fetch-Mode,Sec-Fetch-Dest: 최신 크롬에서 보내는 보안 헤더Sec-Ch-Ua,Sec-Ch-Ua-Mobile,Sec-Ch-Ua-Platform: User-Agent Client Hints
이 중 하나라도 빠지면 정상 브라우저 트래픽과 패턴이 어긋납니다. 봇 디텍션 솔루션은 헤더 세트의 "조합"을 보지 하나의 값만 보지 않습니다. UA는 윈도우 크롬으로 보내면서 Sec-Ch-Ua-Platform은 빠뜨리면, 그 자체로 비정상 시그널입니다.
세션 단위로 헤더를 일관되게 들고 다니는 게 핵심입니다. 첫 요청에서 보낸 UA와 후속 요청의 UA가 다르면 그것도 봇 시그널입니다.
import requests
import random
import time
UA_POOL = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
]
def build_session():
ua = random.choice(UA_POOL)
s = requests.Session()
s.headers.update({
"User-Agent": ua,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Site": "none",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "document",
"Sec-Ch-Ua": '"Chromium";v="138", "Not)A;Brand";v="8"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"Windows"',
"Upgrade-Insecure-Requests": "1",
})
return s
session = build_session()
for url in target_urls:
resp = session.get(url, timeout=10)
if resp.status_code == 200:
process(resp.text)
time.sleep(random.uniform(1.5, 4.0))
요청 간격에 random.uniform으로 흔들기를 주는 것도 같은 맥락입니다. 정확히 2초마다 호출이 들어오는 패턴 자체가 사람이 아니라는 신호입니다. 1.5초에서 4초 사이로 흔들면 시각적으로 자연스러워지고, 분당 호출도 자동으로 안전 구간(20회 안팎)에 들어옵니다.
2단계 — 세션과 쿠키 관리
헤더가 정상이어도 매 요청마다 쿠키가 비어 있으면 두 번째 층에서 막힙니다. 사이트 입장에서는 "처음 들어왔는데 곧장 상세 페이지부터 호출"하는 트래픽이 한 시간에 수백 건씩 들어오는 게 정상일 리 없으니까요.
진짜 사용자처럼 보이려면 세션 흐름을 흉내내야 합니다. 일반적으로 사용자는 메인 페이지 → 카테고리 → 검색 → 상세 순으로 이동합니다. 그 과정에서 세션 쿠키가 발급되고, 추적 쿠키가 쌓이고, CSRF 토큰이 자리잡습니다. 크롤러도 같은 흐름을 한 번은 흘려보내야 합니다.
requests.Session을 쓰면 쿠키가 자동으로 누적됩니다. 처음 한 번 메인 페이지를 GET 해서 세션 쿠키를 받고, 그 세션으로 이후 요청을 보내는 패턴이 기본입니다. 사이트에 따라 메인 페이지 호출 한 번으로 __Secure- 계열 쿠키, JSESSIONID, 봇 디텍션 솔루션이 심는 _cfuvid 같은 쿠키가 한꺼번에 들어옵니다. 이걸 들고 다녀야 후속 호출이 통과합니다.
CSRF 토큰이 폼이나 헤더로 요구되는 사이트도 흔합니다. 로그인이 필요한 페이지, 폼 제출이 필요한 API는 거의 대부분 토큰을 검사합니다. 메인 페이지나 로그인 폼 페이지의 HTML에서 <meta name="csrf-token" content="..."> 또는 <input type="hidden" name="_token" value="...">를 정규식이나 BeautifulSoup으로 추출해서, 후속 요청 헤더(X-CSRF-Token)나 폼 데이터에 다시 넣어야 합니다.
쿠키를 영속화하면 디버깅도 편합니다. requests는 requests.utils.dict_from_cookiejar와 cookiejar_from_dict로 쿠키를 dict로 직렬화할 수 있고, http.cookiejar.LWPCookieJar나 MozillaCookieJar를 쓰면 파일로 저장됩니다. 매번 세션을 새로 만들지 말고 디스크에 저장된 세션을 재활용하면 워밍업 비용이 줄어듭니다.
세션 만료에도 대비해야 합니다. 일정 시간이 지나면 세션 쿠키가 만료되거나 서버가 강제로 끊습니다. 401, 403이 한 번이라도 들어오면 무조건 재시도하지 말고, 세션을 새로 워밍업한 뒤 다시 호출하는 분기를 넣어야 안정성이 올라갑니다.
3단계 — IP 차단 우회 (프록시 종류 + 로테이션)
헤더와 세션을 다 맞췄는데도 403이 떨어진다면 IP 자체가 차단됐을 가능성이 높습니다. 한 IP에서 의심 패턴이 한 번 잡히면, 사이트가 그 IP를 일정 기간 블랙리스트에 넣습니다. 그러면 헤더를 어떻게 바꿔도 같은 IP인 한 계속 막힙니다.
해결은 IP를 바꾸는 것, 즉 프록시입니다. 프록시는 크게 세 종류로 갈립니다.
데이터센터 프록시는 가장 저렴합니다. AWS, OVH, DigitalOcean 같은 클라우드 사업자의 IP를 빌려 쓰는 구조라서, IP당 단가가 월 1~3달러 수준입니다. 다만 모든 봇 디텍션 솔루션이 이 IP 대역을 알고 있어서, 보안이 강한 사이트(이커머스, 항공, 부동산)에서는 거의 첫 호출부터 막힙니다. 가벼운 공개 데이터, 뉴스 사이트, 정부 사이트에서는 충분히 쓸 만합니다.
**레지덴셜 프록시(Residential Proxy)**는 실제 가정용 ISP의 IP를 사용합니다. 사용자가 동의한 SDK가 백그라운드로 트래픽을 중계하는 구조라서, 사이트 입장에서는 "한국 KT 가정집 IP에서 들어온 요청"으로 보입니다. 차단 회피력이 압도적으로 높지만 단가도 비쌉니다. 트래픽 GB당 2~15달러 수준이라, 대량 크롤링에서는 비용이 빠르게 누적됩니다. Bright Data, Oxylabs, Smartproxy(현 Decodo)가 대표 사업자입니다.
모바일 프록시는 4G/5G 통신사 IP를 빌립니다. 한국이라면 SK, KT, LGU+의 모바일 데이터 IP입니다. 통신사 캐리어 그레이드 NAT 특성상 같은 IP 뒤에 수천 명의 실 사용자가 있어서, 사이트가 차단을 망설이게 됩니다. 가장 안정적이지만 단가가 가장 높습니다.
선택 기준은 단순합니다. 타겟 사이트의 방어 수준 × 트래픽 볼륨 × 예산 세 축으로 결정합니다. 공공 사이트, 정부 사이트, 일반 뉴스라면 데이터센터 프록시로 충분합니다. 이커머스 가격 비교, 항공권 비교, 부동산 매물 수집처럼 적극적으로 차단하는 사이트는 레지덴셜이 거의 필수입니다.
프록시는 단일 IP로는 의미가 적습니다. 로테이션이 핵심입니다. 풀에서 IP를 매 요청마다, 또는 일정 시간/요청 수마다 갈아끼우는 구조여야 합니다. 503, 429, 403이 떨어진 IP는 즉시 풀에서 빼서 쿨다운 큐로 넘기고, 일정 시간 후 복귀시킵니다.

import requests
import random
import time
from collections import deque
from threading import Lock
class ProxyPool:
def __init__(self, proxies, cooldown_sec=300):
self.active = deque(proxies)
self.cooldown = {}
self.cooldown_sec = cooldown_sec
self.lock = Lock()
def acquire(self):
with self.lock:
now = time.time()
for proxy, ts in list(self.cooldown.items()):
if now - ts >= self.cooldown_sec:
self.cooldown.pop(proxy)
self.active.append(proxy)
if not self.active:
raise RuntimeError("no proxy available")
proxy = self.active.popleft()
self.active.append(proxy)
return proxy
def mark_bad(self, proxy):
with self.lock:
try:
self.active.remove(proxy)
except ValueError:
pass
self.cooldown[proxy] = time.time()
pool = ProxyPool([
"http://user:pass@proxy1.example.com:8000",
"http://user:pass@proxy2.example.com:8000",
"http://user:pass@proxy3.example.com:8000",
])
def fetch(url, max_retry=3):
for attempt in range(max_retry):
proxy = pool.acquire()
try:
resp = requests.get(
url,
proxies={"http": proxy, "https": proxy},
timeout=10,
)
if resp.status_code in (403, 429, 503):
pool.mark_bad(proxy)
continue
return resp
except requests.RequestException:
pool.mark_bad(proxy)
raise RuntimeError(f"failed after {max_retry} retries")
위 코드는 가장 단순한 형태입니다. 실제 운영용으로는 IP별 성공률 추적, 사이트별 별도 풀, 지역별 라우팅(한국 사이트는 한국 IP만), 비동기(asyncio + httpx) 처리까지 들어갑니다. 그래도 시작점은 이 정도면 충분합니다.
4단계 — Cloudflare 차단 (Turnstile, IUAM, Bot Fight)
전체 사이트의 약 20%가 Cloudflare 뒤에 있고, 그중 상당수가 봇 보호 기능을 적극적으로 켜둡니다. Cloudflare 차단은 크게 세 모드로 나뉩니다.
Bot Fight Mode는 가장 가벼운 단계입니다. 의심 트래픽에 자동으로 챌린지를 띄우거나 즉시 차단합니다. 정상 헤더 + 좋은 IP만 갖추면 통과하는 경우가 많습니다.
**IUAM(I'm Under Attack Mode)**은 모든 방문자에게 5초간 JavaScript 챌린지를 강제로 띄웁니다. 페이지 로딩 전에 브라우저가 일정 연산을 수행해야 본문에 도달할 수 있습니다. 일반 requests로는 통과 불가능하고, 헤드리스 브라우저나 JS 엔진을 띄워야 합니다.
Cloudflare Turnstile은 reCAPTCHA를 대체하는 Cloudflare의 자체 캡차 솔루션입니다. 사용자에게 보이지 않는 invisible 방식이 기본이고, 의심 점수가 높으면 인터랙티브 체크박스가 뜹니다. Turnstile은 캡차 풀이 서비스로도 우회 가능하지만 비용과 성공률 모두 변동성이 큽니다.
Cloudflare 우회에는 몇 가지 옵션이 있습니다.
**cloudscraper**는 Python에서 가장 흔히 쓰는 라이브러리입니다. requests API와 호환되면서 IUAM JavaScript 챌린지를 자동으로 풀어줍니다. 다만 Cloudflare가 챌린지 로직을 업데이트할 때마다 깨지는 일이 잦고, 최근 버전의 Turnstile에는 거의 무력합니다.
**undetected-chromedriver**는 Selenium 기반으로 Cloudflare 디텍션을 우회하도록 패치된 크롬드라이버입니다. navigator.webdriver 속성, Chrome DevTools Protocol 시그널 등을 가려서 자동화 브라우저로 인식되지 않게 합니다. 일반 사이트에서는 잘 통하지만, Cloudflare 측이 디텍션 로직을 업데이트하면 한동안 깨집니다.
playwright-stealth 또는 playwright-extra + stealth 플러그인은 가장 안정적인 조합입니다. 다음 섹션에서 코드와 함께 다룹니다.
Flaresolverr는 별도 컨테이너로 띄워두는 프록시 서비스입니다. localhost:8191로 HTTP 요청을 보내면 내부에서 헤드리스 브라우저를 돌려 챌린지를 풀고 쿠키와 HTML을 돌려줍니다. 운영 부담이 있지만, 챌린지 우회 로직을 분리할 수 있다는 게 장점입니다.
선택 기준은 단순합니다. 챌린지가 IUAM 수준이면 cloudscraper로 시작하고, Turnstile이 끼면 헤드리스 브라우저로 올라가는 게 일반적입니다. 둘 다 안 통하는 사이트는 보통 Cloudflare 위에 또 다른 봇 디텍션(PerimeterX, DataDome 등)을 얹어둔 케이스라서, 거기서부터는 비용과 시간이 급격히 올라갑니다.

5단계 — JavaScript 챌린지 (Playwright stealth 모드)
JavaScript 챌린지, 동적 렌더링, SPA(Single Page App) 사이트는 requests로는 본문이 비어서 옵니다. 본문이 자바스크립트로 렌더링된 후에야 채워지기 때문입니다. 이때는 헤드리스 브라우저가 답입니다.
Playwright는 Microsoft가 만든 브라우저 자동화 라이브러리로, Selenium 대비 더 빠르고 API가 깔끔합니다. Chromium, Firefox, WebKit을 모두 지원하고, async/await 기반이라 비동기 크롤링에도 잘 맞습니다.
기본 Playwright만으로는 봇 디텍션을 통과하기 어렵습니다. navigator.webdriver가 true로 노출되고, Chrome DevTools Protocol을 통해 자동화 중임이 곳곳에 새기 때문입니다. 그래서 playwright-stealth 플러그인을 함께 씁니다. 이 플러그인은 webdriver 속성 가리기, Plugin 배열 정상화, WebGL 벤더 위장, permission 쿼리 정상 응답 같은 일련의 패치를 자동으로 적용합니다.
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
async def fetch_with_stealth(url):
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-dev-shm-usage",
],
)
context = await browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/138.0.0.0 Safari/537.36",
viewport={"width": 1920, "height": 1080},
locale="ko-KR",
timezone_id="Asia/Seoul",
)
page = await context.new_page()
await stealth_async(page)
await page.goto(url, wait_until="networkidle", timeout=30000)
await page.wait_for_timeout(2000)
html = await page.content()
await browser.close()
return html
html = asyncio.run(fetch_with_stealth("https://target.example.com/list"))
위 코드에서 몇 가지 디테일이 중요합니다.
--disable-blink-features=AutomationControlled 플래그는 크롬이 자동화 중임을 알리는 내부 플래그를 끕니다. 이게 없으면 navigator.webdriver가 true로 새는 경로가 한 군데 남습니다.
locale과 timezone_id를 명시적으로 지정하는 이유는, 헤드리스 브라우저 기본 로케일이 en-US로 잡혀서 한국 사이트에서 비정상으로 보이기 때문입니다. Accept-Language 헤더와 JavaScript의 navigator.language가 일치해야 자연스럽습니다.
wait_until="networkidle"은 네트워크가 500ms 동안 조용해질 때까지 기다립니다. SPA 사이트에서는 페이지 로드 이벤트가 발생한 후에도 데이터가 비동기로 들어오는 경우가 많아서, domcontentloaded만 기다리면 빈 페이지를 받습니다.
추가로 진짜처럼 보이려면 마우스 움직임을 흉내내는 코드도 넣습니다. page.mouse.move(x, y)로 의미 없는 마우스 이동을 한두 번 넣고, 스크롤도 자연스럽게 흘리는 게 정석입니다. 일부 봇 디텍션은 마우스 이벤트 자체가 없는 세션을 의심 트래픽으로 간주합니다.
리소스 비용도 잊지 마세요. Chromium 인스턴스 하나가 RAM 200~500MB를 잡아먹습니다. 동시에 50개를 띄우면 단일 서버로는 감당이 안 됩니다. 대량 크롤링은 워커 풀 + 큐 + Docker로 인스턴스를 분산해야 합니다.
6단계 — 캡차 우회 (2Captcha, AntiCaptcha, OCR)
캡차가 뜬다는 건 의심 점수가 임계치를 넘었다는 뜻입니다. 헤더, IP, 핑거프린트 어디선가 이미 봇으로 의심받고 있다는 신호고요. 그래서 캡차 풀이 자체보다 캡차가 안 뜨게 앞 단계를 잘 만드는 게 거의 항상 우선입니다.
그래도 캡차를 풀어야 하는 케이스가 있습니다. 사이트 자체가 모든 트래픽에 캡차를 강제하거나, 특정 액션(회원가입, 로그인, 검색 제출)에서 무조건 노출되는 경우입니다.
캡차 풀이 서비스는 크게 두 가지 모델로 갈립니다.
휴먼 솔버 모델은 2Captcha, AntiCaptcha, CapMonster Cloud, DeathByCaptcha가 대표적입니다. 캡차 이미지나 사이트 키를 서비스에 전송하면, 백엔드에서 실 사람(주로 동남아, 인도)이 풀어서 토큰을 돌려줍니다. 정확도가 높고 모든 종류(reCAPTCHA v2/v3, hCaptcha, Turnstile, Funcaptcha, 이미지 캡차)에 대응합니다. 단가는 캡차 1000개당 1~3달러 수준입니다.
AI 솔버 모델은 CapSolver, NextCaptcha 같은 최신 서비스입니다. 머신러닝 모델로 자동 풀이를 하기 때문에 응답이 빠르고(평균 5~10초), 단가도 휴먼 솔버보다 약간 저렴한 편입니다. 다만 새로운 캡차 변형이 나오면 모델이 적응할 때까지 정확도가 흔들립니다.
reCAPTCHA v2 invisible이나 Turnstile 같은 무인터랙션 캡차도 풀 수 있습니다. 사이트 키(data-sitekey)와 페이지 URL만 솔버에 넘기면, 솔버가 가짜 토큰을 생성해서 돌려주는 구조입니다. 이 토큰을 폼 데이터의 g-recaptcha-response 필드에 넣어서 제출하면 통과합니다.
import requests
import time
def solve_recaptcha_v2(site_key, page_url, api_key):
submit = requests.post(
"https://2captcha.com/in.php",
data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1,
},
).json()
if submit.get("status") != 1:
raise RuntimeError(f"submit failed: {submit}")
captcha_id = submit["request"]
for _ in range(40):
time.sleep(5)
result = requests.get(
"https://2captcha.com/res.php",
params={
"key": api_key,
"action": "get",
"id": captcha_id,
"json": 1,
},
).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "CAPCHA_NOT_READY":
continue
raise RuntimeError(f"solve failed: {result}")
raise TimeoutError("captcha not solved within 200s")
token = solve_recaptcha_v2(
site_key="6LeXXXXXXXXXXXXXXXXXXXXXX",
page_url="https://target.example.com/login",
api_key="YOUR_2CAPTCHA_KEY",
)
resp = requests.post(
"https://target.example.com/login",
data={
"username": "...",
"password": "...",
"g-recaptcha-response": token,
},
)
이미지 기반 옛날 캡차(왜곡된 글자 이미지)는 OCR로도 풀 수 있습니다. Tesseract 같은 오픈소스 OCR을 쓰거나, 특정 사이트 전용으로 CNN 모델을 직접 학습시키는 방법도 있습니다. 다만 모던한 캡차는 OCR 회피 노이즈를 강하게 줘서 일반 OCR로는 정확도가 3050% 선에 그치는 게 일반적입니다. 사이트 전용 모델을 학습시키려면 라벨링된 캡차 이미지 10005000장 정도가 필요하고, 학습/유지보수 부담이 작지 않습니다.
캡차 풀이는 비용이 누적되는 영역입니다. 일 1만 건 크롤링에 매 호출마다 캡차가 뜬다면 월 비용이 300~900달러 단위로 쌓입니다. 캡차가 자주 뜨는 사이트는 거의 항상 앞 단계(헤더, IP, 핑거프린트)부터 다시 잡는 게 비용 효율이 훨씬 좋습니다.
7단계 — Fingerprint 탐지 (webdriver, Canvas, WebGL)
여기까지 다 통과해도 마지막에 막히는 게 핑거프린팅입니다. 봇 디텍션의 최신 무기는 브라우저 환경 자체를 정밀하게 측정해서 "이게 진짜 사용자 브라우저인지"를 점수화하는 방식입니다.
가장 단순한 시그널은 navigator.webdriver 속성입니다. WebDriver 표준에 의해 자동화 브라우저에서는 이 값이 true로 설정됩니다. 패치 안 된 Selenium, Puppeteer, Playwright 기본 세팅은 그대로 노출됩니다. stealth 플러그인이 첫 번째로 가리는 게 이 속성입니다.
Canvas Fingerprinting은 동일한 그리기 명령에 대해 GPU와 드라이버에 따라 미세하게 다른 픽셀이 나오는 점을 이용합니다. 헤드리스 환경은 실 사용자 환경 대비 그래픽 스택이 단순해서, 기본 헤드리스 Chromium의 Canvas 지문은 너무 깨끗하게 나옵니다. 실 사용자 분포와 어긋나면 의심 점수가 올라갑니다.
WebGL Fingerprinting도 비슷합니다. WEBGL_debug_renderer_info 확장을 통해 GPU 벤더와 렌더러 문자열을 가져올 수 있는데, 헤드리스에서는 Google SwiftShader 같은 소프트웨어 렌더러로 잡히는 경우가 많습니다. 일반 사용자 분포(NVIDIA, AMD, Intel UHD)와 어긋나는 시그널입니다.
Audio Context Fingerprinting은 OscillatorNode의 출력 신호를 측정해서 오디오 스택의 미세 차이를 잡습니다. 헤드리스는 보통 오디오 출력이 모킹되어 있어서 분포가 좁습니다.
Font Enumeration은 시스템에 설치된 폰트 목록을 측정해서 OS와 사용자 환경을 추정합니다. 헤드리스 도커 컨테이너는 기본 폰트만 깔려 있어서 폰트 목록이 비정상적으로 짧습니다.
Timezone, Locale, Plugin Mismatch: navigator.language는 한국어인데 timezone은 UTC, Accept-Language는 또 영어로 보낸다면 일관성이 깨집니다. 봇 디텍션은 이런 mismatch를 누적해서 점수를 매깁니다.
이 모든 걸 사람이 손으로 패치하려면 끝이 없습니다. playwright-stealth, puppeteer-extra-plugin-stealth 같은 stealth 플러그인은 알려진 핑거프린팅 벡터 30~40개를 한꺼번에 패치해줍니다. 다만 stealth는 만능이 아닙니다. DataDome, PerimeterX, Akamai Bot Manager 같은 상용 솔루션은 plugin이 패치한 것까지 알고 카운터 디텍션을 만들어둡니다.
진짜 어려운 사이트에서는 핑거프린트를 만드는 게 아니라 빌려옵니다. 실 사용자 환경에서 측정한 핑거프린트 프로파일을 모아두고, 매 세션마다 하나씩 꺼내 쓰는 구조입니다. Multilogin, Kameleo, GoLogin 같은 상용 안티디텍션 브라우저가 이 방식을 씁니다. 단가가 비싸지만(월 100~500달러), 가장 어려운 사이트에서도 통합니다.
차단 패턴 × 우회 기법 매트릭스
| 차단 패턴 | 탐지 방법 | 우회 기법 | 난이도 |
|---|---|---|---|
| 헤더 검사 | User-Agent, Accept-*, Sec-* 누락/이상 | 정상 헤더 세트 구성, UA 풀 회전 | ★☆☆☆☆ |
| 요청 빈도 | 동일 IP 분당 호출 수, 등간격 패턴 | random.uniform 딜레이, 분당 20회 이하 | ★☆☆☆☆ |
| IP 차단 | 데이터센터 IP, IP 평판 점수, 블랙리스트 | 레지덴셜/모바일 프록시 + 로테이션 | ★★★☆☆ |
| 세션 일관성 | 쿠키 누락, CSRF 토큰 부재, 세션 흐름 부재 | requests.Session 워밍업, 메인→상세 흐름 | ★★☆☆☆ |
| Cloudflare IUAM | 5초 JS 챌린지, __cf_chl_* 쿠키 | cloudscraper, 헤드리스 브라우저 | ★★★☆☆ |
| Cloudflare Turnstile | invisible/인터랙티브 챌린지 | playwright-stealth, 캡차 솔버 | ★★★★☆ |
| reCAPTCHA v2/v3 | 점수 기반, 이미지 챌린지 | 2Captcha, AntiCaptcha, CapSolver | ★★★★☆ |
| hCaptcha | 이미지 챌린지, 행동 분석 | 2Captcha, CapMonster | ★★★★☆ |
navigator.webdriver | JavaScript 속성 검사 | stealth 플러그인 | ★★☆☆☆ |
| Canvas/WebGL 지문 | 픽셀 해시, GPU 렌더러 문자열 | stealth 플러그인, 핑거프린트 스푸핑 | ★★★★☆ |
| 상용 봇 디텍션 (DataDome 등) | 다층 디텍션 + 행동 분석 | 안티디텍션 브라우저, 실 핑거프린트 풀 | ★★★★★ |
매트릭스의 핵심은 층을 건너뛰지 말라입니다. 1~2단계가 안 됐는데 6단계 캡차 솔버에 돈을 쏟아붓는 건 비효율입니다. 헤더 정리만 잘해도 차단의 절반 이상은 그냥 사라지는 경우가 많습니다.
합법성 — 어디까지 우회해도 되나
기술적으로 가능한 것과 법적으로 허용되는 것은 다릅니다. 우회 기법을 다루면서 합법성을 빠뜨리면 무책임한 글이 됩니다. 핵심만 짚어드리겠습니다.
robots.txt는 권고지 법적 강제력은 아닙니다. robots.txt를 어겼다고 그 자체로 형사 처벌을 받지는 않습니다. 다만 사이트 운영자가 명시적으로 크롤링을 금지한 의사 표시이긴 합니다. 분쟁이 생겼을 때 "악의적 접근"의 정황 증거로 쓰일 수 있습니다.
이용약관(ToS) 위반은 민사적 책임의 영역입니다. 거의 모든 상용 사이트의 이용약관에는 "자동화된 수단으로 데이터를 수집하지 않는다"는 조항이 들어 있습니다. 이걸 어기면 계약 위반이 되고, 사이트 측이 손해배상을 청구할 수 있습니다. 한국에서는 한국정보보호진흥원(KISA), 잡코리아·사람인 분쟁, 다나와 분쟁 같은 판례가 이미 있습니다. 실제 손해배상 인정 사례도 누적되고 있습니다.
저작권 침해와 개인정보보호법은 별개의 강한 영역입니다. 데이터 자체가 저작물(예: 기사 본문, 영화 평론, 사진)이면 저작권법이 적용됩니다. 출처를 표시해도 무단 복제와 영리 이용은 침해입니다. 개인정보(이름, 연락처, 주소, 주민번호)는 더 강력합니다. 동의 없이 수집·이용하면 개인정보보호법 위반으로 형사 처벌까지 갈 수 있습니다.
부정한 접근으로 해석되면 형사 영역으로 갑니다. 단순히 공개된 페이지를 긁는 건 정보통신망법상 "정당한 접근 권한 없는 침입"으로 보지 않는 게 한국 판례의 일반적 흐름입니다. 다만 로그인 페이지를 무단으로 뚫거나, 캡차를 우회해서 사이트의 명확한 보호 조치를 회피하면 해석이 달라질 수 있습니다. 미국 CFAA(Computer Fraud and Abuse Act)는 더 엄격하고, 최근 hiQ vs. LinkedIn 판례 이후 다소 완화됐다 해도 여전히 회색 지대입니다.
그럼 어디까지가 안전한가, 실무 기준으로 정리하면 이렇습니다.
- 공개된 정보를 일반 사용자가 접근 가능한 페이지에서 수집
- 서버에 부담을 주지 않는 합리적 속도로 호출
- 저작권 있는 본문은 인덱싱/검색 용도 외에는 복제하지 않음
- 개인정보는 수집하지 않거나 즉시 폐기
- 이용약관에 명시적으로 금지된 경우 사이트와 사전 협의 시도
- 공공 API가 제공되면 가급적 그쪽 사용
마지막 항목이 중요합니다. 정부나 공공기관 데이터는 거의 모두 공공 데이터 포털 또는 DART OPEN API, 국가법령정보 OPEN API 같은 공식 채널이 있습니다. 차단 우회로 어렵게 가져올 게 아니라 정공법으로 가는 게 합법적이고 안정적입니다.
회색 지대에 들어가는 프로젝트는 진행 전에 변호사 자문을 받으시길 권합니다. 자체 ToS와의 정확한 충돌 지점, 데이터 활용 범위, 출판물 표시 방식까지 사전에 정리하면 분쟁 가능성이 크게 줄어듭니다.
직접 vs 외주 — 차단 우회는 누가 해야 하나
차단 우회 작업은 단순 크롤링 대비 학습 곡선이 가파릅니다. 한 사이트 우회를 처음부터 끝까지 안정화하는 데 보통 1~3주가 걸리고, 그 뒤로도 사이트가 디텍션을 업데이트할 때마다 유지보수 비용이 생깁니다.
직접 구축이 합리적인 경우
- 학습 자체가 목적인 경우 (사이드 프로젝트, 스터디)
- 사내에 풀타임 데이터 엔지니어가 1명 이상 있고, 그 사람의 시간을 빼서 쓰는 게 사업적으로 정당화되는 경우
- 차단 강도가 약한 공공 사이트, 정부 사이트, 1~2단계 우회로 끝나는 경우
- 단발성 추출이고 유지보수 부담이 없는 경우
외주가 합리적인 경우
- 본업이 데이터 엔지니어링이 아닌 분 (애널리스트, 마케터, 운영자)
- 차단 강도가 강한 사이트(이커머스, 항공, 부동산)에서 안정적 수집이 필요한 경우
- 장기 운영해야 하는 시스템 (분기성 추출이 아니라 매일/매시간 돌아가야 하는 파이프라인)
- 정확도와 누락률에 SLA가 필요한 경우
- 합법성 검토와 함께 진행해야 하는 경우
저희 매크로마스터의 경우 차단 우회가 끼는 크롤링 프로젝트는 보통 다음 순서로 진행됩니다. 첫째, 타겟 사이트의 방어 수준과 데이터 볼륨을 측정해서 17단계 중 어디까지 필요한지 견적을 냅니다. 둘째, 프록시 풀과 헤드리스 워커 인프라를 사이트에 맞게 구성합니다. 셋째, 12주간 안정화 운영을 보고 SLA를 확정합니다. 넷째, 코드 소유권 전체 이전과 30일 무상 유지보수가 기본 패키지에 포함됩니다.
비용 측면에서, 인하우스로 0에서 시작해서 안정화까지 가는 인건비를 계산해보면 외주가 거의 항상 저렴합니다. 같은 일을 여러 번 해본 사람이 짧게 끝내기 때문입니다. 실제 진행했던 작업들의 형태는 포트폴리오 페이지에서 확인하실 수 있습니다. 부동산 매물 수집처럼 차단을 정면으로 마주치는 케이스는 네이버 부동산 크롤링 가이드에서도 일부 다뤘습니다.
자주 묻는 질문
Q1. 그냥 헤더만 잘 바꾸면 대부분 통과되나요?
공공·정부·뉴스 사이트는 헤더 + 딜레이만으로도 60~80%는 통과됩니다. 상용 이커머스나 가격 비교 사이트는 1단계로는 거의 못 갑니다. 사이트마다 디텍션 강도가 다르니, 가장 가벼운 1단계부터 차근차근 올리는 게 정석입니다.
Q2. 무료 프록시 리스트를 써도 되나요?
거의 쓸 수 없다고 보면 됩니다. 무료 프록시는 95% 이상이 이미 블랙리스트에 들어있고, 응답 속도가 매우 느리거나 중간자 공격 의심까지 있습니다. 정 무료로 가야 한다면 Tor가 차라리 깨끗하지만, Tor exit node 자체가 차단된 사이트가 많습니다. 프록시는 유료가 맞습니다.
Q3. Cloudflare 우회 라이브러리가 자꾸 깨집니다.
Cloudflare가 챌린지 로직을 업데이트할 때마다 cloudscraper, undetected-chromedriver 같은 라이브러리는 한동안 깨졌다가 패치됩니다. 라이브러리 GitHub 이슈 트래커를 구독하거나, 깨질 가능성을 안고 가는 대안으로 Flaresolverr 같은 별도 서비스를 두는 게 안정적입니다.
Q4. 캡차 솔버 비용이 부담스럽습니다.
거의 모든 경우, 캡차가 자주 뜬다는 건 앞 단계가 새고 있다는 신호입니다. 헤더 일관성, IP 품질, 핑거프린트 중 어느 하나가 약합니다. 캡차 솔버에 월 수백 달러 쓰기 전에 15단계를 다시 점검해보시면 캡차 노출률이 8090% 떨어지는 경우가 많습니다.
Q5. 헤드리스 브라우저는 너무 무거운데 대안은?
requests + cloudscraper로 70%, httpx + playwright-stealth로 95%, 정도가 일반적인 분포입니다. SPA 사이트나 JS 챌린지가 강한 사이트에서만 헤드리스를 쓰고, 나머지는 가벼운 HTTP 클라이언트로 처리하는 하이브리드가 비용 효율적입니다. 1차 분류는 curl로 본문을 받아보고 핵심 데이터가 들어있는지 확인하는 거부터 시작합니다.
Q6. 사이트가 GraphQL을 쓰는데 크롤링이 어렵나요?
오히려 쉬워지는 경우가 많습니다. 브라우저 개발자도구의 Network 탭에서 GraphQL 요청의 페이로드를 그대로 따와서, 헤더 + 페이로드만 맞추면 백엔드 API를 직접 호출하는 형태가 됩니다. HTML 파싱이 사라지는 만큼 안정성이 올라갑니다. 다만 인증 토큰과 CSRF 토큰을 워밍업으로 받아오는 과정은 동일합니다.
Q7. 모바일 앱 API를 크롤링하면 더 쉽나요?
웹보다 더 어려워지는 경우가 많습니다. 모바일 앱은 SSL Pinning, 앱 서명 검증, 디바이스 핑거프린트 같은 보호 기법을 추가로 걸어둡니다. 우회는 가능하지만 mitmproxy 세팅, Frida를 통한 패치 같은 고급 기법이 필요해서 학습 곡선이 가파릅니다. 웹 쪽이 차단된 게 아니라면 웹 우선이 효율적입니다.
Q8. 차단된 IP는 영구 차단인가요?
대부분 시간 기반 자동 해제입니다. 짧으면 1시간, 길면 7~30일 후 자동으로 해제됩니다. 다만 사이트가 IP 평판 데이터베이스(MaxMind, IPQualityScore)를 참조해서 차단하는 경우, 그 IP의 평판 점수 자체가 회복돼야 합니다. 평판 점수는 한 번 떨어지면 회복에 수 주 걸리기도 합니다. 그래서 좋은 IP를 함부로 태우지 않는 게 중요합니다.
마무리: 차단 우회는 기술이지 만능이 아닙니다
7단계의 우회 기법을 전부 풀어드렸지만, 가장 중요한 메시지를 다시 한 번 정리하겠습니다. 차단 우회는 사이트 운영자와의 비대칭 게임이고, 사이트가 본격적으로 막으면 거의 모든 자동화는 결국 비용 싸움으로 바뀝니다. 그래서 공공 API가 있는 데이터는 정공법으로, 차단이 가벼운 사이트는 12단계 우회로, 정말 필요한 사이트만 57단계로 가는 단계적 접근이 합리적입니다.
자체 운영 중인 시스템이 이미 차단으로 깨졌거나, 새 프로젝트에서 차단 우회까지 포함된 견적이 필요하시다면 서비스 페이지에서 상세 견적을 확인하실 수 있습니다. 카카오톡으로 바로 사이트 URL과 수집 항목을 보내주시면, 24시간 안에 1~7단계 중 어디까지 필요한지 진단해드립니다.
현금영수증 또는 세금계산서 발행 가능합니다.
