zone 표시명 데이터 파이프라인 해설 — 공공데이터에서 "서면 A-14"까지¶
요약
격자 이름("서면 A-14")은 ①배포 전 1회: 국가 상권 데이터를 사각형 40개로 다듬어 DB에 넣고 → ②런타임 매번: 화면(FE)이 그 사각형을 보고 뺄셈 두 번으로 이름을 읽는 2단 구조다. 같은 일을 두 번 하는 게 아니라, ①은 "서면이 어디까지인지"를, ②는 "이 칸이 서면의 몇 번째인지"를 만든다. 격자마다 이름을 저장하지 않는 이유: 사각형 1개(숫자 6개)가 그 안 수백 칸의 이름을 전부 결정하기 때문(초안 기준 42배 절약). 준비물은 PR #76(PRD·스펙 D-결정 10건·스크립트 3종)으로 완료, 2026-07-31 현재 후보 29건 = 공공데이터 17 + 수동 작도 12 validate PASS — 남은 건 팀 검수(통칭 목록 확정·눈검수)와 시딩뿐이다 (MSG-259).
이 노트로 답할 수 있는 질문¶
- "서면 A-14"라는 이름은 누가, 언제, 어떻게 만드나?
- 상권 범위 데이터는 어디서 왔고 사람이 뭘 하나?
- 왜 격자마다 이름을 DB에 저장하지 않나?
- 왜 서버가 아니라 화면(FE)이 이름을 계산하나?
- 배포할 때마다 이 작업을 반복해야 하나?
- 데이터가 잘못 들어가는 걸 어떻게 막나(검증)?
0. 배경 — 무엇이 문제였나¶
FillMap의 격자는 내부적으로 "39070_112223" 같은 기계용 번호로만 식별된다. 디자인은 격자에
"서면 A-14" 같은 사람용 이름을 요구한다. 그런데 "서면"은 행정동이 아니다(행정동으로는 부전동) —
"홍대"·"가로수길" 같은 통칭은 행정 구역 어디에도 없어서, "여기부터 여기까지가 서면"이라는 범위를
누군가 정해줘야만 한다. 그 범위를 zone(구역)이라 부른다.
1. 전체 그림 — 시간축으로 두 단계¶
핵심은 "처리"가 두 번 나오지만 완전히 다른 일이라는 것. 헷갈리면 이 그림 하나만 기억하면 된다.
━━━━━━ ① 배포 전, 딱 1번 (사람 + 스크립트) ━━━━━━━━━━━━━━━━━
국가 공공데이터 (전국 상권 1,227개의 경계 좌표)
│ 변환 스크립트 (자동)
▼
초안 233건 — "서면역7번 출구" 같은 잘게 쪼개진 사각형들
│ 팀 검수 1~2시간 (사람): 유명한 것만 고르고, 합치고, 이름 다듬기
▼
확정본 zones.json (~40건) → 검증기 PASS → 앱 기동 시 DB 적재
│
┌───┴──────────────────────────────────────┐
│ DB에 남는 것 = "이름 + 숫자 6개"짜리 행 ~40개 │
│ 서면: 세로 39056~39072, 가로 112220~112229 │
│ 홍대입구: 세로 41719~41740, 가로 110359~110375 │
└──────────────────────────────────────────┘
여기서 백엔드 일은 끝. "A-14"라는 글자는 아직 세상에 없다.
━━━━━━ ② 런타임, 사용자마다 매번 (화면이 계산) ━━━━━━━━━━━━━
앱 켜짐 → 위 40행을 한 번 내려받아 보관 (8KB, 세션당 1회)
도감 진입 → 내 격자 목록 수신 (번호만 옴 — 이름은 안 옴)
카드 그릴 때 → "이 번호… 어느 사각형 안이지? → 서면.
서면 안에서 몇 번째 칸이지? → 뺄셈 두 번 → A-14"
①은 "서면이 어디까지인지"를 만들고, ②는 "이 칸이 서면의 몇 번째인지"를 읽는다. ① 없이 ②는 못 돌고(서면이 어딘지 모름), ② 없이 ①은 이름을 못 만든다(사용자가 어느 칸을 보는지 모름). 겹치는 작업은 없다.
비유 — 영화관 좌석표¶
극장 짓기(1번): "3관은 A~Q열, 1~10번 좌석" ← ① 좌석표 제작 입장(매번): 티켓 번호를 좌석표에 대조해 "C열 4번입니다" ← ② 안내
좌석표를 만들 때 "C-4" 안내문을 좌석마다 미리 인쇄해 붙이지 않는다. 관객이 오면 표를 보고 읽으면 되니까. 마찬가지로 격자마다 "서면 C-4"를 저장하지 않는다.
2. ②의 실체 — 뺄셈 두 번¶
화면이 하는 "계산"은 이게 전부다. 서면 사각형과 내 격자를 겹쳐 그리면:
가로: 112220 112221 112222 112223 …
1 2 3 4 ← 열 번호 (서쪽부터)
세로 39072 A ┌──────┬──────┬──────┬──────┬───
세로 39071 B ├──────┼──────┼──────┼──────┼───
세로 39070 C ├──────┼──────┼──────╔══════╗───
│ │ │ ║ 내 격자 ║
"서면" 사각형 (DB에서 받은 숫자 6개)
행 = 39072 − 39070 = 2 → A, B, C의 "C" (사각형 북쪽 끝이 A)
열 = 112223 − 112220 + 1 = 4
이름 = "서면 C-4" ← 이 문자열이 태어나는 유일한 순간
실측: 도감 카드 30장 이름을 전부 계산하는 데 7.1µs(0.0000071초) — 화면이 카드 한 장 그리는 비용의 1/100도 안 된다. "화면에서 계산하면 느리지 않나"는 걱정할 수준이 아니다.
3. 왜 격자마다 이름을 저장하지 않나¶
"어떤 격자는 서면, 어떤 격자는 홍대"를 DB에 박아두는 방식(격자→zone 매핑 테이블)을 일부러 피했다. 세 가지 이유:
- 사각형 하나가 수백 칸을 결정한다 — 홍대 사각형 1건(숫자 6개)이 그 안 격자 374칸의 이름을 전부 만든다. 초안 233건 기준으로 격자별 저장 대비 42배 절약. "안에 있나?"의 판정이 부등호 비교 4번이라 저장해둘 이유가 없다.
- 수정이 한 줄로 끝난다 — "홍대입구역9번 출구"를 "홍대"로 바꾸거나 경계를 한 칸 넓혀도 사각형 1행만 고치면 된다. 격자별로 저장했다면 수백 행을 다시 써야(백필) 한다.
- 점진 배포가 된다 — zone이 0건이어도 앱은 행정동 이름("부전동")으로 정상 동작하고, 사각형 하나를 넣는 순간 그 안 격자만 "서면 A-14"로 바뀐다. 서버·화면 배포 없이 데이터만으로.
단, 같은 "격자의 이름"이라도 행정동(부전동)은 반대로 격자에 저장한다 (ADR 격자 행정동 라벨 grids.region_code) — 행정동 경계는 사각형이 아니라 울퉁불퉁한 폴리곤이어서 "안에 있나?" 판정이 비싸기 때문. 판정이 공짜면 저장 안 하고, 비싸면 한 번 판정해 저장한다가 일관된 원칙이다.
4. 왜 서버가 아니라 화면이 계산하나¶
서버가 모든 응답에 이름을 얹는 안을 기각한 이유 (ADR 격자 표시명 zone 이후 MSG-234 §D3 확정):
- 이름이 필요한 화면 중 지도 뷰포트가 최고 트래픽 경로다(팬할 때마다 폴링, 응답속도 SLO p95<300ms). 서버가 이름을 얹으면 응답이 화면당 +9~23KB씩 매번 커지고 그 SLO 예산을 갉아먹는다.
- 화면 계산이면 zone 데이터 8KB를 세션당 1번만 내려받고 끝. 이후 비용은 위 뺄셈뿐.
- 부작용: 계산 규칙이 FE·Android·iOS로 3벌 복제되어 어긋날(드리프트) 위험 → 언어 중립 기준표(계약 픽스처)로 방어하는 것이 MSG-259 스코프에 포함됨.
5. ①의 상세 — 사람이 며칠 그릴 뻔한 걸 검수 1~2시간으로¶
"서면이 어디까지인가"는 계산할 수 없는 사람의 지역 지식이라 원래 수작업 며칠이 필요했고, 그래서 티켓이 보류됐었다. 해법(2026-07-28 결정, cf-26181633): 국가(소상공인시장진흥공단)가 전국 상권 경계를 이미 그려서 무료 개방하고 있다 → 그걸 자동 변환하고 사람은 검수만 한다.
자동 변환 (스크립트)¶
상권 경계 좌표(위경도) → 우리 격자 눈금으로 반올림한 사각형. 소스 3종: 주요상권현황 CSV(1차) + 회식상권 SHP(2차, 1차에 없는 이름만) + 해변 점포 밀집 파생(광안리·해운대 — 두 데이터셋 모두에 없어서). 결과 = 초안 233건 (서울 176 · 부산 55 · 해변 2).
사람 검수 (규칙 4개 — 2026-07-30 확정)¶
초안은 그대로 못 쓴다. "서면역7번 출구"처럼 잘게 쪼개져 있고, 우체국·호텔 같은 비상권이 51건 섞여 있고, 사각형끼리 138곳 겹친다. 검수 규칙:
- 유명 통칭 목록을 먼저 정한다 (30~50개, 팀 임의 선정) — 비상권과 안 유명한 이웃 상권 (역삼·합정 등)은 안 뽑는 것으로 겹침을 원천 차단
- 같은 통칭은 같은 시/도 안에서만 합친다 — 조건이 없으면 서울시청+부산시청이 한 사각형으로 묶여 265km짜리가 된다 (실측으로 확인된 함정)
- 목록에 있는데 공공데이터에 없는 곳은 손으로 그린다 — 가로수길·연남·대학로 등 13곳. 공공데이터가 "상가 밀집" 기준이라 관광·거리형 상권이 빠져 있다
- 남는 겹침은 숫자 하나로 가른다 — 서면과 전포가 한 열 겹침 → 서면의 가로 끝값을 112230→112229로 한 칸 물리면 분리 끝. 정수 사각형 방식의 최대 장점
드라이런 결과 (2026-07-30)¶
규칙 1·2·4를 스크립트로 미리 적용해봤다(build-candidate.py):
- 후보 17건 생성, 검증기 PASS — 경계 조정은 정수 2개(서면·홍대입구 각 1)가 전부
- 팀 검수가 "233건에서 고르기"에서 "17건 확인 + 13곳 그리기"로 축소
- 부수 발견: 겹침 예상 3쌍 중 1쌍(강남↔압구정)은 분석 스크립트의 과매칭이 만든 허상이었음 —
머리로 세운 계획이 데이터에 닿으면 틀리고, 드라이런이 그걸 검수 전에 잡는다
6. 검증 — "어떻게 확인하냐"의 답¶
눈으로는 233×232 조합의 겹침을 못 본다. 3층으로 잡는다:
| 층 | 도구 | 잡는 것 |
|---|---|---|
| 시딩 전 | validate-zones.py |
크기 한계(세로 26칸=2.6km)·겹침·식별자 중복 — 위반 시 실패 코드, 통과 전 적재 금지 |
| 적재 시 | DB 제약(CHECK) | 크기·범위 오류를 DB가 최종 거부 |
| 눈 | geojson.io 지도 오버레이 | 위치가 실제 상권과 맞는지, 이름이 자연스러운지 (기계가 못 보는 것) |
미검수 초안을 넣으면 위반 138건으로 실패하고(정상 — 검수 전이니까), 검수 완료의 기계적 정의가 "위반 0건"이 된다.
7. 배포마다 반복하나? — 아니다¶
- 평소 배포: zone 관련 비용 0. 데이터는 DB에 있고, 적재기(시더)는 플래그 꺼짐이 기본
- 데이터 갱신(상권 추가·이름 수정, 드묾): zones.json 몇 줄 수정 → 검증기 → 적재 1회. 같은 파일을 몇 번 넣어도 결과가 같아서(멱등) 사고 위험이 없다
- 공공데이터 재수집: 지역 확장(전국) 때나. 연 1회면 충분
- 유일한 함정 — 삭제: 적재는 "넣기/고치기"만 하고 지우지 못한다. zones.json에서 지워도 DB에 남는다 → 삭제는 수동 처리(규모 40행이라 충분), 반복되면 동기화 로직 추가 검토
8. 현재 상태와 남은 일 (MSG-259)¶
- 기능 코드(테이블·API·적재기·테스트 23건): MSG-234로 구현 완료, develop 반영
- 시드 데이터: 0건 — 그래서 지금 모든 격자는 행정동 이름으로 표시됨 (의도된 폴백)
- 2026-07-30 PR #76 (6 files +714, 서버 코드 변경 0): PRD(
docs/prd/MSG-259-prd.md, FR 19건 — 파이프라인 개편의 첫 PRD 적용 사례) + 스펙(docs/spec/MSG-259.md, D-결정 10건 — 픽스처 JSON 언어중립화·시더 상시 on· zone 삭제 수동 처리 등) + 스크립트 3종. 검수 규칙 4개·priority 전원 0(보험)·검증 3층을 실측으로 확정 - 2026-07-31 갱신: 수동 작도 12곳(
zones-manual.json) 병합 완료 — 후보 29건 = 공공데이터 17 + 수동 12, validate-zones.py PASS. 공공데이터 출처·선택 근거 상세는 zone 상권 공공데이터 근거 - 남은 일: 팀 스크럼에서 유명 통칭 목록 확정(후보 23곳은 제안) → geojson.io 눈검수 → 이름 다듬기 →
확정본
zones.json커밋 → 시딩. 더불어 명명 규칙 기준표(FE·앱 드리프트 방어)와 용어사전 등재 - 요구사항 정본: BE 레포
docs/prd/MSG-259-prd.md