Google Maps JavaScript APIを使うと、Vueアプリに地図を埋め込み、店舗や拠点をマーカーで示せます。ただし、古い解説のコードをそのまま使うと、非推奨のAPIを使うことになったり、課金設定やAPIキーの制限でつまずいたりします。この記事では、現在の推奨に沿った実装を、Vue 3の動くコードで解説します。
この記事の構成は、①APIキーの取得と制限、②スクリプトの動的読み込み、③地図コンポーネント(propsでマーカーを受け取る)の3つです。マーカーには、非推奨のgoogle.maps.Markerではなく、AdvancedMarkerElementを使います。
事前準備:APIキーと課金設定
Google Cloud Consoleで、次の準備をします。
- プロジェクトを作成する
- 請求先アカウントを設定する(Google Maps Platformの利用に必須です。未設定だと地図が正常に表示されません)
- 「Maps JavaScript API」を有効にする
- APIキーを発行し、制限をかける
料金や無料枠は、改定されることがあります。利用前に、公式の料金ページで最新の内容を確認してください。
APIキーは公開される前提で制限する
「.envに入れれば漏洩を防げる」と説明されることがありますが、これは正確ではありません。ViteなどのフロントエンドのビルドでAPIキーを読み込むと、完成したJavaScriptにキーが含まれ、ブラウザから見えてしまいます。.envに入れる目的は、キーをGitに載せないことです。
# .env.local(Gitに入れない) VITE_GOOGLE_MAPS_API_KEY=取得したAPIキー
実際の防御は、Cloud Consoleでの制限です。
- アプリケーションの制限:「ウェブサイト(HTTPリファラー)」を選び、自分のドメイン(例:
https://example.com/*)だけを許可します。開発用にhttp://localhost:*も追加します。 - APIの制限:使うAPI(Maps JavaScript API)だけに絞ります。
環境ごとにキーを分ける方法は、環境ごとにAPIのURLを切り替える方法をご覧ください。
スクリプトを動的に読み込む
旧来の解説は、index.htmlに<script>タグを書きます。この方法では、コンポーネントが動くときにまだ読み込みが終わっておらず、google is not definedになることがあります。また、地図を使わないページでも読み込みが走ります。
そこで、地図が必要になった時点で読み込み、完了を待つ関数を用意します。loading=asyncを付けて読み込むと、google.maps.importLibrary()で必要なライブラリだけを取り出せます。
// utils/loadGoogleMaps.js
let promise = null
export function loadGoogleMaps(apiKey) {
// すでに読み込み済みなら、何もしない
if (window.google?.maps?.importLibrary) return Promise.resolve()
if (promise) return promise
promise = new Promise((resolve, reject) => {
window.__onGoogleMapsLoaded = resolve
const script = document.createElement('script')
script.src =
'https://maps.googleapis.com/maps/api/js' +
`?key=${apiKey}&v=weekly&loading=async&callback=__onGoogleMapsLoaded`
script.async = true
script.onerror = () => {
promise = null // 失敗したら、次回やり直せるようにする
reject(new Error('Google Mapsの読み込みに失敗しました'))
}
document.head.append(script)
})
return promise
}
地図コンポーネントを作る
緯度・経度・マーカーをpropsで受け取り、マーカーが変わったら描き直します。
<script setup>
import { ref, shallowRef, onMounted, onBeforeUnmount, watch } from 'vue'
import { loadGoogleMaps } from '@/utils/loadGoogleMaps'
const props = defineProps({
center: { type: Object, default: () => ({ lat: 35.681236, lng: 139.767125 }) }, // 東京駅
zoom: { type: Number, default: 14 },
locations: { type: Array, default: () => [] } // [{ name, lat, lng }]
})
const container = ref(null)
const error = ref('')
// 地図オブジェクトはVueのリアクティブにしない(shallowRef)
const map = shallowRef(null)
let markers = []
let AdvancedMarkerElement = null
let InfoWindow = null
function renderMarkers() {
markers.forEach((m) => (m.map = null)) // 古いマーカーを地図から外す
markers = props.locations.map((loc) => {
const marker = new AdvancedMarkerElement({
map: map.value,
position: { lat: loc.lat, lng: loc.lng },
title: loc.name
})
const info = new InfoWindow({ content: loc.name })
marker.addListener('click', () => info.open({ anchor: marker, map: map.value }))
return marker
})
}
onMounted(async () => {
try {
await loadGoogleMaps(import.meta.env.VITE_GOOGLE_MAPS_API_KEY)
const { Map, InfoWindow: IW } = await google.maps.importLibrary('maps')
const { AdvancedMarkerElement: AME } = await google.maps.importLibrary('marker')
AdvancedMarkerElement = AME
InfoWindow = IW
map.value = new Map(container.value, {
center: props.center,
zoom: props.zoom,
mapId: 'DEMO_MAP_ID' // AdvancedMarkerElementに必須。本番ではCloud Consoleで作成したIDに
})
renderMarkers()
} catch (e) {
error.value = '地図を読み込めませんでした。'
}
})
watch(() => props.locations, () => map.value && renderMarkers(), { deep: true })
onBeforeUnmount(() => {
markers.forEach((m) => (m.map = null))
markers = []
})
</script>
<template>
<p v-if="error" role="alert">{{ error }}</p>
<div ref="container" class="map"></div>
</template>
<style scoped>
/* 高さを指定しないと0pxになり、地図が表示されない */
.map { width: 100%; height: 400px; }
</style>
ポイントは次のとおりです。
- mapIdが必須:
AdvancedMarkerElementを使うには、地図の作成時にmapIdが必要です。動作確認ならDEMO_MAP_IDが使えます。本番では、Cloud Consoleの「地図の管理」で作成したIDを指定します。 - shallowRefを使う:地図のオブジェクトを
refで包むと、Vueが深くProxy化してしまい、不具合の原因になります。shallowRefか、通常の変数で持ちます。 - 緯度と経度の順番:
{ lat, lng }です。逆にすると、まったく別の場所が表示されます。 - 後片付け:コンポーネントが消えるときは、マーカーを
map = nullで地図から外します。 - 高さの指定:地図を入れる要素に高さがないと、0pxになって何も表示されません。
<script setup>
import GoogleMap from '@/components/GoogleMap.vue'
const shops = [
{ name: '東京駅', lat: 35.681236, lng: 139.767125 },
{ name: '上野駅', lat: 35.713768, lng: 139.777254 }
]
</script>
<template>
<GoogleMap :locations="shops" :zoom="12" />
</template>
使う側では、マーカーの配列を渡すだけです。共通の処理を切り出す考え方はComposable関数でロジックを共通化する方法でも解説しています。
Markerは非推奨になっています
古い解説で使われているgoogle.maps.Markerは、2024年2月から非推奨になり、AdvancedMarkerElementへの移行が案内されています。現時点でも動きますが、コンソールに警告が出ます。新しく作るなら、最初からAdvancedMarkerElementを使いましょう。
// NG: 非推奨。コンソールに警告が出る
new google.maps.Marker({ position, map, title })
// OK: AdvancedMarkerElement(mapIdが必要)
const { AdvancedMarkerElement } = await google.maps.importLibrary('marker')
new AdvancedMarkerElement({ position, map, title })
よくあるエラーと原因
- google is not defined:スクリプトの読み込みが終わる前に、
googleを使っています。上のloadGoogleMapsで完了を待ちます。 - BillingNotEnabledMapError:請求先アカウントが設定されていません。
- ApiNotActivatedMapError:Maps JavaScript APIが有効になっていません。
- RefererNotAllowedMapError:HTTPリファラーの制限に、今のURLが含まれていません。開発中は
localhost、本番は実際のドメインを許可してください。 - 地図が真っ白、または見えない:地図を入れる要素の高さが0です。
エラーの詳細は、ブラウザの開発者ツールのコンソールに出ます。
表示を軽くするコツ
地図は、ページの初期表示とは関係のない、大きなスクリプトを読み込みます。画面の下のほうにある地図は、見える位置に来てから読み込むと、初期表示が軽くなります。仕組みは画像の遅延読み込みと同じで、コードスプリッティングと遅延読み込みとも相性がよい使い方です。
よくある質問(FAQ)
marker.addListener('click', ...)で処理を登録し、InfoWindowを開く方法が基本です。まとめ
Vue 3でGoogle Mapsを使うときは、①請求先の設定とAPIキーの制限、②スクリプトの動的読み込み、③AdvancedMarkerElementとmapId、の3点が要になります。APIキーはブラウザから見える前提で、リファラーとAPIの制限で守ってください。地図のオブジェクトはshallowRefで持ち、不要になったマーカーは外す、という点も押さえておくと、安定して動きます。

