메모장으로 여는 지도 — 웹 표준 GeoJSON의 구조와 [경도, 위도]의 비밀

12

메모장으로 여는 지도 — 웹 표준 GeoJSON의 구조와 [경도, 위도]의 비밀 이미지

지난번엔 파일 다섯 개, 이번엔 메모장 하나

지난 편 Shapefile은 .shp·.dbf·.prj… 파일 서너 개가 한 몸이라, 하나만 빠져도 데이터가 조용히 망가졌습니다. 그런데 이번 주인공은 정반대예요. 메모장으로 툭 열면 사람이 그냥 읽힙니다. { "type": "Point", "coordinates": [126.98, 37.57] } — 이 한 줄이 서울시청을 가리키는 '지도 데이터'라면, 믿어지시나요?

지난 Shapefile 편에서 우리는 30년 묵은 표준이 왜 파일 묶음일 수밖에 없었는지, 그리고 그 오래됨의 대가가 얼마나 큰지 봤습니다. 그 글 끝에서 예고했듯, 웹은 완전히 다른 길을 갔어요. 압축해서 폴더째 주고받는 무거운 바이너리 대신, 텍스트 한 덩어리면 끝나는 포맷. 오늘의 주인공은 그 웹이 택한 포맷, GeoJSON입니다. 구조가 어떻게 생겼는지, 좌표 순서가 왜 하필 거꾸로처럼 보이는지, 그리고 이 편한 포맷이 어디서 무너지는지까지 파헤쳐 봅니다.

메모장으로 열린다 — GeoJSON이라는 발상

이름 그대로입니다. GeoJSON = Geo + JSON. 우리가 API 주고받을 때 쓰는 바로 그 JSON에, "여긴 지리 데이터야"라는 약속을 얹은 것뿐이에요. 그러니 별도 뷰어도, 전용 라이브러리도 필요 없습니다. 텍스트 에디터로 열면 사람이 그대로 읽고, 브라우저는 JSON.parse() 한 줄로 삼킵니다.

이게 왜 대단하냐면, Shapefile을 떠올려 보세요. 도형은 .shp, 속성은 .dbf, 좌표계는 .prj, 인코딩은 .cpg… 각자 다른 파일에 흩어져 살았죠. GeoJSON은 그걸 텍스트 하나에 몽땅 담습니다. 좌표도, 속성도, 전부 한 덩어리예요.

태생을 보면 납득이 갑니다. GeoJSON은 2008년, 웹 개발자들이 자기들 손으로 만들었습니다. 어느 표준화 기구가 위에서 내려준 게 아니라, "브라우저에서 지도 데이터를 XML로 주고받자니 너무 무겁다"는 현장의 불만에서 태어난 거예요. 그래서 처음부터 웹에 최적화돼 있습니다.

💡 한 줄로 비유하면, GeoJSON은 "지도에 붙일 수 있는 JSON" 입니다. JSON을 다뤄본 개발자라면, 새로 배울 게 사실상 없어요. 딱 몇 가지 약속만 익히면 됩니다.

구조 뜯어보기 — Geometry · Feature · FeatureCollection

그 '몇 가지 약속'이 바로 GeoJSON의 구조입니다. 딱 3겹으로 중첩된 러시아 인형이라고 생각하면 편해요.

가장 안쪽부터 봅시다. 실제 예시 하나가 백 마디 설명보다 낫습니다.

json
{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [126.9779, 37.5663]
  },
  "properties": {
    "name": "서울시청",
    "type": "공공기관"
  }
}

여기서 잠깐 멈춰 봅시다. Shapefile에선 도형(.shp)과 속성(.dbf)이 딴 파일로 갈라져 살았고, 그래서 .dbf가 빠지면 속성이 통째로 사라졌죠. GeoJSON은 하나의 Feature 객체 안에 도형과 속성이 나란히 붙어 있습니다. 갈라질 일이 없으니 "속성이 텅 비는" 사고 자체가 구조적으로 안 생겨요.

그리고 이 Feature 여럿을 배열로 담은 게 가장 바깥 껍데기, FeatureCollection 입니다. 우리가 실제로 주고받는 GeoJSON 파일은 대부분 이 모습이에요.

json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [126.9779, 37.5663] },
      "properties": { "name": "서울시청" }
    },
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [129.0756, 35.1796] },
      "properties": { "name": "부산시청" }
    }
  ]
}

geometry에 들어가는 도형은 딱 7종류로 정해져 있습니다. 벡터와 래스터 편에서 만난 점·선·면이 그대로 여기 들어와요.

타입예시
Point점 하나편의점, 지하철역
LineString이어진 선도로, 강
Polygon닫힌 면건물, 행정구역
MultiPoint점 여러 개체인점 전 지점
MultiLineString선 여러 개여러 갈래 하천
MultiPolygon면 여러 개섬이 여럿인 시·도
GeometryCollection종류 섞인 묶음(드물게 씀)
메모장으로 여는 지도 — 웹 표준 GeoJSON의 구조와 [경도, 위도]의 비밀 이미지

좌표가 왜 거꾸로일까 — [경도, 위도]의 비밀

이제 이 포맷의 가장 악명 높은 함정입니다. 위 예시를 다시 보세요. 서울시청 좌표가 [126.9779, 37.5663] 이죠. 그런데 우리가 평소 GPS나 지도 앱에서 보던 서울 좌표는 "위도 37.5, 경도 126.9" 순서 아니었나요? GeoJSON은 이걸 뒤집어서 씁니다. 앞이 경도, 뒤가 위도예요. 대체 왜?

범인은 수학의 관습입니다. 수학 시간에 배운 좌표 (x, y)를 떠올려 보세요. x는 가로(동서), y는 세로(남북)죠. 지도에 그대로 대입하면 x = 경도(longitude), y = 위도(latitude) 입니다. GeoJSON은 컴퓨터·수학의 이 (x, y) 순서를 곧이곧대로 따랐어요. 그래서 [경도, 위도], 즉 [x, y] 순서인 겁니다.

문제는 사람의 관습은 정반대라는 거예요. 우리는 "위도, 경도" 순으로 말합니다. 내비게이션도, 위경도 검색도 대개 위도가 먼저죠. 심지어 지도 라이브러리끼리도 갈립니다. Leaflet은 [위도, 경도], GeoJSON은 [경도, 위도] — 이 둘을 섞어 쓰다 좌표가 뒤집히는 건 웹 지도 개발의 통과의례예요.

뒤집으면 무슨 일이 벌어질까요? 서울 [126.98, 37.57]을 실수로 [37.57, 126.98]로 넣었다고 합시다. 프로그램은 이걸 "경도 37.57, 위도 126.98"로 읽습니다. 그런데 — 위도는 −90°에서 +90°까지밖에 없어요. 126.98°라는 위도는 지구상에 존재하지 않습니다. 그래서 점이 조용히 사라지거나, 렌더러가 좌표를 뭉개 (0, 0) 근처로 떨어뜨립니다.

🔗 (0, 0)… 어디서 봤죠? 좌표 변환 편의 그 악명 높은 Null Island(기니만 앞바다)입니다. Shapefile 편에서 .prj가 없어 데이터가 아프리카 앞바다로 날아간 사고와도 뿌리가 같아요. 층만 바꿔 되살아나는 같은 함정 — 좌표계와 순서를 착각하면 데이터는 늘 바다로 갑니다.

💡 재미있는 건, 한국은 이 실수가 오히려 잘 드러난다는 점이에요. 경도가 127° 근처라 뒤집으면 "있을 수 없는 위도"가 되어 대놓고 터지거든요. 만약 경도가 90°보다 작은 지역이었다면, 뒤집힌 좌표가 조용히 엉뚱한 나라에 멀쩡히 찍혀서 한참을 못 찾았을 겁니다.

메모장으로 여는 지도 — 웹 표준 GeoJSON의 구조와 [경도, 위도]의 비밀 이미지

RFC 7946 — 2016년에 정리된 규칙들

앞에서 GeoJSON이 2008년 웹 개발자들 손에서 태어났다고 했죠. 그렇게 자유롭게 시작한 탓에, 초창기엔 애매한 구석이 많았습니다. 그래서 2016년, 국제 인터넷 표준화 기구(IETF)가 이걸 정식 표준 문서 RFC 7946으로 못박습니다. 오늘 우리가 쓰는 GeoJSON은 사실상 이 문서예요. 핵심 변경점 몇 개만 챙기면 됩니다.

가장 중요한 건 좌표계를 딱 하나로 고정한 것입니다. 2008년 버전은 crs라는 항목으로 "이 파일은 EPSG:5179를 쓴다"처럼 임의의 좌표계를 지정할 수 있었어요. RFC 7946은 이걸 없애 버리고, 무조건 WGS84 경위도(EPSG:4326) 만 쓰도록 했습니다.

🔗 그러니까, EPSG:5179로 받은 한국 공공데이터를 표준 GeoJSON에 그냥 담을 수 없습니다. 반드시 좌표 변환을 거쳐 4326으로 바꿔서 넣어야 해요. "GeoJSON이면 좌표계는 물어볼 필요 없이 무조건 위경도" — 이 단순함이 오히려 최고의 장점입니다.

항목2008 초기 스펙RFC 7946 (2016)
좌표계crs로 아무거나 지정WGS84(4326) 하나로 고정
좌표 순서[경도, 위도][경도, 위도(, 고도)] 로 명문화
폴리곤 감기 방향애매오른손 법칙(바깥 링은 반시계) 권장
날짜변경선규정 없음경도 ±180° 에서 잘라서 표현

폴리곤 '감기 방향'이 낯설 텐데, 어렵지 않아요. 면(Polygon)을 그릴 때 바깥 경계는 반시계 방향, 구멍은 시계 방향으로 좌표를 나열하자는 약속입니다(오른손 법칙, right-hand rule). 이래야 프로그램이 "어디가 안이고 어디가 밖(구멍)인지" 헷갈리지 않거든요. 도넛 모양 행정구역 같은 걸 그릴 때 특히 중요합니다.

왜 다들 GeoJSON을 쓸까 — 웹의 승리

여기까지 오면 GeoJSON이 왜 웹 지도의 사실상 표준이 됐는지 자연스럽게 보입니다. 장점을 한데 모으면 이렇습니다.

한국에서도 그대로 통합니다. 공공데이터포털이나 VWorld에서 데이터를 받을 때, 요즘은 Shapefile과 함께 GeoJSON 내려받기를 제공하는 경우가 많아요. 받아서 브라우저에 던지면 바로 지도가 됩니다. QGIS에서 Shapefile을 열어 "다른 이름으로 저장 → GeoJSON"만 해도 되고요.

편리함의 대가 — 커지는 순간 무너진다

그런데 세상에 공짜는 없습니다. GeoJSON의 모든 장점이 뿌리내린 그 "텍스트" 라는 성질이, 파일이 커지는 순간 고스란히 약점으로 돌변합니다.

① 텍스트라 뚱뚱하다. 좌표 하나가 126.9779 같은 글자로 저장됩니다. 게다가 "type", "coordinates", "properties" 같은 키가 피처마다 똑같이 반복돼요. 같은 정보를 바이너리로 담으면 몇 바이트면 될 걸, 수십 바이트씩 쓰는 셈입니다.

② 공간 인덱스가 없다. Shapefile엔 그래도 .shx 인덱스가 있었죠. GeoJSON엔 그런 게 없습니다. "화면에 보이는 이 영역의 피처만 줘"라고 못 해요. 딱 한 개를 찾으려 해도 파일 전체를 처음부터 끝까지 읽어야 합니다.

③ 통째로 메모리에 올려야 한다. 인덱스가 없으니 브라우저는 파일을 전부 다운로드하고, 전부 파싱해서, 전부 메모리에 얹은 다음에야 첫 점 하나를 그릴 수 있습니다. 그래서 전국 건물처럼 덩치 큰 데이터를 GeoJSON으로 넘기면 — 수 GB짜리 텍스트가 브라우저 탭을 그대로 잡아먹습니다.

💡 여기에 조용한 낭비 하나가 더 있어요. 좌표 정밀도입니다. 위경도 소수점 6자리면 약 11cm 정확도예요. 웬만한 지도엔 차고 넘칩니다. 그런데 JSON 직렬화기는 종종 37.56630000000001처럼 소수점 15자리를 뱉습니다. 15자리는 사실상 원자 단위 정밀도 — 아무 의미 없는 자릿수가 파일을 몇 배로 부풀리는 거죠.

정리하면, GeoJSON은 작고 가벼운 데이터엔 최고지만, 크고 무거운 데이터엔 최악입니다. 딱 이 지점에서, 사람들은 "텍스트의 편함은 버리더라도 바이너리 + 공간 인덱스를 얹은 후계자"를 찾기 시작합니다. (그 이야기는 곧 이어집니다.)

메모장으로 여는 지도 — 웹 표준 GeoJSON의 구조와 [경도, 위도]의 비밀 이미지

그래서 다듬어 쓴다 — 실무 요령

후계자로 갈아타기 전에, GeoJSON을 계속 쓰면서 살을 빼는 방법도 있습니다. 실무에서 자주 쓰는 처방 몇 가지예요.

좌표 정밀도를 잘라라. 위에서 본 15자리 낭비를 없애는 것만으로 용량이 확 줍니다. Shapefile 편에서 만난 GDAL의 ogr2ogr로 한 줄이면 됩니다.

bash
# 좌표를 소수점 6자리(약 11cm)로 잘라 GeoJSON으로 저장
ogr2ogr -f GeoJSON seoul.geojson seoul.shp \
  -lco COORDINATE_PRECISION=6

단순화하라. 화면에 1px도 안 되는 해안선 굴곡을 굳이 다 담을 필요는 없죠. 지오메트리를 성기게 다듬으면 용량이 크게 줍니다. (이 '단순화'는 나중에 PostGIS의 ST_Simplify 편에서 제대로 다룹니다.)

스트리밍 포맷을 써라. 피처를 한 줄에 하나씩 쌓는 줄바꿈 구분 GeoJSON(GeoJSONSeq, .geojsonl)을 쓰면, 파일 전체를 메모리에 올리지 않고 한 줄씩 흘려 읽을 수 있습니다.

TopoJSON을 고려하라. GeoJSON의 확장 격인 TopoJSON은, 이웃한 행정구역이 맞닿은 경계선을 한 번만 저장(위상 공유)해 용량을 크게 줄입니다. D3 같은 시각화에서 특히 사랑받죠. (자세한 건 뒤의 데이터 포맷 편에서 다룹니다.)

정리 — 한 문장으로

GeoJSON은 '메모장으로 열리는 지도'다. 도형과 속성을 텍스트 한 덩어리에 담아 웹의 표준이 됐지만, 좌표는 [경도, 위도] 순서이고, 파일이 커지는 순간 인덱스 없는 텍스트라는 약점이 그대로 드러난다.

ShapefileGeoJSON
파일 구성3~5개 묶음텍스트 하나
사람이 읽나아니오(바이너리)예(그냥 읽힘)
좌표계자유(.prj)WGS84(4326) 고정
좌표 순서도구마다항상 [경도, 위도]
큰 파일2GB 천장인덱스 없어 무너짐

메모장으로 열리는 편함의 대가가 '큰 파일에서의 붕괴'라는 걸 봤으니, 다음 질문은 분명합니다. 텍스트의 가독성은 살리면서, 도형·속성·인덱스를 한 파일에 야무지게 담을 순 없을까? 다음 편은 그 답으로 나온 포맷, GeoPackage입니다. 놀랍게도 우리가 아는 그 SQLite 데이터베이스 한 파일에 지도를 통째로 담는, 발상의 전환을 만나 봅니다.

참고