地図と位置情報
ルール
- 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: false または "use client" で読み込むこと
- クラスタリングなしで1000個以上のマーカーを表示 — パフォーマンスを著しく低下させるため、supercluster を使用
- 座標をハードコード — 住所はビルド時またはオンデマンドでジオコーディングすること
- 位置情報の権限拒否を無視 — 常にフォールバック(手動住所入力)を用意すること