콘텐츠로 이동

H3 격자 체계와 해상도

11/10(화)

한 줄 요약: H3는 지구 전체를 미리 육각형 칸으로 나눠 두고, 칸마다 전 세계 공통 번호를 붙인 격자 체계다. 같은 해상도의 칸은 겹치지 않는다. 작은 칸은 번호만으로 큰 칸(부모)에 묶을 수 있다. 서로 다른 데이터를 "같은 장소"로 맞출 때 쓴다.

📍 만난 상황

  • 2026-11-10 — 대시보드 지도를 다듬으면서 물었다. 3번 물음 → 그날 일지
  • 13:45 지도의 칸이 왜 흩어져 있는지 물었다. "행이 있는 칸만 칠하고, 한 행은 점 하나라 칸 하나에만 찍힌다"는 답을 들었다. 그래서 "r9로 정규화했는데 r7처럼 크게 그릴 수 있나?"를 물었다. → h3ToParent로 된다.
  • 16:44 셀 정보가 어디서 왔는지 묻던 중이었다. "H3가 세계 표준이야? 육각형이 겹쳐서 데이터가 겹치지 않아?"라고 걱정했다.
  • 16:46 이어서 "셀 번호가 우리가 정한 게 아니라 세계 공통이야? 지구 반대편에서 그려도 같은 곳을 덮어?"라고 물었다. → 맞다. 번호만 풀어도 광화문 근처가 나왔다.

🧩 쉽게 말하면

전 세계에 똑같이 인쇄된 벌집 모양 모눈종이가 있다고 생각하자. 칸마다 번호가 이미 찍혀 있다.

누구나 같은 종이를 쓴다. 그래서 "8930e1d8c8fffff 칸"이라고 하면 서울에서도 브라질에서도 같은 땅을 가리킨다.

종이는 촘촘함이 다른 16장이 있다. 큰 칸 종이와 작은 칸 종이다.

이게 없으면 데이터 A와 B가 같은 장소인지 알려고 점마다 거리를 계산해야 한다. 행정구역은 나라마다 다르고 자주 바뀐다.

📖 제대로 알기

H3란

  • Uber가 만들어 오픈소스로 공개한 공간 인덱스(spatial index: 위치를 빨리 찾고 묶도록 붙이는 번호 체계)다.
  • ISO 같은 공식 국제표준은 아니다. 하지만 지도 분석에서 사실상 표준처럼 쓴다.
  • ClickHouse, DuckDB, BigQuery, Python(h3-py), JavaScript(h3-js)가 모두 같은 격자를 쓴다.

해상도

  • 해상도(resolution)는 0부터 15까지 16단계다. 숫자가 클수록 칸이 작다.
  • 해상도 0에는 칸이 122개 있다. 한 단계 내려갈 때마다 칸 수가 약 7배가 된다.
  • 이 "약 7배"를 아퍼처 7(aperture: 한 단계에 몇 개로 나뉘나)이라고 한다.
해상도 한 변 길이(약) 평균 넓이(약)
r7 1.2 km 5.2 km²
r8 460 m 0.74 km²
r9 170 m 0.1 km²

넓이는 위치마다 조금씩 다르다. 광화문 근처 r9 칸은 약 72,500 m²였다.

  • 구(공)는 육각형만으로 덮을 수 없다. 그래서 해상도마다 오각형이 12개 섞인다. 모두 바다 한가운데라 한국과는 관계없다.

셀 번호(인덱스)

  • 셀 번호는 64비트 숫자 하나다. 보통 16진수 15자리 문자열로 쓴다.
  • 안에는 해상도, 기본 칸(122개 중 어느 것), 단계별 자리값(각 단계에서 7개 자식 중 몇 번째)이 들어 있다.
  • 주소와 닮았다. "대한민국 → 서울 → 종로구 → 사직동"처럼 큰 것부터 작은 것 순서다.
  • 두 번째 글자가 해상도다. 뒤쪽의 f는 "이보다 작은 단계는 안 씀" 표시다.

r9  8 9 30e1d8c 8fffff
r7  8 7 30e1d8c ffffff
가운데 "30e1d8c"가 같다. 부모를 구하는 건 뒷자리를 지우는 일이다. 도형 계산이 아니다.

⚙️ 어떻게 동작하나

  1. 정규화 때 한 번: 행마다 위치를 점 하나로 줄이고, 그 점이 속한 r9 칸 번호를 구한다. h3_r9 열에 저장한다.
  2. 같은 해상도에서 하나뿐: 칸들이 타일처럼 빈틈없이 덮는다. 그래서 점 하나는 정확히 한 칸에 들어간다.
  3. 크게 보기: 지도가 r7을 원하면 h3ToParent(r9, 7)로 번호를 바꾼 뒤 칸별로 센다.
  4. 그리기: 브라우저가 칸 번호에서 육각형 꼭짓점을 계산해 그린다(h3-js의 cellToBoundary). 별도 지도 자료가 필요 없다.
  5. 칸 누르기: 큰 칸 번호로 "이 부모에 속한 r9 칸들"을 다시 거른다.

flowchart LR
    P["좌표<br/>위도·경도"] -->|"정규화 때 한 번"| R9["r9 칸"]
    R9 -->|"부모로"| R8["r8 칸"]
    R8 -->|"부모로"| R7["r7 칸"]
    P -.->|"❌ 따로 계산"| X["r7 칸<br/>경계에서 엇갈림"]
위 줄이 지키는 규칙이다. 좌표에서 r7을 따로 계산하면(점선) 경계 근처 점이 다른 칸에 들어갈 수 있다.

왜 엇갈리나. 큰 육각형 하나와 그 아래 작은 육각형 7개는 모양이 정확히 포개지지 않는다. 가장자리에서 살짝 삐져나온다.

그래서 "기준은 r9, 큰 칸은 항상 r9의 부모로"를 지킨다. 자식은 부모가 딱 하나라 항상 일관되고, 두 번 세지 않는다.

🧪 예시

  • 예시 1 · 일상에 빗대어: 학생 679명이 있다. 반은 142개, 학년은 54개, 학교는 16개다. 학생마다 반이 하나고, 반마다 학년이 하나, 학년마다 학교가 하나다. 그래서 어느 단위로 세도 합은 679명이다. 그런데 학교를 "집 주소로 다시 배정"하면 문제가 생긴다. 경계에 사는 학생은 반은 A학교 소속인데 학교 명단은 B학교에 오를 수 있다.

  • 예시 2 · 실제로는: 그날 ClickHouse에서 r9를 r7로 묶어 센 쿼리다.

SELECT
  h3ToString(h3ToParent(stringToH3(h3_r9), 7)) AS h3,
  count() AS n
FROM `disaster`.`norm_flood_trace`
WHERE …지금 조건…
GROUP BY h3
① stringToH3: 문자열로 저장된 h3_r9를 숫자로 바꾼다. ② h3ToParent(…, 7): r7 부모 번호를 구한다. ③ h3ToString: 다시 문자열로 돌린다. ④ GROUP BY h3: 부모 칸별로 센다.

결과(침수흔적 · 강남구 · 2022년 8월, 679행):

해상도 칸 수 행 합계
r9 142칸 679
r8 54칸 679
r7 16칸 679

합계가 그대로라서 모든 행이 정확히 한 번씩 세어졌다. 칸이 커질수록 흩어진 칸이 묶여 "어디에 몰렸는지"가 보인다.

브라우저에서도 같은 번호가 나온다.

import { latLngToCell, cellToParent } from "h3-js";
const c9 = latLngToCell(37.57375, 126.97693, 9); // "8930e1d8c8fffff"
cellToParent(c9, 7);                            // "8730e1d8cffffff"
ClickHouse로 만든 r7 칸 167개와 h3-js로 만든 r7 칸도 번호까지 모두 같았다.

⚖️ 비교 · 흔한 오해

방식 칸 모양 특징
H3 육각형 이웃 칸까지 거리가 같음, 전 세계 공통
사각 격자(지오해시 등) 사각형 자르기 쉬움, 대각선 이웃은 거리가 다름
행정구역 제각각 사람에게 익숙, 나라마다 다르고 바뀜

오해 1. "셀 번호는 우리가 정한 임의의 값이다." DB에 8930e1d8c8fffff 같은 문자열로 들어 있어서 UUID 같은 ID처럼 보인다. 그래서 그렇게 생각하기 쉽다. 실제로는 위치 그 자체를 담은 주소다. 번호만 풀어도 중심 좌표·해상도·부모가 나온다. 기준표에서 이 칸이 종로구 사직동으로 배정된 것과도 맞았다.

오해 2. "육각형이 겹쳐서 데이터가 두 번 세어진다." 같은 해상도의 칸은 겹치지도, 빈틈이 생기지도 않는다. 정규화도 행마다 h3_r9를 하나만 갖는다.

오해 3. "다른 데이터셋이 같은 칸에 있으면 겹친 것이다." 겹침이 아니다. "같은 장소의 데이터"라는 뜻이다. 이걸 쉽게 찾으려고 H3를 쓴다.

진짜 겹침이 생기는 곳은 따로 있다. 구 단위 데이터를 구 안의 모든 칸에 같은 값으로 펼칠 때다. 이 칸 값들을 더하면 한 행이 여러 번 세어진다.

🛠️ 실무에서는

  • 저장할 기준 해상도를 하나만 정했나(여기서는 r9)
  • 큰 칸은 항상 기준 해상도의 부모로 만드나
  • 번호 형식(문자열·숫자)이 시스템마다 같나. 다르면 stringToH3·h3ToString으로 맞춘다
  • 구 단위로 펼친 칸 값을 더하고 있지 않나
  • 지도 칸 수 제한(1,000칸)에 걸리면 한 단계 큰 해상도로 묶는다

먼저 볼 것 - 같은 위치인데 번호가 다르다: 해상도가 다른지(두 번째 글자), 위도·경도 순서가 바뀌지 않았는지 본다. - 함수 이름이 안 맞는다: h3-js·h3-py v4부터 이름이 바뀌었다(geoToH3 → latLngToCell, h3ToParent → cellToParent). ClickHouse는 지금도 h3ToParent를 쓴다.

❓ 자주 묻는 질문

  • Q. 셀 번호만 있으면 위치를 알 수 있나? A. 그렇다. 라이브러리가 번호에서 중심 좌표와 경계를 계산한다. 외부 자료가 필요 없다.
  • Q. r9 칸을 r10으로 더 잘게 나눌 수 있나? A. 자식 칸 목록은 구할 수 있다(cellToChildren). 하지만 데이터에는 r9까지의 위치만 있어서 행을 더 정확히 나누지는 못한다.
  • Q. H3 버전이 바뀌면 번호도 바뀌나? A. 격자와 번호는 그대로다. 함수 이름만 바뀌었다.

🔭 더 깊이 보기

📚 참고 자료

  • H3 Documentation — Introduction (H3 공식 문서, 영문) — 격자 구조와 해상도, 오각형 · 링크
  • Tables of Cell Statistics Across Resolutions (H3 공식 문서, 영문) — 해상도별 칸 수·넓이·변 길이 표 · 링크
  • Functions for Working with H3 Indexes (ClickHouse 공식 문서, 영문) — h3ToParent, stringToH3 사용법 · 링크

🗓️ 이력

  • 2026-11-10: r9를 r7로 묶을 수 있는지, 육각형이 겹치지 않는지, 셀 번호가 전 세계 공통인지 3번 물음 → 일지

🕸️ 연결된 주제