LLM을 사용하여 일회성 코드 작성 없이 로컬에서 웹 스크래핑하기
요약
fitter는 LLM이 일회성 코드를 작성하는 대신 JSON/YAML 설정을 통해 로컬에서 웹 스크래핑을 수행하도록 돕는 Go 기반 엔진입니다. MCP 서버를 지원하여 Claude Code나 Claude Desktop과 같은 도구에서 즉시 실행 가능한 재사용 가능한 스크래핑 인프라를 구축할 수 있습니다.
핵심 포인트
- 웹 추출을 코드가 아닌 선언적 설정(config) 방식으로 처리
- MCP 서버 지원으로 Claude 등 AI 에이전트와 즉시 연동 가능
- LLM이 생성한 설정을 파일로 저장하여 크론 잡이나 서비스로 활용 가능
- HTTP, Headless Browser, 파일 등 다양한 데이터 소스 지원
AI 어시스턴트에게 "이 웹사이트에서 데이터를 가져와줘"라고 요청할 때마다 두 가지 중 하나가 발생합니다. 데이터를 환각(hallucinate)하거나, 다시는 실행하지 않을 일회성 Python 스크립트를 작성하는 것입니다. 두 가지 모두 낭비입니다. 모델은 이미 데이터가 어디에 있는지, 그리고 그것을 어떻게 선택해야 하는지 알고 있습니다. 부족한 점은 그 지식을 실행할 안전하고 재사용 가능한 방법입니다.
그것이 바로 fitter가 하는 일입니다. 이는 하나의 핵심 아이디어를 가진 작은 Go 엔진입니다: 웹 추출(web extraction)은 코드가 아니라 설정(config)이어야 한다는 것입니다. fitter 설정은 데이터가 어디에 있는지(HTTP 요청, headless browser, 정적 값, 파일)와 무엇을 추출할지(gjson 경로, CSS selector, XPath)를 선언합니다. 나머지는 엔진이 처리합니다.
그리고 설정은 일반적인 JSON/YAML 형식이므로, LLM이 이를 작성할 수 있습니다. Fitter는 MCP 서버를 제공하므로, Claude Code, Claude Desktop 또는 모든 MCP 클라이언트가 단 한 번의 대화 과정 내에서 설정을 작성하고, 검증하고, 사용자의 기기에서 실행할 수 있습니다.
60초 설정
releases page에서 바이너리를 가져오거나 (go build -o fitter_mcp ./cmd/mcp), 다음을 수행하세요:
claude mcp add fitter -s user -- /path/to/fitter_mcp
Claude Desktop 사용자: 동일한 release page에서 사용 중인 플랫폼에 맞는 .mcpb 번들을 다운로드하여 열기만 하면 됩니다. 설치의 전부입니다.
서버는 다섯 가지 도구를 노출합니다: fitter_run, fitter_run_file, fitter_run_url, fitter_validate_config, 그리고 fitter_config_reference입니다. 마지막 도구는 요약된 형식 참조를 반환하므로, 모델이 대화를 중단하지 않고도 스스로 설정 형식을 학습할 수 있습니다.
"최신 Hacker News 기사를 가져와줘"
정확히 그렇게 요청해 보세요. 백그라운드에서 모델은 다음과 같은 내용을 작성하고 fitter_run을 호출합니다:
{
"item": {
"connector_config": {
...
위에서 아래로 읽어 내려갑니다: ID 목록을 가져오고, 처음 10개를 선택한 다음, 각 요소에 대해 스토리 객체를 가져오기 위한 중첩된 요청({PL}은 현재 값)을 보냅니다. 이때 HN API에 대해 5개의 병렬 요청으로 속도 제한 (rate-limited)을 적용합니다. 결과는 깔끔한 JSON입니다:
[
{"title": "Fields Medals 2026", "by": "nill0", "score": 2, "url": "https://www.mathunion.org/..."},
{"title": "Writing by Hand is Good for your Brain", "by": "dwwoelfel", "score": 2, "url": "..."}
...
여기서 중요한 부분은 다음과 같습니다: 해당 설정 (config)은 사후에 생각난 것이 아니라 하나의 결과물 (artifact)입니다. 이를 파일로 저장하면 CLI 명령어가 되거나, 크론 잡 (cron job), 또는 서비스가 됩니다. 모델의 일회성 답변이 인프라 (infrastructure)가 된 것입니다.
하나의 설정, 네 개의 데이터 소스
날씨, HN, 세계 헤드라인, 그리고 비트코인 가격을 포함하는 저의 실제 모닝 브리핑은 네 개의 독립적인 브랜치를 가진 하나의 설정 (one config)입니다. 또한 이는 플레이스홀더 (placeholders)의 활용법도 보여줍니다:
"connector_config": {
"response_type": "json",
"url": "https://wttr.in/{{{FromInput=.}}}?format=j1"
...
{{{FromInput=.}}}은 실행 시점에 도구의 input 인자로부터 채워지므로, _"베를린에 대한 브리핑 실행"_과 _"파리에 대한 브리핑 실행"_은 동일한 설정을 재사용합니다. 환경 변수 (env vars), 캐시된 참조 (예: JWT를 한 번 가져온 뒤 모든 요청의 헤더에서 사용), 배열 인덱스 (array indices) 등을 위한 플레이스홀더도 존재합니다.
소스 결합: API가 없는 것은 스크래핑하고, 있는 것은 풍부하게 만들기
GitHub의 트렌딩 페이지에는 유명하게도 API가 없습니다. 하지만 그 페이지에 있는 리포지토리 (repos)들은 API를 가지고 있습니다. 따라서 다음과 같이 수행합니다: HTML을 스크래핑하여 리포지토리 슬러그 (repo slugs)를 가져온 다음, 각각을 실제 GitHub REST API로 확장(fan out)합니다:
{
"item": {
"connector_config": {
...
오늘의 실제 출력 결과:
[
{"repo": "block/buzz", "stars": 6214, "language": "Rust", "open_issues": 460,
"description": "A hive mind communication platform"},
...
배울 만한 세부 사항들:
- HTML과 JSON을 자유롭게 혼합합니다. 외부 커넥터(outer connector)는 CSS 선택기(
article.Box-row h2 a)를 사용하여 HTML을 파싱하며, 매칭된 각 요소는 JSON API 요청으로 확장(fan out)됩니다. 하나의 설정으로 두 세계를 다룹니다. html_attribute는 텍스트가 아닌 속성을 읽습니다. 앵커(anchor)의href는 이미/owner/repo형태이며, 이는https://api.github.com/repos{PL}에 정확히 필요한 값입니다. 조인 키(join key)가 페이지 자체에서 오기 때문에 동기화가 어긋날 염려가 없습니다.length_limit+host_request_limiter로 예의를 지킵니다. 상위 5개 리포지토리, 최대 2개의 동시 API 호출. 인증되지 않은 GitHub API는 시간당 60개의 요청을 허용합니다. 이를 존중하는 설정은 스케줄에 따라 실행할 수 있는 설정입니다.
이것이 현대적인 웹의 일반적인 패턴입니다. 흥미로운 *순위(ranking)*는 어떤 HTML 페이지에 존재하고, *사실(facts)*은 API에 존재하며, 그 둘 사이의 조인은 정확히 단 하나의 {PL}만큼 떨어져 있습니다.
조인 키가 전체 값이 아닌 필드인 경우
{PL}은 현재 값의 전체를 주입합니다. 이는 배열 항목이 스칼라(scalar, 예: 스토리 ID, 리포지토리 슬러그)일 때 완벽합니다. 하지만 종종 항목은 **객체(object)**이며 조인 키가 그 내부에 존재하는 경우가 많습니다. 이것이 바로 표현식 플레이스홀더(expression placeholders)가 필요한 이유입니다. 도서 검색 → OpenLibrary를 통한 저자 정보 보강(enrichment) 사례:
{
"item": {
"connector_config": {
...
input = dune으로 실행 시:
[
{"title": "Dune", "year": 1965,
"author": {"name": "Frank Herbert", "born": "8 October 1920", "died": "11 February 1986"}},
...
핵심 라인은 중첩된 URL입니다: {{{FromExp=fromJSON(fRes).author_key[0]}}}는 현재 항목(fRes)에 대해 평가되는 expr-lang 표현식입니다. 이를 파싱하여 첫 번째 저자 키를 가져온 뒤 URL에 끼워 넣습니다. expr-lang이 계산할 수 있는 모든 것은 요청의 일부가 될 수 있습니다: 필드를 선택하거나, 연결(concatenate)하거나, 오프셋(offset)을 더하거나, 조건에 따라 분기(branch)할 수 있습니다. 그리고 출력 형태를 주목하세요: 각 도서는 고유의 스칼라 필드를 유지하면서, 보강된 정보는 중첩된 author 객체로 들어갑니다. 즉, 설정의 구조가 곧 응답의 구조가 됩니다.
단순히 데이터를 반환하지 마세요 — 보고서를 작성하세요
때로는 결과물이 채팅창의 JSON이 아니라, _디스크 상의 파일_일 때가 있습니다. file_storage 생성 필드는 어떤 필드든 쓰기 작업으로 전환합니다. 예를 들어, 상위 5개 암호화폐 코인을 CSV로 바로 저장할 수 있습니다:
{
"item": {
"connector_config": {
...
도구 호출(tool call)은 기록된 경로를 반환하며, 디스크에는 다음과 같이 저장됩니다:
$ sort -n /tmp/fitter-report/coins.csv
1,Bitcoin,64778,-2.3
2,Ethereum,1881.01,-3.4
...
해당 content 템플릿에서 일어나고 있는 일은 다음과 같습니다:
- **순수
{{{json.path}}}플레이스홀더(placeholder)**는 현재 아이템에서 필드를 직접 가져옵니다 —{{{name}}},{{{current_price}}}등 — 단순 조회를 위해 별도의 표현식이 필요하지 않습니다. (템플릿이 아이템의 가공되지 않은 JSON을 볼 수 있도록 필드 타입을object로 지정하세요.) {HUMAN_INDEX}는 아이템의 위치를 기록합니다 (1부터 시작;{INDEX}는 0부터 시작하는 형제 요소입니다). Fitter는 배열 아이템을 병렬(concurrently)로 처리하므로, 추가(append) 작업은 완료 순서대로 이루어집니다. 순위(rank) 컬럼이sort -n을 통해 진실을 복구할 수 있게 해주는 핵심입니다. 이는 이 시스템이 진정으로 병렬로 작동한다는 좋은 증거입니다.- 다운로드를 수행하는 형제 필드 타입인
file이 있습니다. 이를 이미지/PDF URL로 지정하면 해당 필드의 값은 로컬 파일 경로가 됩니다. 갤러리를 스크래핑하고 사진을 그대로 보관할 수 있습니다.
스케줄 기반 서비스 모드와 결합하면, 이는 하나의 JSON 문서 내에서 완전히 정의된 매시간 실행되는 작은 ETL 파이프라인(fetch → shape → growing CSV에 추가)이 됩니다.
사이트에 API가 없는 경우
위의 예시들은 JSON API를 호출하지만, response_type은 HTML (CSS 선택자), xpath, 또는 XML로 설정할 수 있습니다. 또한 페이지에 JavaScript가 필요한 경우 커넥터(connector)를 실제 브라우저로 사용할 수 있습니다:
"connector_config": {
"response_type": "HTML",
"url": "https://example.com/spa-page",
...
Chromium, Playwright (stealth 모드 포함), 그리고 도커화(dockerized)된 브라우저들이 모두 내장되어 있습니다. 단일 정적 바이너리(static binary)로 제공되므로 npm install이 필요 없습니다.
로컬 우선(local-first) 방식이 중요한 이유
호스팅된 스크래핑 API들은 좋은 제품들이지만, 에이전트 워크플로우(agent workflows)의 경우 로컬 모델이 실질적인 이점을 가집니다:
-
감사 가능 (Auditable) — 에이전트가 정확히 무엇을 어떻게 가져왔는지 읽을 수 있습니다. 설정(config)의 차이점(diff)은 검토가 가능하지만, 어딘가 샌드박스(sandbox) 내에서 생성된 코드는 그렇지 않습니다.
-
구조적으로 재사용 가능 (Reusable by construction) — 동일한 설정을 MCP, CLI (
fitter_cli --path config.json), Go 라이브러리 호출, 또는 스케줄러와 알림 기능(Telegram, webhook, Redis, file)을 갖춘 fitter의 서비스 모드를 통해 실행할 수 있습니다. 모델이 한 번 작성하면, 유용함이 증명되었을 때 크론 잡(cron job)으로 승격시킬 수 있습니다.
팀을 위한 호스팅
최신 릴리스의 새로운 기능: MCP 서버가 스트리밍 가능한 HTTP(streamable HTTP)도 지원하므로, 팀 전체를 위해 하나의 fitter를 실행할 수 있습니다. 최소한의 docker-compose.yml 구성은 다음과 같습니다:
services:
fitter-mcp:
image: ghcr.io/pxyup/fitter-mcp:latest
...
export FITTER_MCP_AUTH_TOKEN=$(openssl rand -hex 16)
docker compose up -d
curl http://localhost:8080/healthz # -> ok
그 후 모든 /mcp 요청은 반드시 베어러 토큰(bearer token)을 포함해야 하며, 이미지에 내장된 헬스체크(healthcheck) 기능이 오케스트레이터(orchestrator)의 상태를 정확하게 유지해 줍니다. Claude Code에 엔드포인트를 등록하세요:
claude mcp add --transport http fitter http://your-host:8080/mcp \
--header "Authorization: Bearer $FITTER_MCP_AUTH_TOKEN"
이 이미지는 설계 단계부터 슬림하게 제작되었습니다 (바이너리 + CA 인증서, linux/amd64 + arm64 지원). API/HTML 스크래핑은 즉시 작동하지만, 브라우저로 렌더링되는 페이지는 Chromium 또는 Playwright가 설치된 호스트의 릴리스 바이너리(release binary)가 필요합니다.
클라이언트 측 취소(Client-side cancellation)는 진행 중인 HTTP 요청과 브라우저 세션까지 모두 전달되므로, 중단된 에이전트 실행이 백그라운드에서 계속 스크래핑을 수행하는 일이 발생하지 않습니다.
시도해 보기
- 리포지토리(Repo): github.com/PxyUp/fitter (MIT)
- MCP 레지스트리(registry):
io.github.PxyUp/fitter - 준비된 설정(Ready-made configs): examples/
Claude Code를 열고, 서버를 추가한 다음, 매일 아침 실제로 원하는 데이터를 요청해 보세요. 모델이 작성한 설정은 여러분의 소유가 됩니다 — 그것이 바로 이 도구의 핵심입니다.
질문이나 설정 기여를 환영합니다 — examples 폴더는 PR(Pull Request)을 받습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기