はじめに
React の学習を兼ねて、郵便番号から近くのリチウムイオン電池回収協力店を地図上に表示する Web アプリを個人利用向けに作りました。
きっかけは、自宅に溜まっていた古いワイヤレスイヤホンやモバイルバッテリーなどを廃棄する際にどこに持っていけば良いかが検索しても分かりにくかったことでした。
また、以前から Google マップを使ったサービスを開発してみたかったということもあり、React 学習の題材として作成することにしました。
回収協力店の情報は 一般社団法人 JBRC が公開しているものがベースになりますが、データの取り扱いなどの懸念から非公開の自分用アプリとして作成しました。
本記事では、React と Supabase + PostGIS を使った近傍検索・地図表示まわりの技術的な学びを中心に記載しています。
作成したアプリ
リチウムイオン電池の回収協力店・自治体を地図上で確認できるWebアプリになります。

主な機能
- 郵便番号を入力すると、その地点を中心に半径 5km 以内の回収拠点を検索
- 検索結果を Google マップ上にピン表示
- ピンをタップすると店舗名・住所を表示し、「Google マップで開く」リンクからナビゲーション可能
郵便番号から緯度・経度への変換には HeartRails Geo API を、拠点データの近傍検索にはSupabase(厳密にはPostgreSQL)上の拡張機能であるPostGISを使用しています。
技術スタック
| カテゴリ | 技術 |
|---|---|
| フロントエンド | React 19 / TypeScript / Vite |
| スタイリング | Tailwind CSS v4 |
| 地図 | react-google-maps(@vis.gl/react-google-maps) |
| バックエンド / DB | Supabase(PostgreSQL + PostGIS) |
| ホスティング | Firebase Hosting |
| CI/CD | GitHub Actions |
| テスト | Vitest / Testing Library |
全体構成
Vite + Reactを利用したシンプルなSPAになります。
検索は、Edge functionsなどは使用せずにSupabaseクライアントからRPCを叩いて行う構成としました。
データベース側の設計
拠点情報はlocationsテーブルに保存し、位置情報はPostGISのgeography型カラムlocationに POINT(経度 緯度)形式で持たせる用にしました。
// location カラムへの保存例
const location = `POINT(${long} ${lat})`;
フロントエンド
ユーザーが郵便番号を入力すると、次の 2 段階で検索します。
- HeartRails Geo APIで郵便番号 → 緯度・経度に変換
- Supabaseで登録したストアドプロシージャで半径5km以内の拠点を取得
検索にあたっては、Supabase 上で緯度・経度・検索範囲を引数に持つストアドプロシージャを作成しておき、それをsupabaseクライアントのrpc()メソッドで呼び出す方式としました。
プロシージャ側では、PostGIS のST_DWithin()関数を使用し、geography 型のカラムから範囲検索を行っています。
- フロント側
const { data, error } = await supabase.rpc("get_nearby_locations", {
// 検索用プロシージャ
target_lat: targetPoint.lat,
target_lng: targetPoint.lng,
radius_meters: 5000,
});
- ストアドプロシージャ
SELECT *
FROM locations
WHERE ST_DWithin(
location::geography, -- 緯度・経度をもとに保存されたgeography型のカラム
ST_Point(target_lng, target_lat)::geography, -- 中心点
radius_meters -- 検索範囲の半径距離(m)
)
取得した拠点は react-google-maps の AdvancedMarker コンポーネント で地図上に描画するようにしました。
マーカーをクリックすると吹き出しが開き、Google マップへのディープリンクも表示されます。
苦労した点
開発中、半径5km以外のレコードも大量に検索結果に引っかかるバグに遭遇しました。
コンソールを確認してもエラーログは出ず、検索自体は成功するため、次のような切り分けに時間を取られました。
- SQL の距離条件がkmになっている?
- プロシージャの定義が間違っている?
- フロントからの呼び出し方が間違っている?
原因
原因は、プロシージャの引数名がテーブルのカラム名と干渉していたことでした。
最初は引数を lat, lngと命名していましたが、検索先のテーブル側にも緯度・経度を保存するのに lat, longカラムを用意していたため、関数内でlatと書いたとき、引数ではなくカラムを参照してしまっていました。
その結果、距離条件が意図どおりに効かず、ほぼ全件が返ってきていました。
-- ❌ 引数名 lat がカラム lat と干渉しうる
CREATE FUNCTION get_nearby_locations(lat double precision, lng double precision, ...)
対処
引数名をtarget_lat, target_lngのようにカラム名と被らない名前に変更して解決しました。
-- ✅ 引数名を明示的に区別
CREATE FUNCTION get_nearby_locations(
target_lat double precision,
target_lng double precision,
radius_meters double precision
)
分かれば単純な原因ですが、エラーにならない分気づきにくいポイントでした。
PostGIS の RPC を書くときは引数名にプレフィックスを付けるなど、最初から区別しておくのが安全です。
おわりに
今回の開発で得られた学びをまとめると、次のとおりです。
- Supabase + PostGISは個人開発で位置情報アプリを試すのに非常に有用
- Google Maps APIはreact-google-mapsでReactから扱いやすかったが、一般公開するならLeaflet.jsなど無料で選択肢も検討したい
- PostGIS の RPC では引数名とカラム名の衝突に注意
位置情報 + マップを使用したアプリは応用できる範囲が大きく、学習にあたっては良い題材になりました。
他にも似たようなアイデアがあるので、今後の開発に活かしていきたいと思います。