Next.js × TypeScript × Google Maps API を試す
2024-12-16
azblob://2024/12/16/eyecatch/2024-12-16-nextjs-engineering-googlemapapi-000.jpg

こんにちは、皆さん!
最近、フロントエンドの開発から少し離れていたんですが、今日は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の指示に従ってくださいね。

  1. Google Cloud Consoleにアクセス
    • Google Cloud Platformのコンソールにアクセスします。
  2. プロジェクトの作成
    • ダッシュボードの上部にあるプロジェクトドロップダウンから「プロジェクトを作成」を選択します。
    • プロジェクト名を入力し、「作成」をクリックします。
  3. APIライブラリの有効化
    • 新しいプロジェクトのダッシュボードに移動し、「APIとサービス」から「ライブラリ」を選択します。
    • 「Maps JavaScript API」を検索し、選択して有効にします。
  4. 認証情報の作成
    • APIを有効にした後、画面上部の「認証情報」をクリックします。
    • 「認証情報を作成」ボタンをクリックし、「APIキー」を選択します。
  5. APIキーの設定
    • 新しいAPIキーが生成されます。このキーは後ほどプロジェクトの.envファイルに保存します。

パッケージをインストール

まず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を表示するコンポーネントを作成しました。地図上に複数のマーカーを設置し、それぞれに情報ウィンドウを設定することで、インタラクティブな地図アプリケーションの基礎ができました。ほなまた〜〜〜〜