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 │ ... │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
주요 기능
- 스크린샷 & PDF 생성: 웹페이지를 이미지나 PDF로 저장
- 웹 스크래핑: JavaScript 렌더링된 콘텐츠도 추출 가능
- 폼 자동화: 입력, 클릭, 제출 자동화
- E2E 테스트: UI 테스트 자동화
- 성능 측정: 페이지 로드 시간, 네트워크 요청 분석
설치 및 기본 설정
설치
# 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