튜터님 피드백
- 룩북에 대한 게시글을 작성하면서 사용자가 직접 상품에 대한 구매처까지 등록하는 것은 번거로움을 유발할 수 있을 듯 합니다.
- 데이터가 필요하다면 크롤링을 시도해도 좋지만 기술적으로 구현이 어려울 수 있습니다.
- 오픈 api를 활용한 룩북 가상 시착 시스템 커뮤니티로서의 속성을 강화해서 유저 중심의 기능이 추가되면 좋을 것 같습니다.
Puppeteer란?
Puppeteer는 Chrome DevTools 프로토콜을 이용해 Chrome/Chromium을 제어할 수 있는 Node.js 라이브러리로, 기본적으로 헤드리스 모드(백그라운드에서 UI 없이 브라우저 실행)에서 동작한다.
npm install puppeteer-core
npm install @sparticuz/chromium-min
// 필요없으면 그냥 npm install puppeteer
NEXT_PUBLIC_BLOG_URL=https://velog.io/@kimbangul/posts
NEXT_PUBLIC_CDN_LINK=/* chromium 파일을 올린 링크 입력 */
NEXT_LOCAL_CHROME_PATH=Chrome 실행 경로
//src/app/api/crawl/route.ts
import { NextResponse } from "next/server";
import puppeteer from "puppeteer";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return NextResponse.json({ error: "URL is required" }, { status: 400 });
}
try {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
// User-Agent 설정 (크롤링 차단 우회)
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
);
// 페이지 이동 (타임아웃 60초)
await page.goto(url, { waitUntil: "networkidle2", timeout: 60000 });
// 페이지에서 상품 데이터 추출
const products = await page.evaluate(() => {
const productElements = document.querySelectorAll(
".sc-1xhqrcq-0.cMIGzr"
);
return Array.from(productElements).map((element) => {
const titleElement = element.querySelector(
".sc-1xhqrcq-8.izVYxY.gtm-select-item p"
) as HTMLElement;
const imageElement = element.querySelector(
".sc-1xhqrcq-3.dxvdoH"
) as HTMLImageElement;
const priceElement = element.querySelector(
".sc-1xhqrcq-10.kmYwkG.text-black"
) as HTMLElement;
const discountElement = element.querySelector(
".sc-1xhqrcq-10.kmYwkG.text-red"
) as HTMLElement;
return {
id: element.getAttribute("data-item-id") || "N/A",
title: titleElement?.innerText || "No Title",
image: imageElement?.getAttribute("src") || "",
price: priceElement?.innerText || "N/A",
discount: discountElement?.innerText || "0%",
brand: element.getAttribute("data-item-brand") || "No Brand",
originalPrice: element.getAttribute("data-original-price") || "N/A",
};
});
});
await browser.close();
return NextResponse.json({ products });
} catch (error: any) {
console.error("Puppeteer Error:", error);
return NextResponse.json(
{ error: error.message || "Internal Server Error" },
{ status: 500 }
);
}
}