Puppeteer 가이드

웹페이지 스크린샷을 여러 장 찍거나, 로그인이 필요한 사이트에서 데이터를 가져오거나, 반복적인 폼 입력을 자동화해야 할 때가 있습니다. 이런 작업을 브라우저에서 직접 하면 시간이 많이 걸리고 실수하기도 쉽습니다.

Puppeteer는 이런 브라우저 작업을 코드로 자동화할 수 있게 해주는 Node.js 라이브러리입니다. Google Chrome 팀에서 만들었고, Chrome을 프로그래밍 방식으로 제어할 수 있습니다.

왜 Puppeteer를 배워야 할까요?

브라우저에서 반복적으로 수행하는 작업이 있다면 Puppeteer로 자동화할 수 있습니다. 특히 JavaScript로 동적 렌더링되는 페이지를 다룰 때 유용합니다.

주요 사용 사례

수동 작업                              Puppeteer 자동화
─────────────────────────────────────────────────────────────
여러 페이지 스크린샷                →    스크립트로 일괄 처리
PDF 보고서 생성                     →    템플릿 기반 자동 생성
로그인 후 데이터 수집               →    헤드리스 브라우저로 크롤링
반복적인 UI 테스트                  →    CI/CD 파이프라인 연동
동적 OG 이미지 생성                 →    서버에서 실시간 생성

흔한 오해와 실제

  • ❌ “HTTP 요청으로 충분하지 않나?” → JavaScript로 렌더링되는 SPA는 HTTP 요청만으로 크롤링 불가
  • ❌ “Selenium이랑 뭐가 다르지?” → Puppeteer는 Chrome 팀이 만들어서 Chrome과 완벽 호환
  • ❌ “헤드리스 브라우저는 느리다” → UI 렌더링을 생략해서 오히려 빠름

Puppeteer란?

Google Chrome 팀이 만든 Node.js 라이브러리로, 프로그래밍 방식으로 Chrome 또는 Chromium을 제어합니다.

┌─────────────────────────────────────────────────────────────┐
│                        Your Code (Node.js)                   │
├─────────────────────────────────────────────────────────────┤
│                        Puppeteer API                         │
├─────────────────────────────────────────────────────────────┤
│                   Chrome DevTools Protocol                   │
├─────────────────────────────────────────────────────────────┤
│                    Chrome / Chromium                         │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐                 │
│   │  Tab 1   │  │  Tab 2   │  │  Tab 3   │   ...           │
│   └──────────┘  └──────────┘  └──────────┘                 │
└─────────────────────────────────────────────────────────────┘

주요 기능

  1. 스크린샷 & PDF 생성: 웹페이지를 이미지나 PDF로 저장
  2. 웹 스크래핑: JavaScript 렌더링된 콘텐츠도 추출 가능
  3. 폼 자동화: 입력, 클릭, 제출 자동화
  4. E2E 테스트: UI 테스트 자동화
  5. 성능 측정: 페이지 로드 시간, 네트워크 요청 분석

설치 및 기본 설정

설치

# npm
npm install puppeteer

# yarn
yarn add puppeteer

# pnpm
pnpm add puppeteer

기본 설치 시 Chromium이 함께 다운로드됩니다 (~170MB).

가벼운 설치 (시스템 Chrome 사용)

# Chromium 없이 설치
npm install puppeteer-core
// 시스템에 설치된 Chrome 사용
const browser = await puppeteer.launch({
  executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  // Windows: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
});

첫 번째 스크립트: Hello Puppeteer

웹페이지 스크린샷 찍기

// screenshot.js
const puppeteer = require('puppeteer');

async function takeScreenshot() {
  // 1. 브라우저 실행
  const browser = await puppeteer.launch();

  // 2. 새 페이지(탭) 열기
  const page = await browser.newPage();

  // 3. URL로 이동
  await page.goto('https://example.com');

  // 4. 스크린샷 저장
  await page.screenshot({ path: 'example.png' });

  // 5. 브라우저 종료
  await browser.close();

  console.log('스크린샷 저장 완료!');
}

takeScreenshot();
node screenshot.js
# → example.png 파일이 생성됩니다

실행 결과 시각화

실행 흐름:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[Node.js] → puppeteer.launch()
              │
              ▼
         ┌─────────────────┐
         │ Chromium 프로세스 │  ← 헤드리스 모드로 실행
         │   (보이지 않음)   │
         └─────────────────┘
              │
[Node.js] → page.goto()
              │
              ▼
         ┌─────────────────┐
         │ example.com 로딩  │  ← DOM, CSS, JS 모두 실행
         └─────────────────┘
              │
[Node.js] → page.screenshot()
              │
              ▼
         ┌─────────────────┐
         │  example.png    │  ← 파일 저장
         └─────────────────┘
              │
[Node.js] → browser.close()
              │
              ▼
         Chromium 프로세스 종료

브라우저와 페이지 제어

브라우저 실행 옵션

const browser = await puppeteer.launch({
  // UI 표시 여부 (디버깅할 때 true로 설정)
  headless: false,

  // 브라우저 창 크기
  defaultViewport: {
    width: 1920,
    height: 1080,
  },

  // 실행 속도 조절 (디버깅용)
  slowMo: 100, // 각 동작마다 100ms 대기

  // DevTools 자동 열기
  devtools: true,

  // 추가 Chrome 인자
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--window-size=1920,1080',
  ],
});

페이지 설정

const page = await browser.newPage();

// 뷰포트 설정
await page.setViewport({
  width: 1280,
  height: 720,
  deviceScaleFactor: 2, // Retina 디스플레이 시뮬레이션
});

// User Agent 설정
await page.setUserAgent(
  'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ' +
  'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
);

// 쿠키 설정
await page.setCookie({
  name: 'session',
  value: 'abc123',
  domain: 'example.com',
});

// HTTP 헤더 설정
await page.setExtraHTTPHeaders({
  'Accept-Language': 'ko-KR,ko;q=0.9',
});

모바일 디바이스 에뮬레이션

const puppeteer = require('puppeteer');

// 내장된 디바이스 프리셋 사용
const iPhone = puppeteer.KnownDevices['iPhone 13 Pro'];

async function mobileScreenshot() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // iPhone 13 Pro 에뮬레이션
  await page.emulate(iPhone);

  await page.goto('https://example.com');
  await page.screenshot({ path: 'mobile.png' });

  await browser.close();
}

사용 가능한 디바이스 목록:

console.log(Object.keys(puppeteer.KnownDevices));
// ['Blackberry PlayBook', 'BlackBerry Z30', 'Galaxy Note 3',
//  'Galaxy Note II', 'Galaxy S III', 'Galaxy S5', 'Galaxy S8',
//  'Galaxy S9+', 'Galaxy Tab S4', 'iPad', 'iPad Mini', 'iPad Pro',
//  'iPhone 4', 'iPhone 5', 'iPhone 6', 'iPhone 6 Plus', 'iPhone 7',
//  'iPhone 8', 'iPhone SE', 'iPhone X', 'iPhone XR', 'iPhone 11',
//  'iPhone 11 Pro', 'iPhone 11 Pro Max', 'iPhone 12', 'iPhone 12 Pro',
//  'iPhone 13', 'iPhone 13 Pro', 'iPhone 13 Pro Max', ...]

페이지 탐색과 대기

페이지 이동

// 기본 이동
await page.goto('https://example.com');

// 옵션 지정
await page.goto('https://example.com', {
  // 대기 조건
  waitUntil: 'networkidle0', // 네트워크 요청이 0개가 될 때까지

  // 타임아웃 (기본 30초)
  timeout: 60000,
});

waitUntil 옵션 비교

waitUntil 옵션 비교:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

'load'          HTML + 모든 리소스 로드 완료
                └─ <img>, <script>, <link> 등

'domcontentloaded'  HTML 파싱 완료 (리소스 로드 전)
                    └─ 빠르지만 이미지/스크립트 미완료

'networkidle0'  500ms 동안 네트워크 요청 0개
                └─ 가장 안전, SPA에 적합

'networkidle2'  500ms 동안 네트워크 요청 2개 이하
                └─ 분석 스크립트 등 무시

권장 사용:
─────────────────────────────────────
정적 사이트     → 'domcontentloaded' (빠름)
SPA (React 등)  → 'networkidle0' (안전)
스크린샷        → 'networkidle0' (완전한 렌더링)

요소 대기

// 선택자가 나타날 때까지 대기
await page.waitForSelector('.my-element');

// 선택자가 사라질 때까지 대기
await page.waitForSelector('.loading-spinner', { hidden: true });

// XPath로 대기 (xpath/ 접두사 사용)
await page.waitForSelector('xpath///button[contains(text(), "Submit")]');

// 특정 시간 대기 (비추천, 하지만 가끔 필요)
await new Promise(r => setTimeout(r, 1000));

// 함수 조건 대기
await page.waitForFunction(() => {
  return document.querySelectorAll('.item').length > 10;
});

// 네트워크 요청 대기
await page.waitForResponse(response =>
  response.url().includes('/api/data') && response.status() === 200
);

DOM 조작과 데이터 추출

요소 선택

// 단일 요소
const element = await page.$('.my-class');                      // CSS 선택자
const element = await page.$('xpath///div[@id="app"]');         // XPath

// 복수 요소
const elements = await page.$$('.items');                       // 모든 일치 요소
const elements = await page.$$('xpath///li');                   // XPath로 모든 요소

요소 정보 추출

// 텍스트 내용 가져오기
const text = await page.$eval('.title', el => el.textContent);

// 속성 가져오기
const href = await page.$eval('a.link', el => el.getAttribute('href'));

// 여러 요소에서 데이터 추출
const items = await page.$$eval('.product', products =>
  products.map(product => ({
    name: product.querySelector('.name').textContent,
    price: product.querySelector('.price').textContent,
    image: product.querySelector('img').src,
  }))
);

// 페이지 전체에서 JavaScript 실행
const data = await page.evaluate(() => {
  // 이 코드는 브라우저 컨텍스트에서 실행됨
  return {
    title: document.title,
    url: window.location.href,
    userAgent: navigator.userAgent,
  };
});

실전 예제: 뉴스 헤드라인 크롤링

const puppeteer = require('puppeteer');

async function scrapeNews() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('https://news.ycombinator.com', {
    waitUntil: 'networkidle0',
  });

  // 모든 뉴스 제목과 링크 추출
  const news = await page.$$eval('.titleline > a', links =>
    links.slice(0, 10).map(link => ({
      title: link.textContent,
      url: link.href,
    }))
  );

  console.log('Top 10 Hacker News:');
  news.forEach((item, i) => {
    console.log(`${i + 1}. ${item.title}`);
    console.log(`   ${item.url}\n`);
  });

  await browser.close();
}

scrapeNews();

사용자 상호작용 자동화

클릭

// 기본 클릭
await page.click('.button');

// 더블 클릭
await page.click('.item', { clickCount: 2 });

// 우클릭
await page.click('.context-menu-trigger', { button: 'right' });

// 좌표로 클릭
await page.mouse.click(100, 200);

텍스트 입력

// 기본 입력
await page.type('#username', 'myuser');

// 입력 속도 조절 (사람처럼 보이게)
await page.type('#password', 'mypassword', { delay: 100 });

// 기존 값 지우고 입력
await page.click('#search', { clickCount: 3 }); // 전체 선택
await page.type('#search', 'new search term');

// 또는
await page.$eval('#search', el => el.value = '');
await page.type('#search', 'new search term');

키보드 조작

// 특수 키 입력
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Tab');

// 단축키 (Ctrl+A 전체 선택)
await page.keyboard.down('Control');
await page.keyboard.press('A');
await page.keyboard.up('Control');

실전 예제: 로그인 자동화

const puppeteer = require('puppeteer');

async function login() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.goto('https://example.com/login');

  // 로그인 폼 입력
  await page.type('#email', 'user@example.com');
  await page.type('#password', 'password123');

  // 로그인 버튼 클릭하고 페이지 이동 대기
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'networkidle0' }),
    page.click('button[type="submit"]'),
  ]);

  // 로그인 성공 확인
  const loggedIn = await page.$('.dashboard');
  if (loggedIn) {
    console.log('로그인 성공!');

    // 로그인 후 작업 수행...
    await page.screenshot({ path: 'dashboard.png' });
  } else {
    console.log('로그인 실패');
  }

  await browser.close();
}

login();

스크린샷과 PDF 생성

스크린샷 옵션

// 기본 스크린샷
await page.screenshot({ path: 'page.png' });

// 전체 페이지 (스크롤 포함)
await page.screenshot({
  path: 'fullpage.png',
  fullPage: true,
});

// 특정 영역만
await page.screenshot({
  path: 'clip.png',
  clip: {
    x: 0,
    y: 0,
    width: 800,
    height: 600,
  },
});

// 특정 요소만
const element = await page.$('.hero-section');
await element.screenshot({ path: 'hero.png' });

// PNG vs JPEG
await page.screenshot({
  path: 'page.jpg',
  type: 'jpeg',
  quality: 80, // 0-100, JPEG만 해당
});

// Base64로 반환 (파일 저장 없이)
const base64 = await page.screenshot({ encoding: 'base64' });

PDF 생성

// 기본 PDF
await page.pdf({ path: 'page.pdf' });

// 상세 옵션
await page.pdf({
  path: 'document.pdf',
  format: 'A4',           // A4, Letter, Legal 등
  printBackground: true,   // 배경색/이미지 포함
  margin: {
    top: '20mm',
    right: '20mm',
    bottom: '20mm',
    left: '20mm',
  },
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size: 10px; text-align: center;">헤더</div>',
  footerTemplate: '<div style="font-size: 10px; text-align: center;">페이지 <span class="pageNumber"></span> / <span class="totalPages"></span></div>',
});

// 가로 방향
await page.pdf({
  path: 'landscape.pdf',
  landscape: true,
});

실전 예제: 대량 스크린샷 자동화

const puppeteer = require('puppeteer');
const fs = require('fs');

const urls = [
  'https://example.com',
  'https://example.com/about',
  'https://example.com/products',
  'https://example.com/contact',
];

const viewports = [
  { name: 'desktop', width: 1920, height: 1080 },
  { name: 'tablet', width: 768, height: 1024 },
  { name: 'mobile', width: 375, height: 812 },
];

async function captureAllPages() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // 결과 폴더 생성
  if (!fs.existsSync('screenshots')) {
    fs.mkdirSync('screenshots');
  }

  for (const url of urls) {
    const pageName = new URL(url).pathname.replace(/\//g, '_') || 'home';

    for (const viewport of viewports) {
      await page.setViewport(viewport);
      await page.goto(url, { waitUntil: 'networkidle0' });

      const filename = `screenshots/${pageName}_${viewport.name}.png`;
      await page.screenshot({ path: filename, fullPage: true });

      console.log(`✓ ${filename}`);
    }
  }

  await browser.close();
  console.log('\n모든 스크린샷 완료!');
}

captureAllPages();

네트워크 요청 가로채기

요청 인터셉션

// 요청 인터셉션 활성화
await page.setRequestInterception(true);

page.on('request', request => {
  // 이미지 요청 차단 (빠른 크롤링을 위해)
  if (request.resourceType() === 'image') {
    request.abort();
  }
  // 분석 스크립트 차단
  else if (request.url().includes('google-analytics.com')) {
    request.abort();
  }
  // 나머지 요청은 허용
  else {
    request.continue();
  }
});

리소스 타입별 차단

await page.setRequestInterception(true);

const blockedResources = ['image', 'stylesheet', 'font', 'media'];

page.on('request', request => {
  if (blockedResources.includes(request.resourceType())) {
    request.abort();
  } else {
    request.continue();
  }
});

// 이제 페이지 로드가 훨씬 빨라집니다
await page.goto('https://example.com');

응답 수정

await page.setRequestInterception(true);

page.on('request', async request => {
  if (request.url().includes('/api/user')) {
    // 가짜 응답 반환 (테스트용)
    request.respond({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        name: 'Test User',
        email: 'test@example.com',
      }),
    });
  } else {
    request.continue();
  }
});

네트워크 모니터링

// 모든 응답 로깅
page.on('response', async response => {
  const url = response.url();
  const status = response.status();

  if (url.includes('/api/')) {
    console.log(`[${status}] ${url}`);

    // JSON 응답 내용 확인
    if (response.headers()['content-type']?.includes('json')) {
      const json = await response.json();
      console.log(json);
    }
  }
});

// 실패한 요청 로깅
page.on('requestfailed', request => {
  console.error(`Failed: ${request.url()}`);
  console.error(`Reason: ${request.failure().errorText}`);
});

에러 처리와 디버깅

try-catch로 에러 처리

async function safeScrape(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  try {
    await page.goto(url, {
      waitUntil: 'networkidle0',
      timeout: 30000,
    });

    const data = await page.$eval('.content', el => el.textContent);
    return data;

  } catch (error) {
    if (error.name === 'TimeoutError') {
      console.error('페이지 로딩 타임아웃:', url);
    } else if (error.message.includes('net::ERR')) {
      console.error('네트워크 에러:', error.message);
    } else {
      console.error('알 수 없는 에러:', error);
    }
    return null;

  } finally {
    await browser.close();
  }
}

디버깅 팁

// 1. 헤드리스 모드 끄기 (브라우저 보이게)
const browser = await puppeteer.launch({ headless: false });

// 2. 동작 속도 늦추기
const browser = await puppeteer.launch({
  headless: false,
  slowMo: 250, // 각 동작마다 250ms 대기
});

// 3. DevTools 자동 열기
const browser = await puppeteer.launch({
  headless: false,
  devtools: true,
});

// 4. 콘솔 로그 캡처
page.on('console', msg => {
  console.log('Browser console:', msg.text());
});

// 5. 페이지 에러 캡처
page.on('pageerror', error => {
  console.error('Page error:', error.message);
});

// 6. 특정 시점에 일시 정지
await page.evaluate(() => {
  debugger; // DevTools가 열려있으면 여기서 멈춤
});

재시도 로직

async function retryOperation(operation, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      console.log(`시도 ${attempt}/${maxRetries} 실패:`, error.message);

      if (attempt === maxRetries) {
        throw error;
      }

      // 재시도 전 대기 (지수 백오프)
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }
}

// 사용
const data = await retryOperation(async () => {
  const page = await browser.newPage();
  await page.goto(url, { timeout: 10000 });
  return await page.content();
});

성능 최적화

병렬 처리

const puppeteer = require('puppeteer');

async function scrapeMultiplePages(urls) {
  const browser = await puppeteer.launch();

  // 동시에 여러 페이지 처리
  const results = await Promise.all(
    urls.map(async url => {
      const page = await browser.newPage();

      try {
        await page.goto(url, { waitUntil: 'domcontentloaded' });
        const title = await page.title();
        return { url, title, success: true };
      } catch (error) {
        return { url, error: error.message, success: false };
      } finally {
        await page.close();
      }
    })
  );

  await browser.close();
  return results;
}

// 사용
const urls = [
  'https://example1.com',
  'https://example2.com',
  'https://example3.com',
];

scrapeMultiplePages(urls).then(console.log);

브라우저 재사용

// ❌ 비효율적: 매번 브라우저 실행
async function badApproach(urls) {
  for (const url of urls) {
    const browser = await puppeteer.launch(); // 매번 새 브라우저
    const page = await browser.newPage();
    await page.goto(url);
    // ...
    await browser.close();
  }
}

// ✅ 효율적: 브라우저 재사용
async function goodApproach(urls) {
  const browser = await puppeteer.launch(); // 한 번만 실행

  for (const url of urls) {
    const page = await browser.newPage();
    await page.goto(url);
    // ...
    await page.close(); // 페이지만 닫기
  }

  await browser.close();
}

불필요한 리소스 차단

async function fastScrape(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // 불필요한 리소스 차단
  await page.setRequestInterception(true);
  page.on('request', request => {
    const resourceType = request.resourceType();
    if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
      request.abort();
    } else {
      request.continue();
    }
  });

  await page.goto(url, { waitUntil: 'domcontentloaded' });
  // ...

  await browser.close();
}

함정과 해결책

함정 1: 동적 콘텐츠 로딩 실패

// ❌ 문제: React/Vue 앱에서 데이터가 비어있음
await page.goto('https://spa-example.com');
const items = await page.$$('.item'); // 빈 배열!

// ✅ 해결: 요소가 렌더링될 때까지 대기
await page.goto('https://spa-example.com');
await page.waitForSelector('.item', { timeout: 10000 });
const items = await page.$$('.item');

함정 2: 봇 감지 차단

// ❌ 문제: "봇이 감지되었습니다" 메시지
await page.goto('https://protected-site.com');

// ✅ 해결: 사람처럼 보이게 설정
const browser = await puppeteer.launch({
  headless: 'new', // 새로운 헤드리스 모드
  args: [
    '--no-sandbox',
    '--disable-blink-features=AutomationControlled',
  ],
});

const page = await browser.newPage();

// 자동화 감지 우회
await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, 'webdriver', {
    get: () => undefined,
  });
});

// 랜덤 지연 추가
await page.goto('https://protected-site.com');
await new Promise(r => setTimeout(r, Math.random() * 2000 + 1000));

함정 3: 메모리 누수

// ❌ 문제: 페이지를 닫지 않아 메모리 누수
async function badMemory() {
  const browser = await puppeteer.launch();

  for (let i = 0; i < 100; i++) {
    const page = await browser.newPage();
    await page.goto('https://example.com');
    // page.close() 호출 안 함!
  }
}

// ✅ 해결: 항상 페이지 닫기
async function goodMemory() {
  const browser = await puppeteer.launch();

  for (let i = 0; i < 100; i++) {
    const page = await browser.newPage();
    try {
      await page.goto('https://example.com');
    } finally {
      await page.close(); // 항상 닫기
    }
  }

  await browser.close();
}

함정 4: iframe 내부 접근 실패

// ❌ 문제: iframe 내부 요소를 찾을 수 없음
const button = await page.$('#button-inside-iframe'); // null!

// ✅ 해결: iframe 컨텍스트로 전환
const frameHandle = await page.$('iframe');
const frame = await frameHandle.contentFrame();
const button = await frame.$('#button-inside-iframe');
await button.click();

함정 5: 파일 다운로드 처리

// ❌ 문제: 파일 다운로드 버튼 클릭해도 다운로드 안됨

// ✅ 해결: 다운로드 경로 설정
const path = require('path');

const downloadPath = path.resolve('./downloads');

// CDP 세션으로 다운로드 설정
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
  behavior: 'allow',
  downloadPath: downloadPath,
});

// 이제 다운로드 버튼 클릭
await page.click('#download-btn');

// 다운로드 완료 대기
await new Promise(r => setTimeout(r, 5000));

Puppeteer vs Playwright vs Selenium

비교표:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    Puppeteer       Playwright       Selenium
────────────────────────────────────────────────────────────────────
개발사              Google          Microsoft        SeleniumHQ
언어                Node.js         Node/Python/     Java/Python/
                                    Java/C#          C#/Ruby/JS
브라우저            Chrome/         Chrome/Firefox/  모든 브라우저
                    Chromium        Safari/Edge
속도                ⭐⭐⭐⭐⭐        ⭐⭐⭐⭐⭐         ⭐⭐⭐
자동 대기           수동            자동             수동
병렬 테스트         가능            내장             Grid 필요
러닝 커브           쉬움            쉬움             중간
커뮤니티            크고 성숙       빠르게 성장      매우 큼
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

추천:
─────────────────────────────────────────────────────────────────────
Chrome만 필요 + Node.js 환경     → Puppeteer
다중 브라우저 + 최신 기능        → Playwright
기존 Selenium 코드 유지          → Selenium