빅데이타 & 머신러닝/생성형 AI (ChatGPT etc)

CDP & Playwright를 이용한 브라우저 자동화

Terry Cho 2026. 2. 7. 13:51

🚀 CDP & Playwright

조대협 (http://bcho.tistory.com)
아래 코드는 프롬프트를 이용해서, Sonnet-4를 사용해 생성한 컨텐츠 입니다.

브라우저 자동화 학습 교본

Chrome DevTools Protocol과 Playwright를 활용한 웹 자동화 완벽 가이드

1. 소개

🎯 학습 목표

이 교본에서는 다음 내용을 학습합니다:

  • Chrome DevTools Protocol (CDP)의 기본 개념과 사용법
  • Playwright를 활용한 브라우저 자동화
  • 봇 감지 우회 기법 (CAPTCHA 처리, 사람처럼 행동하기)
  • 실무에서 활용 가능한 웹 스크래핑 및 자동화 기술

📚 브라우저 자동화란?

브라우저 자동화는 웹 브라우저를 프로그래밍 방식으로 제어하는 기술입니다. 이를 통해 반복적인 웹 작업을 자동화하거나, 웹 애플리케이션 테스트, 웹 스크래핑 등을 수행할 수 있습니다.

🛠️ 주요 기술

  • CDP (Chrome DevTools Protocol): Chrome의 저수준 디버깅 프로토콜
  • Playwright: Microsoft에서 개발한 고수준 브라우저 자동화 라이브러리
💡 유스케이스
  • 웹 스크래핑 및 데이터 수집
  • E2E (End-to-End) 테스트 자동화
  • 웹 애플리케이션 모니터링
  • 반복 작업 자동화 (폼 작성, 스크린샷 캡처 등)

2. 아키텍처 이해

🏗️ CDP 아키텍처

Node.js 애플리케이션

WebSocket 연결

Chrome DevTools Protocol

Chrome 브라우저 (--remote-debugging-port)

CDP는 WebSocket을 통해 Chrome 브라우저와 직접 통신합니다. 브라우저를 --remote-debugging-port 옵션으로 실행하면 디버깅 포트가 열리고, 이를 통해 브라우저를 원격으로 제어할 수 있습니다.

🎭 Playwright 아키텍처

Node.js 애플리케이션

Playwright API

CDP (내부적으로 사용)

Chrome/Firefox/Safari 브라우저

Playwright는 내부적으로 CDP를 사용하지만, 고수준 API를 제공하여 더 쉽고 안정적인 브라우저 제어를 가능하게 합니다. 또한 여러 브라우저를 지원합니다.

✅ 핵심 차이점
  • CDP: 저수준 제어, 높은 유연성, 복잡한 코드
  • Playwright: 고수준 API, 사용 편의성, 안정성

3. 프로젝트 설정

📦 1. 프로젝트 초기화

mkdir browser-automation-demo
cd browser-automation-demo
npm init -y

📦 2. package.json 설정

{
  "name": "browser-automation-demo",
  "version": "1.0.0",
  "type": "module",
  "description": "CDP and Playwright browser automation examples",
  "scripts": {
    "cdp": "node cdp-example.js",
    "playwright": "node playwright-example.js"
  },
  "dependencies": {
    "playwright-core": "^1.58.2",
    "ws": "^8.19.0"
  }
}

📦 3. 의존성 설치

npm install
💡 의존성 설명
  • playwright-core: Playwright 코어 라이브러리
  • ws: WebSocket 클라이언트 (CDP용)

📁 프로젝트 구조

browser-automation-demo/
├── package.json
├── cdp-example.js              # Pure CDP 예제
├── playwright-example.js        # Playwright 예제
└── README.md

CDP 4. CDP 예제

🎯 목표

순수 CDP를 사용하여 Chrome을 실행하고, Google에서 "AI"를 검색한 후 스크린샷을 캡처합니다.

📝 전체 코드

import { spawn } from 'child_process';
import WebSocket from 'ws';
import http from 'http';
import fs from 'fs';

const CDP_PORT = 9222;

function launchChrome() {
  const chromeArgs = [
    `--remote-debugging-port=${CDP_PORT}`,
    '--no-first-run',
    '--no-default-browser-check',
    '--disable-background-networking',
    '--disable-default-apps',
    '--disable-extensions',
    '--disable-sync',
    '--disable-translate',
    'about:blank'
  ];

  const chromePaths = [
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
    '/usr/bin/google-chrome',
    '/usr/bin/chromium-browser'
  ];

  let chromePath = chromePaths.find(path => {
    try {
      return fs.existsSync(path);
    } catch {
      return false;
    }
  });

  if (!chromePath) {
    chromePath = 'google-chrome';
  }

  console.log(`🚀 Launching Chrome from: ${chromePath}`);
  const chrome = spawn(chromePath, chromeArgs);

  chrome.on('error', (err) => {
    console.error('Failed to start Chrome:', err);
    process.exit(1);
  });

  return chrome;
}

async function getCdpWebSocketUrl() {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error('Timeout waiting for CDP endpoint'));
    }, 10000);

    const tryConnect = () => {
      http.get(`http://127.0.0.1:${CDP_PORT}/json/version`, (res) => {
        let data = '';
        res.on('data', chunk => data += chunk);
        res.on('end', () => {
          clearTimeout(timeout);
          const info = JSON.parse(data);
          resolve(info.webSocketDebuggerUrl);
        });
      }).on('error', () => {
        setTimeout(tryConnect, 100);
      });
    };
    
    tryConnect();
  });
}

async function sendCdpCommand(ws, method, params = {}) {
  return new Promise((resolve, reject) => {
    const id = Date.now();
    
    const handler = (data) => {
      const response = JSON.parse(data);
      if (response.id === id) {
        ws.off('message', handler);
        if (response.error) {
          reject(new Error(response.error.message));
        } else {
          resolve(response.result);
        }
      }
    };

    ws.on('message', handler);
    ws.send(JSON.stringify({ id, method, params }));
    
    setTimeout(() => {
      ws.off('message', handler);
      reject(new Error(`Timeout for ${method}`));
    }, 30000);
  });
}

async function main() {
  console.log('=== CDP Example: Google Search ===\\n');

  const chrome = launchChrome();
  
  await new Promise(resolve => setTimeout(resolve, 2000));

  const wsUrl = await getCdpWebSocketUrl();
  console.log(`✅ CDP WebSocket URL: ${wsUrl}\\n`);

  const ws = new WebSocket(wsUrl);

  await new Promise((resolve) => {
    ws.on('open', resolve);
  });

  console.log('✅ WebSocket connected\\n');

  await sendCdpCommand(ws, 'Page.enable');
  await sendCdpCommand(ws, 'DOM.enable');
  await sendCdpCommand(ws, 'Runtime.enable');

  console.log('📂 Navigating to Google...');
  await sendCdpCommand(ws, 'Page.navigate', { url: 'https://www.google.com' });
  await new Promise(resolve => setTimeout(resolve, 3000));

  console.log('🔍 Searching for "AI"...');
  
  const searchScript = `
    (function() {
      const input = document.querySelector('textarea[name="q"]') || 
                   document.querySelector('input[name="q"]');
      if (input) {
        input.value = 'AI';
        input.form.submit();
        return 'Search submitted';
      }
      return 'Search box not found';
    })();
  `;

  const result = await sendCdpCommand(ws, 'Runtime.evaluate', {
    expression: searchScript,
    returnByValue: true,
    awaitPromise: false
  });

  console.log(`📝 Script result: ${result.value}`);
  
  await new Promise(resolve => setTimeout(resolve, 3000));

  const screenshot = await sendCdpCommand(ws, 'Page.captureScreenshot', {
    format: 'png',
    fromSurface: true
  });

  fs.writeFileSync('cdp-screenshot.png', Buffer.from(screenshot.data, 'base64'));
  console.log('\\n📸 Screenshot saved: cdp-screenshot.png');

  ws.close();
  chrome.kill();
  
  console.log('\\n✅ Done!');
}

main().catch(console.error);

🔍 코드 분석

1. Chrome 실행

const chrome = spawn(chromePath, [
  '--remote-debugging-port=9222',
  // ... 기타 옵션
]);

--remote-debugging-port 옵션으로 Chrome을 실행하면 CDP 프로토콜을 사용할 수 있는 포트가 열립니다.

2. WebSocket 연결

const wsUrl = await getCdpWebSocketUrl();
const ws = new WebSocket(wsUrl);

HTTP GET 요청으로 /json/version 엔드포인트에서 WebSocket URL을 가져온 후 연결합니다.

3. CDP 명령 전송

await sendCdpCommand(ws, 'Page.navigate', { 
  url: 'https://www.google.com' 
});

CDP 명령은 JSON 형식으로 전송되며, 각 명령에는 고유한 ID가 부여됩니다.

4. JavaScript 실행

await sendCdpCommand(ws, 'Runtime.evaluate', {
  expression: searchScript,
  returnByValue: true
});

페이지 컨텍스트에서 JavaScript를 실행하여 검색창을 찾고 검색을 수행합니다.

5. 스크린샷 캡처

const screenshot = await sendCdpCommand(ws, 'Page.captureScreenshot', {
  format: 'png',
  fromSurface: true
});

fs.writeFileSync('cdp-screenshot.png', 
  Buffer.from(screenshot.data, 'base64'));

▶️ 실행

npm run cdp
⚠️ CDP의 한계
  • 모든 작업을 수동으로 관리해야 함 (대기, 타이밍 등)
  • 요소 선택이 불안정할 수 있음
  • 코드가 복잡하고 길어짐

Playwright 5. Playwright 예제 (기본)

🎯 목표

Playwright의 고수준 API를 사용하여 동일한 작업을 더 간단하게 수행합니다.

📝 기본 코드

import { chromium } from 'playwright-core';
import fs from 'fs';

async function main() {
  console.log('=== Playwright Example: Google Search ===\\n');

  const chromePaths = [
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
    '/usr/bin/google-chrome',
    '/usr/bin/chromium-browser'
  ];

  let executablePath = chromePaths.find(path => {
    try {
      return fs.existsSync(path);
    } catch {
      return false;
    }
  });

  console.log(`🚀 Launching Chrome from: ${executablePath || 'system default'}\\n`);

  const browser = await chromium.launch({
    headless: false,
    executablePath: executablePath || undefined,
    args: [
      '--no-first-run',
      '--no-default-browser-check',
      '--disable-background-networking'
    ]
  });

  console.log('✅ Browser launched\\n');

  const context = await browser.newContext({
    viewport: { width: 1280, height: 720 }
  });

  const page = await context.newPage();

  console.log('📂 Navigating to Google...');
  await page.goto('https://www.google.com', {
    waitUntil: 'networkidle',
    timeout: 30000
  });

  console.log('✅ Page loaded\\n');

  console.log('🔍 Typing "AI" in search box...');
  
  const searchInput = await page.locator('textarea[name="q"], input[name="q"]').first();
  await searchInput.waitFor({ state: 'visible', timeout: 5000 });
  await searchInput.fill('AI');
  
  console.log('⏎ Pressing Enter...');
  await searchInput.press('Enter');

  console.log('⏳ Waiting for search results...');
  await page.waitForLoadState('networkidle', { timeout: 10000 });
  await page.waitForTimeout(2000);

  const title = await page.title();
  console.log(`📄 Page title: ${title}\\n`);

  console.log('📸 Taking screenshot...');
  await page.screenshot({ 
    path: 'playwright-screenshot.png',
    fullPage: false
  });
  console.log('✅ Screenshot saved: playwright-screenshot.png\\n');

  const results = await page.locator('h3').count();
  console.log(`🔢 Found ${results} result headers\\n`);

  await browser.close();
  console.log('✅ Browser closed\\n');
  console.log('✅ Done!');
}

main().catch(console.error);

🔍 코드 분석

1. 브라우저 실행

const browser = await chromium.launch({
  headless: false,
  executablePath: executablePath
});

Playwright가 Chrome 실행 및 CDP 연결을 자동으로 처리합니다.

2. 페이지 열기

const page = await context.newPage();
await page.goto('https://www.google.com', {
  waitUntil: 'networkidle'
});

waitUntil 옵션으로 페이지 로드 완료를 자동으로 대기합니다.

3. 요소 찾기 및 입력

const searchInput = await page.locator('textarea[name="q"]').first();
await searchInput.waitFor({ state: 'visible' });
await searchInput.fill('AI');

Playwright는 요소가 준비될 때까지 자동으로 대기하며, 안정적인 요소 선택을 제공합니다.

4. 스크린샷

await page.screenshot({ 
  path: 'playwright-screenshot.png',
  fullPage: false
});

한 줄로 스크린샷 캡처가 가능합니다.

▶️ 실행

npm run playwright
✅ Playwright의 장점
  • 코드가 간결하고 읽기 쉬움
  • 자동 대기 기능으로 안정성 향상
  • 직관적인 API
  • 여러 브라우저 지원 (Chrome, Firefox, Safari)

Advanced 6. 사람처럼 행동하기

🎯 목표

봇 감지 시스템을 우회하기 위해 사람처럼 행동하는 기능을 구현합니다. CAPTCHA 자동 처리, 랜덤 마우스 움직임, 사람같은 타이핑 등을 추가합니다.

⚠️ 윤리적 고려사항

이 기술은 교육 목적으로만 사용해야 합니다. 웹사이트의 서비스 약관을 위반하거나 불법적인 목적으로 사용하지 마세요.

📝 고급 기능 코드

1. 랜덤 딜레이 함수

function randomDelay(min = 0, max = 1000) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

async function humanLikeDelay(min = 300, max = 1000) {
  const delay = randomDelay(min, max);
  await new Promise(resolve => setTimeout(resolve, delay));
}

0-1000ms 사이의 랜덤 딜레이를 생성하여 사람처럼 불규칙한 타이밍을 만듭니다.

2. 랜덤 마우스 움직임

async function moveMouseRandomly(page, count = 5) {
  const viewport = page.viewportSize();
  for (let i = 0; i < count; i++) {
    const x = Math.floor(Math.random() * viewport.width);
    const y = Math.floor(Math.random() * viewport.height);
    await page.mouse.move(x, y, { steps: randomDelay(5, 15) });
    await humanLikeDelay(100, 500);
  }
}

화면 내 랜덤 위치로 마우스를 이동시킵니다. steps 옵션으로 부드러운 이동을 구현합니다.

3. 사람처럼 클릭하기

async function humanLikeClick(page, locator) {
  const box = await locator.boundingBox();
  if (box) {
    const x = box.x + box.width / 2 + randomDelay(-5, 5);
    const y = box.y + box.height / 2 + randomDelay(-5, 5);
    
    await page.mouse.move(x, y, { steps: randomDelay(10, 30) });
    await humanLikeDelay(100, 300);
    
    await page.mouse.down();
    await new Promise(resolve => setTimeout(resolve, randomDelay(50, 150)));
    await page.mouse.up();
  }
}

요소의 정확한 중앙이 아닌 약간 랜덤한 위치를 클릭하고, 마우스 다운과 업 사이에 자연스러운 딜레이를 추가합니다.

4. 사람처럼 타이핑하기

async function humanLikeType(page, locator, text) {
  await locator.click();
  await humanLikeDelay(200, 500);
  
  for (const char of text) {
    await page.keyboard.type(char);
    await new Promise(resolve => setTimeout(resolve, randomDelay(50, 200)));
  }
}

각 문자마다 50-200ms의 랜덤 딜레이를 주어 자연스러운 타이핑을 시뮬레이션합니다.

5. CAPTCHA 자동 처리

async function checkAndHandleCaptcha(page) {
  console.log('🤖 Checking for "I\\'m not a robot" checkbox...');
  
  try {
    const captchaFrame = page.frameLocator('iframe[src*="recaptcha"]').first();
    const checkbox = captchaFrame.locator('.recaptcha-checkbox-border, #recaptcha-anchor');
    
    const isVisible = await checkbox.isVisible({ timeout: 3000 }).catch(() => false);
    
    if (isVisible) {
      console.log('✅ Found CAPTCHA checkbox!');
      await humanLikeDelay(500, 1000);
      
      await moveMouseRandomly(page, 3);
      
      console.log('🖱️  Clicking "I\\'m not a robot" checkbox...');
      await humanLikeClick(page, checkbox);
      
      await humanLikeDelay(2000, 3000);
      
      console.log('✅ CAPTCHA checkbox clicked\\n');
      return true;
    } else {
      console.log('ℹ️  No CAPTCHA detected\\n');
      return false;
    }
  } catch (error) {
    console.log('ℹ️  No CAPTCHA detected (error checking)\\n');
    return false;
  }
}

reCAPTCHA iframe을 찾아 "I'm not a robot" 체크박스를 자동으로 클릭합니다. 체크박스를 찾지 못하면 조용히 넘어갑니다.

6. 봇 감지 우회 설정

const browser = await chromium.launch({
  headless: false,
  args: [
    '--disable-blink-features=AutomationControlled'
  ]
});

const context = await browser.newContext({
  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...'
});

await context.addInitScript(() => {
  Object.defineProperty(navigator, 'webdriver', {
    get: () => undefined
  });
});

navigator.webdriver 속성을 숨기고, 자동화 관련 Chrome 기능을 비활성화하여 봇 감지를 우회합니다.

📝 전체 코드 (통합)

import { chromium } from 'playwright-core';
import fs from 'fs';

function randomDelay(min = 0, max = 1000) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

async function humanLikeDelay(min = 300, max = 1000) {
  const delay = randomDelay(min, max);
  await new Promise(resolve => setTimeout(resolve, delay));
}

async function moveMouseRandomly(page, count = 5) {
  const viewport = page.viewportSize();
  for (let i = 0; i < count; i++) {
    const x = Math.floor(Math.random() * viewport.width);
    const y = Math.floor(Math.random() * viewport.height);
    await page.mouse.move(x, y, { steps: randomDelay(5, 15) });
    await humanLikeDelay(100, 500);
  }
}

async function humanLikeClick(page, locator) {
  const box = await locator.boundingBox();
  if (box) {
    const x = box.x + box.width / 2 + randomDelay(-5, 5);
    const y = box.y + box.height / 2 + randomDelay(-5, 5);
    
    await page.mouse.move(x, y, { steps: randomDelay(10, 30) });
    await humanLikeDelay(100, 300);
    
    await page.mouse.down();
    await new Promise(resolve => setTimeout(resolve, randomDelay(50, 150)));
    await page.mouse.up();
  }
}

async function humanLikeType(page, locator, text) {
  await locator.click();
  await humanLikeDelay(200, 500);
  
  for (const char of text) {
    await page.keyboard.type(char);
    await new Promise(resolve => setTimeout(resolve, randomDelay(50, 200)));
  }
}

async function checkAndHandleCaptcha(page) {
  console.log('🤖 Checking for "I\\'m not a robot" checkbox...');
  
  try {
    const captchaFrame = page.frameLocator('iframe[src*="recaptcha"]').first();
    const checkbox = captchaFrame.locator('.recaptcha-checkbox-border, #recaptcha-anchor');
    
    const isVisible = await checkbox.isVisible({ timeout: 3000 }).catch(() => false);
    
    if (isVisible) {
      console.log('✅ Found CAPTCHA checkbox!');
      await humanLikeDelay(500, 1000);
      await moveMouseRandomly(page, 3);
      console.log('🖱️  Clicking "I\\'m not a robot" checkbox...');
      await humanLikeClick(page, checkbox);
      await humanLikeDelay(2000, 3000);
      console.log('✅ CAPTCHA checkbox clicked\\n');
      return true;
    } else {
      console.log('ℹ️  No CAPTCHA detected\\n');
      return false;
    }
  } catch (error) {
    console.log('ℹ️  No CAPTCHA detected (error checking)\\n');
    return false;
  }
}

async function main() {
  console.log('=== Playwright Example: Google Search (Human-like) ===\\n');

  const browser = await chromium.launch({
    headless: false,
    args: [
      '--disable-blink-features=AutomationControlled'
    ]
  });

  const context = await browser.newContext({
    viewport: { width: 1280, height: 720 },
    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
  });

  await context.addInitScript(() => {
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    });
  });

  const page = await context.newPage();

  await page.goto('https://www.google.com', {
    waitUntil: 'networkidle'
  });

  await humanLikeDelay(1000, 2000);
  await moveMouseRandomly(page, 3);
  await checkAndHandleCaptcha(page);

  const searchInput = await page.locator('textarea[name="q"]').first();
  await humanLikeType(page, searchInput, 'AI');
  await humanLikeDelay(500, 1000);
  await page.keyboard.press('Enter');

  await page.waitForLoadState('networkidle');
  await humanLikeDelay(1000, 2000);
  await checkAndHandleCaptcha(page);
  await moveMouseRandomly(page, 4);

  await page.screenshot({ path: 'playwright-screenshot.png' });

  await browser.close();
}

main().catch(console.error);

🔑 핵심 기법 정리

기법목적구현

랜덤 딜레이 불규칙한 타이밍 0-1000ms 랜덤 대기
마우스 움직임 사람처럼 행동 랜덤 좌표 + 부드러운 이동
자연스러운 클릭 봇 감지 우회 요소 중앙 ±5px 오프셋
사람같은 타이핑 키 입력 자연화 문자당 50-200ms 딜레이
CAPTCHA 처리 자동화 차단 우회 iframe 내 체크박스 감지 및 클릭
webdriver 숨김 봇 감지 우회 navigator.webdriver = undefined

7. CDP vs Playwright 비교

항목CDPPlaywright

난이도 어려움 (저수준 API) 쉬움 (고수준 API)
코드 길이 길고 복잡함 짧고 간결함
요소 선택 JavaScript 직접 작성 .locator() API
자동 대기 수동 setTimeout 자동 대기 내장
안정성 타이밍 이슈 발생 가능 매우 안정적
유연성 매우 높음 적당함
브라우저 지원 Chrome 계열만 Chrome, Firefox, Safari
학습 곡선 가파름 완만함
사용 사례 특수한 제어 필요 시 대부분의 자동화 작업

📊 언제 무엇을 사용할까?

✅ Playwright를 사용하세요
  • 일반적인 웹 스크래핑
  • E2E 테스트
  • 빠른 프로토타이핑
  • 안정성이 중요한 경우
💡 CDP를 사용하세요
  • 매우 세밀한 제어가 필요한 경우
  • Playwright로 불가능한 작업
  • CDP 프로토콜 학습이 목적인 경우
  • 최소한의 의존성이 필요한 경우

8. 모범 사례

✅ DO (권장사항)

  • 에러 처리: 모든 비동기 작업에 try-catch 사용
  • 타임아웃 설정: 무한 대기 방지를 위한 적절한 타임아웃
  • 리소스 정리: 브라우저와 페이지를 항상 닫기
  • 선택자 안정성: 변하지 않는 선택자 사용 (data-testid 등)
  • 대기 전략: 명시적 대기 사용 (waitFor, waitForLoadState)
  • 봇 감지 우회: 사람처럼 행동하기 (랜덤 딜레이, 마우스 움직임)
  • Rate Limiting: 서버 부하를 고려한 요청 간격 조절

❌ DON'T (피해야 할 사항)

  • 고정 딜레이: setTimeout(5000) 같은 고정 대기 대신 조건부 대기 사용
  • CSS 선택자 남용: 너무 구체적이거나 불안정한 선택자
  • 리소스 누수: 브라우저를 닫지 않고 프로세스 종료
  • 동기 코드: async/await 없이 Promise 사용
  • 서비스 약관 위반: 웹사이트 정책 확인 없이 스크래핑
  • 과도한 요청: 짧은 시간에 대량 요청으로 서버 부하 유발

🛡️ 보안 및 윤리

⚠️ 중요
  • robots.txt 준수
  • 서비스 약관 확인
  • 개인정보 보호
  • 저작권 존중
  • 교육 목적으로만 사용

🚀 성능 최적화

  • Headless 모드: 실제 배포 시 headless: true 사용
  • 리소스 차단: 불필요한 이미지, 폰트 등 차단
  • 병렬 처리: 여러 페이지를 동시에 처리
  • 브라우저 재사용: 매번 새로 실행하지 않고 재사용

리소스 차단 예제

await page.route('**/*.{png,jpg,jpeg,gif,svg}', route => route.abort());
await page.route('**/*.{woff,woff2,ttf}', route => route.abort());

📝 코드 구조화

class GoogleSearchBot {
  constructor() {
    this.browser = null;
    this.page = null;
  }

  async init() {
    this.browser = await chromium.launch({ headless: false });
    const context = await this.browser.newContext();
    this.page = await context.newPage();
  }

  async search(query) {
    await this.page.goto('https://www.google.com');
    const input = this.page.locator('textarea[name="q"]');
    await input.fill(query);
    await input.press('Enter');
    await this.page.waitForLoadState('networkidle');
  }

  async getResults() {
    return await this.page.locator('h3').allTextContents();
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

const bot = new GoogleSearchBot();
await bot.init();
await bot.search('AI');
const results = await bot.getResults();
console.log(results);
await bot.close();

🎓 추가 학습 자료

📚 공식 문서

🔧 유용한 도구

  • Playwright Inspector: 디버깅 도구
  • Playwright Codegen: 자동 코드 생성
  • Chrome DevTools: 요소 검사 및 선택자 테스트

Playwright Codegen 사용

npx playwright codegen https://www.google.com

브라우저에서 수행한 작업을 자동으로 코드로 변환해줍니다.

💡 다음 단계

  • 다양한 웹사이트에서 실습
  • 복잡한 시나리오 구현 (로그인, 폼 작성 등)
  • 데이터베이스 연동
  • 스케줄링 (cron jobs)
  • 분산 스크래핑 (여러 인스턴스 동시 실행)