Puppeteer로 스크롤까지는 잘 작동하는데, 갑자기 브라우저가 꺼진다?
코드는 이상 없는데 아무런 경고도 없이 브라우저가 종료된다?
이럴 땐 의심해야 할 첫 번째 원인: 봇 감지 (anti-bot)
대형 쇼핑몰, 예약 사이트, 티켓팅 페이지 등은 수많은 봇이 들어오는 걸 막기 위해 다양한 수준의 탐지 기법을 사용한다.
특히 쿠팡처럼 SPA 구조 + 사용자 리뷰 + 무한스크롤이 포함된 경우, 정적 접근보다 훨씬 정교한 "브라우저 행동 추적" 기반 탐지가 이뤄진다.
**봇 감지(Anti-Bot Detection)**는 다음을 기준으로 "이거 사람 아냐?"를 판단한다:
→ 결국: 사람 흉내를 얼마나 잘 내느냐가 봇 우회의 핵심이다.
User-Agent는 브라우저가 서버에 자신을 소개하는 신분증 같은 문자열이다.
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
기본적으로 Puppeteer는 HeadlessChrome을 포함한 UA를 자동으로 사용한다.
→ “이건 봇이다”라는 티가 남는다.
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'
);
단순한 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
→ 이 헤더들이 없거나 이상하게 설정되어 있으면 "사람 아님"으로 간주됨.
await page.setExtraHTTPHeaders({
'Accept-Language': 'ko-KR,ko;q=0.9',
'Referer': 'https://www.coupang.com/',
'Connection': 'keep-alive',
});
setUserAgent()로 UA만 바꾸면 소용없다. 추가 헤더까지 정교하게 설정해줘야 진짜 사람처럼 보인다.Referer는 페이지 내부 이동에서 정상적인 이동 플로우처럼 보이게 한다.많은 사이트는 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한 사이트에서 매우 중요해.
window.chrome 객체 삽입일반적인 브라우저에는 window.chrome이라는 객체가 자동으로 존재한다.
하지만 headless 환경에서는 이게 없다.
→ 일부 사이트는 이를 통해 자동화 브라우저 여부를 판별한다.
if (!window.chrome) {
alert('봇인 것 같아요!');
}
await page.evaluateOnNewDocument(() => {
window.chrome = {
runtime: {},
// 필요한 속성 더 추가 가능
};
});
이렇게 하면 headless 브라우저에도 window.chrome이 존재하는 것처럼 가장할 수 있다.
navigator.plugins, languages, mimeTypes 위조navigator.plugins.length > 0navigator.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'],
});
});
→ 실사용 브라우저와 동일한 환경 구성이 핵심이다.
Fingerprinting은 수많은 속성을 조합해 브라우저의 "지문"을 만든다.
이게 흔한 패턴과 다르면 = 봇으로 추정된다.
navigator.maxTouchPointsWEBGL_debug_renderer_info 대응Fingerprint 우회는 어렵고, 전문 프레임워크 사용이 권장됨
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch();
이걸 쓰면 앞서 설명한 대부분의 우회기법이 자동으로 적용된다.
너무 빠르게 행동하는 크롤러는 금방 눈에 띈다.
사람처럼 천천히 행동하는 것이 중요하다.
await page.waitForTimeout(1500); // 페이지마다 다르게 설정
await page.evaluate(() => {
window.scrollBy(0, 500);
});
await page.waitForTimeout(1000);
setTimeout() 랜덤값 사용Math.random()으로 유저 지연시간처럼 꾸미기서버는 IP/타임존 정보와 브라우저 내 정보가 다르면 비정상 사용자로 간주한다.
navigator.language는 "fr-FR"?timezoneOffset() 값이 미국 기준?await page.emulateTimezone('Asia/Seoul');
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를 이용해 쿠팡 리뷰를 수집하는 작업을 하면서, 무한스크롤까지는 구현했지만 리뷰 추출 이전에 브라우저가 비정상 종료되는 문제를 겪었다.
코드상 오류는 없지만, 헤드리스 환경이라는 이유만으로 서버 측에서 나를 "봇"으로 인식하고 차단했을 가능성이 높았다.
Puppeteer를 사용할 때 필요한 시스템 패키지가 누락되면 렌더링 실패나 브라우저 실행 오류가 발생할 수 있다.
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
✅ 시스템 안정성이 떨어지면, headless 브라우저 자체가 아예 뜨지 않거나 무한 대기 상태가 되기도 한다.
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)...');
HeadlessChrome이 포함되어 있음 → 즉시 차단 대상const userAgents = [ ... ];
const selectedUA = userAgents[Math.floor(Math.random() * userAgents.length)];
await page.setUserAgent(selectedUA);
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는 브라우저 언어와 일치해야 자연스러움navigator.webdriver 우회await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
true일 경우 대부분의 대형 쇼핑몰은 차단 로직이 실행됨await page.evaluate(() => {
const scrollStep = Math.floor(Math.random() * 400) + 200;
window.scrollBy(0, scrollStep);
});
await page.waitForTimeout(Math.random() * 1500 + 1000);
Math.random() 기반 랜덤 스크롤 + 간헐적 대기가 핵심봇 감지를 포괄적으로 우회하는 플러그인:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
window.chrome, navigator.languages, plugins 등 자동 위조let retries = 0;
while (retries < 5) {
// scroll ...
if (스크롤 높이 변화 없음) {
retries++;
} else {
retries = 0;
}
}
page.close()가 아닌 browser.disconnect() 방식 고려try {
await page.waitForSelector('.sdp-review__article__list', { timeout: 5000 });
} catch (e) {
logger.warn('리뷰 DOM이 감지되지 않았습니다.');
}
exit code, SIGKILL, OOM 로그를 남기도록 설정page.on('error', callback) 이벤트 활용 가능| 항목 | 개선 전 | 개선 후 |
|---|---|---|
| 브라우저 실패율 | 약 40% | 10% 이하 |
| 리뷰 수집량 | 평균 400개 | 최대 1,500개까지 가능 |
| 코드 재사용성 | 낮음 | 높은 수준의 추상화 (Factory + Base 클래스) |
| 유지보수성 | 수작업 중심 | 자동화 기반 로그 + 에러관리 |
크롤링이 점점 어려워지는 이유는 단순히 기술이 어려워서가 아니라,
웹이 사람만을 위한 공간이 되고 있기 때문이다.
우리가 만들어야 하는 건 브라우저를 흉내내는 코드가 아니라,
사람처럼 행동하는 브라우저다.