Use Cases (更新: 2026/6/6)

「現在地を使う」で離脱されない位置情報の実装 — 許可・精度・プライバシー

Claude CodeにGeolocation実装を任せると許可拒否で離脱しがち。許可のタイミング、精度設定、拒否時の手入力、座標を残さないログ設計を実体験から解説。

「現在地を使う」で離脱されない位置情報の実装 — 許可・精度・プライバシー

「近くの店舗を出すだけだから」と軽く考えて、現在地ボタンをページの一番上に置いたことがあります。

公開した翌日、許可ダイアログの拒否率を見て青ざめました。8割が「ブロック」。位置を取る前に離脱する人まで含めると、機能としてほぼ死んでいたんです。

賢いコードを書いたつもりでした。でも問題はコードじゃなく、いつ・なぜ位置を聞くかを設計していなかったことでした。

Claude Codeに「現在地を取るボタンを作って」とだけ頼むと、だいたい getCurrentPosition を呼ぶだけのコードが返ってきます。デモなら満点。でも公開サービスでこれをそのまま出すと、僕がやった「拒否率8割」を高確率で再現します。位置情報は便利ですが、ユーザーにとっては相当に敏感な情報だからです。

この記事では、ブラウザ標準のGeolocation APIを「拒否されにくく、データを残さず、テストできる」形で実装する手順を、失敗込みでまとめます。

この記事の要点

  • 許可は機能を使う直前に求める。ページ表示直後のダイアログは拒否されやすい。これだけで拒否率が大きく変わる。
  • 精度は初期値を高精度OFFに。enableHighAccuracy: true は遅くて電池を食う。店舗検索なら粗い位置で足りる。
  • 拒否は異常ではなく正常なユーザー選択。郵便番号・駅名の手入力fallbackを必ず横に置く。
  • 緯度経度はそのままログに残さない。小数2桁に丸めるか、イベント名だけ記録する。
  • watchPosition を使ったら clearWatch必ずクリーンアップに入れる。停止忘れは電池とログを無駄に食う。

まず「何を聞くか」を決める

Geolocation APIは、ブラウザに「この端末の位置を教えてほしい」と頼むためのAPIです。位置の取得元はGPS、Wi-Fi、携帯基地局、IPアドレス、端末設定などで、こちらが「GPSで取って」と直接指定できるものではありません。W3Cの仕様も、APIが実際の位置を保証するものではないと明記しています。

コードを書く前に、僕はいつもこの4つを先に決めます。ここを飛ばすと、後でプライバシーまわりの手戻りが必ず出ます。

決めること僕の初期値理由
いつ許可を求めるか機能を使う直前ページ表示直後の要求は拒否されやすい
精度高精度OFF(false)GPS高精度は遅く、電池消費も大きい
失敗したら郵便番号・住所の手入力を必ず用意拒否・圏外・企業端末の制限に耐える
保存するか原則は保存しない緯度経度は個人情報に近い扱いで設計する

ポイントは「その機能に本当に正確な座標が要るのか?」を疑うことです。「近くの店舗を探す」なら市区町村レベルで足りる場合が多い。「配送員の走行軌跡」なら watchPosition が要りますが、常時追跡ではなく開始と停止を明示します。「不正検知」なら座標そのものより、国・都道府県・距離帯のような粗い特徴量で足りないかをまず検討します。要件を粗くできるほど、実装も審査も漏えい時の負担も軽くなります。

どんな場面で効くのか

具体的なユースケースを4つ。どれも「現在地が取れなくても先に進める」が共通の設計方針です。

1つ目は店舗検索。飲食店、クリニック、コワーキング、イベント会場の「近くを探す」で、現在地を中心に候補を並べます。ここで大事なのは「現在地を使う」ボタンの横に「駅名・郵便番号で探す」を必ず置くこと。許可しないユーザーも自然に進めます。僕が拒否率8割でも売上が死ななかったのは、この手入力導線を後から足したからでした。

2つ目は配送・出張サービスの到着見込み。ユーザーの位置と担当者の位置を比べて、配達範囲、追加料金、最短到着枠を出します。この場合も正確な移動履歴を保存する必要はなく、計算に使ったあとは粗いエリアや判定結果だけ残す設計にできます。

3つ目は現場作業のチェックイン。メンテナンス、清掃、イベント運営、営業訪問で「対象地点から何メートル以内か」を確認します。ここで絶対にやってはいけないのは、位置が取れなかったときに作業を止めること。写真、管理者承認、手入力メモなどの代替フローを用意します。

4つ目は地域別コンテンツ。天気、自治体のお知らせ、近隣イベント、在庫のある店舗などを出し分けます。ただし記事や広告の出し分けだけのために正確な位置を求めるのは過剰です。IPベースの粗い地域判定や手入力の地域設定で足りるなら、そちらを優先します。

getCurrentPositionの最小実装(コピペで動く)

下のHTMLは1ファイルで動くサンプルです。ローカルでは localhost で試し、本番ではHTTPSで配信してください。file:// や通常のHTTPだと、ブラウザによって位置情報が拒否されます。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Geolocation demo</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        line-height: 1.6;
        margin: 2rem;
      }
      button,
      input {
        font: inherit;
        padding: 0.7rem 0.9rem;
      }
      .panel {
        border: 1px solid #ddd;
        max-width: 36rem;
        padding: 1rem;
      }
    </style>
  </head>
  <body>
    <main class="panel">
      <h1>近くの店舗を探す</h1>
      <p>
        現在地は店舗検索だけに使います。
        正確な住所や移動履歴は保存しません。
      </p>

      <button id="useLocation" type="button">現在地を使う</button>
      <p id="status" role="status" aria-live="polite"></p>
      <pre id="result"></pre>

      <form id="manualForm">
        <label for="postcode">郵便番号または駅名</label>
        <input id="postcode" name="postcode" autocomplete="postal-code" />
        <button type="submit">手入力で探す</button>
      </form>
    </main>

    <script type="module">
      const status = document.querySelector("#status");
      const result = document.querySelector("#result");
      const button = document.querySelector("#useLocation");
      const form = document.querySelector("#manualForm");

      // 失敗時は責めずに、次の導線(手入力)を案内する
      function showManual(reason) {
        status.textContent =
          `${reason}。郵便番号または駅名でも検索できます。`;
      }

      function onSuccess(position) {
        const { latitude, longitude, accuracy } = position.coords;
        status.textContent = "現在地を取得しました。";
        // 座標は5桁に丸めて表示(必要以上の精度を残さない)
        result.textContent = JSON.stringify(
          {
            lat: Number(latitude.toFixed(5)),
            lng: Number(longitude.toFixed(5)),
            accuracyMeters: Math.round(accuracy),
          },
          null,
          2,
        );
      }

      function onError(error) {
        const messages = {
          1: "位置情報の許可が拒否されました",
          2: "端末の位置を判定できませんでした",
          3: "位置情報の取得がタイムアウトしました",
        };
        showManual(messages[error.code] ?? "位置情報を取得できません");
      }

      button.addEventListener("click", () => {
        if (!("geolocation" in navigator)) {
          showManual("このブラウザはGeolocation APIに対応していません");
          return;
        }

        // 許可ダイアログは「現在地を使う」を押した直後にだけ出す
        status.textContent = "位置情報の許可を確認しています...";
        navigator.geolocation.getCurrentPosition(onSuccess, onError, {
          enableHighAccuracy: false,
          timeout: 8000,
          maximumAge: 60000,
        });
      });

      form.addEventListener("submit", (event) => {
        event.preventDefault();
        const data = new FormData(form);
        status.textContent =
          `「${data.get("postcode")}」を基準に検索します。`;
      });
    </script>
  </body>
</html>

ここで触っている3つのオプションの意味を、誤解しやすい順に書きます。enableHighAccuracy は「できるだけ高精度に」というお願いであって、必ずGPSになるわけではありません。店舗検索の初回は false で十分。timeout は待ち時間の上限、maximumAge はキャッシュされた位置を何ミリ秒まで許すかです。毎回 maximumAge: 0 にすると新鮮な位置を取りやすい一方、取得が遅くなります。詳しい仕様は MDNのGeolocation APIgetCurrentPosition が一次情報として正確です。

ReactでwatchPositionを安全に扱う

watchPosition は移動に合わせて位置を受け取り続けるAPIです。便利なんですが、これが僕の二度目の事故の原因でした。停止処理を忘れると、画面を離れても追跡が続き、電池とログを静かに食い続けます。Reactでは clearWatch必ずクリーンアップに入れます

import { useEffect, useRef, useState } from "react";

type LocationPoint = {
  lat: number;
  lng: number;
  accuracy: number;
  at: string;
};

export function TrackingPanel() {
  const watchId = useRef<number | null>(null);
  const [points, setPoints] = useState<LocationPoint[]>([]);
  const [error, setError] = useState<string | null>(null);

  function start() {
    if (!navigator.geolocation || watchId.current !== null) return;

    watchId.current = navigator.geolocation.watchPosition(
      (position) => {
        const { latitude, longitude, accuracy } = position.coords;
        setPoints((current) => [
          {
            lat: Number(latitude.toFixed(5)),
            lng: Number(longitude.toFixed(5)),
            accuracy: Math.round(accuracy),
            at: new Date(position.timestamp).toISOString(),
          },
          ...current.slice(0, 9),
        ]);
      },
      (err) => {
        setError(`追跡できません: ${err.code}`);
      },
      {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 5000,
      },
    );
  }

  function stop() {
    if (watchId.current === null) return;
    navigator.geolocation.clearWatch(watchId.current);
    watchId.current = null;
  }

  // 画面を離れたら必ず追跡を止める。ここを忘れると電池が減り続ける
  useEffect(() => stop, []);

  return (
    <section>
      <button type="button" onClick={start}>追跡開始</button>
      <button type="button" onClick={stop}>停止</button>
      {error && <p role="alert">{error}</p>}
      <ol>
        {points.map((point) => (
          <li key={point.at}>
            {point.lat}, {point.lng}
            {" / "}
            {point.accuracy}m
          </li>
        ))}
      </ol>
    </section>
  );
}

移動ログを保存するなら、業務上の必要性・保存期間・削除方法を先に決めます。現場チェックインなら「開始時と終了時だけ」で足りることが多い。1秒ごとの軌跡は実装が派手に見えますが、レビュー、問い合わせ対応、漏えい時の負担が一気に増えます。「動かせること」と「持ち続けてよいこと」は別問題です。

プライバシーとログ設計

ここが一番やらかしやすいところです。緯度経度をそのままログに出すと、例外ログ、分析イベント、検索パラメータ、サーバーのアクセスログ、サポート用スクリーンショットに残り、意図せず位置データを長期保存することになります。僕は最初 console.log(position) を消し忘れて、本番ログに座標が流れていたことがありました。

対策はシンプルで、ログに出す前に「粗く」します。

type GeoLogInput = {
  lat: number;
  lng: number;
  accuracy: number;
  permission: "granted" | "prompt" | "denied" | "unknown";
};

// ログに出す前に座標を粗くする。生の緯度経度は残さない
export function toPrivacySafeGeoLog(input: GeoLogInput) {
  return {
    permission: input.permission,
    accuracyBucket:
      input.accuracy <= 50 ? "high" :
      input.accuracy <= 500 ? "medium" : "low",
    latBucket: Number(input.lat.toFixed(2)),
    lngBucket: Number(input.lng.toFixed(2)),
  };
}

console.info("geo_request_finished", toPrivacySafeGeoLog({
  lat: 35.681236,
  lng: 139.767125,
  accuracy: 42,
  permission: "granted",
}));

小数2桁の緯度経度は数キロ単位の粗い位置になります。サービスによってはそれでも細かすぎる。広告計測やA/Bテストに使うだけなら、座標ではなく「店舗候補を表示できた」「手入力fallbackを使った」「タイムアウトした」のようなイベントで十分なことがほとんどです。

権限状態を先に知りたいときは Permissions API が使えます。ただし prompt は「まだ許可も拒否もされていない」状態で、実際の許可ダイアログは getCurrentPositionwatchPosition を呼んだ瞬間に出ます。ブラウザ差もあるので、表示文言の切り替え程度に留めるのが実務的です。

export async function readGeoPermission() {
  if (!("permissions" in navigator)) return "unknown";

  try {
    const status = await navigator.permissions.query({
      name: "geolocation",
    });
    return status.state; // "granted" | "prompt" | "denied"
  } catch {
    return "unknown";
  }
}

地図SDKとの境界を切る

Geolocation APIは「座標を取る」だけです。地図タイルを表示する、住所を座標に変換する、経路を計算する、周辺施設を検索する——これらはGoogle Maps Platform、Mapbox、OpenStreetMap系サービス、サーバー側の住所データなど、まったく別の責務です。

この境界を曖昧にすると、Claude Codeがブラウザの位置取得コードに地図APIキーを直書きしたり、クライアントからジオコーディングAPIを大量に叩いたりします。安全な切り方は「現在地取得コンポーネントは座標だけを親に返す。地図SDKの初期化、APIキー、サーバー検索は既存の地図層に閉じ込める」です。

export type BrowserLocation = {
  lat: number;
  lng: number;
  accuracy: number;
};

// 地図ライブラリを一切知らない。座標を返すだけの関数にしておく
export function getBrowserLocation(): Promise<BrowserLocation> {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error("Geolocation is not supported"));
      return;
    }

    navigator.geolocation.getCurrentPosition(
      (position) => {
        resolve({
          lat: position.coords.latitude,
          lng: position.coords.longitude,
          accuracy: position.coords.accuracy,
        });
      },
      reject,
      {
        enableHighAccuracy: false,
        timeout: 8000,
        maximumAge: 60000,
      },
    );
  });
}

この関数は地図ライブラリを知りません。地図側は BrowserLocation を受け取って中心位置を変えるだけ。こうしておくと、現在地取得のテスト、地図表示のテスト、APIキー管理を別々に扱えます。地図側の具体的な組み込みは Claude CodeでGoogle Mapsを統合する実践ガイド にまとめてあります。

やりがちな失敗5つ(全部やった)

正直に書くと、ここに挙げる5つは全部、僕か僕のチームがやらかしたものです。

1つ目はHTTP配信。Chromeは保護されていないオリジンからのGeolocation APIを制限していて、開発中は http://localhost で動いても、本番の一部サブドメインや埋め込みiframeがHTTP混在だと失敗します。Chromeの安全なオリジン要件 を一度読んでおくと、原因不明の「本番だけ動かない」を防げます。

2つ目は権限拒否を例外扱いすること。拒否は正常なユーザー選択です。「位置情報を許可してください」と何度も迫るより、「郵便番号で検索できます」と次の導線を出す方が、結果的に信頼されて使われます。

3つ目は**watchPosition の停止忘れ**。配送や移動記録で実装したあと、画面遷移時の clearWatch が漏れると、電池消費と不要なデータ処理が残ります。Reactなら useEffect のcleanup、通常JSなら「停止」ボタンとページ離脱時の処理を用意します。

4つ目は地図SDKとの混同。Geolocation APIは無料の地図APIではありません。住所検索、逆ジオコーディング、経路、地図表示には別サービスが必要で、APIキー制限・課金・キャッシュルールを確認します。

5つ目はログ漏えいconsole.log(position)、エラー監視ツールの追加コンテキスト、アクセス解析イベントに緯度経度を入れると、想定外の保管先が増えます。Claude Codeには「座標をログ出力しない。必要なら粗いbucketだけ」と明示してください。

テストとモックで「失敗系」まで固める

位置情報は端末・OS設定・ブラウザ・企業ポリシー・iframeのPermissions-Policyに左右されます。だから「成功するケース」だけ確認しても品質保証になりません。失敗系を機械で再現できるようにしておきます。

手動確認ではChrome DevToolsのSensorsパネルが便利です。位置をTokyo、Berlin、Custom location、Location unavailableに切り替え、成功・拒否・取得不能・手入力fallbackを順に確認します。

E2EではPlaywrightの geolocationpermissions を使うと安定します。

import { expect, test } from "@playwright/test";

test.use({
  geolocation: {
    latitude: 35.681236,
    longitude: 139.767125,
    accuracy: 50,
  },
  permissions: ["geolocation"],
});

test("モックした現在地から近くの店舗を表示する", async ({ page }) => {
  await page.goto("/stores");
  await page.getByRole("button", { name: "現在地を使う" }).click();
  await expect(page.getByText("現在地を取得しました")).toBeVisible();
});

拒否のテストも必ず書きます。権限を与えない状態でボタンを押し、手入力フォームが表示されることを確認する。ここまでやって初めて「拒否されても壊れない」と言えます。

Claude Codeへの安全な依頼の出し方

Claude Codeには実装範囲と禁止事項を先に渡します。位置情報は「動けばOK」ではなく、データを残さない・許可拒否を尊重する・地図APIキーに触らない・テストを追加する、という制約のほうが本体です。

claude <<'PROMPT'
Implement a beginner-friendly Geolocation feature.

Scope:
- Edit only src/features/location and related tests.
- Do not change billing, analytics, or map provider config.
- Preserve existing API keys and environment variable names.

Requirements:
- Request location only after the user clicks a button.
- Explain why location is needed before the browser prompt.
- Use getCurrentPosition with timeout and maximumAge.
- Add manual postcode/address fallback for denied or timeout cases.
- Do not log raw latitude or longitude.
- Add a Playwright test with mocked geolocation.
- Return a short verification checklist.
PROMPT

チームで使うなら、.claude/settings.json で危険な読み取りやpushを制限する運用も検討します。permission ruleは Tool(specifier) 形式です。

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Bash(git push *)"
    ],
    "allow": [
      "Bash(npm test *)",
      "Bash(npm run lint)"
    ]
  }
}

権限設計をもっと詰めたい人は、Claude Codeセキュリティ監査の進め方 と、モバイルでの見え方を整える レスポンシブデザインの実装 も合わせて読むと、位置情報まわりの設計がひと続きでつながります。

よくある質問

Q. 許可ダイアログはなぜ毎回出ないことがあるんですか? A. ブラウザがオリジン単位で許可・拒否を記憶するからです。一度「許可」または「ブロック」を選ぶと、次回からダイアログを出さずにその結果を使います。テスト時はサイトの権限をリセットするか、Playwrightの permissions で明示的に制御してください。

Q. enableHighAccuracy: true にすれば必ずGPSの精度になりますか? A. なりません。あくまで「できるだけ高精度に」という依頼で、最終的な取得元と精度は端末とブラウザが決めます。屋内やGPS非搭載のPCでは高精度を指定しても粗いままです。店舗検索などは false で十分です。

Q. 緯度経度は個人情報として扱うべきですか? A. 実務上はそのつもりで設計するのが安全です。座標は自宅や勤務先を高い精度で示すため、保存・送信・ログ出力のすべてで「本当に必要か」を確認し、不要なら丸めるかイベント名だけ残します。

Q. HTTPSじゃないと本当に動かないんですか? A. http://localhost での開発は例外的に動きますが、本番の通常HTTPやHTTP混在のiframeでは制限されます。本番は素直にHTTPSで配信してください。これはブラウザ側のセキュリティ要件です。

Q. IPアドレスからの位置取得とどう違いますか? A. Geolocation APIは端末側のGPS・Wi-Fiなども使えるため、IP判定より精度が高い代わりに、ユーザーの明示的な許可が必要です。逆に「都道府県が分かれば十分」ならIPベースの粗い判定で足り、許可ダイアログ自体が不要になります。要件次第で使い分けます。

実際に試した結果

この記事のサンプルは、Windows上のChromeで localhost 配信、DevTools SensorsのTokyo指定、Location unavailable、Playwrightのmock位置を使って確認しました。成功時は座標が5桁に丸めて表示され、取得不能時はちゃんと手入力フォームへ誘導されます。

冒頭の「拒否率8割」事件以降、僕がコードより先に直すようにしたのは設計の順番でした。①許可は機能の直前に求める、②精度は粗いところから始める、③拒否は正常系として手入力を出す、④座標はログに残さない。この4つを先に決めてからClaude Codeに渡すと、返ってくる実装の質がまるで変わります。実案件ではこれに加えて、OS側の位置情報をOFFにしたケース、企業端末でブラウザ権限が固定されたケース、iframe埋め込みでPermissions-Policyにブロックされたケースも確認してください。

位置情報機能はプロダクトの信頼に直結します。Claude Codeに任せるほど、プロンプト側で「何を保存しないか」「どの失敗を正常系にするか」「どこまで地図層に任せるか」を指定してください。導入や社内レビュー基準づくりまで一緒に詰めたい場合は、Claude Code研修・相談 で既存コードを前提にした実装プロンプトとチェックリスト化まで支援しています。

#Claude Code #Geolocation #位置情報 #プライバシー #Web API
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。