공공데이터 API로 주차장 정보 사이트 18,000페이지를 자동 생성한 이야기

” 공공데이터 API로 주차장 정보 사이트 18,000페이지를 자동 생성한 이야기 “

뉴스 리라이팅 블로그를 6년 넘게 운영하면서 얻은 결론은 하나였다. 콘텐츠를 “쓰는” 모델은 내 노동(또는 LLM 비용)에 비례해서만 성장한다는 것. 그래서 방향을 바꿨다. 콘텐츠를 쓰는 게 아니라, 데이터를 페이지로 조립하는 프로그래매틱 SEO 사이트를 만들기로 했다.

그 결과물이 Hazelfy다. 전국 공영·민영 주차장의 요금, 운영시간, 위치를 지역별로 정리한 사이트로, 현재 서울·경기 지역 약 7,000페이지가 배포되어 있다. 페이지를 한 장도 손으로 쓰지 않았다. 전부 공공데이터 API → 빌드 파이프라인 → 정적 페이지로 생성된다. 호스팅 비용은 0원이다.

이 글은 그 과정에서 겪은 기술적 의사결정과 삽질의 기록이다.

왜 하필 주차장인가

프로그래매틱 SEO의 성패는 코드보다 니치 선정에서 갈린다. 공공데이터포털(data.go.kr)을 뒤지면서 후보를 추렸다.

  • 전기차 충전소: 데이터는 좋은데 이미 포화. 전문 사이트와 앱이 다수 존재해서 탈락
  • 휴일·심야 약국: 검색 수요는 최대급이지만 공식 서비스(pharm114, E-GEN)가 존재하고, 당번제라 데이터가 틀리면 실제 피해가 발생하는 도메인. 정확성 리스크 때문에 보류
  • 주차장: 채택. 전국 통합 정보 사이트가 사실상 없고, 지자체별로 정보가 파편화되어 있으며, 요금 변경이 드물어 데이터가 정적이다. 기존 강자인 주차 앱들은 예약·결제에 집중하고 있어서 “검색해서 찾아보는 정보” 영역은 비어 있었다

정리하면 판단 기준은 세 가지였다. 데이터가 구조화되어 있는가, 변경 빈도가 낮은가, 검색 영역에 빈틈이 있는가.

데이터 검증: 만들기 전에 죽일 놈은 죽인다

행정안전부가 취합하는 전국주차장정보표준데이터를 썼다. 본격 개발 전에 데이터 품질부터 전수 검증했다. 여기서 GO/NO-GO를 정한다는 원칙이었다.

  • 전체 18,530건, 필드 35개(명칭, 공영/민영, 주차면수, 시간당 요금, 정기권, 운영시간, 좌표, 기준일 등)
  • 결측률: 이름 0%, 요금구분 0%, 기본요금 0%, 좌표 2%
  • 함정 하나: 도로명주소 결측이 57%였다. 순간 접을 뻔했는데, 지번주소(lnmadr) 필드가 100% 채워져 있어서 보완 가능 → GO

검증 단계에서 겪은 첫 삽질은 인증키였다. 공공데이터포털의 Encoding 키를 URLSearchParams에 넣으면 %%25이중 인코딩되어 인증이 깨진다. 키는 URL 문자열에 raw로 이어 붙여야 한다. 공공데이터 API를 써본 사람이라면 한 번쯤 만나는 함정일 텐데, 문서 어디에도 안 적혀 있다.

또 하나, 지자체별로 데이터 갱신 주기가 다르다. 그래서 모든 페이지에 해당 데이터의 기준일(referenceDate)을 노출하도록 템플릿에 강제했다. 낡은 정보를 최신인 척 보여주는 순간 이런 사이트는 신뢰를 잃는다.

아키텍처: 서버 없는 18,000페이지

스택은 단순하다.

  • Next.js 15 (App Router) + output: 'export' — 순수 정적 생성(SSG). 서버 없음
  • Cloudflare Pages — 무료, SSL·CDN 자동, 대역폭 무제한
  • 갱신 파이프라인 — API 수집 스크립트 → JSON → 빌드 → 배포. 주차장 데이터는 원본이 월~분기 단위로 갱신되므로 주 1회 리빌드면 충분하다

“데이터가 바뀌면 어떻게 반영하나”에 대한 답이 이 구조의 핵심이다. DB를 갱신하는 게 아니라 재빌드+재배포가 곧 갱신이다. 실시간성이 필요 없는 데이터라서 가능한 선택이고, 덕분에 서버 비용이 0원이 됐다. 정적 파일 + CDN이라 Core Web Vitals에서도 유리하다. 무료 티어가 SEO에 불리할 거라는 통념과 반대로, 이 구조에서는 오히려 이득이다.

Cloudflare Pages의 20,000파일 제한

첫 번째 구조적 제약. Cloudflare Pages 무료 플랜은 배포당 20,000파일까지만 받는다. Next.js는 페이지당 index.html + index.txt(RSC 페이로드) 2개를 만들기 때문에, 전국 전체를 빌드하면 약 44,000파일이 나온다. 초과 확정.

해결은 지역 게이팅이었다. 환경변수 BUILD_SIDO=seoul,gyeonggi로 빌드 대상 시도를 제한해서, 1차로 서울+경기만 약 14,000파일로 배포했다. 이게 제약 회피용 꼼수 같지만 사실 SEO 관점에서도 정석에 가깝다. 신생 도메인은 크롤 예산이 적어서 처음부터 수만 페이지를 던지면 오히려 색인이 망가진다. 지역을 순차 오픈하면서 서치콘솔 색인률을 보고 확장하는 쪽이 안전하다. 제약이 전략을 강제한 셈이다.

가장 재미있었던 문제: 좌표 → 행정동 역지오코딩

사람들은 “강남구 주차장”보다 “역삼동 주차장”으로 검색한다. 그래서 URL 계층에 동(洞)을 넣었다: /parking/{시도}/{시군구}/{동}/{주차장}/.

문제는 동 정보를 주소에서 추출해야 하는데, 서울·부산처럼 도로명주소만 있는 데이터는 동 추출이 실패해서 전체의 10.1%가 “기타” 버킷에 몰렸다. 하필 검색 가치가 제일 높은 대도시가 몽땅 기타행.

해결책은 좌표 기반 역지오코딩이었다. 외부 API를 쓰면 1.8만 건 호출 비용과 속도가 부담이라, 로컬에서 처리했다.

  1. 전국 행정동 경계 GeoJSON(17개 시도, 약 3,500개 동)을 확보
  2. 각 주차장 좌표에 대해 bbox 프리필터로 후보 동을 좁히고
  3. ray-casting 알고리즘으로 폴리곤 내부 판정

결과: 좌표→행정동 매칭 95.7%, “기타” 비율 10.1% → 1.2%. 덤으로 법정동이 아니라 행정동 기준이 됐는데, 이게 실제 검색 의도(“역삼1동”이 아니라 “역삼동”)와 자연스럽게 맞아떨어졌다. 외부 API 없이 순수 로컬 연산이라 빌드 파이프라인에 그대로 편입됐다.

전략 수정: 개별 페이지로는 못 이긴다

배포 전에 실제 검색 결과를 진단하다가 뼈아픈 사실을 확인했다. “○○주차장” 같은 지명형 키워드는 네이버 지도/플레이스가 최상단을 독식한다. 신생 사이트가 개별 주차장 페이지 18,000개로 정면승부하면 진다.

그래서 무게중심을 옮겼다. 지도 서비스가 구조적으로 답할 수 없는 질문형 키워드 — “○○구에서 제일 싼 주차장은?”, “무료 개방 주차장 목록”, “월정기권 저렴한 곳” — 이건 데이터 전체를 쥐고 있어야만 만들 수 있는 페이지다. 개별 주차장 페이지는 삭제하지 않되 내부링크와 상세정보를 받치는 하부구조로 강등하고, 시군구 단위 집계 페이지(최저가 TOP, 무료 전체, 월정기권 순위)를 주력으로 세웠다. 예를 들어 수원시 주차장 요금 비교 같은 허브 페이지가 그 결과물이다.

키워드 도구로 검색량도 검증했다. 예상과 달리 “주말 무료”, “경차 할인” 같은 조건 필터 축은 검색량이 거의 없어서 폐기했고, “역 근처”, “월주차”, “동 단위” 축이 압도적이었다. 만들고 싶은 페이지가 아니라 검색되는 페이지를 만들어야 한다는, 알지만 매번 다시 배우는 교훈.

현재 상태와 다음 계획

  • 서울+경기 약 7,000페이지 라이브, 서치콘솔 사이트맵 제출 완료. 이제 색인을 기다리는 단계다
  • 다음: 지하철역·랜드마크 “근처 주차장” 축 확장(좌표 하버사인 조인이라 기존 구조에 얹기만 하면 된다), 서치콘솔 색인률을 보며 지역 순차 확장
  • 색인률, 노출, 첫 트래픽이 잡히면 후속편으로 실측 데이터를 공유할 예정이다. 프로그래매틱 SEO의 진짜 검증은 지금부터다

만든 사이트는 여기서 볼 수 있다: Hazelfy — 전국 주차장 요금·위치 정보


이 시리즈는 공공데이터 기반 프로그래매틱 SEO 사이트를 만들며 겪는 과정을 기록합니다. 데이터 출처: 공공데이터포털(행정안전부).