콘텐츠로 이동

좌표계 변환과 pyproj

11/9(월)

한 줄 요약: 같은 위치라도 좌표계마다 숫자가 다르다. H3는 위경도(EPSG:4326)만 받으므로, 한국 공공데이터의 평면 좌표는 먼저 바꿔야 한다. 그 변환을 정확히 해 주는 표준 도구가 pyproj다.

📍 만난 상황

  • 2026-11-09 — 1번 물음 → 그날 일지
  • 10:42 위치 "입구"(좌표 열, WKT, GeoJSON 등 위치가 들어오는 모양별 변환 함수)를 정리하는 중이었다. 새 형식이 오면 어떻게 할지도 막 들었다. 그 목록에 계속 나오는 pyproj를 왜 쓰는지 물었다.
  • 그 뒤 흐름을 확정했다. 좌표가 있으면 pyproj로 4326 → H3. 좌표가 없으면 기준 자료와 매칭 → 4326 → H3.

🧩 쉽게 말하면

귤껍질을 떠올리면 된다. 지구(귤)는 둥글다. 지도(종이)는 평평하다. 껍질을 펴서 종이에 붙이는 방법은 여러 가지이고, 방법마다 눈금이 다르다.

서울의 어느 교통량 지점이 A 지도에서는 "가로 195,489m, 세로 452,136m", B 지도에서는 "북위 37.56도, 동경 126.96도"다. H3는 B 방식만 알아듣는다. 그래서 A 숫자를 B 숫자로 통역해야 한다. pyproj가 그 통역사다.

📖 제대로 알기

좌표계의 두 종류

  • 지리 좌표계: 지구 위 위치를 각도(위도·경도, 단위 도)로 나타낸다. 예: EPSG:4326(WGS84 위경도). GPS가 쓰는 방식이다.
  • 투영 좌표계: 둥근 지구를 평면에 펼친 뒤 미터 단위 x·y로 나타낸다. 거리·면적 계산이 쉬워 한국 공공데이터가 많이 쓴다.

EPSG 코드는 좌표계마다 붙은 국제 번호다. 숫자 하나로 "어떤 방식으로 펼쳤고, 지구를 어떤 모양으로 봤는지"가 정해진다.

측지계와 타원체

지구는 완전한 공이 아니라 살짝 눌린 귤 모양이다. 이 모양을 수식으로 잡은 것을 타원체라고 한다. 타원체와 기준점을 합친 것을 측지계(datum)라고 한다.

  • 베셀(Bessel) 타원체: 옛 한국 측지계. 옛 좌표계(5174, 2097 등)가 쓴다.
  • GRS80 / WGS84: 오늘날 기준. 둘은 사실상 같은 위치를 준다(차이 수 mm 수준).

같은 지점이라도 베셀 기준 위경도와 WGS84 기준 위경도는 수백 m 차이가 난다.

한국에서 자주 보는 좌표계

EPSG 이름(요약)
4326 WGS84 위경도. H3 입력
5179 UTM-K(GRS80). 전국 한 판
5181 중부원점(GRS80), 북쪽 가산값 500,000
5186 중부원점(GRS80), 북쪽 가산값 600,000
5174 · 2097 중부원점(베셀). 옛 좌표계

가산값(false easting/northing)은 좌표가 음수가 되지 않게 원점에 더해 둔 숫자다. 5181과 5186은 이 값만 10만 m 다르다.

pyproj

pyproj는 PROJ(좌표 변환 엔진)를 파이썬에서 쓰게 해 주는 라이브러리다. PROJ는 PostGIS, QGIS, GDAL이 모두 쓰는 엔진이라 어느 도구로 검증해도 결과가 같다.

⚙️ 어떻게 동작하나

옛 좌표계(5174) 점 하나를 H3 칸으로 바꾸는 과정이다.

flowchart TD
  A["원본 x·y<br/>5174 미터 좌표"] -->|"① 투영 해제"| B["베셀 기준 위경도"]
  B -->|"② 측지계 변환"| C["WGS84 위경도<br/>4326"]
  C -->|"③ H3 칸 계산"| D["H3 칸 번호"]

①②가 pyproj가 하는 일이다. GRS80 좌표계(5179·5181·5186)는 ②가 거의 없고 ①만 하면 된다.

  1. 투영 해제: 평면에 펼친 x·y를 다시 둥근 지구 위 각도로 되돌린다. 단순한 곱셈이 아니라 타원체 위의 복잡한 공식이다.
  2. 측지계 변환: 지구 모양을 베셀로 보던 위경도를 GRS80·WGS84 기준으로 옮긴다. 이걸 빼먹으면 수백 m가 어긋난다.
  3. H3 칸 계산: 지구를 육각형 칸으로 나눈 격자에서 그 위경도가 속한 칸을 찾는다.

왜 손으로 짜지 않나: 두 계산 모두 공식이 길고 매개변수가 많다. 실제로 다른 도구(polars-st)는 측지계 변환을 제대로 하지 않아 5174·2097에서 약 362m 어긋났다. 그래서 변환에는 pyproj만 쓰기로 했다.

🧪 예시

  • 예시 1 · 일상에 빗대어: 같은 건물 주소를 옛 지번 주소와 도로명 주소로 쓸 수 있다. 둘 다 맞지만, 내비게이션이 도로명만 알아듣는다면 옛 주소를 바꿔 넣어야 한다. 번지수를 대충 짐작해서 바꾸면 옆 블록(수백 m)에 도착한다.

  • 예시 2 · 실제로는: 교통량 지점 원본은 grs80tm_x = 195489, grs80tm_y = 452136(EPSG:5181)이었다.

from pyproj import Transformer
import h3

t = Transformer.from_crs("EPSG:5181", "EPSG:4326", always_xy=True)  # ①
lon, lat = t.transform(195489, 452136)                              # ②
print(lat, lon)            # → 37.56…, 126.96…

assert 37.4 < lat < 37.8 and 126.7 < lon < 127.3                    # ③
cell = h3.latlng_to_cell(lat, lon, 9)                               # ④
① 변환기를 한 번 만든다. 출발 5181, 도착 4326. always_xy=True로 항상 "경도, 위도" 순서를 쓴다. ② 미터 좌표를 넣으면 위경도가 나온다. x가 원점(가산값 200,000)보다 약 4.5km 작으니 기준 경도 127도보다 조금 서쪽이다. ③ 결과가 서울 범위 안인지 검사한다. 좌표계를 잘못 골랐을 때 잡아낸다. ④ H3에는 위도, 경도 순서로 넣는다. 해상도 9 칸은 한 변이 약 170m인 육각형이다.

⚖️ 비교 · 흔한 오해

잘못 읽은 경우 실측 어긋남
5186을 5181로 약 100km (가산값 차이)
5174를 5181로 약 313m (측지계 차이)
측지계 변환 생략 약 362m
  • 헷갈렸던 점: "좌표는 좌표 아닌가, 왜 바꾸나" — 숫자 두 개로 보이니 같은 종류처럼 느껴진다. 하지만 미터 단위 평면 좌표와 각도 단위 위경도는 단위부터 다르다. H3는 각도만 받는다.
  • 헷갈렸던 점: "변환은 공식 하나면 된다" — 투영 해제만 생각하면 그렇게 보인다. 옛 좌표계는 지구 모양(타원체)까지 달라서 측지계 변환이 한 번 더 필요하다.
  • 오해: "pyproj가 좌표계도 알아서 맞춰 준다" — 아니다. pyproj는 주어진 좌표계대로 정확히 바꿀 뿐이다. 원본이 5174인데 5181이라고 알려 주면, 틀린 위치를 정확하게 계산한다. 서울 범위에서는 숫자만 보고 구분이 안 되는 경우도 있다.
  • 오해: "4326은 경도, 위도 순서다" — 공식 정의는 위도, 경도 순서다. 많은 프로그램은 경도, 위도(x, y)로 쓴다. 그래서 always_xy=True로 고정한다.

🛠️ 실무에서는

  • 체크리스트
  • 원본 좌표계를 문서로 확인하고 등록 카드에 EPSG 번호로 적었나.
  • 변환 결과가 대상 지역(서울 경계) 안에 들어오는지 자동 검사하나.
  • always_xy=True를 켰나. H3에 넣을 때 위도·경도 순서를 지켰나.
  • 변환기(Transformer)를 줄마다 새로 만들지 않고 한 번 만들어 재사용하나. 수만 줄이면 속도 차이가 크다.
  • EPSG 번호가 없는 좌표계: KATEC처럼 번호가 없으면 투영식 문자열(PROJ 문자열)로 정의해 넘긴다. 코드는 그대로다.
  • 문제가 생기면 먼저 볼 것
  • 점이 바다나 다른 도시에 찍힌다 → x·y 순서가 뒤바뀌었거나 가산값이 다른 좌표계(5181↔5186)다.
  • 점이 수백 m씩 한쪽으로 밀린다 → 베셀 좌표계를 GRS80으로 잘못 읽었다.

❓ 자주 묻는 질문

  • Q. 원래 4326으로 온 데이터도 pyproj를 거쳐야 하나? A. 거쳐도 그대로 통과한다. 코드를 하나로 유지하려고 같은 길로 보내는 편이 단순하다.
  • Q. 좌표 없이 "강남구"만 있는 데이터는? A. pyproj로는 못 한다. "이 이름은 여기다"라고 적힌 기준 자료(경계·위치 표)와 먼저 이어 붙여 좌표를 얻어야 한다.
  • Q. 면(폴리곤) 데이터도 같은가? A. 꼭짓점마다 같은 변환을 한다. 다만 면을 H3 칸 하나로 정할지, 덮는 칸 전체로 할지는 따로 정해야 한다.

🔭 더 깊이 보기

  • H3 해상도는 어떻게 고르나? 칸 크기와 데이터 정밀도의 균형을 정하는 문제다.
  • 좌표가 없는 데이터에 위치를 붙이는 법(지오코딩)은? 좌표계 변환 바로 앞 단계다.
  • PostGIS의 ST_Transform은 무엇이 같고 다른가? 같은 PROJ 엔진을 DB 안에서 쓰는 방법이다.
  • 관련 주제: 데이터 파이프라인 실행과 갱신 — 이 변환이 정규화 파이프라인 안에서 돈다
  • 관련 주제: DB 정규화와 연결 표 — 이름이 같은 "정규화"의 다른 뜻

📚 참고 자료

  • 좌표 참조 시스템 (QGIS 문서 한국어, "GIS에 대한 친절한 소개") — 지리·투영 좌표계와 측지계의 기초 · 링크
  • pyproj Documentation (영문) — Transformer, always_xy 사용법 · 링크
  • EPSG.io (영문) — EPSG 번호로 좌표계 정의·범위 찾기(예: 5181, 5186) · 링크

🗓️ 이력

  • 2026-11-09: H3에 넣기 전에 왜 pyproj로 좌표를 바꾸는지 물음 → 일지

🕸️ 연결된 주제