본문 바로가기
카테고리 없음

[React] Day_74 데일리 정리

by 림졍 2025. 1. 3.
728x90
반응형

 

뿌엥엥 살려줘요..

 

오늘 처음 크롤링이란 것을 해본 림졍..

순조롭게(?) 성공 할 줄 알았으나.. 아쉽게도 프로젝트에 적용하기엔 한계점이 많아서 울었다고.. (엉엉)

그래서 작성해보는 맨땅에 헤딩마냥 도전해본 크롤링 도전기,

가시죠.

 

 

 

최종 프로젝트 기술적 의사결정 - Pyppeteer를 활용한 무신사 크롤링 진행

친구들아 미안해!!!

 

나는 이게 될 줄 알았어. 흐어엉...

 

 

상황은 이랬다.

담당 페이지인 게시글 작성 페이지에 착용한 상품에 대한 구매처를

직접 링크를 걸어 구매처까지 등록하는 것은 UX적으로 사용자에게 번거로움을 유발하는 사항이라

쇼핑몰 데이터를 가져와서 빠르게 연동까지 가능하도록 하는 크롤링에 대해 의견을 받았고,

이를 통해서.. 림졍은 크롤링을 진행하게 되는데..

 

Puppeteer를 이용한 데이터 크롤링

 

- Puppeteer란?

Puppeteer는 Chrome DevTools 프로토콜을 이용해 Chrome/Chromium을 제어할 수 있는 Node.js 라이브러리로,

기본적으로 헤드리스 모드(백그라운드에서 UI 없이 브라우저 실행)에서 동작함

 

- 터미널 설치법

npm / yarn (배포 환경에 제한 있을 시)

npm install puppeteer-core
yarn add puppeteer-core

 

없으면 그냥 아래의 명령어를 입력해주면 된다. (Chromium을 포함한 모든 실행 파일 존재 ver.) 일명 종합패키지.

npm install puppeteer
yarn install puppeteer

 

 

- 환경변수 설정

NEXT_PUBLIC_BLOG_URL=https://velog.io/@kimbangul/posts
NEXT_PUBLIC_CDN_LINK=/* chromium 파일을 올린 링크 입력 */
NEXT_LOCAL_CHROME_PATH=Chrome 실행 경로

 

 

어디한번 크롤링...을 진행해볼까?

 

2030이 가장 많이 사용하는 쇼핑 플랫폼인 무신사 초기 페이지을 크롤링하여

상품 정보(상품명, 이미지, 가격, 할인 등)를 가져와 보기로 했다.

환경변수를 사용하는 것이 물론 보안 상으로는 베리 굳이지만

이번 상황에선.. 정말 간단하게 실험 정도에서 그쳤기 때문에,

page.tsx에 바로 무신사 페이지 URL을 넣어 작업을 진행하였다.

 

- 페이지 부분 (src/app/page.tsx)

"use client";

import React, { useEffect, useState } from "react";
import axios from "axios";

const Home = () => {
  // 상품 데이터와 에러 상태 관리
  const [products, setProducts] = useState<any[]>([]);
  const [error, setError] = useState<string | null>(null);

  // 크롤링 데이터를 가져오는 비동기 함수
  const fetchCrawlData = async () => {
    try {
      const url = "https://www.musinsa.com/main/musinsa/recommend";
      const response = await axios.get(
        `/api/crawl?url=${encodeURIComponent(url)}`
      );
      setProducts(response.data.products);
    } catch (err) {
      setError("Error fetching data");
      console.error(err);
    }
  };

  // 컴포넌트가 마운트될 때 크롤링 데이터 가져오기
  useEffect(() => {
    fetchCrawlData();
  }, []);

  return (
    <div>
      <h1>무신사 추천 상품</h1>
      {error && <p>{error}</p>}
      {products.length > 0 ? (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)" }}>
          {products.map((product, index) => (
            <div key={index} style={{ margin: "20px", textAlign: "center" }}>
              <img
                src={product.image}
                alt={product.title}
                style={{ width: "200px", height: "auto" }}
              />
              <p>{product.title}</p>
              <p>{product.price}</p>
            </div>
          ))}
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
};

export default Home;

 

아, 추가적으로 코드를 살펴보다보면 페이지 내부에 보면 알수없는 이름을 가진 코드들이 보일 것이다.

이것은 해당 페이지에서 라이브러리가 자동생성된 이름이니,

나중에 크롤링을 위해서라면 해당 내용에 대한 코드를 가져와야 한다는 것 정도만 알아두자.

 

- 크롤링 api 라우트 파일 (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 }
    );
  }
}

 

- 결과

오, 내가 무신사 데이터를 가져오다니!!!!

 

 

 

 

 

 

마무리 - 과연, 크롤링을 썼을까요~ 안썼을까요?

 

ㅠㅠ

 

당연 정답은 No 🙅🏻‍♂️.

아쉽게도 한계점이 많아 사용하지 못했다.

 

- 전부 다 불러와지지 않는 이미지

우선 모든 페이지 내부에 있는 모든 의상의 데이터 중

이미지의 일부가가 불러와지지 않는 현상이 발생했다.

이유를 찾아보던 중.. 가장 가능성이 높아보였던 원인은 함수 실행 시간 제한으로

Vercel과 같은 서버리스 환경에서는 함수 실행 시간이 제한되어 있어,

페이지 로드나 스크롤, 이미지 로드 작업이 완전히 이루어지지 않을 가능성이 높다고 한다.

 

즉, 대상 페이지의 로딩 시간, 스크롤, 데이터 추출 등 여러 작업이 포함되기 때문에

해당 페이지에 대한 콘텐츠가 많아지면 많아질 수록 가져오는 시간이 길어지는 크롤링 작업의 특성과 맞물리게 되어

결국 방대한 데이터를 페이지에 담기는 어렵다는 판단이 되어, 결국 크롤링 작업은 시도로 끝나게 되었다고...

 

하지만 그 어렵다는 크롤링을 관련 내용들을 공부해보면서

원하는 데이터를 자동으로 수집할 수 있다는 점에 조금 매료되어

나중에 크롤링에 대한 공부를 해보고 싶어지는 욕심이 생겼달..까나 ㅎㅎ

 

여튼 팀원들에겐 미안했지만..

그래도 맨땅에 헤딩하면서 경험한 값진 경험이라고 생각하며 넘어가기로 했다고 ^-^)b

(아 몰라 애들도 그렇다고 해줬음. 암튼 그럼. 진짜임.)

오늘도 알차게 끝! 일단 오늘은 드르렁~

 

 

오늘의 KPT 회고

 

Keep : 드디어 크롤링에 발을 딛다.

Problem : 그리고 접혔다! (와장창!)

Try : 럭키비키 마인드 장착하기 🍀

728x90
반응형