タイトルが全て
モチベーション
以前 プリ☆チャンMAP を作ったことがあるのですが、下記のような不満がありました。
- プリ☆チャンの公式店舗一覧 をスクレイピングしてスプレッドシートに入れるところまでは自動化できたが、そこからGoogleマイマップに取り込む作業が手作業
- スプレッドシートに住所カラムを作っておけばGoogleマイマップがいい感じに緯度経度に変換してくれるのだが、公式の住所のフォーマットがおかしいと稀に緯度経度が取れないので手動で補正する必要がある
- 公式のショップ一覧は定期的に更新されるのでそれに追従するのが大変
- プリ☆チャンMAP作成後にプリパラオールフレンズの稼働が始まったのだがそれの地図を作るのも大変
そのため完全に作り直しました。
使い方
https://primap.web.app/ を開くといい感じに現在地周辺のプリ☆チャンやプリパラで遊べる店舗の一覧が出ます。
技術的なこと
リポジトリ
大雑把な仕組み
開発期間はトータルで1ヶ月くらいです
サーバサイド
俗に言うサーバサイドは最初はあったけど途中で捨てました
当初の構成だとCloud RunでGoで書いたAPIやジョブをホスティングする予定だったんですが、APIはFirebaseでいいことに気づいたので途中で方針転換して、その結果サーバサイドがなくなりました。(サーバレス)
ジョブ
採用技術は下記
- Go
- Cloud Functions
- Cloud Scheduler
- Cloud Pub/Sub
- Secret Manager
- Geocoding API
- Serverless Framework
店舗が数千件あって1つのファンクションの中で全ての緯度経度の取得をするのは厳しいと判断して、PrismDBで取得した店舗の件数分だけPub/Subでqueueに積んで非同期で処理するようにしました
Geocoding APIを一度にたくさん叩きすぎるとReteLimitに引っかかるのでリトライも必要になるのですが、Pub/Subで店舗1件ずつ処理することでその辺のリトライを何も考えなくてよかったのは楽でした。(Pub/Subで呼んだファンクションがエラーになれば勝手にリトライされるので)
フロントエンド
採用技術は下記
- TypeScript
- React
- Vue.js + TSは過去にやったことあるので今回は今まで使ったことのないReactを採用
- React歴3週間くらい
- Tailwind CSS
- 社内でちょいちょい名前を聞いていたので採用
- Firebase Hosting
- Google Maps
- ReactでGoogle Mapsを使うライブラリは2〜3個検討したのですが「Google Mapsの面倒くさい部分を全部いい感じにラップしてくれている」という点で https://www.npmjs.com/package/google-maps-react を採用しました
その他使ったもの
- GitHub Actions
- Sentry
- Terraform
あたり
こだわりポイント
Cloud RunからCloud Functionsに移行して変わったこと
メリット
- デプロイフローがシンプルになった
- Cloud Runでアプリを動かすためにはDockerイメージが必要なので必然的にCIでもdocker buildしてContainer Registryにpushする必要があるのだが(GitHub Actions上で1分弱)、Cloud Functions化することでそのフローが完全に不要になった
- Cloud RunでCloud SchedulerやPub/Subのリクエストを受けるためにはhttpのエンドポイントが必要だったが、Cloud Functions化することでhttp周りの実装が不要になった
- Cloud Functions側にPub/Sub専用のエンドポイントがある
デメリット
- Cloud Run時代はデプロイが2分弱だったが*1、Cloud Functions移行後にデプロイに5〜6分かかるようになった
- Serverless Frameworkのバックエンドで使ってるDeployment Managerの様子を見る感じ純粋にCloud Functionsへのデプロイが一番時間がかかってるのだが、具体的にどこで時間がかかってるのかが特定できないので謎。(おそらくgo getかビルドのどちらかだと思うのだが...)
- 1つのバイナリに2つのファンクションが含まれているのでそれらを分割すれば早くなる可能性はありそう
- Cloud Functionsだと最新のGoが1.13
- 早く1.15使わせてほしい...
GoのstructからTypeScriptのclassを自動生成するようにした
FirestoreにはGoのstructで保存してるのですが、全く同じデータをフロントエンドで使うために tscriptify でTypeScriptのclassに変換しています。
Firestoreに緯度経度の型はあるのだが、実際にはそれだけだと検索で使えなくてハマった
現在地の緯度経度からFirestoreで検索しようとしたのですが当初うまくいきませんでした。(明らかに現在地から外れた店舗が検索にヒットする)
調べると https://stackoverflow.com/questions/46630507/how-to-run-a-geo-nearby-query-with-firestore に
Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby.
とあるように、Firestoreは実際のGeoPointクエリをサポートしていないため、latlng型で検索しても緯度しか考慮されないということでした。
そのため緯度経度から算出したGeoHashも別途Firestoreに保存することでようやく現在地周辺の検索ができるようになりました。
ディレクトリ構成を色々変えた
開発中に何回かディレクトリ構成を作り直しました
- 1段階目
- 最初はオーソドックスなGoのアプリのディレクトリ構成
- 2段階目
- frontend実装直後
- GoはそのままでTSとReact関係のソースだけfrontendに入れた形
- 3段階目
- serverとfrontendの2つのディレクトリ構成
- この時点ではmakeやnpmのコマンド実行時にいちいちcdするのが嫌だという気持ちがあってこの構成でした
- TSやReact関係のソースは全部frontendに格納できたのですが、Goはビルドの関係でトップに一部ソースが残ってます
- 4段階目(今)
- 「makeやnpmのコマンド実行時にいちいちcdするのが嫌だ」という気持ちはあったんですが、それ以上にトップの見通しが悪いのが嫌だったので完全にディレクトリを分けました(手の平クルー)
frontendとfunctionの接点がfirestoreだけだったのでここまで振り切れたかもしれないです。
本当ならfrontendとfunctionでリポジトリを分けてもよかったのですが、前述の通りGoのstructからTypeScriptのclassを生成してるためリポジトリを分けると逆に連携が難しくなるためモノリシックリポジトリにしました。
port 55301
せっかくなので(?)devServerは55301番ポートを使うようにしました。
今週一番意味不明なコミットができた pic.twitter.com/NnxmyS7t2C
— sue445 (@sue445) 2020年10月13日
副産物
プリマップでSecretManagerを使ってるのですがラッパとして途中でgcp-secretmanagerenvができました
*1:docker buildに1分半でgcloud run deployが30秒