· 6분 읽기
웹 지도 개발 실전: Mapbox GL 인터랙션과 Turf.js 공간 연산
웹 지도 서비스는 대개 두 축으로 나뉩니다. 사용자가 보는 화면을 그리는 렌더링 레이어와, 좌표를 다루는 공간 연산 레이어입니다. Mapbox GL JS는 전자를, Turf.js는 후자를 담당하며 실무에서는 두 도구가 항상 짝을 이뤄 동작합니다. 이 글은 Mapbox GL의 레이어·상태 관리 패턴을 먼저 정리하고, Turf.js v7의 폴리곤 연산 규칙을 이어서 다룬 뒤, 두 도구를 하나의 파이프라인으로 연결하는 방법까지 살펴봅니다.
1부: Mapbox GL JS 레이어와 상태 관리
Mapbox GL JS 실무 구현의 핵심은 화려한 시각효과가 아니라 "레이어 구조와 상태 관리"입니다. 아래 순서로 설계하면 이후 기능이 늘어나도 구조가 무너지지 않습니다.
- 데이터 모델 정의(GeoJSON 속성 스키마)
- source/layer 분리 설계
- hover/selected 상태를 feature-state로 관리
- 줌/이동 시 데이터 갱신 전략 수립
패턴 1: 격자(Grid) 레이어
- 현재 뷰포트 기준으로 격자를 동적으로 생성합니다.
fill+line이중 레이어로 표현합니다.- 클릭 선택 상태는
feature-state에 저장합니다.
뷰포트가 넓어질수록 셀 수가 폭증하므로, 줌 레벨별로 셀 크기를 조정하는 로직이 반드시 필요합니다.
패턴 2: 영역(Polygon) 레이어
- 수치 속성을 기준으로 색상을 보간(interpolate)합니다.
- 라벨 레이어는 축소/확대 시 가독성 규칙을 별도로 적용합니다.
- hover 시 outline을 강화하고 tooltip/popup을 연동합니다.
격자 레이어와 영역 레이어는 역할이 다릅니다. 격자는 균일한 단위 분석에, 영역은 실제 경계(행정구역 등) 표현에 적합하며, 두 레이어를 같은 지도에서 동시에 쓸 때는 z-order와 상호작용 우선순위를 명확히 정해야 합니다.
성능 팁
- 대형 GeoJSON은 타일화/클러스터링을 고려합니다.
- 잦은
setData호출에는 디바운스를 적용합니다. - 이벤트 핸들러에서 DOM 작업을 최소화합니다.
- 모바일 GPU 성능을 기준으로 스타일 강도를 조절합니다.
2부: Turf.js v7 폴리곤 공간 연산
Turf.js는 GeoJSON Feature를 입력으로 받는 공간 연산 라이브러리입니다. 좌표계는 보통 WGS84(경도, 위도) 기준이며, 폴리곤의 링은 닫혀 있어야 합니다. Mapbox GL이 그려낸 격자·영역 데이터에 union, difference, intersect 같은 집합 연산을 적용할 때 Turf.js를 사용합니다.
설치:
npm install @turf/helpers @turf/union @turf/difference @turf/intersect @turf/area
샘플 데이터:
import { polygon, featureCollection } from '@turf/helpers';
const a = polygon([[
[0, 0], [2, 0], [2, 2], [0, 2], [0, 0],
]]);
const b = polygon([[
[1, 1], [3, 1], [3, 3], [1, 3], [1, 1],
]]);
합집합 union
import { union } from '@turf/union';
const merged = union(featureCollection([a, b]));
겹치지 않는 경우 결과가 MultiPolygon일 수 있습니다. 입력이 많으면 먼저 단순화한 뒤 연산하는 편이 안정적입니다.
차집합 difference
import { difference } from '@turf/difference';
const onlyA = difference(featureCollection([a, b]));
b가 a를 완전히 덮으면 결과는 null입니다. 반환값이 null일 수 있다는 전제를 항상 코드에 반영해야 합니다.
교집합 intersect
import { intersect } from '@turf/intersect';
const overlap = intersect(featureCollection([a, b]));
if (!overlap) {
console.log('겹치는 영역 없음');
}
결과 검증
import area from '@turf/area';
if (overlap) {
console.log(area(overlap));
}
공간 연산 결과는 반드시 면적/좌표 검증을 함께 두는 것이 안전합니다.
입력 검증 규칙
Turf 연산 자체는 간단하지만, 실패의 대부분은 입력 품질 문제에서 비롯됩니다. 다음 세 가지를 기본 규칙으로 둡니다.
- null 처리:
difference,intersect는 결과가null일 수 있으므로 호출 직후 분기 처리합니다. - 좌표 유효성: 자기 교차(self-intersection) 폴리곤은 연산 전에 정리합니다.
- 후처리 검증: 연산 결과의 면적·좌표를 다시 확인해 빈 결과나 왜곡된 형태를 걸러냅니다.
폴리곤이 매우 복잡하면 연산 전 단순화(simplify)를 적용하고, 클라이언트 연산이 무거우면 서버(PostGIS)로 오프로드하는 것도 고려합니다.
3부: 연산 결과를 지도 레이어에 반영하기
Turf.js 연산 결과는 그 자체로는 데이터일 뿐이며, 실제 가치는 Mapbox GL 레이어에 반영될 때 나타납니다. 예를 들어 격자·영역 레이어에서 선택한 여러 feature의 GeoJSON을 union으로 병합한 뒤 별도 소스(source)에 setData로 반영하면, 앞서 다룬 색상 보간과 outline 강조 패턴을 그대로 재사용해 "선택 영역 하이라이트" 같은 상호작용을 구성해볼 수 있습니다. 마찬가지로 intersect/difference 결과를 별도 레이어로 분리해 겹치는 부분과 겹치지 않는 부분에 다른 스타일을 적용하는 것도 가능합니다. 다만 이 두 연산은 결과가 null일 수 있으므로, 두 도구를 연결하는 코드에서는 항상 빈 FeatureCollection으로 초기화하는 방어 처리를 함께 넣는 것이 안전합니다.
정리
지도 UX의 품질은 복잡한 시각화보다 "일관된 상태 전이"에서 나옵니다. Mapbox GL에서는 데이터 스키마와 레이어 책임을 먼저 분리해 두어야 유지보수가 쉬워지고, Turf.js에서는 null 처리, 좌표 유효성, 후처리 검증을 기본 규칙으로 지켜야 연산 결과를 안정적으로 신뢰할 수 있습니다. 두 도구를 연결할 때는 공간 연산 결과를 지도 소스에 반영하는 지점에서 항상 빈 결과 처리를 함께 설계하는 것이 실무 안정성의 핵심입니다.