KunYu
h3geohashs2spatial-indexgis-basics

H3 vs Geohash vs S2: 올바른 공간 인덱스 선택하기

H3, Geohash, S2 공간 인덱스 비교: 정밀도 표, 위도 왜곡, 경계 버그, JS 및 Python 코드 예제, 그리고 의사결정 매트릭스.

KunYu TeamMarch 12, 202624분 읽기

수백만 개의 GPS 포인트를 대상으로 근접 검색을 구현하다 보면 결국 같은 벽에 부딪히게 됩니다. 브루트 포스 방식의 거리 스캔은 너무 느리고, 날것의 위경도 좌표는 인덱싱이 잘 되지 않습니다. 공간 그리드 시스템은 좌표를 미리 계산된 셀에 스냅함으로써 이 문제를 해결합니다 — 빠른 조회, 깔끔한 집계, 공유 가능한 식별자. Geohash는 2008년부터 이 역할을 해왔습니다. H3는 Uber가 2018년에 오픈소스로 공개하면서 육각형 그리드를 주류로 끌어올렸습니다. S2는 Google Maps가 내부적으로 사용하는 시스템입니다. 이 세 가지는 서로 호환되지 않으며, 프로토타입 단계에서는 학문적으로 보이던 차이점이 데이터가 셀 경계를 넘거나 여러 대륙에 걸칠 때 실질적인 문제로 이어집니다.

공간 그리드 인덱싱 시스템이란?

공간 그리드 인덱싱 시스템은 지구 표면을 이산적인 셀로 나누고, 각 셀에 고유 식별자를 부여합니다. 날것의 좌표 대신 셀 ID를 저장함으로써 문자열 접두사나 정수 값을 단순히 비교하는 방식으로 빠른 조회, 범위 쿼리, 데이터 집계가 가능해집니다. 핵심 트레이드오프는 셀 균일성, 구현 단순성, 쿼리 성능 사이에 있습니다.

세 시스템은 서로 다른 엔지니어링 문화에서 탄생했습니다. Geohash는 2008년 Gustavo Niemeyer가 공개 도메인 지오코딩 시스템으로 개발했으며, 설계상 가장 단순합니다. S2는 2011년경 Google이 내부 지도 인프라용 기하학 라이브러리로 개발했습니다. H3는 Uber가 2018년에 도시 단위 운전자/승객 매칭 문제를 해결하기 위해 오픈소스로 공개했습니다.

셀 형태는 이러한 기원을 반영합니다. Geohash는 직사각형, S2는 구면 사각형(구 위에서 대략 정사각형), H3는 육각형(구를 닫기 위한 12개의 오각형 포함)을 생성합니다.

각 시스템의 작동 원리

각 시스템의 인코딩 방식은 프로덕션에서의 강점과 실패 패턴 모두를 결정합니다.

H3는 지구를 정이십면체(20면 다면체)에 투영한 후, 각 면을 육각형으로 세분화합니다. 결과적으로 15개의 해상도 레벨이 만들어지며, 각 셀은 대략 7개의 자식 셀로 세분화됩니다. 셀 ID는 64비트 정수입니다. 육각형 형태 덕분에 모든 셀은 정확히 6개의 엣지 공유 이웃을 가집니다 — 꼭짓점 공유로 인한 모호함이 없습니다.

Geohash는 위도와 경도의 이진 표현을 비트 단위로 교차 배치한 후, 결과를 Base32 문자열로 인코딩합니다. Geohash 문자열이 사람이 읽기 쉬운 이유가 바로 이것입니다. 예를 들어 u4pruydqqvj는 파리 중심부의 3m × 3m 구역을 고유하게 식별합니다. 정밀도는 문자열 길이(1~12자)로 제어됩니다. 접두사 속성 — u4pru는 항상 u4pr 안에 있음 — 덕분에 범위 쿼리가 매우 간단합니다.

S2는 지구 표면을 정육면체의 여섯 면에 펼친 후, 각 면에 힐베르트 공간 충전 곡선을 적용하고 위치를 64비트 정수로 매핑합니다. 힐베르트 곡선은 공간적 지역성을 보존합니다. 즉, 지구상에서 인접한 점들은 인접한 정수를 생성합니다. S2는 31개의 해상도 레벨을 가지며, S2RegionCoverer를 통해 임의의 폴리곤을 최소 개수의 셀로 근사하는 방식으로 임의 폴리곤 커버리지를 지원합니다.

정밀도와 커버리지 — 비교표

세 시스템은 서로 호환되지 않는 정밀도 체계를 사용합니다 — H3의 15개 해상도, Geohash의 12개 문자 길이, S2의 31개 레벨은 서로 대응되지 않습니다. 아래 표는 세 가지 실용적인 규모(도시 수준 집계, 동네 라우팅, 건물 수준 정밀도)에서 대략적인 셀 면적을 기준으로 정렬한 것입니다.

경도의 도 단위 거리는 코사인 곡선을 따라 줄어들며, 대부분의 개발자가 예상하는 것보다 빠르게 심각해집니다. 북위 30°(로스앤젤레스, 카이로, 상하이)에서는 경도 1도가 110.6km인 위도 1도에 비해 96.5km를 커버합니다 — 동서 방향으로 14.9% 부족합니다. 북위 60°(스톡홀름, 앵커리지, 상트페테르부르크)에서는 비율이 2:1에 달합니다. 즉, 경도 1도는 너비가 55.7km에 불과합니다. 적도에서 약 0.74km²를 커버하는 6자리 Geohash 셀은 북위 60°에서는 약 0.37km²밖에 커버하지 못합니다. 균일한 근접 임계값(예: "500미터 이내")을 사용하는 글로벌 서비스를 구축한다면, 고정된 정밀도 레벨의 Geohash는 위도에 따라 일관성 없는 결과를 낼 것입니다. 실제 세계에서 동등한 커버리지를 얻으려면 저위도에서는 더 긴 해시를, 극지방에서는 더 짧은 해시를 사용해야 하므로 쿼리 로직이 복잡해집니다. H3와 S2는 모두 구면 왜곡을 보정합니다. H3 해상도 9는 모든 위도에서 면적 변화가 3% 미만입니다.

규모 H3 해상도 H3 셀 면적 Geohash 길이 Geohash 셀 면적 S2 레벨 S2 셀 면적
도시 5 ~253 km² 4자 ~780 km² 10 ~325 km²
동네 9 ~0.11 km² 6자 ~0.74 km² 15 ~0.32 km²
건물 11 ~0.0022 km² 7자 ~0.023 km² 18 ~0.005 km²

이웃 검색과 경계 효과

이웃 쿼리는 가장 일반적인 공간 인덱스 연산입니다. "이 셀이 주어졌을 때, 인접한 모든 셀을 찾아라." 세 시스템은 이를 매우 다른 방식으로 처리합니다.

H3는 가장 깔끔한 이웃 모델을 가집니다. 모든 육각형은 정확히 6개의 엣지 공유 이웃을 가집니다 — 꼭짓점 이웃도 없고 모호함도 없습니다. gridDisk(cell, k) 함수는 단 한 번의 호출로 k 링 내의 모든 셀을 반환합니다. k=1 디스크는 7개의 셀(중심 포함)을, k=2는 19개를 반환합니다. 육각형은 모든 변의 길이가 동일하므로, 링 수를 이용한 거리 쿼리가 기하학적으로 일관됩니다.

Geohash는 8개의 이웃을 가집니다(4개의 엣지 + 4개의 꼭짓점). 대부분의 셀에서는 잘 작동합니다. 악명 높은 문제는 그리드 경계에 있습니다. 지리적으로 인접한 두 셀이 공통 접두사를 전혀 공유하지 않는 Geohash 문자열을 가질 수 있습니다. 예를 들어, 본초 자오선(경도 0°)이나 적도를 가로지르는 셀은 완전히 다른 해시를 가질 수 있습니다. 전형적인 증상은 쿼리가 경계 한쪽의 결과는 반환하지만 바로 반대편의 포인트는 조용히 누락하는 것입니다.

잘 알려진 실사례가 있습니다. 프랑스 남서부의 La Roche-Chalais는 Geohash 접두사가 u000인 반면, 불과 30km 떨어진 Pomerol은 접두사가 ezzz입니다. 이 두 인접 위치는 주요 셀 경계가 두 지점 사이를 직접 가로지르기 때문에 어떤 길이에서도 공통 접두사를 공유하지 않습니다. "La Roche-Chalais 근처 포인트"에 대한 접두사 기반 근접 쿼리는 Pomerol의 결과를 조용히 반환하지 않습니다. 동일한 문제가 그리니치 자오선(경도 0°)에서도 발생합니다. 본초 자오선 양쪽에서 10미터 떨어진 두 GPS 포인트는 각각 e(서쪽)와 s(동쪽)로 시작하는 해시를 갖습니다 — 공통 접두사 없음, 공통 인덱스 범위 없음. 표준 해결책은 항상 대상 셀과 8개의 이웃을 함께 쿼리한 후, 실제 Haversine 거리로 필터링하는 것입니다. Redis의 GEOSEARCH 명령은 이를 내부적으로 처리하는데, 이것이 Geohash 근접 로직을 직접 구현하는 것보다 데이터베이스 네이티브 지원을 선호하는 이유 중 하나입니다.

S2는 힐베르트 곡선을 사용하여 강력한 지역성 보장을 제공합니다 — 지구상에서 인접한 점들은 인덱스에서도 인접한 정수로 매핑됩니다. 그러나 "힐베르트 공간에서 인접"과 "공간적으로 인접"이 항상 같지는 않습니다. 정육면체의 여섯 면은 이음새를 만들어내며, 면 경계를 넘어 이웃을 계산하려면 신중한 처리가 필요합니다. S2의 S2CellId::EdgeNeighbors()는 이를 올바르게 처리하지만, H3의 gridDisk보다 구현 작업이 더 많습니다.

데이터가 지리적 경계(해안선, 국경, 날짜변경선)에 걸쳐 있다면, H3의 균일한 육각형 토폴로지가 더 안전한 기본 선택입니다. Geohash와 같은 직사각형 그리드 시스템은 예측 가능한 경도와 위도에서 이음새가 발생하며, 프로덕션에서 경계 누락을 디버깅하는 것은 매우 불쾌한 경험입니다.

각 시스템을 언제 사용할까

이미 Redis나 Elasticsearch를 운영 중이라면 Geohash를 사용하세요 — 추가 코드 없이 바로 근접 검색이 작동합니다. 글로벌 일관성이나 육각형 시각화가 필요하다면 H3를. 임의 폴리곤 인덱싱이 필요하거나 기존 Google 인프라와 함께 작업하는 경우에만 S2를 선택하세요. 아래 표에서 일반적인 시나리오를 다루고 있습니다.

사용 사례 권장 이유
Redis GEO 명령 / Elasticsearch geo_point Geohash 네이티브 내장 지원, 설정 불필요
라이드셰어링 / 음식 배달 수요 히트맵 H3 일관된 셀 크기 + 육각형 시각화
글로벌 데이터 집계 (기후, 텔레메트리) H3 극지방 위도 왜곡 제거
임의 폴리곤 지역 인덱싱 S2 S2RegionCoverer로 모든 형태 근사
게임 그리드 시스템 (Pokémon GO가 사용) S2 Niantic의 초기 인프라 선택
빠른 프로토타입과 간단한 지오펜싱 Geohash 가장 낮은 학습 곡선, 사람이 읽기 쉬운 ID
다중 해상도 분석 (줌 인/아웃) H3 깔끔한 부모/자식 계층 구조
PostGIS / 공간 데이터베이스 Geohash 표준 GIS 에코시스템 지원

에코시스템 성숙도가 기하학적 요소보다 먼저 선택을 결정하는 경우가 많습니다. Geohash는 2015년부터 Redis에 내장되어 있으며(GEOADD, GEORADIUS), Elasticsearch의 geo_point 필드 타입은 그리드 집계를 위해 내부적으로 Geohash를 사용합니다. 스택에 이미 두 가지 중 하나가 포함되어 있다면, Geohash 통합은 추가 작업이 거의 필요 없습니다.

H3는 강력한 Python(h3) 및 JavaScript(h3-js) 라이브러리, Deck.gl의 H3HexagonLayer를 포함한 성장하는 시각화 도구 에코시스템, 그리고 DuckDB 공간 확장에서의 일급 지원을 갖추고 있습니다. S2는 가장 강력한 기하학 연산을 제공하지만 JavaScript 지원이 가장 약합니다 — 메인 라이브러리는 C++이며, 다른 언어용 포트는 커뮤니티가 유지 관리합니다.

GeoHash 변환기

GeoHash, H3, Plus Codes 간 변환.

Try it now

H3, Geohash, S2 코드로 다루기

세 시스템 모두 JavaScript와 Python 라이브러리를 갖추고 있습니다. 아래 예제는 가장 많이 사용하는 세 가지 작업을 다룹니다. 좌표 인코딩, 이웃 찾기, 셀 경계 폴리곤 가져오기. h3-js와 ngeohash npm 패키지의 상세 비교(번들 크기, 인코딩 벤치마크, 프로덕션 주의사항)는 h3-js vs ngeohash 가이드를 참고하세요.

배포 전에 알아두어야 할 라이브러리 주의사항 두 가지가 있습니다.

ngeohash 정수 정밀도: encode_int / decode_int 함수는 JavaScript에서 52비트로 제한됩니다(표준 IEEE 754 float64 안전 정수 범위). 대부분의 사용 사례에서는 문제없지만, bitDepth를 52 이상으로 설정하면 결과가 조용히 정밀도를 잃습니다. 정수로 작업해야 할 특별한 이유가 없다면 문자열 API(encode / decode)를 사용하고, 항상 encode와 decode 호출 모두에 동일한 bitDepth 값을 전달하세요.

h3-js 셀 ID 표현: H3 셀 ID는 64비트 정수로, JavaScript의 안전 정수 범위(2^53 − 1)를 초과합니다. h3-js 라이브러리는 기본적으로 문자열로 반환합니다(예: "8928308280fffff"). 흔한 프로덕션 버그: API가 H3 ID를 문자열 대신 JSON 숫자로 직렬화하면, 일부 JSON 파서가 값을 조용히 반올림하여 셀 ID를 손상시킵니다. 데이터베이스 스키마, API 응답, 프론트엔드 코드 등 전체 스택에서 H3 ID를 항상 문자열로 취급하세요.

JavaScript

import { latLngToCell, gridDisk, cellToBoundary } from "h3-js";
import ngeohash from "ngeohash";

// --- H3 ---
// Encode coordinate to H3 cell (resolution 9 ≈ 0.1 km²)
const h3Cell = latLngToCell(37.7749, -122.4194, 9);
// → "8928308280fffff"

// Get all cells within 1 ring (7 cells including center)
const h3Neighbors = gridDisk(h3Cell, 1);

// Get cell boundary polygon (array of [lat, lng] pairs)
const h3Boundary = cellToBoundary(h3Cell);

// --- Geohash ---
// Encode coordinate to Geohash (precision 6 ≈ 0.74 km²)
const hash = ngeohash.encode(37.7749, -122.4194, 6);
// → "9q8yyk"

// Decode back to coordinate
const { latitude, longitude } = ngeohash.decode(hash);

// Get 8 neighbors (N, NE, E, SE, S, SW, W, NW)
const geohashNeighbors = ngeohash.neighbors(hash);

// Get cell bounding box [minLat, minLon, maxLat, maxLon]
const bbox = ngeohash.decode_bbox(hash);

Python

import h3
import geohash  # pip install python-geohash

# --- H3 ---
cell = h3.latlng_to_cell(37.7749, -122.4194, 9)
neighbors = h3.grid_disk(cell, 1)
boundary = h3.cell_to_boundary(cell)  # list of (lat, lng) tuples

# --- Geohash ---
hash_str = geohash.encode(37.7749, -122.4194, precision=6)
decoded_lat, decoded_lng = geohash.decode(hash_str)
neighbors = geohash.neighbors(hash_str)  # dict with N/NE/E/SE/S/SW/W/NW keys

JavaScript에서 S2는 C++ 라이브러리보다 뒤처진 커뮤니티 포트(s2-geometry npm 패키지)가 필요합니다. JS 환경에서 본격적인 S2 작업을 위한 가장 실용적인 방법은 Python/Java 서비스를 호출하거나, WASM과 함께 Cloudflare Workers를 사용하는 것입니다. Python에서는 s2sphere 라이브러리가 깔끔한 인터페이스를 제공합니다.

import s2sphere

# Encode coordinate to S2 cell (level 15 ≈ 0.32 km²)
latlng = s2sphere.LatLng.from_degrees(37.7749, -122.4194)
cell_id = s2sphere.CellId.from_lat_lng(latlng).parent(15)

# Get 4 edge neighbors
neighbors = cell_id.edge_neighbors()

# Cover an arbitrary polygon with S2 cells
region_coverer = s2sphere.RegionCoverer()
region_coverer.min_level = 10
region_coverer.max_level = 15
covering = region_coverer.get_covering(some_s2_region)

KunYu의 GeoHash 인코더 도구를 사용하면 브라우저에서 직접 Geohash와 H3 인코딩을 실험해볼 수 있습니다 — 라이브러리 설치 없이 바로 사용할 수 있습니다.

GeoHash 변환기

GeoHash, H3, Plus Codes 간 변환.

Try it now

FAQ

H3가 Geohash보다 더 나은가요?

기존 인프라 제약이 없는 새 프로젝트라면 H3가 더 나은 기본 선택입니다. 균일한 셀 크기, 더 깔끔한 이웃 쿼리, 경계 버그 없음이 그 이유입니다. Redis GEO 명령이나 Elasticsearch 집계를 바로 사용해야 하거나, 사람이 읽기 쉬운 ID가 중요할 때는 Geohash가 유리합니다 — Geohash 문자열은 로그에서 읽기 쉽지만, 8928308280fffff 같은 H3 ID는 그렇지 않습니다.

Redis는 H3를 지원하나요?

Redis에는 H3 네이티브 지원이 없습니다. 내장 GEO 명령(GEOADD, GEORADIUS, GEOSEARCH)은 내부적으로 Geohash를 사용합니다. Redis와 함께 H3를 사용하려면, 애플리케이션 레이어에서 좌표를 H3 셀 ID로 인코딩하고 일반 문자열 키나 정렬된 집합에 저장해야 합니다. Redis 7.x 기준으로, 코어 서버에 H3 프리미티브를 추가할 계획은 없습니다.

S2는 무엇의 약자인가요?

S2는 Sphere²(구의 제곱)의 약자로, 구를 2차원 표면에 매핑하는 수학적 개념을 지칭합니다. 이 라이브러리는 Google에서 개발되었으며, Google Maps, Google Earth 및 기타 Google 지오 제품 전반에서 내부적으로 사용됩니다.

해상도 5에서 H3 셀은 지구를 몇 개로 덮나요?

해상도 5에서 지구 표면을 덮는 H3 셀은 2,016,842개입니다(정이십면체를 닫는 12개의 오각형 포함). 각 셀은 약 252.9 km²를 커버하며, 이 해상도는 국가 수준이나 대도시권 분석에 유용합니다.

H3와 Geohash 사이에서 변환할 수 있나요?

직접적인 수학적 변환은 없습니다. 두 그리드는 독립적입니다 — 해상도 9의 H3 육각형과 6자리 Geohash 셀은 서로 다른 영역을 커버하며, 근사적으로만 매핑할 수 있습니다. 실용적인 방법은 두 형식 중 하나를 위경도 좌표로 디코딩한 후, 대상 형식으로 재인코딩하는 것입니다. 크기와 형태 차이로 인해 결과 셀이 원래 셀을 완전히 포함하지 않을 수 있다는 점에 주의하세요.

Geohash의 최대 정밀도는 얼마인가요?

Geohash 사양은 최대 12자를 지원하며, 약 3.7cm × 1.9cm 크기의 셀을 생성합니다. 실제로 대부분의 애플리케이션은 6~9자(1km²에서 약 7m²)를 사용합니다. 9자를 초과하면 정밀도가 대부분의 GPS 하드웨어가 제공할 수 있는 수준을 넘어서며, 긴 문자열 키는 불필요한 저장 공간과 인덱스 오버헤드를 유발합니다.