지도 및 위치 기반 기능
규칙
- Mapbox GL JS: 풍부한 대화형 지도 제공 — React 래퍼로
react-map-gl 사용
- Leaflet: 무료 대안 — React 래퍼로
react-leaflet 사용, 기본 타일에는 API 키 불필요
- API 키 관리: 클라이언트 사이드 Mapbox용
NEXT_PUBLIC_MAPBOX_TOKEN, 서버 측 비밀은 절대 노출 금지
- 지오코딩: 주소를 좌표로 변환 — Mapbox Geocoding API 또는 OpenStreetMap Nominatim (무료)
- 마커 클러스터링: 100개 이상의 대규모 마커 데이터셋에는
supercluster 또는 Mapbox 내장 클러스터링 사용
- 매장 찾기 패턴: 사용자 위치 지오코딩 → Haversine/PostGIS로 가까운 매장 조회 → 지도에 표시
- 경로 표시: Mapbox Directions API로 운전/도보 경로 생성 → GeoJSON LineString 레이어로 렌더링
- 브라우저 위치 정보:
navigator.geolocation.getCurrentPosition() 사용 시 권한 거부 상황을 항상 우아하게 처리
- 지도 컴포넌트 지연 로딩: 지도는 무거우므로
dynamic(() => import("./Map"), { ssr: false }) 사용
패턴
import Map, { Marker, NavigationControl } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
function StoreMap({ stores }: { stores: Store[] }) {
return (
<Map
initialViewState={{ longitude: -122.4, latitude: 37.8, zoom: 11 }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
style={{ width: "100%", height: 400 }}
>
<NavigationControl position="top-right" />
{stores.map((store) => (
<Marker key={store.id} longitude={store.lng} latitude={store.lat}>
<Pin />
</Marker>
))}
</Map>
);
}
function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a = Math.sin(dLat / 2) ** 2 + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
피해야 할 사항
- 지도 컴포넌트를 서버사이드 렌더링(SSR)하지 말 것 — 항상
ssr: false 또는 "use client"로 로드
- 클러스터링 없이 1000개 이상의 마커를 렌더링하지 말 것 — 성능 저하 발생; supercluster 사용
- 좌표를 하드코딩하지 말 것 — 빌드 시 또는 필요 시점에 주소를 지오코딩
- 위치 정보 권한 거부를 무시하지 말 것 — 항상 대체 수단(수동 주소 입력) 제공