본문 바로가기
AI 자동화

Playwright 크롤링 파이프라인 실전 코드

by H . Sol 2026. 4. 21.

이론은 끝났다. 이제 실제로 만든다.


전체 구조

수집 → 필터링 → 저장 → 알림 → 반복

이 5단계를 코드로 구현한다.


1단계: 기본 수집 코드

가격 모니터링 예시

const { chromium } = require('playwright');

async function checkPrice() {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

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

  // 가격 추출
  const price = await page.locator('.price').textContent();
  const priceNumber = parseInt(price.replace(/[^0-9]/g, ''));

  await browser.close();

  return priceNumber;
}

핵심:

  • headless: true → 화면 안 띄우고 백그라운드 실행
  • .locator() → 자동 대기 포함
  • 브라우저 닫기 필수

2단계: 필터링 로직

const TARGET_PRICE = 50000;

async function monitorPrice() {
  const currentPrice = await checkPrice();

  if (currentPrice < TARGET_PRICE) {
    console.log(`🚨 가격 하락! ${currentPrice}원`);
    sendAlert(currentPrice);
  } else {
    console.log(`현재 가격: ${currentPrice}원 (대기 중)`);
  }
}

3단계: 데이터 저장

JSON 파일 저장

const fs = require('fs');

function saveData(price) {
  const data = {
    timestamp: new Date().toISOString(),
    price: price,
  };

  // 기존 데이터 읽기
  let history = [];
  if (fs.existsSync('price_history.json')) {
    history = JSON.parse(fs.readFileSync('price_history.json'));
  }

  // 새 데이터 추가
  history.push(data);

  // 저장
  fs.writeFileSync('price_history.json', JSON.stringify(history, null, 2));
}

SQLite 저장 (추천)

const sqlite3 = require('sqlite3');
const db = new sqlite3.Database('prices.db');

// 테이블 생성
db.run(`
  CREATE TABLE IF NOT EXISTS prices (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    product_id TEXT,
    price INTEGER,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// 데이터 삽입
function saveToDb(productId, price) {
  db.run(
    'INSERT INTO prices (product_id, price) VALUES (?, ?)',
    [productId, price]
  );
}

4단계: 알림 전송

텔레그램 봇

const axios = require('axios');

const TELEGRAM_BOT_TOKEN = 'your_bot_token';
const CHAT_ID = 'your_chat_id';

async function sendTelegramAlert(message) {
  const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;

  await axios.post(url, {
    chat_id: CHAT_ID,
    text: message,
    parse_mode: 'Markdown'
  });
}

// 사용
await sendTelegramAlert(`🚨 가격 하락!\n현재: ${price}원`);

이메일 (Gmail)

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: 'your_email@gmail.com',
    pass: 'your_app_password'
  }
});

async function sendEmail(price) {
  await transporter.sendMail({
    from: 'your_email@gmail.com',
    to: 'customer@example.com',
    subject: '🚨 목표 가격 도달!',
    html: `<h2>현재 가격: ${price}원</h2><a href="구매링크">바로 구매하기</a>`
  });
}

5단계: 스케줄링 (반복 실행)

Node.js 기본

setInterval(async () => {
  await monitorPrice();
}, 1000 * 60 * 30); // 30분마다

node-cron (추천)

const cron = require('node-cron');

// 매일 오전 9시, 오후 3시, 오후 9시 실행
cron.schedule('0 9,15,21 * * *', async () => {
  console.log('가격 체크 시작...');
  await monitorPrice();
});

전체 통합 코드

const { chromium } = require('playwright');
const cron = require('node-cron');
const axios = require('axios');

const TARGET_PRICE = 50000;
const TELEGRAM_TOKEN = 'your_token';
const CHAT_ID = 'your_chat_id';

async function checkPrice() {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto('https://example.com/product/12345');
  const price = await page.locator('.price').textContent();
  const priceNumber = parseInt(price.replace(/[^0-9]/g, ''));

  await browser.close();
  return priceNumber;
}

async function sendAlert(price) {
  const url = `https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage`;
  await axios.post(url, {
    chat_id: CHAT_ID,
    text: `🚨 가격 하락!\n현재: ${price.toLocaleString()}원\n\n👉 [구매하기](링크)`
  });
}

async function monitor() {
  try {
    const price = await checkPrice();
    console.log(`현재 가격: ${price}원`);

    if (price < TARGET_PRICE) {
      await sendAlert(price);
    }
  } catch (error) {
    console.error('에러:', error);
  }
}

// 매 30분마다 실행
cron.schedule('*/30 * * * *', monitor);

console.log('모니터링 시작...');

배포 방법

1) 로컬 서버 24시간 가동

# pm2 설치
npm install -g pm2

# 실행
pm2 start monitor.js --name price-monitor

# 재부팅 시 자동 실행
pm2 startup
pm2 save

2) 클라우드 (AWS EC2)

# EC2 인스턴스 생성 (Ubuntu)
# Node.js 설치
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs

# Playwright 의존성 설치
npx playwright install-deps

# 코드 업로드 후 실행
pm2 start monitor.js

3) GitHub Actions (무료)

# .github/workflows/monitor.yml
name: Price Monitor

on:
  schedule:
    - cron: '0 */1 * * *'  # 매시간

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm install
      - run: npx playwright install chromium
      - run: node monitor.js

장점: 무료, 설정 간단
단점: 최대 6시간마다만 가능


비용 구조

로컬 서버

  • 전기세 정도
  • 초기 투자 거의 없음

AWS EC2 (t2.micro)

  • 프리티어: 1년 무료
  • 이후: 소규모 운영 가능

GitHub Actions

  • 퍼블릭 레포: 완전 무료
  • 프라이빗: 월 2000분 무료

성능 최적화

1) 컨텍스트 재사용

// ❌ 매번 브라우저 새로 띄움
async function wrong() {
  const browser = await chromium.launch();
  // ... 작업
  await browser.close();
}

// ✅ 브라우저 하나로 컨텍스트만 생성
const browser = await chromium.launch();

async function right() {
  const context = await browser.newContext();
  const page = await context.newPage();
  // ... 작업
  await context.close();
}

결과: 메모리 사용량 70% 감소

2) 병렬 처리

// 여러 상품 동시 체크
const products = ['12345', '67890', '11111'];

await Promise.all(
  products.map(id => checkPrice(id))
);

에러 처리

async function safeMonitor() {
  const MAX_RETRIES = 3;

  for (let i = 0; i < MAX_RETRIES; i++) {
    try {
      return await monitor();
    } catch (error) {
      console.error(`시도 ${i + 1} 실패:`, error);

      if (i === MAX_RETRIES - 1) {
        // 마지막 시도도 실패 시 알림
        await sendAlert('⚠️ 모니터링 시스템 오류 발생');
      }

      // 1분 대기 후 재시도
      await new Promise(r => setTimeout(r, 60000));
    }
  }
}

틀린 방식 vs 맞는 방식

❌ 틀린 방식

  • 매번 브라우저 새로 띄움
  • 에러 처리 없음
  • 수동으로 실행
  • 로그 없음

✅ 맞는 방식

  • 컨텍스트 재사용
  • 3회 재시도 + 알림
  • cron 자동 실행
  • 모든 동작 로깅

파이프라인을 만들었으면, 이제 돌리기만 하면 된다.

 

실제로 운영하면서 발견한 노하우는 AI 사업 실험실에서 계속 업데이트할 예정이다.