こんにちは、皆さん!
最近、フロントエンドの開発から少し離れていたんですが、今日はNext.js × TypeScript × Google Maps APIを試してみたお話を共有したいと思います。
まずはじめに
突然ですが、皆さんは地図を使ったWebアプリケーションを作ったことはありますか?Google Maps APIを使うと、驚くほど簡単に地図を表示したり、マーカーを設置したりできます。今回は、Next.jsとTypeScriptを組み合わせて、Google Mapsを表示するシンプルなコンポーネントを作ってみました。
開発のポイント
- Next.js: サーバーサイドレンダリングやルーティングが簡単に行えるReactフレームワーク。
- TypeScript: 型安全なコードを書くことで、バグを減らし開発効率を上げる。
- Google Maps API: 豊富な地図機能を持つGoogleのAPI。
Google Maps APIキーの取得方法
まず、Google Mapsを利用するためにはGoogle Maps JavaScript APIキーが必要になります。取得方法は以下の手順で進めていきますが、もし手順が最新のものと異なる場合は、公式のGoogleの指示に従ってくださいね。
- Google Cloud Consoleにアクセス
- Google Cloud Platformのコンソールにアクセスします。
- プロジェクトの作成
- ダッシュボードの上部にあるプロジェクトドロップダウンから「プロジェクトを作成」を選択します。
- プロジェクト名を入力し、「作成」をクリックします。
- APIライブラリの有効化
- 新しいプロジェクトのダッシュボードに移動し、「APIとサービス」から「ライブラリ」を選択します。
- 「Maps JavaScript API」を検索し、選択して有効にします。
- 認証情報の作成
- APIを有効にした後、画面上部の「認証情報」をクリックします。
- 「認証情報を作成」ボタンをクリックし、「APIキー」を選択します。
- APIキーの設定
- 新しいAPIキーが生成されます。このキーは後ほどプロジェクトの
.env
ファイルに保存します。
- 新しいAPIキーが生成されます。このキーは後ほどプロジェクトの
パッケージをインストール
まずNext.jsでGoogle Mapsを使用するにはパッケージをインストールしないといけないです。
npm i google-map-react
実装してみたコード
では、実際のコードを見ていきましょう。まずは型定義
type MapLoadedData = {
map: google.maps.Map;
maps: typeof google.maps;
};
type SpotInfo = {
id: number;
spotName: string;
introduction: string;
prefectures: string;
municipality: string;
address_etc: string;
lat: string;
lng: string;
};
type MapProps = {
spotInfo: SpotInfo[];
};
実装する
import React, { useEffect, useRef, useState } from 'react';
import GoogleMapReact from 'google-map-react';
const Map: React.FC<MapProps> = ({ spotInfo }) => {
const spotLatlngRef = useRef<{ lat: number; lng: number }[]>([]);
const [mapKey, setMapKey] = useState<string>('');
useEffect(() => {
spotLatlngRef.current = spotInfo
.filter((spot) => spot.lat && spot.lng)
.map((spot) => ({
lat: parseFloat(spot.lat) || 0,
lng: parseFloat(spot.lng) || 0,
}));
setMapKey(`${Date.now()}-${Math.random()}`);
}, [spotInfo]);
// デフォルトの場所を設定
const defaultProps = {
center: {
lat: 37.1478,
lng: 138.236,
},
zoom: 14,
};
// みたいスポットを描画
const getInfoWindowString = (spot: SpotInfo) => `
<div>
<span style="font-size: 16px; color: #333 ">
${spot.spotName}<br />
</span>
<span style="font-size: 13px; color: #888;">
${spot.prefectures}${spot.municipality}${spot.address_etc}
</span>
</div>`;
// APIを発火する
const handleApiLoaded = ({ map, maps }: MapLoadedData): void => {
const bounds = new maps.LatLngBounds();
const markers: google.maps.Marker[] = [];
const infowindows: google.maps.InfoWindow[] = [];
let activeInfoWindow: google.maps.InfoWindow | null = null;
spotLatlngRef.current.forEach((latlng, index) => {
const marker = new maps.Marker({
position: new maps.LatLng(latlng.lat, latlng.lng),
map: map,
label: {
color: 'white',
fontSize: '12px',
fontWeight: 'medium',
text: String(index + 1),
},
});
bounds.extend(marker.getPosition().toJSON() as google.maps.LatLngLiteral);
const infowindow = new maps.InfoWindow({
content: getInfoWindowString(spotInfo[index]),
});
marker.addListener('click', () => {
if (activeInfoWindow) {
activeInfoWindow.close();
}
infowindow.open(map, marker);
activeInfoWindow = infowindow;
});
markers.push(marker);
infowindows.push(infowindow);
});
setTimeout(() => {
map.fitBounds(bounds);
}, 0);
map.addListener('click', () => {
if (activeInfoWindow) {
activeInfoWindow.close();
}
});
// Mapのスタイル調整
map.setOptions({
styles: [
{
featureType: 'landscape',
elementType: 'all',
stylers: [
{
saturation: 0,
},
{
lightness: 0,
},
],
},
{
featureType: 'poi',
elementType: 'all',
stylers: [
{
visibility: 'off',
},
],
},
{
featureType: 'road',
elementType: 'all',
stylers: [
{
saturation: 0,
},
{
lightness: 0,
},
],
},
{
featureType: 'transit',
elementType: 'all',
stylers: [
{
visibility: 'off',
},
],
},
{
featureType: 'water',
elementType: 'all',
stylers: [
{
saturation: 0,
},
{
lightness: 20,
},
],
},
],
});
};
return (
<div id="map-container" className="h-96">
{mapKey && (
<GoogleMapReact
bootstrapURLKeys={{
key: process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY as string,
}}
key={mapKey}
defaultCenter={defaultProps.center}
defaultZoom={defaultProps.zoom}
onGoogleApiLoaded={({ map, maps }) => handleApiLoaded({ map, maps })}
yesIWantToUseGoogleMapApiInternals={true}
/>
)}
</div>
);
};
export default Map;
解説
再描画のための工夫
useEffect内でユニークなキーを生成してmapKeyとして状態に保っています。これは、マップを再描画するため、key属性にこのユニークな値を渡すことで、GoogleMapReactコンポーネントが再レンダリングされるようになっています。
setMapKey(`${Date.now()}-${Math.random()}`);
マーカーと情報ウィンドウの設定
各スポットの緯度経度を`spotLatlngRef`に格納し、`handleApiLoaded`関数内でマーカーと情報ウィンドウを設定しています。マーカーをクリックすると対応する情報ウィンドウが表示され、他の情報ウィンドウは閉じるようにしています。
marker.addListener('click', () => {
if (activeInfoWindow) {
activeInfoWindow.close();
}
infowindow.open(map, marker);
activeInfoWindow = infowindow;
});
マップのスタイル設定
map.setOptionsでマップのスタイルをカスタマイズしています。今回はシンプルに各要素の彩度や明度を調整し、不必要なポイやトランジット情報を非表示にしています。
map.setOptions({
styles: [
// スタイル設定の配列
],
});
感想
最初はうまく描画されずに苦戦しましたが、再描画のタイミングに着目してユニークキーを使うことで解決しました。久しぶりのフロントエンド開発でしたが、意外とスムーズに進めることができて楽しかったです^^
まとめ
今回は、Next.jsとTypeScriptを使ってGoogle Mapsを表示するコンポーネントを作成しました。地図上に複数のマーカーを設置し、それぞれに情報ウィンドウを設定することで、インタラクティブな地図アプリケーションの基礎ができました。ほなまた〜〜〜〜