공공기관 정책 25곳 크롤링하면서 배운 것
공공기관 정책 25곳을 매일 자동으로 수집하고 분류하는 파이프라인을 운영하면서 가장 자주 들었던 질문은 "차단되지 않나요?"였습니다. 답은 의외로 단순합니다 — 차단당할 짓을 하지 않으면 됩니다. 그러나 그 "짓을 하지 않는다"가 25개 사이트, 65개 게시판으로 늘어나면 단순한 윤리 선언만으로 풀리지 않습니다. 이 글은 apify-policies 케이스에서 우리가 내린 기술 결정의 회고입니다.
25개 사이트, 25개의 다른 HTML
수집 대상은 국가 API 10종, 국가 크롤러 4종, 서울시 크롤러 3종, 서울 25개 자치구의 ~65개 게시판, 그리고 네이버 블로그 27곳입니다. 합치면 100개 가까운 채널이고, 각 채널마다 HTML 구조가 다릅니다.
처음에는 cheerio + 정규식으로 일관 처리하려 했습니다. 한 달 못 갔습니다. 게시판 한 곳이 개편되면 셀렉터가 깨지고, 그러면 그 게시판의 모든 수집이 멈춥니다. 더 나쁜 건 조용히 멈추는 것입니다. 빈 결과가 정상으로 보이기 때문에 며칠 뒤에야 발견됩니다.
그래서 채널 특성에 따라 수집 방식을 분리했습니다.
| 채널 유형 | 도구 | 비고 |
|---|---|---|
| 국가 공공 API | fetch + Drizzle 정규화 | data.go.kr, 서울시 OpenAPI |
| 정형 HTML 게시판 | Crawlee HTTP crawler | bizinfo, K-Startup, 정부24 |
| JS 렌더링 사이트 | Crawlee Playwright (stealth) | 일부 자치구 |
| 네이버 블로그 | Apify Actor | 토큰 기반 인증 |
Apify를 선택한 이유
Puppeteer를 직접 운영했다면 인프라가 운영의 절반을 잡아먹었을 겁니다. 우리는 다음 세 가지 이유로 Apify를 골랐습니다.
- 차단 회피 — 자체 프록시 풀. 공공기관 사이트는 의도적으로 막진 않지만, 같은 IP에서 100건/분이 들어오면 자동 방어가 켜집니다. Apify의 분산 IP는 운영의 부담을 줄여줍니다.
- 스케줄링 + 알림 내장. cron + Sentry를 직접 붙이지 않아도 됩니다.
- Actor 단위로 코드 재사용. 네이버 블로그 27개를 한 Actor로 처리할 수 있습니다.
// Apify Actor 호출 예시
import { ApifyClient } from 'apify-client'
const client = new ApifyClient({ token: process.env.APIFY_TOKEN })
const run = await client
.actor('hgnet/naver-blog-collector')
.call({ blogIds: NAVER_BLOG_IDS, since: lastRunAt })
const dataset = await client.dataset(run.defaultDatasetId).listItems()직접 Puppeteer를 돌렸다면 위 코드 뒤에 IP 풀, 재시도 로직, 헤드리스 브라우저 헬스체크가 따라붙었을 겁니다.
Claude API로 분류 — Why not regex
수집된 정책은 그대로는 쓸 수 없습니다. "청년 창업 지원" "1인 사업자 임대료 지원" "여성 새일 프로그램" — 이걸 우리 시스템 카테고리(창업·고용·복지·주거 등)로 매핑해야 합니다.
처음엔 키워드 사전을 만들었습니다. "창업·스타트업·창업지원 → 창업". 80%까지는 됩니다. 그러나 "소상공인 디지털 전환 지원"이 창업인지 디지털인지 결정해야 할 때 키워드는 둘 다 잡힙니다.
Claude API는 맥락을 이해합니다. 제목과 본문을 함께 주면 카테고리를 한 번에 결정합니다. 비용은 한 건당 $0.001 미만이고, 정확도는 키워드 분류보다 12%p 높았습니다.
// 분류 호출 예시 (간소화)
const result = await anthropic.messages.create({
model: 'claude-3-5-sonnet-latest',
max_tokens: 200,
messages: [{
role: 'user',
content: classifyPrompt({ title: policy.title, body: policy.body }),
}],
})
const { category, tags } = parseClassification(result.content)차단 회피 = 윤리 + 기술
크롤링은 차단 회피가 아니라 차단되지 않을 행동의 누적입니다.
robots.txt우선 확인. Disallow가 명시된 경로는 수집하지 않습니다.- 분당 30회 이상의 요청을 보내지 않습니다 (
p-limit로 강제). - User-Agent에 회사명·연락처를 명시합니다. 차단 시 메일을 받을 채널을 만들어 놓습니다.
- 변경된 게시판이 있으면 알림을 받고, 다시 협의합니다.
결과: 25개 사이트, 매일 100건+, 1명도 안 걸림
운영 11개월 동안 차단 사례는 0건입니다. 이 숫자가 자랑이 아니라 표준 동작이라야 한다는 것이 핵심입니다. 크롤링 프로젝트에서 차단을 경험하고 있다면 회피 기술을 더 쓰는 게 아니라, 위 네 가지를 다시 점검하는 것을 권합니다.
자세한 케이스: apify-policies