Puppeteer와 타입스크립트로 시작하는 웹 스크래핑 자동화의 모든 것 🚀

안녕하세요, 여러분! 오늘은 2025년 3월 10일 기준으로 최신 트렌드를 반영한 Puppeteer와 타입스크립트를 활용한 웹 스크래핑 자동화에 대해 알아볼게요. 개발자든 비개발자든 웹에서 데이터를 수집하고 자동화하는 기술은 이제 선택이 아닌 필수가 되었죠! 😎
이 글을 통해 여러분은 웹 스크래핑의 기초부터 Puppeteer와 타입스크립트를 활용한 고급 자동화 기법까지 배우게 될 거예요. 특히 프로그래밍에 관심 있는 분들이라면 재능넷에서 이런 기술을 배우거나 가르치는 재능을 공유할 수도 있답니다! 자, 그럼 시작해볼까요? 🎮
📚 목차
- 웹 스크래핑과 자동화의 기본 개념
- Puppeteer 소개 및 설치 방법
- 타입스크립트와 함께 사용하는 이유
- 기본적인 웹 스크래핑 예제
- 고급 스크래핑 기법
- 데이터 저장 및 분석
- 실전 프로젝트: 자동화 봇 만들기
- 성능 최적화 및 문제 해결
- 법적/윤리적 고려사항
- 마무리 및 추가 자료
1. 웹 스크래핑과 자동화의 기본 개념 🌐
웹 스크래핑이 뭔지 모르는 분들을 위해 초간단 설명! 웹 스크래핑은 웹사이트에서 데이터를 추출하는 기술이에요. 쉽게 말하면 웹페이지의 정보를 긁어오는 거죠. 매번 수동으로 복사-붙여넣기 하는 거 진짜 노가다 아니겠어요? ㅋㅋㅋ 그래서 자동화가 필요한 거예요! 🤖
웹 스크래핑이 필요한 상황들
가격 비교: 여러 쇼핑몰의 제품 가격을 실시간으로 비교
콘텐츠 모니터링: 뉴스 사이트나 블로그의 새 글 알림 받기
데이터 분석: 소셜 미디어의 트렌드 분석
리드 생성: 잠재 고객 정보 수집
연구 데이터 수집: 학술 연구를 위한 대량의 데이터 수집
근데 웹 스크래핑에도 여러 방법이 있어요. 가장 기본적인 방법은 HTTP 요청을 보내고 HTML을 파싱하는 건데, 이건 정적 웹페이지에만 효과적이에요. 요즘 웹사이트들은 다 동적이잖아요? 자바스크립트로 콘텐츠를 로딩하고 그래서... 여기서 Puppeteer의 등장! 짜잔! 🎭
웹 스크래핑은 진짜 유용한데, 주의할 점도 있어요. 모든 웹사이트가 스크래핑을 환영하는 건 아니거든요. 어떤 사이트는 robots.txt 파일로 접근을 제한하기도 하고, 너무 많은 요청을 보내면 IP가 차단될 수도 있어요. 그래서 항상 웹사이트의 이용약관을 확인하고 예의 바르게(?) 스크래핑하는 게 중요해요! 😇
2. Puppeteer 소개 및 설치 방법 🎭
자, 이제 오늘의 주인공 Puppeteer에 대해 알아볼까요? Puppeteer는 구글에서 개발한 Node.js 라이브러리로, 헤드리스 크롬(또는 크로미움)을 제어할 수 있게 해줘요. '헤드리스'라는 건 UI 없이 백그라운드에서 실행된다는 뜻이에요. 눈에 보이는 브라우저 창 없이도 웹 브라우저의 모든 기능을 사용할 수 있다니, 완전 신기하지 않나요? ㄷㄷ 😲
Puppeteer의 주요 기능
🔍 웹페이지 스크린샷 및 PDF 생성
🔄 SPA(Single Page Application) 크롤링
⚡ 자동화된 UI 테스팅
📊 성능 측정 및 모니터링
🤖 폼 제출, 키보드 입력, 클릭 등 사용자 행동 시뮬레이션
2025년 3월 현재 Puppeteer는 v22.x 버전까지 나왔고, 계속해서 발전하고 있어요. 특히 최근에는 성능 개선과 함께 타입스크립트 지원이 더욱 강화되었답니다! 👏
Puppeteer 설치하기
Puppeteer 설치는 진짜 쉬워요! npm이나 yarn을 사용하면 됩니다.
// npm 사용
npm install puppeteer
// yarn 사용
yarn add puppeteer
// 타입스크립트 타입 정의 설치 (이미 포함되어 있지만, 명시적으로 설치할 수도 있어요)
npm install @types/puppeteer
설치할 때 Puppeteer는 자동으로 최신 버전의 크로미움을 다운로드해요. 근데 이게 좀 무거워서(약 180MB) 시간이 걸릴 수 있어요. 인내심을 갖고 기다려주세요! ㅋㅋㅋ 🕒
만약 이미 크롬이 설치되어 있고 그걸 사용하고 싶다면, puppeteer-core를 대신 설치하면 돼요:
npm install puppeteer-core
이렇게 하면 크로미움을 다운로드하지 않고, 이미 설치된 크롬을 사용할 수 있어요. 용량 절약! 👍
💡 2025년 팁: 최근 Puppeteer는 Docker 환경에서의 실행이 더 쉬워졌어요. 공식 Docker 이미지를 제공하기 때문에 컨테이너화된 환경에서도 쉽게 사용할 수 있답니다!
Puppeteer vs Selenium vs Playwright
웹 자동화 도구는 Puppeteer만 있는 게 아니에요. 비슷한 도구로 Selenium과 Playwright도 있죠. 2025년 기준으로 각각의 특징을 비교해볼까요?
도구 | 장점 | 단점 | 적합한 용도 |
---|---|---|---|
Puppeteer | 구글 공식 지원, 크롬 최적화, 빠른 성능 | 크롬/크로미움만 지원 | 크롬 기반 스크래핑, 성능 테스트 |
Selenium | 다양한 브라우저 지원, 언어 호환성 | 설정 복잡, 상대적으로 느림 | 크로스 브라우저 테스트 |
Playwright | 다중 브라우저 지원, 최신 API | 비교적 새로운 도구 | 모던 웹앱 테스트, 자동화 |
2025년에는 Playwright의 인기가 많이 올라갔지만, Puppeteer는 여전히 크롬 기반 자동화에서는 최고의 선택이에요. 특히 타입스크립트와 함께 사용하면 개발 생산성이 확 올라간답니다! 🚀
3. 타입스크립트와 함께 사용하는 이유 🧩
자바스크립트만으로도 Puppeteer를 충분히 사용할 수 있는데, 왜 굳이 타입스크립트를 쓰냐고요? 진짜 좋은 질문이에요! 타입스크립트는 자바스크립트의 슈퍼셋으로, 정적 타입 지원이 가장 큰 특징이에요. 근데 이게 웹 스크래핑에서 어떤 장점이 있는지 알아볼까요? 🤔
타입스크립트 + Puppeteer의 장점
- 코드 자동완성 및 인텔리센스: IDE에서 Puppeteer API의 메서드와 속성을 쉽게 찾을 수 있어요.
- 타입 안전성: 런타임 오류를 컴파일 타임에 잡아낼 수 있어요.
- 리팩토링 용이성: 대규모 스크래핑 프로젝트에서 코드 변경이 더 안전해져요.
- 문서화 효과: 코드 자체가 문서 역할을 해서 유지보수가 쉬워져요.
- 확장성: 복잡한 스크래핑 로직을 더 체계적으로 관리할 수 있어요.
특히 웹 스크래핑처럼 여러 단계의 비동기 작업이 필요한 경우, 타입스크립트의 타입 체킹은 정말 큰 도움이 돼요. 예를 들어, 페이지에서 특정 요소를 선택하고 그 텍스트를 추출하는 과정에서 타입 오류를 미리 잡아낼 수 있거든요. 이거 진짜 꿀팁임! 👌
타입스크립트 설정하기
Puppeteer와 타입스크립트를 함께 사용하려면 몇 가지 설정이 필요해요. 먼저 타입스크립트와 필요한 패키지들을 설치해볼까요?
npm install typescript ts-node @types/node puppeteer @types/puppeteer
그리고 tsconfig.json 파일을 만들어야 해요. 프로젝트 루트 디렉토리에 다음과 같이 작성해주세요:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
이제 src 폴더를 만들고 그 안에 .ts 파일을 작성하면 돼요! 타입스크립트 코드를 실행하려면 ts-node를 사용하면 편해요:
npx ts-node src/scraper.ts
아니면 package.json에 스크립트를 추가해서 더 간단하게 실행할 수도 있어요:
{
"scripts": {
"start": "ts-node src/scraper.ts",
"build": "tsc"
}
}
이렇게 하면 npm start
로 스크립트를 실행하고, npm run build
로 타입스크립트 코드를 자바스크립트로 컴파일할 수 있어요. 완전 꿀조합! 🍯
💡 개발 팁: VSCode를 사용한다면 타입스크립트와 Puppeteer의 조합이 더욱 강력해져요. 자동 완성과 타입 힌트가 개발 속도를 엄청나게 높여준답니다! 재능넷에서 VSCode 사용법을 배우는 것도 좋은 선택이 될 수 있어요.
4. 기본적인 웹 스크래핑 예제 🔍
이론은 충분히 배웠으니 이제 실제로 코드를 작성해볼까요? 먼저 가장 기본적인 웹 스크래핑 예제부터 시작해볼게요. 웹페이지에 접속해서 제목을 가져오는 간단한 예제예요! 😊
// src/basic-scraper.ts
import puppeteer from 'puppeteer';
async function scrapeWebsite() {
// 브라우저 실행
const browser = await puppeteer.launch({
headless: 'new' // 2023년부터 'new' 헤드리스 모드 사용
});
try {
// 새 페이지 열기
const page = await browser.newPage();
// 웹사이트로 이동
console.log('페이지 로딩 중...');
await page.goto('https://news.ycombinator.com/', {
waitUntil: 'networkidle2' // 네트워크 요청이 2개 이하로 떨어질 때까지 대기
});
// 페이지 제목 가져오기
const title = await page.title();
console.log(`페이지 제목: ${title}`);
// 뉴스 제목들 스크래핑
const headlines = await page.$$eval('.titleline > a', (links) =>
links.map(link => ({
title: link.textContent?.trim() || '',
url: link.getAttribute('href') || ''
}))
);
console.log('=== 오늘의 해커 뉴스 헤드라인 ===');
headlines.slice(0, 5).forEach((item, i) => {
console.log(`${i + 1}. ${item.title}`);
console.log(` 링크: ${item.url}`);
});
} catch (error) {
console.error('스크래핑 중 오류 발생:', error);
} finally {
// 브라우저 닫기
await browser.close();
console.log('브라우저 세션 종료');
}
}
// 함수 실행
scrapeWebsite()
.then(() => console.log('스크래핑 완료!'))
.catch(console.error);
이 코드를 실행하면 Hacker News의 최신 헤드라인 5개를 콘솔에 출력해요. 어때요? 생각보다 간단하죠? ㅎㅎ
코드 설명
puppeteer.launch()
: 크로미움 브라우저를 실행해요.headless: 'new'
는 2023년부터 도입된 새로운 헤드리스 모드예요.browser.newPage()
: 새 브라우저 탭을 열어요.page.goto()
: 지정된 URL로 이동해요.waitUntil
옵션으로 페이지 로딩 완료 조건을 설정할 수 있어요.page.title()
: 페이지의 제목을 가져와요.page.$$eval()
: CSS 선택자로 여러 요소를 선택하고, 그 요소들에 대해 함수를 실행해요. 여기서는 뉴스 제목 링크들을 선택하고 제목과 URL을 추출했어요.browser.close()
: 브라우저를 종료해요. 리소스 누수를 방지하기 위해 항상 닫아주는 게 좋아요!
스크린샷 및 PDF 생성하기
Puppeteer는 웹페이지의 스크린샷을 찍거나 PDF로 저장하는 기능도 제공해요. 이런 기능은 보고서 자동화나 웹페이지 모니터링에 정말 유용하답니다! 👀
// src/screenshot-pdf.ts
import puppeteer from 'puppeteer';
import path from 'path';
async function captureWebpage() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 뷰포트 설정 (반응형 테스트에 유용)
await page.setViewport({ width: 1280, height: 800 });
await page.goto('https://www.jaenung.net', {
waitUntil: 'networkidle2'
});
// 스크린샷 찍기
const screenshotPath = path.join(__dirname, '../screenshots', `jaenung_${Date.now()}.png`);
await page.screenshot({
path: screenshotPath,
fullPage: true // 전체 페이지 캡처
});
console.log(`스크린샷 저장됨: ${screenshotPath}`);
// PDF로 저장하기
const pdfPath = path.join(__dirname, '../pdfs', `jaenung_${Date.now()}.pdf`);
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true
});
console.log(`PDF 저장됨: ${pdfPath}`);
await browser.close();
}
captureWebpage().catch(console.error);
이 코드는 재능넷 웹사이트의 스크린샷과 PDF를 생성해요. 근데 PDF 생성은 헤드리스 모드에서만 작동한다는 점 참고하세요! 🧐
폼 작성 및 제출하기
웹 스크래핑에서 자주 필요한 작업 중 하나가 로그인이나 검색 폼 제출이에요. Puppeteer로 이런 작업도 쉽게 자동화할 수 있어요!
// src/form-submit.ts
import puppeteer from 'puppeteer';
async function searchAndScrape(searchTerm: string) {
const browser = await puppeteer.launch({
headless: false, // 브라우저 UI 표시 (작동 확인용)
slowMo: 100 // 각 작업 사이에 100ms 지연 (작동 확인용)
});
const page = await browser.newPage();
await page.goto('https://www.google.com');
// 쿠키 동의 대화상자 처리 (지역에 따라 다를 수 있음)
try {
const acceptButton = await page.$('button[id="L2AGLb"]');
if (acceptButton) {
await acceptButton.click();
}
} catch (error) {
console.log('쿠키 동의 버튼 없음, 계속 진행');
}
// 검색어 입력
await page.type('input[name="q"]', searchTerm);
// 폼 제출 (Enter 키 누르기)
await page.keyboard.press('Enter');
// 검색 결과 로딩 대기
await page.waitForSelector('#search');
// 검색 결과 추출
const searchResults = await page.$$eval('#search .g', (results) =>
results.slice(0, 5).map(result => {
const titleElement = result.querySelector('h3');
const linkElement = result.querySelector('a');
const snippetElement = result.querySelector('.VwiC3b');
return {
title: titleElement ? titleElement.textContent : '',
link: linkElement ? linkElement.getAttribute('href') : '',
snippet: snippetElement ? snippetElement.textContent : ''
};
})
);
console.log(`"${searchTerm}" 검색 결과:`);
searchResults.forEach((result, i) => {
console.log(`\n${i + 1}. ${result.title}`);
console.log(`링크: ${result.link}`);
console.log(`미리보기: ${result.snippet}`);
});
await browser.close();
}
searchAndScrape('재능넷 프리랜서').catch(console.error);
이 예제는 구글에서 "재능넷 프리랜서"를 검색하고 상위 5개 결과를 추출해요. headless: false와 slowMo 옵션을 사용하면 브라우저가 실제로 어떻게 동작하는지 볼 수 있어서 디버깅에 정말 유용해요! 🔍
⚠️ 주의사항: 구글이나 다른 검색 엔진에서 자동화된 검색을 너무 많이 하면 IP가 일시적으로 차단될 수 있어요. 항상 적절한 간격을 두고 요청을 보내는 것이 좋습니다!
5. 고급 스크래핑 기법 🚀
기본적인 스크래핑은 이제 마스터했으니, 조금 더 고급 기술로 넘어가볼까요? 실제 프로젝트에서는 더 복잡한 상황을 마주하게 될 텐데, 이런 상황을 해결하는 방법을 알아봅시다! 💪
무한 스크롤 처리하기
요즘 많은 웹사이트가 무한 스크롤을 사용하죠? 페이지 하단에 도달하면 새로운 콘텐츠가 로드되는 방식이에요. 이런 사이트에서 데이터를 스크래핑하려면 스크롤 동작을 시뮬레이션해야 해요.
// src/infinite-scroll.ts
import puppeteer from 'puppeteer';
async function scrapeInfiniteScroll() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.example-infinite-scroll.com');
// 초기 아이템 수 확인
let previousHeight = 0;
let items: string[] = [];
let scrollAttempts = 0;
const maxScrolls = 5; // 최대 스크롤 횟수 제한
while (scrollAttempts < maxScrolls) {
// 현재 아이템 추출
const newItems = await page.$$eval('.item-selector', (elements) =>
elements.map(el => el.textContent || '')
);
// 중복 제거하고 새 아이템 추가
items = [...new Set([...items, ...newItems])];
console.log(`현재 ${items.length}개 아이템 수집됨`);
// 이전 높이 저장
previousHeight = await page.evaluate('document.body.scrollHeight');
// 페이지 맨 아래로 스크롤
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
// 새 콘텐츠 로딩 대기
await page.waitForFunction(
`document.body.scrollHeight > ${previousHeight}`,
{ timeout: 10000 }
).catch(() => {
console.log('더 이상 새 콘텐츠가 로드되지 않음');
scrollAttempts = maxScrolls; // 루프 종료
});
// 잠시 대기 (너무 빠른 스크롤 방지)
await page.waitForTimeout(1000);
scrollAttempts++;
}
console.log(`총 ${items.length}개 아이템 수집 완료!`);
console.log(items.slice(0, 5)); // 처음 5개 아이템만 출력
await browser.close();
}
scrapeInfiniteScroll().catch(console.error);
이 코드는 페이지 맨 아래로 스크롤하고, 새 콘텐츠가 로드될 때까지 기다린 다음, 다시 스크롤하는 과정을 반복해요. 무한 스크롤 사이트에서 데이터를 수집할 때 정말 유용한 패턴이죠! 🔄
로그인이 필요한 페이지 스크래핑
많은 웹사이트가 로그인해야만 접근할 수 있는 콘텐츠를 제공해요. Puppeteer로 로그인 과정을 자동화할 수 있답니다!
// src/login-scraper.ts
import puppeteer from 'puppeteer';
import dotenv from 'dotenv'; // 환경변수 관리용
// .env 파일에서 환경변수 로드
dotenv.config();
async function scrapeAfterLogin() {
// 환경변수에서 로그인 정보 가져오기
const username = process.env.LOGIN_USERNAME;
const password = process.env.LOGIN_PASSWORD;
if (!username || !password) {
throw new Error('로그인 정보가 .env 파일에 설정되지 않았습니다.');
}
const browser = await puppeteer.launch({
headless: 'new'
});
const page = await browser.newPage();
// 로그인 페이지로 이동
await page.goto('https://example.com/login');
// 로그인 폼 작성
await page.type('#username', username);
await page.type('#password', password);
// 폼 제출 및 리다이렉션 대기
await Promise.all([
page.click('#login-button'),
page.waitForNavigation({ waitUntil: 'networkidle2' })
]);
// 로그인 성공 확인
const isLoggedIn = await page.evaluate(() => {
// 로그인 성공 여부를 확인하는 요소 검사
return document.querySelector('.user-profile') !== null;
});
if (!isLoggedIn) {
throw new Error('로그인에 실패했습니다. 자격 증명을 확인하세요.');
}
console.log('로그인 성공! 이제 보호된 콘텐츠를 스크래핑합니다...');
// 로그인 후 접근 가능한 페이지로 이동
await page.goto('https://example.com/protected-content');
// 보호된 콘텐츠 스크래핑
const protectedData = await page.$$eval('.content-item', items =>
items.map(item => ({
title: item.querySelector('h2')?.textContent || '',
description: item.querySelector('p')?.textContent || ''
}))
);
console.log('보호된 콘텐츠:', protectedData);
// 쿠키 저장 (다음 세션에서 재사용 가능)
const cookies = await page.cookies();
console.log(`${cookies.length}개의 쿠키가 저장되었습니다.`);
await browser.close();
return protectedData;
}
scrapeAfterLogin()
.then(data => console.log(`${data.length}개 항목을 성공적으로 스크래핑했습니다.`))
.catch(error => console.error('오류 발생:', error));
이 예제에서는 dotenv 패키지를 사용해 환경변수에서 로그인 정보를 가져와요. 보안을 위해 로그인 정보를 코드에 직접 넣지 않는 것이 좋아요! 🔒
🔐 보안 팁: .env 파일은 절대 Git에 커밋하지 마세요! .gitignore에 추가해서 민감한 정보가 유출되지 않도록 해야 합니다.
AJAX 및 동적 콘텐츠 처리
현대 웹사이트는 대부분 AJAX를 사용해 동적으로 콘텐츠를 로드해요. 이런 콘텐츠를 스크래핑하려면 특별한 기법이 필요해요.
// src/ajax-content.ts
import puppeteer from 'puppeteer';
async function scrapeAjaxContent() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 네트워크 요청 모니터링 설정
await page.setRequestInterception(true);
// API 응답 캡처를 위한 배열
const apiResponses: any[] = [];
// 요청 인터셉터
page.on('request', request => {
// 필요한 리소스만 로드하도록 필터링 (성능 최적화)
if (['image', 'stylesheet', 'font'].includes(request.resourceType())) {
request.abort();
} else {
request.continue();
}
});
// 응답 인터셉터
page.on('response', async response => {
const url = response.url();
// API 엔드포인트 응답 캡처
if (url.includes('/api/data')) {
try {
const responseData = await response.json();
apiResponses.push(responseData);
console.log(`API 응답 캡처됨: ${url}`);
} catch (e) {
console.log(`JSON이 아닌 응답: ${url}`);
}
}
});
// 페이지 로드
await page.goto('https://example.com/ajax-content');
// 버튼 클릭으로 AJAX 콘텐츠 로드 트리거
await page.click('#load-more-button');
// AJAX 콘텐츠가 로드될 때까지 대기
await page.waitForSelector('.dynamic-content', { timeout: 5000 });
// 동적으로 로드된 콘텐츠 스크래핑
const dynamicContent = await page.$$eval('.dynamic-content', elements =>
elements.map(el => el.textContent?.trim() || '')
);
console.log('동적 콘텐츠:', dynamicContent);
console.log('캡처된 API 응답:', apiResponses);
await browser.close();
return {
dynamicContent,
apiResponses
};
}
scrapeAjaxContent().catch(console.error);
이 예제는 두 가지 방법으로 동적 콘텐츠를 처리해요:
- 요소 대기:
waitForSelector
로 동적으로 로드되는 요소가 나타날 때까지 기다려요. - 네트워크 인터셉션: API 응답을 직접 캡처해서 더 효율적으로 데이터를 추출해요.
두 번째 방법이 특히 유용한데, 웹사이트가 API를 통해 데이터를 가져오는 경우 HTML을 파싱하는 것보다 API 응답을 직접 캡처하는 게 훨씬 효율적이거든요! 이거 진짜 꿀팁임 ㄹㅇ 👍
멀티 페이지 스크래핑
여러 페이지에서 데이터를 수집해야 할 때는 어떻게 해야 할까요? 페이지네이션을 처리하는 방법을 알아봅시다!
// src/pagination.ts
import puppeteer from 'puppeteer';
import fs from 'fs';
async function scrapePagination() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 결과를 저장할 배열
const allResults: any[] = [];
// 첫 페이지 로드
await page.goto('https://example.com/page/1');
let currentPage = 1;
const maxPages = 5; // 스크래핑할 최대 페이지 수
let hasNextPage = true;
while (hasNextPage && currentPage <= maxPages) {
console.log(`페이지 ${currentPage} 스크래핑 중...`);
// 현재 페이지의 데이터 추출
const pageResults = await page.$$eval('.item', items =>
items.map(item => ({
title: item.querySelector('h2')?.textContent?.trim() || '',
price: item.querySelector('.price')?.textContent?.trim() || '',
description: item.querySelector('.description')?.textContent?.trim() || ''
}))
);
console.log(`${pageResults.length}개 항목 발견됨`);
allResults.push(...pageResults);
// 다음 페이지 버튼 확인
const nextButton = await page.$('.pagination .next');
hasNextPage = nextButton !== null;
if (hasNextPage && currentPage < maxPages) {
// 다음 페이지로 이동
await Promise.all([
page.click('.pagination .next'),
page.waitForNavigation({ waitUntil: 'networkidle2' })
]);
currentPage++;
} else {
break;
}
}
console.log(`총 ${currentPage}개 페이지에서 ${allResults.length}개 항목 스크래핑 완료!`);
// 결과를 JSON 파일로 저장
fs.writeFileSync(
`./results_${Date.now()}.json`,
JSON.stringify(allResults, null, 2)
);
await browser.close();
return allResults;
}
scrapePagination().catch(console.error);
이 코드는 페이지네이션이 있는 웹사이트에서 여러 페이지의 데이터를 수집해요. 각 페이지에서 데이터를 추출하고, "다음" 버튼을 클릭해 다음 페이지로 이동하는 과정을 반복하죠. 수집한 데이터는 JSON 파일로 저장해요. 👌
💡 베스트 프랙티스: 대규모 스크래핑에서는 항상 진행 상황을 파일에 저장하세요. 중간에 오류가 발생해도 처음부터 다시 시작할 필요가 없답니다!
6. 데이터 저장 및 분석 💾
스크래핑한 데이터를 어떻게 저장하고 활용할 수 있을까요? 데이터를 다양한 형식으로 저장하고 분석하는 방법을 알아봅시다! 📊
CSV 파일로 저장하기
CSV는 데이터를 저장하는 가장 간단하고 범용적인 형식 중 하나예요. 스프레드시트 프로그램에서 쉽게 열 수 있어 편리하죠!
// src/save-to-csv.ts
import puppeteer from 'puppeteer';
import { createObjectCsvWriter } from 'csv-writer';
import path from 'path';
async function scrapeAndSaveToCsv() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/products');
// 제품 데이터 스크래핑
const products = await page.$$eval('.product', products =>
products.map(product => ({
id: product.getAttribute('data-id') || '',
name: product.querySelector('.name')?.textContent?.trim() || '',
price: product.querySelector('.price')?.textContent?.trim() || '',
rating: product.querySelector('.rating')?.textContent?.trim() || '',
inStock: product.querySelector('.stock')?.textContent?.includes('In Stock') || false
}))
);
// CSV 파일 경로 설정
const csvFilePath = path.join(__dirname, '../data', `products_${Date.now()}.csv`);
// CSV Writer 설정
const csvWriter = createObjectCsvWriter({
path: csvFilePath,
header: [
{ id: 'id', title: 'ID' },
{ id: 'name', title: 'Product Name' },
{ id: 'price', title: 'Price' },
{ id: 'rating', title: 'Rating' },
{ id: 'inStock', title: 'In Stock' }
]
});
// 데이터 쓰기
await csvWriter.writeRecords(products);
console.log(`${products.length}개 제품 데이터가 ${csvFilePath}에 저장되었습니다.`);
await browser.close();
return products;
}
scrapeAndSaveToCsv().catch(console.error);
이 예제에서는 csv-writer
패키지를 사용해 스크래핑한 제품 데이터를 CSV 파일로 저장해요. 이렇게 저장한 데이터는 Excel이나 Google Sheets에서 바로 열어볼 수 있어요! 👀
데이터베이스에 저장하기
대규모 데이터를 다루거나 구조화된 쿼리가 필요한 경우에는 데이터베이스를 사용하는 것이 좋아요. 여기서는 SQLite를 예로 들어볼게요!
// src/save-to-database.ts
import puppeteer from 'puppeteer';
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
async function scrapeAndSaveToDb() {
// SQLite 데이터베이스 연결
const db = await open({
filename: './scraping_data.db',
driver: sqlite3.Database
});
// 테이블 생성 (없는 경우)
await db.exec(`
CREATE TABLE IF NOT EXISTS news_articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT UNIQUE NOT NULL,
author TEXT,
published_date TEXT,
content TEXT,
scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/news');
// 뉴스 기사 링크 수집
const articleLinks = await page.$$eval('.article-link', links =>
links.map(link => link.getAttribute('href'))
);
console.log(`${articleLinks.length}개의 기사 링크를 찾았습니다.`);
// 각 기사 페이지 방문하여 상세 정보 스크래핑
for (const link of articleLinks) {
if (!link) continue;
const fullUrl = new URL(link, 'https://example.com').toString();
console.log(`기사 스크래핑 중: ${fullUrl}`);
await page.goto(fullUrl, { waitUntil: 'networkidle2' });
// 기사 데이터 추출
const articleData = await page.evaluate(() => {
return {
title: document.querySelector('h1')?.textContent?.trim() || '',
author: document.querySelector('.author')?.textContent?.trim() || '',
publishedDate: document.querySelector('.date')?.textContent?.trim() || '',
content: document.querySelector('.article-content')?.textContent?.trim() || ''
};
});
// 데이터베이스에 저장
try {
await db.run(
`INSERT INTO news_articles (title, url, author, published_date, content)
VALUES (?, ?, ?, ?, ?)`,
[articleData.title, fullUrl, articleData.author, articleData.publishedDate, articleData.content]
);
console.log(`기사 저장됨: ${articleData.title}`);
} catch (error) {
console.log(`기사 저장 실패 (중복 가능성): ${fullUrl}`);
}
// 요청 간 딜레이 (예의 바른 스크래핑)
await page.waitForTimeout(1000);
}
// 저장된 기사 수 확인
const count = await db.get('SELECT COUNT(*) as count FROM news_articles');
console.log(`데이터베이스에 총 ${count.count}개의 기사가 저장되어 있습니다.`);
await browser.close();
await db.close();
}
scrapeAndSaveToDb().catch(console.error);
이 예제는 뉴스 기사를 스크래핑해서 SQLite 데이터베이스에 저장해요. 데이터베이스를 사용하면 중복 제거, 인덱싱, 복잡한 쿼리 등 다양한 이점을 누릴 수 있어요! 🗃️
데이터 분석 및 시각화
스크래핑한 데이터는 분석하고 시각화할 때 진정한 가치가 드러나요. 간단한 분석 예제를 살펴볼까요?
// src/analyze-data.ts
import fs from 'fs';
import path from 'path';
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
async function analyzeProductData() {
// CSV 파일에서 데이터 읽기 (이전 예제에서 저장한 파일)
const dataPath = path.join(__dirname, '../data', 'products.csv');
const csvData = fs.readFileSync(dataPath, 'utf-8');
// CSV 파싱 (간단한 구현, 실제로는 csv-parser 같은 라이브러리 사용 권장)
const lines = csvData.split('\n');
const headers = lines[0].split(',');
const products = lines.slice(1).filter(line => line.trim()).map(line => {
const values = line.split(',');
const product: Record<string string number boolean> = {};
headers.forEach((header, index) => {
// 숫자 필드 변환
if (header === 'Price') {
product[header] = parseFloat(values[index].replace(/[^0-9.]/g, ''));
} else if (header === 'Rating') {
product[header] = parseFloat(values[index]);
} else if (header === 'In Stock') {
product[header] = values[index].toLowerCase() === 'true';
} else {
product[header] = values[index];
}
});
return product;
});
console.log(`${products.length}개 제품 데이터 로드됨`);
// 기본 통계 계산
const priceSum = products.reduce((sum, product) => sum + (product['Price'] as number), 0);
const avgPrice = priceSum / products.length;
const inStockCount = products.filter(product => product['In Stock']).length;
const inStockPercentage = (inStockCount / products.length) * 100;
// 가격대별 제품 수 계산
const priceRanges = {
'0-50': 0,
'51-100': 0,
'101-200': 0,
'201+': 0
};
products.forEach(product => {
const price = product['Price'] as number;
if (price <= 50) priceRanges['0-50']++;
else if (price <= 100) priceRanges['51-100']++;
else if (price <= 200) priceRanges['101-200']++;
else priceRanges['201+']++;
});
console.log('=== 제품 데이터 분석 결과 ===');
console.log(`총 제품 수: ${products.length}`);
console.log(`평균 가격: $${avgPrice.toFixed(2)}`);
console.log(`재고 있음: ${inStockCount}개 (${inStockPercentage.toFixed(1)}%)`);
console.log('가격대별 제품 수:');
Object.entries(priceRanges).forEach(([range, count]) => {
console.log(` $${range}: ${count}개`);
});
// 차트 생성 (ChartJS 사용)
const width = 800;
const height = 600;
const chartCallback = (ChartJS: any) => {
// ChartJS 글로벌 설정
};
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback });
// 가격대별 제품 수 차트 설정
const configuration = {
type: 'bar',
data: {
labels: Object.keys(priceRanges).map(range => `$${range}`),
datasets: [{
label: '제품 수',
data: Object.values(priceRanges),
backgroundColor: [
'rgba(255, 99, 132, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(255, 206, 86, 0.6)',
'rgba(75, 192, 192, 0.6)'
]
}]
},
options: {
plugins: {
title: {
display: true,
text: '가격대별 제품 분포'
}
}
}
};
// 차트 이미지 생성 및 저장
const image = await chartJSNodeCanvas.renderToBuffer(configuration);
fs.writeFileSync(path.join(__dirname, '../charts', 'price_distribution.png'), image);
console.log('차트가 생성되었습니다: price_distribution.png');
}
analyzeProductData().catch(console.error);</string>
이 예제는 스크래핑한 제품 데이터의 기본 통계를 계산하고, ChartJS를 사용해 가격대별 제품 분포를 시각화해요. 데이터 분석은 스크래핑의 궁극적인 목표인 경우가 많죠! 📈
💡 분석 팁: 실제 프로젝트에서는 pandas, numpy 같은 파이썬 라이브러리나 R을 사용하는 것이 더 효율적일 수 있어요. Node.js에서는 데이터 수집만 하고, 분석은 다른 도구에 맡기는 것도 좋은 전략이에요!
7. 실전 프로젝트: 자동화 봇 만들기 🤖
지금까지 배운 내용을 종합해서 실용적인 프로젝트를 만들어볼까요? 여기서는 가격 모니터링 봇을 만들어볼 거예요. 이 봇은 정기적으로 특정 제품의 가격을 확인하고, 가격이 떨어지면 알림을 보내는 기능을 가지고 있어요! 🔔
프로젝트 구조
먼저 프로젝트 구조를 설계해볼게요:
price-monitor-bot/
├── src/
│ ├── index.ts # 메인 애플리케이션
│ ├── scraper.ts # 스크래핑 로직
│ ├── database.ts # 데이터베이스 관리
│ ├── notifier.ts # 알림 서비스
│ └── types.ts # 타입 정의
├── data/
│ └── products.json # 모니터링할 제품 목록
├── .env # 환경 변수
├── tsconfig.json # TypeScript 설정
└── package.json # 프로젝트 의존성
타입 정의하기
먼저 타입스크립트의 장점을 활용하기 위해 필요한 타입들을 정의해볼게요:
// src/types.ts
export interface Product {
id: string;
name: string;
url: string;
selector: string;
currentPrice: number | null;
lastPrice: number | null;
lowestPrice: number | null;
lastChecked: Date | null;
priceHistory: PricePoint[];
notifyOnPriceDrop: boolean;
notifyThreshold: number | null;
}
export interface PricePoint {
price: number;
date: Date;
}
export interface NotificationConfig {
email?: string;
discord?: string;
telegram?: string;
}
스크래핑 로직 구현
이제 제품 가격을 스크래핑하는 로직을 구현해볼게요:
// src/scraper.ts
import puppeteer from 'puppeteer';
import { Product, PricePoint } from './types';
export async function checkProductPrice(product: Product): Promise<product> {
console.log(`가격 확인 중: ${product.name}`);
const browser = await puppeteer.launch({
headless: 'new'
});
try {
const page = await browser.newPage();
// 사용자 에이전트 설정 (차단 방지)
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
);
// 제품 페이지 로드
await page.goto(product.url, {
waitUntil: 'networkidle2',
timeout: 30000
});
// 가격 추출
const priceText = await page.$eval(product.selector, (element) => {
return element.textContent || '';
}).catch(() => '');
if (!priceText) {
console.log(`가격을 찾을 수 없음: ${product.name}`);
return product;
}
// 가격 문자열에서 숫자만 추출
const priceMatch = priceText.match(/[\d,]+\.?\d*/);
if (!priceMatch) {
console.log(`가격 형식을 파싱할 수 없음: ${priceText}`);
return product;
}
// 가격 파싱
const priceString = priceMatch[0].replace(/,/g, '');
const currentPrice = parseFloat(priceString);
if (isNaN(currentPrice)) {
console.log(`유효하지 않은 가격: ${priceString}`);
return product;
}
console.log(`현재 가격: $${currentPrice} (이전: ${product.currentPrice ? `$${product.currentPrice}` : 'N/A'})`);
// 가격 기록 업데이트
const now = new Date();
const pricePoint: PricePoint = {
price: currentPrice,
date: now
};
// 제품 정보 업데이트
const updatedProduct: Product = {
...product,
lastPrice: product.currentPrice,
currentPrice,
lastChecked: now,
priceHistory: [...(product.priceHistory || []), pricePoint]
};
// 최저가 업데이트
if (product.lowestPrice === null || currentPrice < product.lowestPrice) {
updatedProduct.lowestPrice = currentPrice;
console.log(`새로운 최저가 기록: $${currentPrice}`);
}
return updatedProduct;
} catch (error) {
console.error(`가격 확인 중 오류 발생: ${product.name}`, error);
return product;
} finally {
await browser.close();
}
}</product>
데이터베이스 관리
제품 정보와 가격 기록을 저장하기 위한 데이터베이스 모듈을 구현해볼게요:
// src/database.ts
import fs from 'fs/promises';
import path from 'path';
import { Product } from './types';
const DB_PATH = path.join(__dirname, '../data/db.json');
// 데이터베이스 초기화
export async function initDatabase(): Promise<void> {
try {
await fs.access(DB_PATH);
} catch (error) {
// 파일이 없으면 생성
await fs.writeFile(DB_PATH, JSON.stringify({ products: [] }, null, 2));
console.log('데이터베이스 파일 생성됨');
}
}
// 모든 제품 가져오기
export async function getAllProducts(): Promise<product> {
try {
const data = await fs.readFile(DB_PATH, 'utf-8');
const db = JSON.parse(data);
return db.products || [];
} catch (error) {
console.error('제품 목록을 가져오는 중 오류 발생:', error);
return [];
}
}
// 제품 업데이트
export async function updateProduct(updatedProduct: Product): Promise<void> {
try {
const data = await fs.readFile(DB_PATH, 'utf-8');
const db = JSON.parse(data);
const productIndex = db.products.findIndex(
(p: Product) => p.id === updatedProduct.id
);
if (productIndex >= 0) {
db.products[productIndex] = updatedProduct;
} else {
db.products.push(updatedProduct);
}
await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2));
} catch (error) {
console.error('제품 업데이트 중 오류 발생:', error);
throw error;
}
}
// 가격이 떨어진 제품 찾기
export async function findPriceDrops(): Promise<product> {
const products = await getAllProducts();
return products.filter(product =>
product.currentPrice !== null &&
product.lastPrice !== null &&
product.currentPrice < product.lastPrice &&
product.notifyOnPriceDrop
);
}</product></void></product></void>
알림 서비스
가격이 떨어졌을 때 알림을 보내는 서비스를 구현해볼게요:
// src/notifier.ts
import nodemailer from 'nodemailer';
import { Product } from './types';
import dotenv from 'dotenv';
dotenv.config();
// 이메일 알림 보내기
export async function sendPriceDropEmail(products: Product[]): Promise<void> {
if (products.length === 0) return;
const EMAIL_USER = process.env.EMAIL_USER;
const EMAIL_PASS = process.env.EMAIL_PASS;
const EMAIL_TO = process.env.EMAIL_TO;
if (!EMAIL_USER || !EMAIL_PASS || !EMAIL_TO) {
console.error('이메일 설정이 없습니다. .env 파일을 확인하세요.');
return;
}
// 트랜스포터 생성
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: EMAIL_USER,
pass: EMAIL_PASS
}
});
// 이메일 내용 생성
let emailContent = `
<h1>가격 인하 알림</h1>
<p>모니터링 중인 제품의 가격이 인하되었습니다:</p>
<ul>
`;
products.forEach(product => {
const priceDrop = product.lastPrice! - product.currentPrice!;
const dropPercentage = (priceDrop / product.lastPrice!) * 100;
emailContent += `
<li>
<h3>${product.name}</h3>
<p>현재 가격: $${product.currentPrice}</p>
<p>이전 가격: $${product.lastPrice}</p>
<p>가격 인하: $${priceDrop.toFixed(2)} (${dropPercentage.toFixed(1)}%)</p>
<p><a href="%24%7Bproduct.url%7D">제품 보기</a></p>
</li>
`;
});
emailContent += '</ul>';
// 이메일 보내기
try {
const info = await transporter.sendMail({
from: `"가격 모니터 봇" <${EMAIL_USER}>`,
to: EMAIL_TO,
subject: `🔔 ${products.length}개 제품 가격 인하 알림`,
html: emailContent
});
console.log(`이메일 알림 전송됨: ${info.messageId}`);
} catch (error) {
console.error('이메일 전송 중 오류 발생:', error);
}
}</void>
메인 애플리케이션
마지막으로, 모든 모듈을 통합하는 메인 애플리케이션을 구현해볼게요:
// src/index.ts
import { checkProductPrice } from './scraper';
import { initDatabase, getAllProducts, updateProduct, findPriceDrops } from './database';
import { sendPriceDropEmail } from './notifier';
import fs from 'fs/promises';
import path from 'path';
import { Product } from './types';
// 모니터링할 제품 목록 로드
async function loadProductsConfig(): Promise<product> {
try {
const configPath = path.join(__dirname, '../data/products.json');
const data = await fs.readFile(configPath, 'utf-8');
const products: Product[] = JSON.parse(data);
// 기존 DB와 병합
const dbProducts = await getAllProducts();
return products.map(configProduct => {
const existingProduct = dbProducts.find(p => p.id === configProduct.id);
if (existingProduct) {
// 기존 제품 정보 유지하면서 설정 업데이트
return {
...existingProduct,
name: configProduct.name,
url: configProduct.url,
selector: configProduct.selector,
notifyOnPriceDrop: configProduct.notifyOnPriceDrop,
notifyThreshold: configProduct.notifyThreshold
};
}
// 새 제품 초기화
return {
...configProduct,
currentPrice: null,
lastPrice: null,
lowestPrice: null,
lastChecked: null,
priceHistory: []
};
});
} catch (error) {
console.error('제품 설정을 로드하는 중 오류 발생:', error);
return [];
}
}
// 모든 제품 가격 확인
async function checkAllProductPrices(): Promise<void> {
console.log('가격 모니터링 시작...');
// 데이터베이스 초기화
await initDatabase();
// 제품 목록 로드
const products = await loadProductsConfig();
if (products.length === 0) {
console.log('모니터링할 제품이 없습니다.');
return;
}
console.log(`${products.length}개 제품 모니터링 중...`);
// 각 제품 가격 확인 및 업데이트
for (const product of products) {
const updatedProduct = await checkProductPrice(product);
await updateProduct(updatedProduct);
// 요청 간 딜레이
await new Promise(resolve => setTimeout(resolve, 2000));
}
// 가격이 떨어진 제품 찾기
const priceDrops = await findPriceDrops();
if (priceDrops.length > 0) {
console.log(`${priceDrops.length}개 제품의 가격이 인하되었습니다!`);
await sendPriceDropEmail(priceDrops);
} else {
console.log('가격 인하된 제품이 없습니다.');
}
console.log('가격 모니터링 완료!');
}
// 주기적으로 가격 확인 (6시간마다)
async function startMonitoring(): Promise<void> {
console.log('가격 모니터링 봇 시작됨');
// 즉시 첫 번째 확인 실행
await checkAllProductPrices();
// 6시간마다 반복
const SIX_HOURS = 6 * 60 * 60 * 1000;
setInterval(checkAllProductPrices, SIX_HOURS);
}
// 애플리케이션 시작
startMonitoring().catch(error => {
console.error('애플리케이션 오류:', error);
process.exit(1);
});</void></void></product>
제품 설정 파일
마지막으로, 모니터링할 제품 목록을 정의하는 JSON 파일을 만들어볼게요:
// data/products.json
[
{
"id": "laptop-1",
"name": "Dell XPS 13",
"url": "https://www.example.com/products/dell-xps-13",
"selector": ".product-price .current-price",
"notifyOnPriceDrop": true,
"notifyThreshold": 50
},
{
"id": "smartphone-1",
"name": "Samsung Galaxy S25",
"url": "https://www.example.com/products/samsung-galaxy-s25",
"selector": "#price-value",
"notifyOnPriceDrop": true,
"notifyThreshold": 100
},
{
"id": "headphones-1",
"name": "Sony WH-1000XM6",
"url": "https://www.example.com/products/sony-wh-1000xm6",
"selector": ".price-box .special-price .price",
"notifyOnPriceDrop": true,
"notifyThreshold": 20
}
]
이렇게 해서 완전한 가격 모니터링 봇이 완성되었어요! 이 봇은 정기적으로 제품 가격을 확인하고, 가격이 떨어지면 이메일로 알려줘요. 실용적이죠? 😎
🚀 확장 아이디어: 이 프로젝트를 더 발전시키고 싶다면 다음 기능을 추가해보세요!
- Discord나 Telegram 알림 추가
- 가격 변동 그래프 생성
- 웹 대시보드 구현
- 경쟁사 가격 비교 기능
- 재능넷에서 이런 봇 제작 서비스 제공하기!
8. 성능 최적화 및 문제 해결 ⚡
웹 스크래핑 프로젝트가 커질수록 성능과 안정성이 중요해져요. 이 섹션에서는 Puppeteer와 타입스크립트로 구현한 스크래핑 프로젝트의 성능을 최적화하고 일반적인 문제를 해결하는 방법을 알아볼게요! 🛠️
성능 최적화 기법
웹 스크래핑 성능을 향상시키는 몇 가지 핵심 기법을 살펴볼게요:
1. 리소스 필터링
불필요한 리소스(이미지, 폰트, CSS 등)를 차단하여 페이지 로딩 시간을 단축할 수 있어요:
await page.setRequestInterception(true);
page.on('request', (req) => {
if (
req.resourceType() === 'image' ||
req.resourceType() === 'stylesheet' ||
req.resourceType() === 'font'
) {
req.abort();
} else {
req.continue();
}
});
2. 병렬 처리
여러 페이지를 동시에 스크래핑하여 시간을 절약할 수 있어요:
async function scrapeInParallel(urls: string[], maxConcurrency = 5) {
const browser = await puppeteer.launch();
const results: any[] = [];
// 병렬 처리를 위한 함수
const processUrl = async (url: string) => {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle2' });
// 스크래핑 로직...
const data = await page.evaluate(() => {
// 데이터 추출...
});
results.push(data);
} finally {
await page.close();
}
};
// 병렬 처리 (동시에 maxConcurrency 개만 실행)
for (let i = 0; i < urls.length; i += maxConcurrency) {
const batch = urls.slice(i, i + maxConcurrency);
await Promise.all(batch.map(processUrl));
console.log(`${i + batch.length}/${urls.length} 완료`);
}
await browser.close();
return results;
}
3. 브라우저 인스턴스 재사용
브라우저 인스턴스를 재사용하여 시작 오버헤드를 줄일 수 있어요:
// 브라우저 인스턴스 관리 클래스
class BrowserManager {
private browser: puppeteer.Browser | null = null;
async getBrowser(): Promise<puppeteer.browser> {
if (!this.browser || !this.browser.isConnected()) {
this.browser = await puppeteer.launch();
}
return this.browser;
}
async closeBrowser(): Promise<void> {
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}
// 싱글톤 인스턴스
export const browserManager = new BrowserManager();</void></puppeteer.browser>
4. 캐싱 전략
이미 방문한 페이지의 데이터를 캐싱하여 중복 요청을 방지할 수 있어요:
// 간단한 캐시 구현
class ScrapeCache {
private cache: Map<string data: any timestamp: number> = new Map();
private TTL: number; // 캐시 유효 시간 (밀리초)
constructor(ttlMinutes = 60) {
this.TTL = ttlMinutes * 60 * 1000;
}
get(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
// 캐시 만료 확인
if (Date.now() - cached.timestamp > this.TTL) {
this.cache.delete(key);
return null;
}
return cached.data;
}
set(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
clear(): void {
this.cache.clear();
}
}
export const scrapeCache = new ScrapeCache();</string>
일반적인 문제 해결
웹 스크래핑 중에 자주 발생하는 문제와 해결 방법을 알아볼게요:
1. 요소를 찾을 수 없음
문제: page.$
또는 page.$$
로 요소를 찾을 수 없어요.
해결책:
- 페이지가 완전히 로드될 때까지 기다리세요:
waitForSelector
또는waitForFunction
사용 - 셀렉터가 정확한지 확인하세요: 개발자 도구에서 테스트
- 프레임이나 iframe 내부의 요소인지 확인하세요
- 동적으로 생성되는 요소인 경우 JavaScript 실행 후 생성될 수 있어요
// 요소가 나타날 때까지 기다리기
await page.waitForSelector('.dynamic-element', { timeout: 10000 });
// 또는 특정 조건이 충족될 때까지 기다리기
await page.waitForFunction(
() => document.querySelectorAll('.item').length > 5,
{ timeout: 10000 }
);
2. 봇 감지 및 차단
문제: 웹사이트가 봇으로 인식하고 접근을 차단해요.
해결책:
- 사용자 에이전트 설정
- 쿠키 및 세션 관리
- 요청 간 지연 시간 추가
- 헤더 설정
- 프록시 사용
// 사용자 에이전트 설정
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
);
// 추가 헤더 설정
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://www.google.com/'
});
// 요청 간 랜덤 지연
const randomDelay = (min: number, max: number) =>
new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (max - min + 1)) + min));
await randomDelay(3000, 7000); // 3-7초 지연
3. 메모리 누수
문제: 장시간 실행 시 메모리 사용량이 계속 증가해요.
해결책:
- 사용 후 페이지와 브라우저를 항상 닫기
- 주기적으로 브라우저 재시작
- 불필요한 데이터 참조 제거
// 주기적으로 브라우저 재시작
async function scrapeWithRestart(urls: string[], batchSize = 50) {
const results = [];
for (let i = 0; i < urls.length; i += batchSize) {
const batch = urls.slice(i, i + batchSize);
// 새 브라우저 인스턴스 시작
const browser = await puppeteer.launch();
try {
// 배치 처리
for (const url of batch) {
const page = await browser.newPage();
try {
// 스크래핑 로직...
results.push(data);
} finally {
await page.close(); // 페이지 닫기
}
}
} finally {
await browser.close(); // 브라우저 닫기
}
console.log(`배치 ${i / batchSize + 1} 완료, 메모리 정리됨`);
}
return results;
}
4. 타임아웃 오류
문제: 네트워크 지연이나 리소스 로딩으로 인한 타임아웃이 발생해요.
해결책:
- 타임아웃 값 증가
- 적절한 waitUntil 옵션 선택
- 재시도 메커니즘 구현
// 재시도 메커니즘 구현
async function scrapeWithRetry(url: string, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000 // 30초 타임아웃
});
// 스크래핑 로직...
await browser.close();
return data; // 성공 시 데이터 반환
} catch (error) {
retries++;
console.log(`시도 ${retries}/${maxRetries} 실패: ${error.message}`);
if (retries >= maxRetries) {
throw new Error(`최대 재시도 횟수 초과: ${url}`);
}
// 지수 백오프: 재시도 간 대기 시간 증가
const waitTime = 2000 * Math.pow(2, retries - 1);
console.log(`${waitTime}ms 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
디버깅 팁
스크래핑 스크립트를 디버깅하는 데 도움이 되는 몇 가지 팁을 알아볼게요:
1. 시각적 디버깅
headless 모드를 끄고 브라우저 동작을 직접 확인하세요:
const browser = await puppeteer.launch({
headless: false, // 브라우저 UI 표시
slowMo: 100 // 작업 간 100ms 지연
});
2. 스크린샷 찍기
문제가 발생한 시점의 페이지 상태를 캡처하세요:
try {
await page.goto('https://example.com');
// 스크래핑 로직...
} catch (error) {
// 오류 발생 시 스크린샷 저장
await page.screenshot({
path: `error-${Date.now()}.png`,
fullPage: true
});
throw error;
}
3. 콘솔 로그 모니터링
브라우저 콘솔 로그를 Node.js 콘솔로 리다이렉션하세요:
// 브라우저 콘솔 로그 캡처
page.on('console', msg => {
const type = msg.type().substr(0, 3).toUpperCase();
console.log(`[브라우저 콘솔] ${type}: ${msg.text()}`);
});
// 페이지에서 콘솔 로그 출력
await page.evaluate(() => {
console.log('페이지 내부에서 로깅');
});
4. 네트워크 요청 모니터링
네트워크 요청과 응답을 모니터링하세요:
// 네트워크 요청 모니터링
page.on('request', request => {
console.log(`요청: ${request.method()} ${request.url()}`);
});
page.on('response', response => {
console.log(`응답: ${response.status()} ${response.url()}`);
});
이러한 성능 최적화 기법과 문제 해결 방법을 적용하면 더 안정적이고 효율적인 웹 스크래핑 프로젝트를 구축할 수 있어요. 특히 대규모 스크래핑에서는 이런 최적화가 필수적이죠! 💯
9. 법적/윤리적 고려사항 ⚖️
웹 스크래핑은 강력한 도구지만, 법적, 윤리적 측면에서 신중하게 접근해야 해요. 이 섹션에서는 웹 스크래핑을 할 때 고려해야 할 중요한 법적, 윤리적 사항들을 알아볼게요. 🧐
법적 고려사항
1. 이용약관(ToS) 준수
대부분의 웹사이트는 이용약관에 자동화된 데이터 수집에 대한 규정을 포함하고 있어요. 스크래핑하기 전에 반드시 해당 사이트의 이용약관을 확인하세요.
2. robots.txt 파일 존중
robots.txt는 웹사이트가 크롤러에게 어떤 페이지에 접근할 수 있는지 알려주는 파일이에요. 이를 무시하는 것은 법적 문제를 야기할 수 있어요.
// robots.txt 파싱 예제
import robotsParser from 'robots-parser';
import fetch from 'node-fetch';
async function checkRobotsPermission(url: string, userAgent: string): Promise<boolean> {
try {
const parsedUrl = new URL(url);
const robotsUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}/robots.txt`;
const response = await fetch(robotsUrl);
const robotsTxt = await response.text();
const robots = robotsParser(robotsUrl, robotsTxt);
return robots.isAllowed(url, userAgent);
} catch (error) {
console.error('robots.txt 확인 중 오류:', error);
// 오류 발생 시 안전하게 false 반환
return false;
}
}</boolean>
3. 저작권법
웹사이트의 콘텐츠는 저작권으로 보호될 수 있어요. 데이터를 수집하는 것은 괜찮을 수 있지만, 그 데이터를 재배포하는 것은 저작권 침해가 될 수 있어요.
4. 개인정보 보호법
개인 식별 정보(PII)를 수집하는 경우, 개인정보 보호법을 준수해야 해요. 특히 EU의 GDPR, 한국의 개인정보 보호법 등을 고려해야 해요.
윤리적 고려사항
1. 서버 부하
너무 많은 요청을 빠르게 보내면 대상 웹사이트에 과도한 부하를 줄 수 있어요. 항상 적절한 간격을 두고 요청을 보내세요.
// 예의 바른 스크래핑을 위한 지연 함수
async function politelyVisitPages(urls: string[]) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
for (const url of urls) {
console.log(`방문 중: ${url}`);
await page.goto(url, { waitUntil: 'networkidle2' });
// 스크래핑 로직...
// 다음 요청 전 2-5초 지연
const delay = 2000 + Math.random() * 3000;
console.log(`${Math.round(delay / 1000)}초 대기 중...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
await browser.close();
}
2. 투명성
가능하다면, 웹사이트 관리자에게 스크래핑 의도를 알리고 허가를 받는 것이 좋아요. 많은 사이트는 공식 API를 제공하기도 해요.
3. 데이터 사용 목적
수집한 데이터를 어떻게 사용할 것인지 신중하게 고려하세요. 다른 사람에게 해를 끼치거나 속이는 데 사용하지 마세요.
4. 경쟁사 데이터
경쟁사의 데이터를 스크래핑하는 것은 법적, 윤리적 문제를 야기할 수 있어요. 특히 가격 정보나 독점 데이터를 수집할 때는 주의하세요.
안전한 스크래핑을 위한 베스트 프랙티스
- 자신을 식별하세요: 사용자 에이전트에 연락처 정보를 포함하세요.
- 속도 제한을 설정하세요: 요청 간 적절한 지연 시간을 두세요.
- 캐싱을 활용하세요: 같은 페이지를 반복해서 요청하지 마세요.
- 오류 처리를 구현하세요: 429(Too Many Requests) 응답을 받으면 더 오래 기다리세요.
- 필요한 데이터만 수집하세요: 목적에 필요한 최소한의 데이터만 수집하세요.
// 사용자 에이전트에 연락처 정보 포함
await page.setUserAgent(
'MyBot/1.0 (+https://example.com/bot; bot@example.com)'
);
// 429 응답 처리
page.on('response', async response => {
if (response.status() === 429) {
console.log('너무 많은 요청을 보냈습니다. 잠시 대기합니다...');
// 지수 백오프: 점점 더 오래 기다림
await new Promise(resolve => setTimeout(resolve, 60000)); // 1분 대기
}
});
스크래핑의 대안
웹 스크래핑이 유일한 옵션이 아닐 수도 있어요. 다음과 같은 대안을 고려해보세요:
- 공식 API 사용: 많은 웹사이트가 공식 API를 제공해요. 이는 일반적으로 더 안정적이고 법적으로 안전해요.
- 데이터 파트너십: 데이터가 필요한 회사와 직접 파트너십을 맺는 것을 고려하세요.
- 오픈 데이터셋: 많은 데이터가 이미 공개 데이터셋으로 제공되고 있어요.
- 데이터 마켓플레이스: 필요한 데이터를 판매하는 마켓플레이스를 이용하세요.
웹 스크래핑은 강력한 도구지만, 책임감 있게 사용해야 해요. 법적, 윤리적 가이드라인을 준수하면 문제 없이 데이터를 수집하고 활용할 수 있어요. 항상 "내가 웹사이트 소유자라면 이런 스크래핑을 허용할까?"라고 자문해보세요. 🤔
"큰 힘에는 큰 책임이 따른다" - 스파이더맨의 삼촌 벤
웹 스크래핑도 마찬가지예요! 😉
10. 마무리 및 추가 자료 📚
여기까지 Puppeteer와 타입스크립트를 활용한 웹 스크래핑의 모든 것을 살펴봤어요! 기본 개념부터 고급 기술, 성능 최적화, 그리고 법적/윤리적 고려사항까지 다양한 주제를 다뤘죠. 이제 여러분은 자신만의 웹 스크래핑 프로젝트를 시작할 준비가 되었어요! 🎉
배운 내용 요약
- 웹 스크래핑과 자동화의 기본 개념
- Puppeteer 설치 및 기본 사용법
- 타입스크립트와 함께 사용하는 이유와 방법
- 기본적인 웹 스크래핑 예제 (스크린샷, PDF 생성, 폼 제출)
- 고급 스크래핑 기법 (무한 스크롤, 로그인, AJAX 콘텐츠, 페이지네이션)
- 데이터 저장 및 분석 방법
- 실전 프로젝트: 가격 모니터링 봇 구현
- 성능 최적화 및 문제 해결 기법
- 법적/윤리적 고려사항
다음 단계
웹 스크래핑 여정을 계속하고 싶다면, 다음과 같은 주제를 더 탐구해보세요:
추천 자료
더 깊이 배우고 싶다면 다음 자료들을 참고하세요:
📚 책
- "Web Scraping with Node.js" - 실용적인 스크래핑 기법
- "Programming TypeScript" - 타입스크립트 심화 학습
- "Web Scraping with Python" - 파이썬 관점에서의 스크래핑 (비교 학습용)
🌐 웹사이트 & 문서
- Puppeteer 공식 문서 - 최신 API 참조
- TypeScript 공식 문서 - 타입스크립트 심화 학습
- Puppeteer 예제 모음 - 다양한 사용 사례
🎓 온라인 강의
- "Puppeteer & Playwright 마스터 클래스" - 심화 자동화 기술
- "TypeScript로 실전 웹 개발하기" - 타입스크립트 활용 기술
- "데이터 수집 및 분석 완전 가이드" - 스크래핑 후 데이터 활용법
커뮤니티 참여
웹 스크래핑과 자동화에 관심 있는 다른 개발자들과 지식을 공유하고 배워보세요:
- GitHub에서 오픈 소스 프로젝트에 기여하기
- Stack Overflow에서 질문하고 답변하기
- Reddit의 r/webscraping, r/typescript 커뮤니티 참여
- Twitter에서 #WebScraping, #Puppeteer 해시태그 팔로우
- 재능넷에서 웹 스크래핑 관련 재능 공유하기
마치며
웹 스크래핑은 단순한 기술 이상의 것이에요. 그것은 인터넷의 바다에서 가치 있는 정보를 발굴하는 능력이죠. Puppeteer와 타입스크립트는 이 여정을 더 안전하고 효율적으로 만들어주는 도구예요.
여러분만의 창의적인 스크래핑 프로젝트를 시작해보세요! 그리고 재능넷에서 여러분의 스크래핑 기술을 공유하거나, 다른 사람의 재능을 활용해보는 것도 좋은 방법이에요. 🚀
질문이나 의견이 있으시면 언제든지 댓글로 남겨주세요. 함께 배우고 성장해요! 👋
1. 웹 스크래핑과 자동화의 기본 개념 🌐
웹 스크래핑이 뭔지 모르는 분들을 위해 초간단 설명! 웹 스크래핑은 웹사이트에서 데이터를 추출하는 기술이에요. 쉽게 말하면 웹페이지의 정보를 긁어오는 거죠. 매번 수동으로 복사-붙여넣기 하는 거 진짜 노가다 아니겠어요? ㅋㅋㅋ 그래서 자동화가 필요한 거예요! 🤖
웹 스크래핑이 필요한 상황들
가격 비교: 여러 쇼핑몰의 제품 가격을 실시간으로 비교
콘텐츠 모니터링: 뉴스 사이트나 블로그의 새 글 알림 받기
데이터 분석: 소셜 미디어의 트렌드 분석
리드 생성: 잠재 고객 정보 수집
연구 데이터 수집: 학술 연구를 위한 대량의 데이터 수집
근데 웹 스크래핑에도 여러 방법이 있어요. 가장 기본적인 방법은 HTTP 요청을 보내고 HTML을 파싱하는 건데, 이건 정적 웹페이지에만 효과적이에요. 요즘 웹사이트들은 다 동적이잖아요? 자바스크립트로 콘텐츠를 로딩하고 그래서... 여기서 Puppeteer의 등장! 짜잔! 🎭
웹 스크래핑은 진짜 유용한데, 주의할 점도 있어요. 모든 웹사이트가 스크래핑을 환영하는 건 아니거든요. 어떤 사이트는 robots.txt 파일로 접근을 제한하기도 하고, 너무 많은 요청을 보내면 IP가 차단될 수도 있어요. 그래서 항상 웹사이트의 이용약관을 확인하고 예의 바르게(?) 스크래핑하는 게 중요해요! 😇
2. Puppeteer 소개 및 설치 방법 🎭
자, 이제 오늘의 주인공 Puppeteer에 대해 알아볼까요? Puppeteer는 구글에서 개발한 Node.js 라이브러리로, 헤드리스 크롬(또는 크로미움)을 제어할 수 있게 해줘요. '헤드리스'라는 건 UI 없이 백그라운드에서 실행된다는 뜻이에요. 눈에 보이는 브라우저 창 없이도 웹 브라우저의 모든 기능을 사용할 수 있다니, 완전 신기하지 않나요? ㄷㄷ 😲
Puppeteer의 주요 기능
🔍 웹페이지 스크린샷 및 PDF 생성
🔄 SPA(Single Page Application) 크롤링
⚡ 자동화된 UI 테스팅
📊 성능 측정 및 모니터링
🤖 폼 제출, 키보드 입력, 클릭 등 사용자 행동 시뮬레이션
2025년 3월 현재 Puppeteer는 v22.x 버전까지 나왔고, 계속해서 발전하고 있어요. 특히 최근에는 성능 개선과 함께 타입스크립트 지원이 더욱 강화되었답니다! 👏
Puppeteer 설치하기
Puppeteer 설치는 진짜 쉬워요! npm이나 yarn을 사용하면 됩니다.
// npm 사용
npm install puppeteer
// yarn 사용
yarn add puppeteer
// 타입스크립트 타입 정의 설치 (이미 포함되어 있지만, 명시적으로 설치할 수도 있어요)
npm install @types/puppeteer
설치할 때 Puppeteer는 자동으로 최신 버전의 크로미움을 다운로드해요. 근데 이게 좀 무거워서(약 180MB) 시간이 걸릴 수 있어요. 인내심을 갖고 기다려주세요! ㅋㅋㅋ 🕒
만약 이미 크롬이 설치되어 있고 그걸 사용하고 싶다면, puppeteer-core를 대신 설치하면 돼요:
npm install puppeteer-core
이렇게 하면 크로미움을 다운로드하지 않고, 이미 설치된 크롬을 사용할 수 있어요. 용량 절약! 👍
💡 2025년 팁: 최근 Puppeteer는 Docker 환경에서의 실행이 더 쉬워졌어요. 공식 Docker 이미지를 제공하기 때문에 컨테이너화된 환경에서도 쉽게 사용할 수 있답니다!
Puppeteer vs Selenium vs Playwright
웹 자동화 도구는 Puppeteer만 있는 게 아니에요. 비슷한 도구로 Selenium과 Playwright도 있죠. 2025년 기준으로 각각의 특징을 비교해볼까요?
도구 | 장점 | 단점 | 적합한 용도 |
---|---|---|---|
Puppeteer | 구글 공식 지원, 크롬 최적화, 빠른 성능 | 크롬/크로미움만 지원 | 크롬 기반 스크래핑, 성능 테스트 |
Selenium | 다양한 브라우저 지원, 언어 호환성 | 설정 복잡, 상대적으로 느림 | 크로스 브라우저 테스트 |
Playwright | 다중 브라우저 지원, 최신 API | 비교적 새로운 도구 | 모던 웹앱 테스트, 자동화 |
2025년에는 Playwright의 인기가 많이 올라갔지만, Puppeteer는 여전히 크롬 기반 자동화에서는 최고의 선택이에요. 특히 타입스크립트와 함께 사용하면 개발 생산성이 확 올라간답니다! 🚀
3. 타입스크립트와 함께 사용하는 이유 🧩
자바스크립트만으로도 Puppeteer를 충분히 사용할 수 있는데, 왜 굳이 타입스크립트를 쓰냐고요? 진짜 좋은 질문이에요! 타입스크립트는 자바스크립트의 슈퍼셋으로, 정적 타입 지원이 가장 큰 특징이에요. 근데 이게 웹 스크래핑에서 어떤 장점이 있는지 알아볼까요? 🤔
타입스크립트 + Puppeteer의 장점
- 코드 자동완성 및 인텔리센스: IDE에서 Puppeteer API의 메서드와 속성을 쉽게 찾을 수 있어요.
- 타입 안전성: 런타임 오류를 컴파일 타임에 잡아낼 수 있어요.
- 리팩토링 용이성: 대규모 스크래핑 프로젝트에서 코드 변경이 더 안전해져요.
- 문서화 효과: 코드 자체가 문서 역할을 해서 유지보수가 쉬워져요.
- 확장성: 복잡한 스크래핑 로직을 더 체계적으로 관리할 수 있어요.
특히 웹 스크래핑처럼 여러 단계의 비동기 작업이 필요한 경우, 타입스크립트의 타입 체킹은 정말 큰 도움이 돼요. 예를 들어, 페이지에서 특정 요소를 선택하고 그 텍스트를 추출하는 과정에서 타입 오류를 미리 잡아낼 수 있거든요. 이거 진짜 꿀팁임! 👌
타입스크립트 설정하기
Puppeteer와 타입스크립트를 함께 사용하려면 몇 가지 설정이 필요해요. 먼저 타입스크립트와 필요한 패키지들을 설치해볼까요?
npm install typescript ts-node @types/node puppeteer @types/puppeteer
그리고 tsconfig.json 파일을 만들어야 해요. 프로젝트 루트 디렉토리에 다음과 같이 작성해주세요:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
이제 src 폴더를 만들고 그 안에 .ts 파일을 작성하면 돼요! 타입스크립트 코드를 실행하려면 ts-node를 사용하면 편해요:
npx ts-node src/scraper.ts
아니면 package.json에 스크립트를 추가해서 더 간단하게 실행할 수도 있어요:
{
"scripts": {
"start": "ts-node src/scraper.ts",
"build": "tsc"
}
}
이렇게 하면 npm start
로 스크립트를 실행하고, npm run build
로 타입스크립트 코드를 자바스크립트로 컴파일할 수 있어요. 완전 꿀조합! 🍯
💡 개발 팁: VSCode를 사용한다면 타입스크립트와 Puppeteer의 조합이 더욱 강력해져요. 자동 완성과 타입 힌트가 개발 속도를 엄청나게 높여준답니다! 재능넷에서 VSCode 사용법을 배우는 것도 좋은 선택이 될 수 있어요.
4. 기본적인 웹 스크래핑 예제 🔍
이론은 충분히 배웠으니 이제 실제로 코드를 작성해볼까요? 먼저 가장 기본적인 웹 스크래핑 예제부터 시작해볼게요. 웹페이지에 접속해서 제목을 가져오는 간단한 예제예요! 😊
// src/basic-scraper.ts
import puppeteer from 'puppeteer';
async function scrapeWebsite() {
// 브라우저 실행
const browser = await puppeteer.launch({
headless: 'new' // 2023년부터 'new' 헤드리스 모드 사용
});
try {
// 새 페이지 열기
const page = await browser.newPage();
// 웹사이트로 이동
console.log('페이지 로딩 중...');
await page.goto('https://news.ycombinator.com/', {
waitUntil: 'networkidle2' // 네트워크 요청이 2개 이하로 떨어질 때까지 대기
});
// 페이지 제목 가져오기
const title = await page.title();
console.log(`페이지 제목: ${title}`);
// 뉴스 제목들 스크래핑
const headlines = await page.$$eval('.titleline > a', (links) =>
links.map(link => ({
title: link.textContent?.trim() || '',
url: link.getAttribute('href') || ''
}))
);
console.log('=== 오늘의 해커 뉴스 헤드라인 ===');
headlines.slice(0, 5).forEach((item, i) => {
console.log(`${i + 1}. ${item.title}`);
console.log(` 링크: ${item.url}`);
});
} catch (error) {
console.error('스크래핑 중 오류 발생:', error);
} finally {
// 브라우저 닫기
await browser.close();
console.log('브라우저 세션 종료');
}
}
// 함수 실행
scrapeWebsite()
.then(() => console.log('스크래핑 완료!'))
.catch(console.error);
이 코드를 실행하면 Hacker News의 최신 헤드라인 5개를 콘솔에 출력해요. 어때요? 생각보다 간단하죠? ㅎㅎ
코드 설명
puppeteer.launch()
: 크로미움 브라우저를 실행해요.headless: 'new'
는 2023년부터 도입된 새로운 헤드리스 모드예요.browser.newPage()
: 새 브라우저 탭을 열어요.page.goto()
: 지정된 URL로 이동해요.waitUntil
옵션으로 페이지 로딩 완료 조건을 설정할 수 있어요.page.title()
: 페이지의 제목을 가져와요.page.$$eval()
: CSS 선택자로 여러 요소를 선택하고, 그 요소들에 대해 함수를 실행해요. 여기서는 뉴스 제목 링크들을 선택하고 제목과 URL을 추출했어요.browser.close()
: 브라우저를 종료해요. 리소스 누수를 방지하기 위해 항상 닫아주는 게 좋아요!
스크린샷 및 PDF 생성하기
Puppeteer는 웹페이지의 스크린샷을 찍거나 PDF로 저장하는 기능도 제공해요. 이런 기능은 보고서 자동화나 웹페이지 모니터링에 정말 유용하답니다! 👀
// src/screenshot-pdf.ts
import puppeteer from 'puppeteer';
import path from 'path';
async function captureWebpage() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 뷰포트 설정 (반응형 테스트에 유용)
await page.setViewport({ width: 1280, height: 800 });
await page.goto('https://www.jaenung.net', {
waitUntil: 'networkidle2'
});
// 스크린샷 찍기
const screenshotPath = path.join(__dirname, '../screenshots', `jaenung_${Date.now()}.png`);
await page.screenshot({
path: screenshotPath,
fullPage: true // 전체 페이지 캡처
});
console.log(`스크린샷 저장됨: ${screenshotPath}`);
// PDF로 저장하기
const pdfPath = path.join(__dirname, '../pdfs', `jaenung_${Date.now()}.pdf`);
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true
});
console.log(`PDF 저장됨: ${pdfPath}`);
await browser.close();
}
captureWebpage().catch(console.error);
이 코드는 재능넷 웹사이트의 스크린샷과 PDF를 생성해요. 근데 PDF 생성은 헤드리스 모드에서만 작동한다는 점 참고하세요! 🧐
폼 작성 및 제출하기
웹 스크래핑에서 자주 필요한 작업 중 하나가 로그인이나 검색 폼 제출이에요. Puppeteer로 이런 작업도 쉽게 자동화할 수 있어요!
// src/form-submit.ts
import puppeteer from 'puppeteer';
async function searchAndScrape(searchTerm: string) {
const browser = await puppeteer.launch({
headless: false, // 브라우저 UI 표시 (작동 확인용)
slowMo: 100 // 각 작업 사이에 100ms 지연 (작동 확인용)
});
const page = await browser.newPage();
await page.goto('https://www.google.com');
// 쿠키 동의 대화상자 처리 (지역에 따라 다를 수 있음)
try {
const acceptButton = await page.$('button[id="L2AGLb"]');
if (acceptButton) {
await acceptButton.click();
}
} catch (error) {
console.log('쿠키 동의 버튼 없음, 계속 진행');
}
// 검색어 입력
await page.type('input[name="q"]', searchTerm);
// 폼 제출 (Enter 키 누르기)
await page.keyboard.press('Enter');
// 검색 결과 로딩 대기
await page.waitForSelector('#search');
// 검색 결과 추출
const searchResults = await page.$$eval('#search .g', (results) =>
results.slice(0, 5).map(result => {
const titleElement = result.querySelector('h3');
const linkElement = result.querySelector('a');
const snippetElement = result.querySelector('.VwiC3b');
return {
title: titleElement ? titleElement.textContent : '',
link: linkElement ? linkElement.getAttribute('href') : '',
snippet: snippetElement ? snippetElement.textContent : ''
};
})
);
console.log(`"${searchTerm}" 검색 결과:`);
searchResults.forEach((result, i) => {
console.log(`\n${i + 1}. ${result.title}`);
console.log(`링크: ${result.link}`);
console.log(`미리보기: ${result.snippet}`);
});
await browser.close();
}
searchAndScrape('재능넷 프리랜서').catch(console.error);
이 예제는 구글에서 "재능넷 프리랜서"를 검색하고 상위 5개 결과를 추출해요. headless: false와 slowMo 옵션을 사용하면 브라우저가 실제로 어떻게 동작하는지 볼 수 있어서 디버깅에 정말 유용해요! 🔍
⚠️ 주의사항: 구글이나 다른 검색 엔진에서 자동화된 검색을 너무 많이 하면 IP가 일시적으로 차단될 수 있어요. 항상 적절한 간격을 두고 요청을 보내는 것이 좋습니다!
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개