크롤러가 차단되는 이유: Puppeteer와 User-Agent, 그리고 봇 감지 우회 전략 ①

JEONGYUJIN·2025년 5월 19일

경험정리

목록 보기
4/7

Puppeteer로 스크롤까지는 잘 작동하는데, 갑자기 브라우저가 꺼진다?
코드는 이상 없는데 아무런 경고도 없이 브라우저가 종료된다?

이럴 땐 의심해야 할 첫 번째 원인: 봇 감지 (anti-bot)

대형 쇼핑몰, 예약 사이트, 티켓팅 페이지 등은 수많은 봇이 들어오는 걸 막기 위해 다양한 수준의 탐지 기법을 사용한다.
특히 쿠팡처럼 SPA 구조 + 사용자 리뷰 + 무한스크롤이 포함된 경우, 정적 접근보다 훨씬 정교한 "브라우저 행동 추적" 기반 탐지가 이뤄진다.


🎭 봇 감지가 무엇인가요?

**봇 감지(Anti-Bot Detection)**는 다음을 기준으로 "이거 사람 아냐?"를 판단한다:

  1. 브라우저 정보가 수상하다
  2. 사용자 행동(스크롤/클릭/속도)이 이상하다
  3. IP나 브라우저 Fingerprint가 의심스럽다
  4. JS 실행 환경이 비정상적이다
  5. 너무 빠르게 페이지 요청을 많이 보낸다

→ 결국: 사람 흉내를 얼마나 잘 내느냐가 봇 우회의 핵심이다.


🧪 1. User-Agent(UserAgent) 조작

✅ 개념

User-Agent는 브라우저가 서버에 자신을 소개하는 신분증 같은 문자열이다.

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...

기본적으로 Puppeteer는 HeadlessChrome을 포함한 UA를 자동으로 사용한다.
“이건 봇이다”라는 티가 남는다.

🛠️ 조작 방법 (Puppeteer)

await page.setUserAgent(
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
  • 실제 브라우저 UA를 그대로 가져다 쓰는 게 가장 안전하다
  • 크롬 버전은 가능한 최신 브라우저와 동일하게 설정 (→ https://www.whatismybrowser.com 참고)

🧪 2. HTTP Header 커스터마이징

✅ 개념

단순한 User-Agent 외에도, 서버는 다양한 헤더를 종합적으로 보고 봇 여부를 판단한다.

Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Referer: https://www.coupang.com/
Connection: keep-alive
Upgrade-Insecure-Requests: 1

→ 이 헤더들이 없거나 이상하게 설정되어 있으면 "사람 아님"으로 간주됨.

🛠️ 설정 방법 (Puppeteer)

await page.setExtraHTTPHeaders({
  'Accept-Language': 'ko-KR,ko;q=0.9',
  'Referer': 'https://www.coupang.com/',
  'Connection': 'keep-alive',
});

🧠 참고 포인트

  • setUserAgent()로 UA만 바꾸면 소용없다. 추가 헤더까지 정교하게 설정해줘야 진짜 사람처럼 보인다.
  • 특히 Referer는 페이지 내부 이동에서 정상적인 이동 플로우처럼 보이게 한다.

🧪 3. Webdriver 감지 차단

✅ 문제

많은 사이트는 navigator.webdriver === true 인지를 체크한다.
이는 Puppeteer가 기본으로 삽입한 속성이다.

Object.getOwnPropertyDescriptor(navigator, 'webdriver');
// { value: true, ... }

→ 이 값이 true면 브라우저가 자동화 도구(Puppeteer, Selenium 등)에 의해 실행 중임을 의미.

🛠️ 우회 방법

await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, 'webdriver', {
    get: () => false,
  });
});

→ Headless 환경임에도 "사람이 조작 중인 것처럼" 가장해준다.

좋아, 이어서 봇 감지 우회 전략 ②편 — 고급 탐지 회피 기술들과 그 기술적 배경, Puppeteer에서의 구현 방법까지 정리해줄게. 이건 실제로 쿠팡처럼 sophisticated한 사이트에서 매우 중요해.

🧪 4. window.chrome 객체 삽입

✅ 문제

일반적인 브라우저에는 window.chrome이라는 객체가 자동으로 존재한다.
하지만 headless 환경에서는 이게 없다.
→ 일부 사이트는 이를 통해 자동화 브라우저 여부를 판별한다.

if (!window.chrome) {
  alert('봇인 것 같아요!');
}

🛠️ 해결

await page.evaluateOnNewDocument(() => {
  window.chrome = {
    runtime: {},
    // 필요한 속성 더 추가 가능
  };
});

이렇게 하면 headless 브라우저에도 window.chrome이 존재하는 것처럼 가장할 수 있다.


🧪 5. navigator.plugins, languages, mimeTypes 위조

✅ 문제

  • 일반 브라우저: navigator.plugins.length > 0
  • Headless: navigator.plugins.length === 0

또한 사용자 언어 설정도 "en-US" 하나만 있을 수 있다.

🛠️ 우회 예시

await page.evaluateOnNewDocument(() => {
  // Plugins
  Object.defineProperty(navigator, 'plugins', {
    get: () => [1, 2, 3], // 가짜 plugin
  });

  // Languages
  Object.defineProperty(navigator, 'languages', {
    get: () => ['ko-KR', 'en-US'],
  });
});

→ 실사용 브라우저와 동일한 환경 구성이 핵심이다.


🧪 6. 브라우저 Fingerprinting 우회

Fingerprinting은 수많은 속성을 조합해 브라우저의 "지문"을 만든다.
이게 흔한 패턴과 다르면 = 봇으로 추정된다.

검사되는 요소들:

  • Canvas: 그래픽 렌더링 결과 해시
  • AudioContext: 오디오 생성 시 파형 패턴
  • WebGL: GPU 렌더링 정보
  • Touch Support: navigator.maxTouchPoints
  • Screen Size, Timezone, Font 등 시스템 정보

대응 전략 (개념 중심)

  • Canvas Spoofing: 그림 그리는 API 결과를 고정값으로 바꾸는 방식
  • AudioContext Hooking: 오디오 렌더링 값을 조작
  • WebGL Renderer 위조: WEBGL_debug_renderer_info 대응

현실적인 해결책

Fingerprint 우회는 어렵고, 전문 프레임워크 사용이 권장됨

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch();

이걸 쓰면 앞서 설명한 대부분의 우회기법이 자동으로 적용된다.


🕐 7. 렌더링 지연 전략

너무 빠르게 행동하는 크롤러는 금방 눈에 띈다.
사람처럼 천천히 행동하는 것이 중요하다.

구현 예시

await page.waitForTimeout(1500); // 페이지마다 다르게 설정
await page.evaluate(() => {
  window.scrollBy(0, 500);
});
await page.waitForTimeout(1000);

  • setTimeout() 랜덤값 사용
  • Math.random()으로 유저 지연시간처럼 꾸미기
  • 클릭 타이밍도 약간 랜덤하게 섞어주면 더 자연스럽다

🧪 8. IP, Timezone, Geolocation 우회

서버는 IP/타임존 정보와 브라우저 내 정보가 다르면 비정상 사용자로 간주한다.

예시

  • 한국 IP인데 navigator.language는 "fr-FR"?
  • timezoneOffset() 값이 미국 기준?

해결 방법

  • VPN 또는 Residential Proxy 사용
  • Puppeteer 내부에서 타임존/지역 설정
await page.emulateTimezone('Asia/Seoul');

보완: Geolocation도 spoof 가능

await page.setGeolocation({ latitude: 37.5665, longitude: 126.9780 }); // 서울

🧱 종합 우회 구조 예시

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

await page.setUserAgent('정상 브라우저 UA');
await page.setExtraHTTPHeaders({ ... });
await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, 'webdriver', { get: () => false });
  window.chrome = { runtime: {} };
  Object.defineProperty(navigator, 'languages', { get: () => ['ko-KR', 'en-US'] });
  Object.defineProperty(navigator, 'plugins', { get: () => [1, 2] });
});

await page.emulateTimezone('Asia/Seoul');
// 무한 스크롤 시작

🧩 정리하기 + 다른 사람들 사례 살펴보기

https://pointer81.tistory.com/entry/evolution-of-selenium-crawler-elegant-ways-to-bypass-anti-bot-detection
해당 블로그 참고했고,

최근 Puppeteer를 이용해 쿠팡 리뷰를 수집하는 작업을 하면서, 무한스크롤까지는 구현했지만 리뷰 추출 이전에 브라우저가 비정상 종료되는 문제를 겪었다.
코드상 오류는 없지만, 헤드리스 환경이라는 이유만으로 서버 측에서 나를 "봇"으로 인식하고 차단했을 가능성이 높았다.


1️⃣ 인프라 안정화

📦 Headless Chrome 구동을 위한 시스템 패키지 구성

Puppeteer를 사용할 때 필요한 시스템 패키지가 누락되면 렌더링 실패브라우저 실행 오류가 발생할 수 있다.

예시 (Amazon Linux 2 기준):

yum install -y \
  cups-libs libX11 libXcomposite libXcursor libXdamage libXext libXi \
  libXrandr libXScrnSaver libXss libXtst at-spi2-atk gtk3 alsa-lib mesa-libgbm \
  xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils \
  xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc
  • CUPS: PDF 렌더링
  • libXScrnSaver: GUI 세션 안정화
  • NSS: 보안 프로토콜

✅ 시스템 안정성이 떨어지면, headless 브라우저 자체가 아예 뜨지 않거나 무한 대기 상태가 되기도 한다.


2️⃣ 안티봇 탐지 우회 전략

2.1 User-Agent 조작

await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)...');
  • Headless Chrome 기본 UA는 HeadlessChrome이 포함되어 있음 → 즉시 차단 대상
  • 모바일 UA + 데스크톱 UA 랜덤 회전 조합 사용
const userAgents = [ ... ];
const selectedUA = userAgents[Math.floor(Math.random() * userAgents.length)];
await page.setUserAgent(selectedUA);

2.2 HTTP Header 정밀 설정

await page.setExtraHTTPHeaders({
  'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8',
  'Referer': 'https://www.coupang.com/',
  'Connection': 'keep-alive',
});
  • Referer 없이 특정 상품 페이지 진입 → 봇 판단
  • Accept-Language는 브라우저 언어와 일치해야 자연스러움

2.3 navigator.webdriver 우회

await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
  • true일 경우 대부분의 대형 쇼핑몰은 차단 로직이 실행됨

2.4 자연스러운 사용자 동작 흉내

await page.evaluate(() => {
  const scrollStep = Math.floor(Math.random() * 400) + 200;
  window.scrollBy(0, scrollStep);
});
await page.waitForTimeout(Math.random() * 1500 + 1000);
  • 무한스크롤 시에도 고정된 속도로 스크롤하면 의심
  • Math.random() 기반 랜덤 스크롤 + 간헐적 대기가 핵심

2.5 Stealth Plugin 활용

봇 감지를 포괄적으로 우회하는 플러그인:

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
  • window.chrome, navigator.languages, plugins 등 자동 위조
  • canvas fingerprint도 일부 우회 가능

3️⃣ 브라우저 충돌 대응 전략

3.1 무한스크롤 중단 조건 명확화

let retries = 0;
while (retries < 5) {
  // scroll ...
  if (스크롤 높이 변화 없음) {
    retries++;
  } else {
    retries = 0;
  }
}
  • 브라우저가 “무한 로딩” 상태에 빠지지 않도록 제한

3.2 브라우저 메모리 과부하 방지

  • 스크롤 후 일부 리뷰 DOM 삭제 처리
  • page.close()가 아닌 browser.disconnect() 방식 고려
  • 1,000개 이상 DOM 누적 시 CPU spike 주의

4️⃣ 에러 처리 및 로깅

try {
  await page.waitForSelector('.sdp-review__article__list', { timeout: 5000 });
} catch (e) {
  logger.warn('리뷰 DOM이 감지되지 않았습니다.');
}
  • 브라우저 강제 종료가 발생하면 exit code, SIGKILL, OOM 로그를 남기도록 설정
  • Puppeteer의 page.on('error', callback) 이벤트 활용 가능

🔚 결론 및 향후 개선 방향

✅ 현재 개선 효과

항목개선 전개선 후
브라우저 실패율약 40%10% 이하
리뷰 수집량평균 400개최대 1,500개까지 가능
코드 재사용성낮음높은 수준의 추상화 (Factory + Base 클래스)
유지보수성수작업 중심자동화 기반 로그 + 에러관리

💬 마무리

크롤링이 점점 어려워지는 이유는 단순히 기술이 어려워서가 아니라,
웹이 사람만을 위한 공간이 되고 있기 때문이다.
우리가 만들어야 하는 건 브라우저를 흉내내는 코드가 아니라,
사람처럼 행동하는 브라우저다.

profile
일단 하고 보자 (펠리컨적 마인드 ㅠㅠ)

0개의 댓글