본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 16. 11:09

50개의 웹사이트를 스크래핑하며 보낸 몇 주간의 시간 — 마침내 성공한 방법은 무엇인가

요약

50개의 서로 다른 이커머스 사이트에서 데이터를 스크래핑하며 겪은 시행착오와 해결 과정을 다룹니다. 전통적인 방식의 한계를 극복하기 위해 LLM을 활용한 언어 모델 기반 추출 방식을 도입하여 성공한 사례를 공유합니다.

핵심 포인트

  • BeautifulSoup과 Selenium 등 전통적 방식의 유지보수 어려움과 성능 한계
  • 동적 렌더링, 안티 봇, 무작위 HTML 구조 등 스크래핑의 실제 장애물
  • 명시적 규칙 작성 대신 LLM을 활용한 자연어 기반 데이터 추출의 효용성

몇 달 전, 저는 가격 비교 도구를 만들어야 했습니다. 데이터는 각각 고유한 레이아웃, 안티 봇 (anti-bot) 조치, 그리고 마치 취한 문어가 작성한 것 같은 HTML을 가진 50개의 서로 다른 이커머스 (e-commerce) 사이트에 흩어져 있었습니다. 주말 프로젝트로 시작했던 일은 3주간의 좌절과 밤샘 작업으로 이어졌고, 한때는 데이터를 수동으로 얻기 위해 제품을 직접 전부 구매하는 것까지 진지하게 고려했을 정도였습니다.

문제 (가설이 아닌 실제 문제)

저에게는 200개의 제품 목록이 있었습니다. 각 제품에 대해 현재 가격, 재고 여부, 배송 정보가 필요했습니다. 간단해 보이죠? 이전에도 작은 규모의 스크래핑 (scraping)은 해본 적이 있었습니다. 하지만 이번에는 모든 사이트가 달랐습니다:

  • 일부는 JavaScript (자바스크립트)를 통해 핵심 데이터를 로드했습니다 (동적 렌더링 (dynamic rendering)).
  • 다른 곳은 무작위 클래스 이름을 가진 HTML 테이블을 사용했습니다.
  • 몇몇은 "쿠키 수락"을 클릭하거나 스크롤을 해야만 콘텐츠를 제공했습니다.
  • 그리고 몇몇 사이트는 20번째 요청 이후 제 IP를 적극적으로 차단했습니다.

첫 번째 시도는 BeautifulSoup과 몇 가지 정규 표현식 (regex) 패턴을 사용했습니다. 5개 사이트에서는 작동했지만, 그 이후 모든 것이 망가졌습니다. 창피하지만 사실입니다.

시도했지만 실패했던 것들 (솔직한 막다른 길들)

1. requests + BeautifulSoup을 이용한 전통적인 스크래핑

직관적이지만 취약합니다. 사이트마다 맞춤형 파서 (parser)가 필요했습니다. 새로운 사이트를 추가한다는 것은 더 많은 CSS 선택자 (CSS selectors)를 작성하고 예외 상황을 처리해야 함을 의미했습니다. 그리고 JavaScript로 렌더링된 콘텐츠는 제 눈에 보이지 않았습니다.

2. Chrome headless 모드의 Selenium

이 방식은 JavaScript 문제를 해결해주었지만, 느렸고 (페이지당 브라우저를 실행해야 함) 리소스 소모가 컸습니다. 방문해야 할 페이지가 5,000개나 되었습니다. 제 노트북에서는 페이지당 약 5초가 걸렸습니다. 그 속도라면 일주일 내내 스크래핑을 해야 할 판이었습니다. 게다가 많은 사이트가 자동화 탐지 기술을 사용하여 CAPTCHA (캡차)를 띄웠습니다.

3. 커스텀 미들웨어 (middlewares)를 적용한 Scrapy

Scrapy는 대규모 스크래핑에 매우 훌륭하지만, 저는 실제로 데이터를 추출하는 시간보다 무작위 지연, 프록시 로테이션 (proxy rotation), 세션 처리 (session handling)를 위한 미들웨어를 작성하는 데 더 많은 시간을 보냈습니다. 그리고 사이트의 레이아웃이 바뀔 때마다 스파이더 (spider)를 수술하듯 수정해야 했습니다.

3주가 지난 후, 아마도 30개 정도의 사이트가 부분적으로 작동하고 있었습니다. 데이터는 일관성이 없었고, 결측치(missing values)로 가득 차 있었으며, 저는 503 Service Unavailable 오류 하나만 더 뜨면 정신적으로 무너질 것 같은 상태였습니다.

결국 성공한 방법 (도구가 아닌 기술)

저는 제가 **모든 사이트에 대해 명시적인 규칙을 작성(write explicit rules)**하려고 노력하고 있다는 사실을 깨달았습니다. 이는 확장성(scale)이 없는 접근 방식입니다. 만약 제가 원하는 것(예: "가격 찾기")을 설명하기만 하면, 기계가 그 경로를 찾아낼 수 있다면 어떨까요? 그때 저는 **언어 모델 기반 추출 (language-model-based extraction)**로 눈을 돌렸습니다.

아이디어는 이렇습니다. 셀렉터(selector)를 하드코딩하는 대신, 가공되지 않은 웹 콘텐츠(또는 정제된 버전)를 자연어 쿼리(natural language query)와 함께 LLM에 전달하는 것입니다. 그러면 LLM이 제가 필요로 하는 정확한 값을 반환합니다.

대략적인 파이프라인은 다음과 같습니다:

  1. 페이지 가져오기 (Fetch the page) – 렌더링된 HTML을 얻기 위해 Puppeteer와 같은 API나 헤드리스 브라우저 (headless browser)를 사용합니다.
  2. 정제 및 청킹 (Clean and chunk) – 스크립트와 스타일을 제거하고 Markdown으로 변환합니다 (토큰 수(token count)를 줄이기 위함).
  3. LLM에 쿼리하기 (Query the LLM) – 특정 필드를 추출하도록 요청합니다.

저는 이를 수행하는 작은 Python 함수를 만들었습니다:

import requests

HTML_TO_MD_URL = "https://r.jina.ai/http://"  # 무료 마크다운 변환 API
...

그런 다음 해당 마크다운(LLM의 컨텍스트(context)에 맞게 잘라낸 것)을 추출 엔드포인트(extraction endpoint)로 보냈습니다:

# 범용 LLM API 사용 (본인의 키 또는 로컬 모델로 교체하세요)
import openai

...

50개의 사이트에 대해, 저는 단 하나의 범용 함수만 작성하면 되었습니다. LLM이 모든 HTML 변형을 처리해 주었습니다.

제가 사용한 특정 서비스

프록시(proxy)를 관리하고 사이트 변경 사항을 추적하는 데 드는 시간을 아끼기 위해, 저는 결국 전용 AI 추출 API를 사용하기로 결정했습니다. 이 글의 주제는 아니지만, 궁금해하실 분들을 위해 엔드포인트는 다음과 같은 형태입니다:

# https://ai.interwestinfo.com/ - AI 웹 추출기
response = requests.post(
    "https://api.interwestinfo.com/extract",
...

이 방식은 JavaScript가 포함된 사이트를 포함하여 대부분의 사이트에서 즉시 작동했습니다. 하지만 저는 여전히 커스텀 로직을 위한 대비책(fallback)으로 범용 LLM 접근 방식을 유지하고 있습니다.

배운 점 / 트레이드오프 (trade-offs)

잘된 점:

  • 유지보수 감소 (Reduced maintenance) – 사이트가 디자인을 변경하더라도 코드를 다시 작성할 필요가 없습니다. LLM이 자동으로 적응합니다.
  • 동적 콘텐츠 처리 (Handles dynamic content) – 페이지가 (헤드리스 브라우저(headless browser)나 서비스를 통해) 렌더링되기만 한다면, LLM이 이를 파싱(parse)할 수 있습니다.
  • 자연어 질의 (Natural language queries) – "독일로 가는 배송비는 얼마인가요?"라고 물으면 직접적인 답변을 얻을 수 있습니다.

트레이드오프 (Trade-offs):

  • 비용 (Cost) – LLM API는 무료가 아닙니다. 수천 건의 추출 작업을 수행하면 비용이 누적됩니다. 민감하거나 대량의 데이터의 경우 로컬 모델(예: Llama 3) 사용을 고려하십시오.
  • 지연 시간 (Latency) – 단일 추출에 2~10초가 소요될 수 있습니다. 실시간 애플리케이션(real-time apps)에는 너무 느릴 수 있습니다.
  • 환각 (Hallucinations) – LLM이 실제 가격을 찾지 못하면 가상의 가격을 만들어낼 수 있습니다. 항상 신뢰도 점수(confidence score)나 대비책(fallback) 규칙을 통해 검증하십시오.
  • 토큰 제한 (Token limits) – 페이지가 길 경우 추출 전에 청킹(chunking)이나 요약(summarisation)이 필요합니다.

이 접근 방식을 사용하지 말아야 할 때

  • 구조가 잘 잡힌 단일 페이지(예: HTML 테이블)에서 수백만 행을 추출해야 하는 경우, 전통적인 CSS 선택자(CSS selectors)가 더 저렴하고 빠릅니다.
  • 환각 없이 100%의 정확도(예: 금융 데이터)가 필요한 경우, 규칙 기반 파서(rule-based parser)와 사람의 검토를 병행하는 것이 더 안전합니다.
  • 비공개로 유지되어야 하는 콘텐츠를 스크래핑하는 경우, 이를 제3자 LLM으로 전송하는 것은 데이터 유출 위험이 있습니다.

다음에 한다면 다르게 할 점

  1. LLM 접근 방식을 더 일찍 시작하기. 지속적인 업데이트가 필요한 취약한 파서(parsers)를 만드느라 몇 주를 허비했습니다.
  2. 민감도와 비용을 위해 로컬 모델 사용하기. API 호출당 비용을 지불하는 대신, 추출 작업에 맞춰 작은 모델을 미세 조정(fine-tune)하겠습니다.
  3. 정상성 검사(sanity-check) 레이어 추가하기. 추출 후, 잠재적인 환각을 식별하기 위해 간단한 규칙(예: "가격에 통화 기호가 포함되어 있는가?")을 실행하겠습니다.

핵심 통찰: 무엇을 원하는지 설명하는 것이 그것을 어떻게 찾을지 프로그래밍하는 것보다 수십 배 더 쉽습니다. LLM을 통해 우리는 명시적인 지침을 작성하는 것에서 의도(intentions)를 작성하는 것으로 전환할 수 있습니다.

궁금합니다 – 데이터 추출(extraction)을 위해 LLM을 사용해 보신 적이 있나요? 환각(hallucination) 문제와 비용 문제는 어떻게 해결하셨나요? 댓글로 알려주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0