nocode 2026년 2월 10일 11분 읽기

봇 감지 우회 완벽 가이드 | Node.js + Playwright 웹 스크래핑 실전 전략

NN
NextNove Team
NextNove

playwright-bot-detection-bypass

Playwright와 Node.js로 웹 스크래핑 시 가장 큰 난관인 봇 감지 회피·우회 방법을 집중적으로 다룬 실전 가이드입니다. navigator.webdriver, fingerprinting, CAPTCHA 탐지 원리를 이해하고 Medium·Advanced 전략으로 실제 사이트 차단을 우회하는 방법을 단계별로 정리했습니다.

참고: 이 글은 파이썬과 n8n을 기준으로 작성된 N8N 웹 스크래핑 시 Bot 탐지 기술 및 우회 의 핵심 개념과 우회 전략을 바탕으로, Node.js + Playwright 환경에 맞게 포팅하여 정리한 글입니다.

왜 웹 스크래핑이 차단될까?

Node.js로 Playwright를 사용해 웹사이트에 접속했는데 갑자기 접근이 차단되거나 CAPTCHA가 뜨는 경험, 다들 한 번쯤 있으실 겁니다. 😅

웹사이트 입장에서는 이런 고민이 있습니다:

  • 서버 부하: 자동화된 봇이 초당 수백 건의 요청을 보내면 서버가 다운될 수 있습니다
  • 데이터 도용: 경쟁사가 가격 정보나 상품 데이터를 무단으로 수집할 수 있습니다
  • 보안 위협: 악의적인 봇이 DDoS 공격이나 계정 탈취를 시도할 수 있습니다

그래서 많은 사이트들이 봇 탐지 시스템을 도입했습니다. 문제는 여러분의 정당한 웹 스크래핑까지 막힌다는 것입니다.

합법적인 웹 스크래핑이란?

시작하기 전에 꼭 기억해야 할 원칙들이 있습니다:

허용되는 경우:

  • 공개된 정보 수집 (뉴스 기사, 제품 정보 등)
  • 웹사이트 이용약관에서 허용하는 범위 내
  • robots.txt를 준수하는 스크래핑
  • 적절한 요청 간격 (Rate Limiting 준수)

금지되는 경우:

  • 로그인이 필요한 개인정보 수집
  • 저작권이 있는 콘텐츠 무단 복제
  • 서버에 과도한 부담을 주는 공격적 크롤링
  • 이용약관 위반

웹사이트는 어떻게 봇을 감지할까?

웹사이트가 봇을 탐지하는 다양한 기법을 이해하면, 왜 우회가 필요한지 명확해집니다. 초보자분들도 쉽게 이해할 수 있도록 하나씩 설명드리겠습니다!

1️⃣ navigator.webdriver 체크 (난이도: ⭐)

가장 기본적인 탐지 방법입니다. 브라우저가 자동화 도구로 제어되고 있는지 확인하는 속성입니다.

javascript
// 일반 사용자의 Chrome 브라우저
console.log(navigator.webdriver); // undefined
 
// Playwright로 실행한 브라우저 (기본 설정)
console.log(navigator.webdriver); // true ⚠️

왜 문제일까요?
웹사이트 입장에서는 navigator.webdriver === true인 브라우저를 발견하면, “아, 이건 자동화 도구네!”라고 즉시 알 수 있습니다.

2️⃣ Chrome 객체 누락 (난이도: ⭐)

실제 Chrome 브라우저에는 window.chrome이라는 특별한 객체가 있습니다.

javascript
// 진짜 Chrome
console.log(window.chrome); 
// { runtime: {...}, loadTimes: function, ... }
 
// Playwright의 Chromium
console.log(window.chrome); 
// undefined ⚠️

차이가 생기는 이유
Playwright는 순수 Chromium을 사용하는데, window.chrome은 Google이 추가한 기능이라서 포함되어 있지 않습니다.

3️⃣ 플러그인 개수 체크 (난이도: ⭐)

일반 브라우저는 PDF 뷰어 같은 기본 플러그인이 설치되어 있습니다.

javascript
// 일반 브라우저
console.log(navigator.plugins.length); // 3~5개
 
// Playwright (기본)
console.log(navigator.plugins.length); // 0개 ⚠️

플러그인이 하나도 없다? 매우 의심스럽습니다!

4️⃣ 언어 설정 부족 (난이도: ⭐)

실제 사용자는 보통 여러 언어를 선호 언어로 설정합니다.

javascript
// 한국 사용자의 실제 브라우저
console.log(navigator.languages);
// ['ko-KR', 'ko', 'en-US', 'en']
 
// Playwright (기본)
console.log(navigator.languages);
// ['en-US'] ⚠️

5️⃣ 고급 탐지 기법 (난이도: ⭐⭐⭐)

더 똑똑한 사이트들은 이런 것들도 체크합니다:

  • Canvas Fingerprinting: 브라우저가 그림을 그리는 방식으로 기기 식별
  • WebGL Fingerprinting: GPU 정보로 고유 ID 생성
  • 행동 패턴 분석: 마우스 움직임, 타이핑 속도 등
  • IP 기반 Rate Limiting: 같은 IP에서 너무 많은 요청

Playwright 기본 설정의 문제점

자, 이제 실제로 Playwright를 사용할 때 어떤 문제가 발생하는지 살펴보겠습니다.

기본 코드 (❌ 봇으로 탐지됨)

javascript
import { chromium } from 'playwright';
 
async function scrapeWebsite() {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  
  await page.goto('https://example.com');
  
  // 이 시점에서 봇으로 탐지될 수 있습니다!
  const title = await page.title();
  console.log(title);
  
  await browser.close();
}

이 코드의 문제점:

  1. navigator.webdriver === true
  2. window.chrome 객체 없음
  3. navigator.plugins.length === 0
  4. 언어 설정이 단순함
  5. User-Agent가 Headless Chrome으로 표시

웹사이트 입장에서는 빨간불이 5개나 켜진 셈입니다! 🚨


단계별 봇 탐지 우회 전략

좋은 소식은, 이 문제들을 단계적으로 해결할 수 있다는 것입니다! 저는 4단계 전략을 추천합니다.

📊 전략 비교표

레벨우회 성공률성능추천 대상
Level 0: 기본0%최고봇 탐지 없는 내부 시스템
Level 1: Basic~30%빠름간단한 User-Agent 체크만 하는 사이트
Level 2: Medium~70%보통대부분의 일반 웹사이트 (추천!)
Level 3: Advanced~85%느림Cloudflare 등 고급 방어 시스템

대부분의 경우 Medium 레벨이면 충분합니다! 처음부터 Advanced를 쓰면 성능만 떨어지고 오히려 의심받을 수 있습니다.


실전 코드 예제

자, 이제 실제로 어떻게 구현하는지 보여드리겠습니다!

Level 1: Basic - User-Agent 변경하기

가장 기본적인 방법은 User-Agent를 진짜 Chrome처럼 바꾸는 것입니다.

javascript
import { chromium } from 'playwright';
 
async function basicStealth() {
  const browser = await chromium.launch({
    headless: true,
    args: [
      // 자동화 탐지 비활성화!
      '--disable-blink-features=AutomationControlled',
    ]
  });
  
  const context = await browser.newContext({
    // 최신 Chrome의 User-Agent
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    
    // 일반적인 화면 해상도
    viewport: { width: 1920, height: 1080 },
    
    // 한국 설정
    locale: 'ko-KR',
    timezoneId: 'Asia/Seoul',
    
    // 실제 브라우저처럼 헤더 설정
    extraHTTPHeaders: {
      'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    }
  });
  
  const page = await context.newPage();
  await page.goto('https://example.com');
  
  await browser.close();
}

✅ 개선 효과:

  • User-Agent가 진짜 Chrome처럼 보입니다
  • 화면 크기와 언어 설정이 자연스럽습니다
  • 간단한 봇 탐지는 통과 가능합니다!

Level 2: Medium - JavaScript 주입으로 속성 숨기기 ⭐

이제 진짜 실력을 발휘할 시간입니다! 브라우저 속성을 직접 수정해보겠습니다.

javascript
async function applyStealthScripts(page) {
  // 페이지 로드 전에 실행되는 스크립트
  await page.addInitScript(() => {
    // 🎭 1단계: navigator.webdriver 숨기기
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined  // true → undefined로 변경!
    });
    
    // 🎨 2단계: Chrome 객체 만들기
    window.chrome = {
      runtime: {},
      loadTimes: function() {},
      csi: function() {},
      app: {}
    };
    
    // 🔌 3단계: 플러그인 추가
    Object.defineProperty(navigator, 'plugins', {
      get: () => [1, 2, 3, 4, 5]  // 5개의 더미 플러그인
    });
    
    // 🌍 4단계: 언어 설정 확장
    Object.defineProperty(navigator, 'languages', {
      get: () => ['ko-KR', 'ko', 'en-US', 'en']
    });
    
    // ⚡ 5단계: Permissions API 수정
    const originalQuery = window.navigator.permissions.query;
    window.navigator.permissions.query = (parameters) => (
      parameters.name === 'notifications' ?
        Promise.resolve({ state: Notification.permission }) :
        originalQuery(parameters)
    );
    
    // 📡 6단계: 네트워크 정보 추가
    Object.defineProperty(navigator, 'connection', {
      get: () => ({
        effectiveType: '4g',
        rtt: 100,
        downlink: 10,
        saveData: false
      })
    });
  });
}
 
// 실제 사용 예제
async function scrapeWithMediumStealth(url) {
  const browser = await chromium.launch({ headless: true });
  
  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    viewport: { width: 1920, height: 1080 }
  });
  
  const page = await context.newPage();
  
  // 여기가 핵심! 스크립트 주입
  await applyStealthScripts(page);
  
  // 이제 안전하게 접속
  await page.goto(url);
  
  const title = await page.title();
  console.log('페이지 제목:', title);
  
  await browser.close();
}

🎯 핵심 포인트:

  1. Object.defineProperty(): JavaScript에서 객체의 속성을 재정의하는 강력한 도구입니다
  2. page.addInitScript(): 페이지가 로드되기 전에 실행됩니다 - 봇 탐지 스크립트보다 먼저 실행됩니다!
  3. 속성 위조: 실제 브라우저처럼 보이도록 여러 속성을 추가합니다

Level 3: Advanced - 고급 Fingerprinting 방지

정말 까다로운 사이트를 만났다면? 이 레벨까지 올라가야 합니다.

javascript
async function applyAdvancedStealth(page) {
  // Medium 레벨 먼저 적용
  await applyStealthScripts(page);
  
  // 추가 고급 기법
  await page.addInitScript(() => {
    // 🎨 Canvas Fingerprinting 방지
    const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
    HTMLCanvasElement.prototype.toDataURL = function(type) {
      // 작은 canvas는 fingerprinting 시도로 간주
      if (type === 'image/png' && this.width === 16 && this.height === 16) {
        return originalToDataURL.apply(this, arguments);
      }
      return originalToDataURL.apply(this, arguments);
    };
    
    // 🎮 WebGL 정보 수정
    const getParameter = WebGLRenderingContext.prototype.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
      // GPU 정보 위조
      if (parameter === 37445) return 'Intel Inc.';
      if (parameter === 37446) return 'Intel Iris OpenGL Engine';
      return getParameter.apply(this, arguments);
    };
    
    // 🔋 Battery API 제거 (Headless 탐지에 사용됨)
    if ('getBattery' in navigator) {
      navigator.getBattery = undefined;
    }
    
    // 🖥️ 하드웨어 정보 정규화
    Object.defineProperty(navigator, 'hardwareConcurrency', {
      get: () => 8  // 일반적인 CPU 코어 수
    });
    
    Object.defineProperty(navigator, 'deviceMemory', {
      get: () => 8  // 8GB RAM
    });
  });
}

⚠️ 주의사항:

  • Advanced 레벨은 성능이 느립니다
  • 모든 사이트에 사용할 필요는 없습니다
  • Medium으로 안 되는 경우에만 사용하세요!

테스트 및 디버깅 방법

코드를 작성했으면 테스트해봐야겠죠? 여기 유용한 사이트들을 소개합니다!

🧪 테스트 사이트 TOP 4

사이트URL특징
Sannysoftbot.sannysoft.com가장 포괄적인 봇 탐지 테스트
Are You Headlessarh.antoinevastel.comHeadless 모드 특화 테스트
BrowserScanbrowserscan.net브라우저 Fingerprint 분석
Pixelscanpixelscan.netCanvas/WebGL 테스트

간단한 테스트 코드

javascript
import { chromium } from 'playwright';
 
async function testMyBrowser() {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  
  // 여기에 여러분의 stealth 스크립트 적용
  // await applyStealthScripts(page);
  
  // 빈 페이지에서 속성 확인
  await page.goto('about:blank');
  
  const properties = await page.evaluate(() => {
    return {
      webdriver: navigator.webdriver,
      chrome: !!window.chrome,
      plugins: navigator.plugins.length,
      languages: navigator.languages,
      userAgent: navigator.userAgent
    };
  });
  
  console.log('🔍 브라우저 속성 검사 결과:');
  console.log('- webdriver:', properties.webdriver);
  console.log('- Chrome 객체:', properties.chrome ? '✅ 있음' : '❌ 없음');
  console.log('- 플러그인 개수:', properties.plugins);
  console.log('- 지원 언어:', properties.languages);
  
  await browser.close();
}
 
testMyBrowser();

결과 해석:

  • webdriver: undefined - 좋습니다!
  • Chrome 객체: 있음 - 완벽합니다!
  • 플러그인 개수: 5 - 자연스럽습니다
  • webdriver: true - 개선 필요합니다!

실전 활용 패턴

패턴 1: 뉴스 기사 스크래핑

javascript
import { chromium } from 'playwright';
 
async function scrapeNews() {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    viewport: { width: 1920, height: 1080 },
    locale: 'ko-KR'
  });
  
  const page = await context.newPage();
  await applyStealthScripts(page);
  
  await page.goto('https://news-example.com');
  
  // 기사 제목 수집
  const articles = await page.$$eval('article h2', elements => 
    elements.map(el => el.textContent.trim())
  );
  
  console.log('오늘의 뉴스:', articles);
  
  await browser.close();
  return articles;
}

패턴 2: 가격 비교 (Rate Limiting 준수)

javascript
async function comparePrices(urls) {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    viewport: { width: 1920, height: 1080 }
  });
  
  const prices = [];
  
  for (const url of urls) {
    const page = await context.newPage();
    await applyStealthScripts(page);
    
    await page.goto(url);
    
    const price = await page.$eval('.price', el => el.textContent);
    prices.push({ url, price });
    
    await page.close();
    
    // ⏰ 중요: 요청 간격 두기 (2-3초)
    await page.waitForTimeout(2000);
  }
  
  await browser.close();
  return prices;
}

자주 묻는 질문 (FAQ)

Q1: 이거 불법 아닌가요?

A: 합법적인 범위 내에서 사용하면 괜찮습니다!

합법적 사용:

  • 공개된 정보 수집
  • 개인 연구/학습 목적
  • 웹사이트 이용약관 준수

불법적 사용:

  • 개인정보 무단 수집
  • 저작권 침해
  • 서버 과부하 유발 (DDoS)
  • 이용약관 위반

Q2: Medium과 Advanced 중 뭘 써야 하나요?

A: Medium으로 시작하세요!

1단계: Medium 레벨로 테스트

차단되나요?

No → Medium 계속 사용 (70%의 경우)
Yes → Advanced로 업그레이드 (30%의 경우)

처음부터 Advanced를 쓰면:

  • 성능 저하 (2배 느림)
  • 오히려 의심스러울 수 있습니다
  • 불필요한 리소스 낭비

Q3: playwright-extra는 뭔가요?

A: 더 강력한 플러그인 시스템입니다!

bash
npm install playwright-extra puppeteer-extra-plugin-stealth
javascript
import { chromium } from 'playwright-extra';
import stealth from 'puppeteer-extra-plugin-stealth';
 
// 플러그인만 추가하면 끝!
chromium.use(stealth());
 
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
// 자동으로 stealth 적용됨

장점:

  • 코드가 간단합니다
  • 지속적으로 업데이트됩니다
  • Cloudflare 같은 고급 방어도 우회 가능합니다

단점:

  • 외부 라이브러리 의존성
  • 커스터마이징이 어렵습니다

Q4: 메모리 사용량이 너무 높아요!

A: 이렇게 최적화하세요:

javascript
// ❌ 나쁜 예: 매번 새 브라우저
for (const url of urls) {
  const browser = await chromium.launch();
  // ... 작업
  await browser.close();
}
 
// ✅ 좋은 예: 브라우저 재사용
const browser = await chromium.launch();
for (const url of urls) {
  const page = await browser.newPage();
  // ... 작업
  await page.close(); // 페이지만 닫기
}
await browser.close(); // 마지막에 브라우저 닫기

추가 팁:

javascript
// 이미지 로딩 비활성화
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
 
// 주기적으로 브라우저 재시작
if (count % 100 === 0) {
  await browser.close();
  browser = await chromium.launch();
}

Q5: CAPTCHA가 나타나면 어떡하죠?

A: CAPTCHA는 “자동화 차단의 최종 보스”입니다.

단계별 대응:

  1. Level 1: Stealth 레벨 올리기
  2. Level 2: 요청 간격 늘리기 (3-5초)
  3. Level 3: User-Agent 로테이션
  4. Level 4: 프록시 사용
  5. 최종: 공식 API 문의

솔직한 조언:
reCAPTCHA v3 같은 고급 시스템은 우회가 매우 어렵습니다. 웹사이트에서 공식 API를 제공하는지 먼저 확인하세요!

Q6: 실무에서는 어떻게 쓰나요?

A: 실무 패턴 예시입니다:

javascript
// 프로덕션 레벨 설정
const RETRY_COUNT = 3;
const REQUEST_DELAY = 2000;
 
async function productionScraper(url) {
  let attempt = 0;
  
  while (attempt < RETRY_COUNT) {
    try {
      const browser = await chromium.launch({ headless: true });
      const context = await browser.newContext({
        userAgent: getRandomUserAgent(), // UA 로테이션
        viewport: { width: 1920, height: 1080 }
      });
      
      const page = await context.newPage();
      await applyStealthScripts(page);
      
      // 타임아웃 설정
      await page.goto(url, { 
        waitUntil: 'networkidle',
        timeout: 30000 
      });
      
      const data = await page.$eval('...', el => el.textContent);
      
      await browser.close();
      return data;
      
    } catch (error) {
      console.error(`시도 ${attempt + 1} 실패:`, error.message);
      attempt++;
      
      if (attempt < RETRY_COUNT) {
        // 지수 백오프
        await new Promise(r => setTimeout(r, REQUEST_DELAY * attempt));
      }
    }
  }
  
  throw new Error('최대 재시도 횟수 초과');
}

마무리하며

웹 스크래핑은 강력한 도구지만, 책임감 있게 사용해야 합니다.

✨ 핵심 정리

  1. 시작은 Medium 레벨로 - 70% 성공률에 좋은 성능
  2. 테스트는 필수 - Sannysoft 등에서 검증
  3. Rate Limiting 준수 - 요청 간격 2-3초
  4. 에러 처리 철저히 - Retry 로직 구현
  5. 이용약관 확인 - 법적 문제 예방

🎓 다음 단계

더 깊이 공부하고 싶다면:


참고 자료

이 글은 다음 리소스를 참고하여 작성되었습니다: