YepBuddy - (5) 매일 아침 8시, 나 대신 프로틴 가격을 긁어오는 봇을 만들었다
2025-10-03
시작은 단순했다
매달 프로틴을 살 때마다 느꼈다. "지금 이 가격, 비싼 건가?" 매번 세일 시기를 확인하고 노션에 기록하는 게 너무 귀찮았다.
그래서 결심했다. "차라리 봇을 만들어서, 나 대신 자동으로 가격을 가져오자."
이렇게 YepBuddy의 자동화 시스템, '프로틴 가격 감시자' 프로젝트가 시작됐다.
과정
프로젝트 초기 설정
독립적인 크롤러 프로젝트를 구성했다.
protein-price-tracker/
├── crawlMyproteinPrice.js
├── package.json
├── .env ← (Supabase 키 저장)
└── .github/workflows/
└── myprotein_crawler.yml
package.json 설정
크롤러 실행 환경과 의존성만 최소로 넣었다.
{
"name": "protein-price-tracker",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@supabase/supabase-js": "^2.74.0",
"dotenv": "^17.2.3",
"puppeteer": "^24.23.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}
- puppeteer-extra + StealthPlugin: 봇 감지 시스템을 피하기 위한 스텔스 브라우저 세팅
Supabase 서비스 키를 .env에
1. Supabase 프로젝트 대시보드에서 project 뒤의 값을 SUPABASE_URL로 저장

2. 사이드바에서 Project Settings 탭 클릭

3. API Keys 탭 클릭

4. service_role 키를 .env에 추가
SUPABASE_URL=https://project뒤의값.supabase.co
SUPABASE_SERVICE_ROLE_KEY=여기에 service_role 복사
크롤러 로직
크롤링은 Puppeteer로, 데이터 저장은 Supabase로 진행한다.
스텔스 브라우저로 봇 탐지 우회
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import { createClient } from "@supabase/supabase-js";
import "dotenv/config";
puppeteer.use(StealthPlugin());
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
크롤링 대상 정의
const PRODUCTS = [
{ name: "임팩트 웨이프로틴", url: "https://...", protein_id: 26 },
{ name: "임팩트 아이솔레이트", url: "https://...", protein_id: 27 },
{ name: "클리어 웨이", url: "https://...", protein_id: 36 },
{ name: "크레아틴", url: "https://...", protein_id: 37 },
{ name: "베타알라닌", url: "https://...", protein_id: 38 },
];
쿠팡은 정책이 엄격하고 공식 API가 있어서, 별도로 파트너스 API를 활용할 예정이다.
가격 크롤링 함수
async function crawlPrice(page, url) {
await page.goto(url, { waitUntil: "networkidle2", timeout: 60000 });
await page.waitForSelector("span.price.font-semibold.my-auto.text-2xl");
const rawPrice = await page.$eval(
"span.price.font-semibold.my-auto.text-2xl",
(el) => el.textContent.trim()
);
const numericPrice = parseInt(rawPrice.replace(/[^\d]/g, ""), 10);
return numericPrice;
}
메인 실행 함수
async function main() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const today = new Date();
const todayStr = today.toISOString().split("T")[0];
const month = today.getMonth() + 1;
const date = today.getDate();
console.log(`오늘 날짜: ${todayStr}`);
// 월과 일이 같은 날만 insert (ex. 3월 3일, 4월 4일)
if (month !== date) {
console.log(`오늘(${month}월 ${date}일)은 월과 일이 같지 않아 저장하지 않음`);
await browser.close();
return;
}
const results = [];
for (const product of PRODUCTS) {
console.log(`${product.name} 크롤링 중...`);
const price = await crawlPrice(page, product.url);
if (!price) continue;
results.push({
protein_id: product.protein_id,
observed_date: todayStr,
price,
available: true,
});
console.log(`${product.name}: ₩${price.toLocaleString()}`);
}
await browser.close();
if (results.length > 0) {
const { error } = await supabase.from("protein_prices_daily").insert(results);
if (error) console.error("Supabase 저장 실패:", error.message);
else console.log("Supabase 저장 완료");
}
}
main();
- Puppeteer로 각 상품 페이지 접속 → 가격 요소 추출 → 숫자만 파싱
- 월과 일이 같은 날일 때만 Supabase
protein_prices_daily테이블에 저장 - MyProtein 세일 주기에 맞춰 자동 저장되도록 구성
GitHub Actions: 매일 오전 8시(KST 오후 5시) 자동화
{% raw %}
name: Myprotein Monthly Tracker
on:
schedule:
- cron: "0 8 * * *" # UTC 8시 = KST 17시
workflow_dispatch:
jobs:
crawl:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci --no-audit --no-fund
- name: Run crawler
run: node crawlMyproteinPrice.js
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
{% endraw %}
결과
- 매일 오후 5시(KST) 정각에 GitHub Actions가 자동 실행된다.
- MyProtein 사이트에 접속해 5개 제품 가격을 크롤링한다.
- 월·일이 같은 날(세일 가능일)에만 Supabase에 자동 저장한다.
- 전체 로깅과 에러는 GitHub Actions 로그로 확인한다.
결과적으로, "매달 세일 시기를 기다리며 수동으로 기록하던" 과정이 완전 자동화된 가격 DB로 바뀌었다.