본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 19. 10:14

혼돈을 길들이기: LLM을 활용한 지저분한 HTML 파싱

요약

다양한 웹사이트의 복잡하고 가변적인 HTML 구조를 파싱하기 위해 LLM을 활용하는 실전적인 방법을 소개합니다. 기존 셀렉터 방식의 한계를 극복하고, 비용과 지연 시간 등 트레이드오프를 고려한 효율적인 데이터 추출 전략을 다룹니다.

핵심 포인트

  • 가변적인 HTML 레이아웃에 유연하게 대응 가능
  • 단일 스키마로 여러 사이트의 데이터 추출 통합
  • GPT-4o-mini를 활용한 비용 효율적 구현
  • 지연 시간 및 환각 현상에 대한 대응 방안 필요
  • 토큰 절약을 위한 HTML 트리밍 기술 적용

지난달, 저는 한계에 부딪혔습니다. 취미용 전자제품 가격 비교 도구를 만들고 있었는데, 약 30개의 서로 다른 판매자 사이트에서 제품명, 가격, 재고 상태를 가져와야 했습니다. 쉬워 보였나요? 그냥 스크래핑(Scrape)하면 되니까요.

틀렸습니다.

모든 사이트가 고유한 레이아웃을 가지고 있었습니다. 어떤 곳은 테이블(Table)을 사용했고, 다른 곳은 product-detail-block__3f2a와 같은 클래스 이름을 가진 중첩된 div를 사용했습니다. 한 사이트는 디자인 A/B 테스트를 진행하는지 요청할 때마다 문자 그대로 다른 HTML 구조를 반환했습니다. 저의 BeautifulSoup 셀렉터(Selector) 체인은 스파게티처럼 꼬여버렸고, 사이트가 업데이트될 때마다 스크립트가 깨졌습니다. 데이터를 분석하는 시간보다 스크래퍼(Scraper)를 수정하는 데 더 많은 시간을 보냈습니다.

먼저 뻔한 막다른 길들을 시도해 보았습니다.

효과가 없었던 것들

  • 원시 HTML에 정규 표현식 (Regex) 적용 – 고통을 즐기고 자신을 미워하는 사람이 아니라면 불가능합니다.
  • CSS 셀렉터 (CSS selectors) – 개발자가 클래스 이름을 바꾸는 순간 바로 깨집니다.
  • 헤드리스 브라우저 자동화 (Headless browser automation) – Selenium과 Playwright가 동적 콘텐츠 문제를 해결해주었지만, 속도가 느리고 리소스를 많이 사용하며 여전히 셀렉터를 업데이트해야 했습니다.
  • 수동 어노테이션 (Manual annotation) – 모델을 학습시킬 수는 있겠지만, 이는 수백 개의 페이지를 라벨링해야 함을 의미합니다. 사양하겠습니다.

저에게는 패턴을 맹목적으로 매칭하는 것이 아니라, HTML을 이해할 수 있는 무언가가 필요했습니다.

또한 동일한 페이지에 대해 API를 반복 호출하는 것을 방지하기 위해 간단한 캐시 계층 (cache layer)을 구축했습니다.

결과는... 놀라울 정도로 좋았습니다. 10개 사이트 중 8개에서 첫 번째 추출이 완벽했습니다. 나머지 2개에 대해서는 스키마 (schema)를 미세 조정하거나 프롬프트 (prompt)에 예시를 추가해야 했습니다.

강점

  • 끊임없이 변하는 레이아웃 (layouts) – LLM이 적응하므로, 셀렉터 (selectors)를 다시 작성할 필요가 없습니다.
  • 하나의 스키마로 여러 사이트 대응Product를 한 번 정의하여 30개 모든 상점에서 사용했습니다.
  • 기괴한 구조 처리 – 어떤 사이트들은 가격이 <table> 안의 <div> 안의 <span> 안에 들어있었습니다. LLM은 이를 스스로 파악해냈습니다.

불쾌한 트레이드오프 (Trade-offs)

이것이 만능 해결책 (silver bullet)인 척하지는 맙시다.

비용 (Cost): gpt-4o-mini를 사용하면 추출당 아주 적은 비용이 듭니다. 제품 1,000개당 약 0.50달러 정도이며, 이는 제 사이드 프로젝트에는 수용 가능한 수준이지만, 대규모 실시간 스크래핑 (scraping)에는 적합하지 않습니다.

지연 시간 (Latency): 페이지당 2~5초가 소요됩니다. 속도가 필요하다면 전통적인 셀렉터 (selectors) 방식이 승리합니다.

환각 (Hallucination): 때때로 LLM이 가격을 지어내기도 합니다. 저는 출력값이 타당한지 확인하는 검증 단계 (예: 가격이 \d+\.\d{2}와 일치하는지 확인)를 추가했습니다.

HTML 크기 제한: 제품 영역을 포함하는 처음 5,000자까지 HTML을 다듬습니다(trim). 페이지 전체(약 10만 토큰)를 저렴한 모델에 통째로 던질 수는 없습니다.

프롬프트 엔지니어링 (Prompt engineering)의 취약성: 프롬프트 문구의 작은 변화가 추출을 망가뜨릴 수 있습니다. 결국 저는 프롬프트 버전 관리 시스템을 구축하게 되었습니다.

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

  • 정적이고 구조가 잘 잡힌 페이지 – BeautifulSoup을 사용하세요. 더 빠르고 무료입니다.
  • 극도로 높은 볼륨 (수백만 페이지) – API 비용이 부담될 것입니다.
  • HTML이 방대한 페이지 (전체 문서 사이트와 같은 경우) – 공격적으로 다듬거나(trim) 먼저 헤드리스 렌더러 (headless renderer)를 사용하세요.
  • 픽셀 단위의 정밀도가 필요한 데이터 (예: 렌더링된 페이지의 정확한 통화 기호) – LLM은 모호합니다 (fuzzy).

두 세계를 혼합하는 방법

이제 제 파이프라인 (pipeline)은 먼저 저렴한 패턴 매칭 (pattern matching)을 시도합니다. 정규 표현식 (regex)이나 BeautifulSoup이 실패하거나 (None을 반환하면), LLM으로 넘어갑니다 (fallback). 그렇게 함으로써 비용은 낮게 유지하면서도 안전망을 확보할 수 있습니다.

예시:

from bs4 import BeautifulSoup

def fallback_extract(soup: BeautifulSoup) -> dict | None:
...

이 하이브리드 접근 방식(hybrid approach)은 나의 주말 악몽을 유지보수 가능한 스크립트로 바꾸어 놓았습니다. 개념적으로 유사한 방식(임베딩 (embeddings) + 구조화된 추출 (structured extraction)을 위한 LLM)을 사용하는 Interwest AI라는 도구도 발견했지만, 캐싱 (caching)과 폴백 (fallbacks)에 대한 세밀한 제어가 필요했기에 저는 저만의 파이프라인 (pipeline)을 고수했습니다.

배운 점 (Lessons Learned)

  1. 항상 HTML 입력을 다듬으세요 (trim) – 당신은 우아함이 아니라 토큰 (tokens)에 비용을 지불하고 있습니다.
  2. 출력을 검증하세요 (Validate) – 간단한 정규 표현식 (regex)이나 타입 체크 (type check)는 환각 (hallucination)된 데이터로부터 당신을 구해줍니다.
  3. 프롬프트 (prompts)의 버전을 관리하세요 – 저는 프롬프트를 Git의 파일로 저장합니다. 단어 하나를 바꾸는 것만으로도 모든 것이 망가질 수 있기 때문입니다.
  4. API 비용을 모니터링하세요 – 저는 OpenAI 대시보드에서 일일 예산 알림을 설정해 두었습니다.

다음에 다시 한다면 다르게 할 점

저는 추출을 위해 로컬 소형 언어 모델 (small language model, 예: Llama 3.1 8B)로 시작할 것입니다. 설정 이후에는 무료이기 때문입니다 (정확도가 약간 떨어지더라도 말이죠). 또한, HTML을 더 공격적으로 전처리 (pre-process)할 것입니다. 노이즈를 줄이기 위해 <script>, <style>, 그리고 인라인 CSS (inline CSS)를 제거할 것입니다.

여러분의 차례

이 접근 방식은 스크래핑 (scraping)에 대해 생각하는 방식을 바꾸어 놓았습니다. DOM과 싸우는 대신, 이제 저는 기계에게 읽는 법을 가르치고 있습니다. 완벽하지는 않지만, 90% 정도는 해결해 줍니다.

데이터 추출을 위해 LLM을 사용해 보셨나요? 여러분의 설정 (setup)은 어떤 모습인가요? 여러분은 이 혼돈을 어떻게 다루는지 정말 듣고 싶습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0