Claude CodeでGoogle MapsをWebアプリに統合する実践ガイド
Claude CodeでGoogle Mapsを安全に組み込み、店舗検索・不動産・配送管理に使えるNext.js実装と落とし穴を解説。
地図機能は「表示」より先に設計する
Claude Codeに「Google Mapsを入れて」と頼むだけでも、地図は表示できます。けれど本番アプリで差が出るのは、APIキー、課金、住所検索、ユーザーの現在地、表示速度、利用規約までを先に設計できているかです。
Masaが店舗検索の試作で一度つまずいたのは、マーカー表示よりも住所検索でした。住所を座標へ変換するジオコーディングは便利ですが、同じ地名でも複数候補が返ることがあり、APIキーをブラウザに出すべき処理とサーバー側に置くべき処理を分けないと、あとでセキュリティと費用の両方を直すことになります。
この記事では、Claude Codeを「地図ウィジェットを貼る係」ではなく「地図機能の実装パートナー」として使う手順をまとめます。対象はNext.js + Reactです。Google Maps JavaScript API、Advanced Marker、Geocoding API、Mapbox GL JSの使い分けも扱います。
関連して、APIキーの扱いはClaude Codeセキュリティ監査、表示速度はClaude Codeパフォーマンス最適化、地理データの可視化はClaude Codeデータ可視化も合わせて確認してください。
Claude Codeに渡す実装ブリーフ
最初にClaude Codeへ渡すべき情報は、ライブラリ名ではなく運用条件です。地図はユーザー体験と費用が直結するため、次のように制約を明文化します。
Next.js App Routerで店舗検索ページを実装してください。
要件:
- Google Maps JavaScript APIを使う
- 従来のMarkerではなくAdvancedMarkerElementを使う
- APIキーはHTTP referrer制限を前提にNEXT_PUBLIC_GOOGLE_MAPS_API_KEYで読む
- Geocoding APIはサーバールートから呼び、GOOGLE_MAPS_SERVER_KEYをブラウザに出さない
- 住所検索、店舗リスト、マーカークリック、選択状態を同期する
- SSRでwindow/googleを参照しない
- エラー、ローディング、0件、権限拒否を画面状態として扱う
- 実装後にAPIキー制限、課金アラート、利用規約の確認項目を出す
全体像は次のように分けると、Claude Codeの出力をレビューしやすくなります。
flowchart LR
User["ユーザー"]
Page["店舗検索ページ"]
Map["Google Maps JS API"]
Route["/api/geocode"]
Google["Geocoding API"]
Store["店舗DBまたはJSON"]
Alerts["課金アラートとログ"]
User --> Page
Page --> Map
Page --> Store
Page --> Route
Route --> Google
Route --> Alerts
地図機能では専門用語も混ざります。ジオコーディングは「住所を緯度経度に変換すること」、逆ジオコーディングは「緯度経度から住所を推定すること」、マップIDは「Google Cloud Consoleで作る地図スタイルとAdvanced Marker用の識別子」と言い換えると、非エンジニアのレビューでも通じやすくなります。
Google MapsをNext.jsで読み込む
まず依存関係を入れます。型定義を入れておくと、Claude Codeが存在しないプロパティを使ったときに検出しやすくなります。
npm i @googlemaps/js-api-loader
npm i -D @types/google.maps
Googleの公式ドキュメントでは、Maps JavaScript APIのAdvanced MarkerにマップIDが必要です。開発中はDEMO_MAP_IDでも動作確認できますが、本番ではGoogle Cloud Consoleで作成したマップIDを使います。APIキーはフロントエンドに出ますが、これはMaps JavaScript APIの性質上避けられません。重要なのは、Google Maps Platformのセキュリティガイダンスに従ってHTTP referrer制限とAPI制限を必ず設定することです。
// src/lib/google-maps-loader.ts
import { Loader } from "@googlemaps/js-api-loader";
let googleMapsPromise: Promise<typeof google> | null = null;
export function loadGoogleMaps() {
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
if (!apiKey) {
throw new Error("NEXT_PUBLIC_GOOGLE_MAPS_API_KEY is missing");
}
if (!googleMapsPromise) {
const loader = new Loader({
apiKey,
version: "weekly",
libraries: ["marker", "places"],
});
googleMapsPromise = loader.load();
}
return googleMapsPromise;
}
次に地図コンポーネントです。ポイントは3つあります。windowやgoogleをサーバー側レンダリング中に触らないこと、マーカーを再描画するときに古いマーカーを消すこと、ユーザー由来のHTMLをInfoWindowへ文字列連結しないことです。
// src/components/GoogleBusinessMap.tsx
"use client";
import { useEffect, useRef } from "react";
import { loadGoogleMaps } from "@/lib/google-maps-loader";
export type MapPoint = {
id: string;
title: string;
lat: number;
lng: number;
category?: "store" | "warehouse" | "property";
};
type Props = {
points: MapPoint[];
center: google.maps.LatLngLiteral;
zoom?: number;
onSelect?: (point: MapPoint) => void;
};
export function GoogleBusinessMap({ points, center, zoom = 13, onSelect }: Props) {
const mapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let cancelled = false;
let markers: google.maps.marker.AdvancedMarkerElement[] = [];
async function renderMap() {
await loadGoogleMaps();
if (!mapRef.current || cancelled) return;
const { Map } = (await google.maps.importLibrary("maps")) as google.maps.MapsLibrary;
const { AdvancedMarkerElement, PinElement } = (await google.maps.importLibrary(
"marker",
)) as google.maps.MarkerLibrary;
const map = new Map(mapRef.current, {
center,
zoom,
mapId: process.env.NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID ?? "DEMO_MAP_ID",
fullscreenControl: false,
gestureHandling: "cooperative",
});
markers = points.map((point, index) => {
const pin = new PinElement({
glyph: String(index + 1),
background: point.category === "warehouse" ? "#0f766e" : "#2563eb",
borderColor: "#ffffff",
glyphColor: "#ffffff",
});
const marker = new AdvancedMarkerElement({
map,
position: { lat: point.lat, lng: point.lng },
title: point.title,
content: pin.element,
});
marker.addListener("click", () => onSelect?.(point));
return marker;
});
}
renderMap().catch((error) => {
console.error("Failed to render Google Map", error);
});
return () => {
cancelled = true;
markers.forEach((marker) => {
marker.map = null;
});
};
}, [center.lat, center.lng, points, zoom, onSelect]);
return <div ref={mapRef} className="h-[420px] w-full rounded-lg border" />;
}
このコンポーネントは「表示だけ」なら十分ですが、実運用では住所検索も必要になります。ここから先はサーバー側の処理を分けます。
住所検索はサーバールートに逃がす
ブラウザ用のMaps JavaScript APIキーと、サーバーからGeocoding APIを呼ぶキーは分けます。前者はHTTP referrer制限、後者はIP制限または実行環境に合わせた制限をかけます。Geocoding APIのレスポンス形式やステータスは公式のリクエストとレスポンスで確認できます。
// src/app/api/geocode/route.ts
import { NextResponse } from "next/server";
type GeocodeResponse = {
status: string;
error_message?: string;
results: Array<{
formatted_address: string;
place_id: string;
geometry: {
location: { lat: number; lng: number };
};
}>;
};
const endpoint = "https://maps.googleapis.com/maps/api/geocode/json";
export async function GET(request: Request) {
const key = process.env.GOOGLE_MAPS_SERVER_KEY;
const { searchParams } = new URL(request.url);
const address = searchParams.get("address")?.trim();
if (!key) {
return NextResponse.json({ error: "Server key is missing" }, { status: 500 });
}
if (!address || address.length > 180) {
return NextResponse.json({ error: "Address is required" }, { status: 400 });
}
const params = new URLSearchParams({
address,
key,
language: "ja",
region: "jp",
});
const response = await fetch(`${endpoint}?${params}`, { cache: "no-store" });
const data = (await response.json()) as GeocodeResponse;
const first = data.results[0];
if (!response.ok || data.status !== "OK" || !first) {
return NextResponse.json(
{ error: data.error_message ?? data.status },
{ status: data.status === "ZERO_RESULTS" ? 404 : 502 },
);
}
return NextResponse.json({
formattedAddress: first.formatted_address,
placeId: first.place_id,
location: first.geometry.location,
});
}
注意点はキャッシュです。住所から緯度経度への変換結果をどの程度保存できるかはGoogle Maps Platformの利用規約とプロダクトごとのポリシーに依存します。料金対策だけを理由に長期保存を入れるのではなく、店舗マスタの座標は自社データとして管理し、ユーザーが入力した住所検索は必要な範囲で扱う設計にします。
店舗検索UIを組み立てる
最後に、地図、店舗リスト、住所検索をつなぎます。Claude Codeにここまでのファイルを作らせたら、次のようなコンポーネントに分けるとレビューが簡単です。
// src/components/StoreLocator.tsx
"use client";
import { useMemo, useState } from "react";
import { GoogleBusinessMap, type MapPoint } from "@/components/GoogleBusinessMap";
type Store = {
id: string;
name: string;
address: string;
hours: string;
phone: string;
position: google.maps.LatLngLiteral;
};
type GeocodeResult = {
formattedAddress: string;
placeId: string;
location: google.maps.LatLngLiteral;
};
const tokyoStation = { lat: 35.681236, lng: 139.767125 };
export function StoreLocator({ stores }: { stores: Store[] }) {
const [query, setQuery] = useState("");
const [center, setCenter] = useState(stores[0]?.position ?? tokyoStation);
const [selectedId, setSelectedId] = useState(stores[0]?.id ?? "");
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
const points = useMemo<MapPoint[]>(
() =>
stores.map((store) => ({
id: store.id,
title: store.name,
lat: store.position.lat,
lng: store.position.lng,
category: "store",
})),
[stores],
);
async function searchAddress() {
if (!query.trim()) return;
setStatus("loading");
const response = await fetch(`/api/geocode?address=${encodeURIComponent(query)}`);
if (!response.ok) {
setStatus("error");
return;
}
const result = (await response.json()) as GeocodeResult;
setCenter(result.location);
setStatus("idle");
}
return (
<section className="grid gap-4 lg:grid-cols-[320px_1fr]">
<aside className="space-y-3">
<div className="flex gap-2">
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => event.key === "Enter" && searchAddress()}
placeholder="駅名・住所で検索"
className="min-w-0 flex-1 rounded-md border px-3 py-2"
/>
<button onClick={searchAddress} className="rounded-md bg-blue-600 px-4 py-2 text-white">
検索
</button>
</div>
{status === "error" && <p className="text-sm text-red-600">住所を見つけられませんでした。</p>}
<div className="max-h-[420px] space-y-2 overflow-auto">
{stores.map((store) => (
<button
key={store.id}
onClick={() => {
setSelectedId(store.id);
setCenter(store.position);
}}
className={`w-full rounded-md border p-3 text-left ${
selectedId === store.id ? "border-blue-500 bg-blue-50" : "bg-white"
}`}
>
<span className="block font-medium">{store.name}</span>
<span className="block text-sm text-gray-600">{store.address}</span>
<span className="block text-sm text-gray-500">{store.hours}</span>
</button>
))}
</div>
</aside>
<GoogleBusinessMap
points={points}
center={center}
zoom={14}
onSelect={(point) => setSelectedId(point.id)}
/>
</section>
);
}
このコードはコピペの土台として動かせるようにしていますが、実プロジェクトではCSSクラス、店舗データ取得、フォームのアクセシビリティ、エラー表示をデザインシステムに寄せてください。
Mapboxを選ぶべきケース
Google Mapsは店舗検索、経路案内、住所検索、プレイス情報に強い選択肢です。一方、独自データを大量に重ねる、ブランドに合わせて地図スタイルを細かく作る、WebGLで滑らかに地理データを動かすならMapbox GL JSも候補になります。Mapboxの基本はMapbox GL JSガイドで確認できます。
| 観点 | Google Maps | Mapbox GL JS |
|---|---|---|
| 店舗検索 | PlacesやGeocodingと組み合わせやすい | 自前データ中心なら柔軟 |
| 見た目の自由度 | Cloud Stylingで調整 | スタイル設計の自由度が高い |
| 学習コスト | 情報が多く導入しやすい | レイヤー、ソース、タイルの理解が必要 |
| 注意点 | APIキー制限と課金監視が必須 | トークン制限と帰属表示が必須 |
// src/components/MapboxPreview.tsx
"use client";
import { useEffect, useRef } from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
export function MapboxPreview() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
if (!containerRef.current || !token) return;
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({
container: containerRef.current,
style: "mapbox://styles/mapbox/streets-v12",
center: [139.767125, 35.681236],
zoom: 12,
});
map.addControl(new mapboxgl.NavigationControl(), "top-right");
return () => map.remove();
}, []);
return <div ref={containerRef} className="h-[420px] w-full rounded-lg border" />;
}
Claude Codeには「Google Maps版とMapbox版を両方作って」ではなく、「住所検索はGoogle、独自レイヤーの可視化はMapbox」など役割を切って頼む方が失敗しにくいです。
3つ以上の実用ユースケース
1つ目は店舗検索です。支店、クリニック、教室、イベント会場の検索では、住所入力、現在地、営業時間、電話番号、予約CTAが重要です。Masaの試作では、最初に地図を大きく出しすぎてスマホで店舗リストが見えなくなりました。実際には「地図5割、リスト5割」よりも、スマホではリストを先に出して選択時に地図へ寄せる方が使いやすい場面が多いです。
2つ目は不動産や宿泊施設の検索です。価格、駅距離、面積、空室状況などの条件と地図を同期します。この場合はマーカーを全部出すより、ズームレベルに応じてクラスタリングし、リストの並びと地図の表示範囲を一致させることが重要です。Claude Codeには「地図に見えている物件だけを一覧に出す」条件を明示します。
3つ目は配送、訪問営業、保守点検の管理画面です。ここでは見た目よりも更新頻度と権限が大切です。ドライバーの現在地をリアルタイム表示するなら、位置情報の同意、保存期間、閲覧権限、異常時のログを先に決めます。ルート最適化まで行う場合は、Directions APIやRoutes APIの費用と制限も確認してから実装します。
4つ目を挙げるなら、記事や観光コンテンツの地図化です。旅行記事、地域ガイド、イベントレポートに地点データを付けると回遊率が上がります。ただし、地図だけではSEO本文が薄くなるので、本文には現地での判断材料、行き方、注意点を残すのが大切です。
具体的な落とし穴
一番危険なのは、制限なしのAPIキーを公開することです。Maps JavaScript API用のキーはブラウザに出ますが、HTTP referrer制限とAPI制限を設定すれば被害をかなり抑えられます。Geocoding APIなどサーバーから呼ぶキーは、ブラウザに出さず、環境変数と実行元制限で守ります。
次に多いのは、古いMarker実装をそのまま採用することです。GoogleはAdvanced Markerを推奨しており、Advanced Markerの公式ガイドではAdvancedMarkerElementとマップIDを使う流れが示されています。Claude Codeが古いサンプルを出したら、Advanced Markerに置き換えるよう指示します。
3つ目はSSRの失敗です。Next.jsでgoogle.maps.Mapをトップレベルに書くと、サーバー側でgoogle is not definedになります。地図コンポーネントは"use client"にし、useEffect内でロードします。
4つ目は住所検索の曖昧さです。「中央区」「銀座」だけでは複数候補が返ります。regionやlanguage、都道府県や国の入力補助を入れ、候補が曖昧な場合はユーザーに選ばせます。住所をそのままDBの主キーにしないことも重要です。
5つ目は表示速度です。大量のマーカーを一度に出すと、地図もリストも重くなります。100件を超えるならクラスタリング、表示範囲内の取得、ページングを検討します。Claude Codeには「最初から全件描画しない」と明記してください。
6つ目はプライバシーです。現在地取得はブラウザの許可が必要です。拒否されたときに壊れないUI、現在地を保存しない設計、保存する場合の説明を用意します。
Claude Codeでレビューするチェックリスト
実装が出てきたら、次の観点で批判的にレビューします。
- APIキーが
.env.localから読まれているか - ブラウザ用キーとサーバー用キーが分かれているか
- Google Cloud ConsoleでAPI制限とアプリケーション制限を設定する手順が残っているか
AdvancedMarkerElementを使っているかgoogleやwindowをSSR中に参照していないか- 住所検索の0件、曖昧な住所、APIエラーを画面で扱っているか
- ユーザー入力をHTML文字列として
InfoWindowへ流していないか - モバイルで地図が画面を占有しすぎていないか
- 課金アラート、クォータ、ログ確認が運用手順に含まれているか
このリストをそのままClaude Codeへ渡し、「上の実装をこの観点でレビューして、修正パッチを出して」と頼むと、地図が表示されるだけの実装から本番に近い実装へ近づきます。
まとめと検証結果
Claude CodeでGoogle Mapsを統合するときの要点は、地図表示、住所検索、APIキー、課金、プライバシーを別々の責務として扱うことです。Google Mapsは店舗検索や住所検索に強く、Mapboxは独自データの表現に強いです。どちらを選ぶ場合も、公式ドキュメントと利用規約を確認し、キー制限とエラー処理を先に入れるのが安全です。
この記事で紹介した内容を実際に試した結果、APIキーを入れない状態でもNext.js側の責務分割、SSR回避、コードフェンス、エラー分岐は確認できました。実キーを使う段階では、Google Cloud ConsoleでMaps JavaScript API、Geocoding API、HTTP referrer制限、サーバーキー制限、予算アラートを設定してから接続するのが最短です。まずは店舗データ3件で小さく動かし、検索、マーカークリック、スマホ表示、請求メトリクスを確認してから本番データへ広げてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。