【GCP】ひなっちのおはっちのサイトをリメイクした話


ひなっちのおはっちのサイトをリメイクしたので、その辺の話をまとめておこうと思う。
リメイク後のサイトはこちら(URLも変えました)


はじめに

ひなっちのおはっちのサイトは2021年4月ころにリリースしたわけだが(当時のブログはこちら)、勢いと実験目的で始めたところが強かったので、最初のサイトの作りはお世辞にも良い作りだったとはいえなかった。
jsとjsxが入り混じってたり、Typescript使ってないから型なんて概念は存在せず、処理ごとに割と自由にオブジェクトを作りまくっていて、後から見たときのカオス具合が凄かった。
いつか作り直したいなーとぼんやり考えていて、それを今回実施した感じである。

全体像

前回のブログで使った方式に則ると、以下のような構成になっている。赤字が今回の変更点。

ちなみに参考までに、変更前は以下の通り。冒頭のブログのリンクから確認できる。(どうでもいいが…)

変更点

細かいこと言うと他にもいくつかあるのだが、主だった変更点というとこのあたりか。

  • 元々Firebase Hostingを使っていたんだけど、これをGoogle App Engine(GAE)に乗り換えた。GAEを選んだことに特に意味はない…今まで本格的に使ってこなかったので、どうせだから使ってみようかなと思った(つまり好奇心によるところ)というのが大きい。
  • javascriptで組んでいたのをTypescriptに組み直した。理由は上で書いた通り、「割と自由にオブジェクトを作りまくっていた(ことによるコードの保守性の悪さ)」を改善したかったというのもあるが、これも半分くらいは「使ってみたい」という好奇心によるところが大きい。
  • CSSとしてTailwindcssを使用するように変更。これに伴い画面レイアウトは全体的に作り直した。メニューもヘッダ常駐型ではなく左上にtailwind-hambergersを使ったハンバーガーメニューを設ける形に変えた。
  • ログ表示機能にreact-infinite-scrollを使用して、以前にはあった月検索の機能を廃止した。それに伴い、もともと月検索の条件指定に使っていたreact-selectを廃止した。だが、折角このコンポーネントの使い方を覚えたのもあって、これの使い道が全くなくなってしまうというのも勿体ないなと思っていて、これはこれでどこかで復活させたいと密かに思っている。(今のところ使い道が思いつかないんだけど)
  • もともとほとんどの画面を単一ページのSSRで構成していたのだが、内部ロジック系は/api/以下のapiに寄せて、フロント側からはそのAPIを呼び出してAPIのレスポンスを描画する形に変えた。なので今回のリメイクで、主要なページはほとんど(一部を除いて)見た目上SSGに変更になっている。(実態はフロント側SSG+API側SSR)なお、ここで培ったAPI呼び出しとレンダリングの実装例をこちらのQiitaにメモでまとめている。

苦労話

  • Typescript化に伴って、javascriptで組んでいたロジックを書き直さなくてはならず、結果内部ロジック部分も含めてほぼ作り直しになってしまった。(流用できる部分はあったが)これが意外に結構面倒だった。一方、どうせだから型を付けようと思って着手したところ、データ部分はFirestoreから変更してない(ドキュメントの構造も全く同じまま)ことと、もともとドキュメント構造をしっかり定義していたことも幸いして、特にFirestore関連処理部分の型付けに関してはそこまで苦労しなくて済んだ。ただ、この辺の思考(テーブル構造をしっかり型にはめて定義するという発想)は、やはりNoSQLではなくRDBっぽいな(自分の癖がそっち方向に思考に偏ってるな)と改めて思った。
  • FirebaseへのDeployはGithub Actionsを使ってやっていたけど、GAEへのdeployはCloud Buildを使って行うように変えた。(まあ多分Github Actionsでも出来るんだろうけど)その際に色々苦労があった。このQiitaにその辺のメモを載せている。
  • GAEにカスタムドメインを設定しなければならなかったのだが、それがうまくいかなくてちょっと困った。Cloudflareを使っているという特有の事情もあるかもしれない。この辺りの苦労話もメモとしてこのQiitaに載せている。
  • react-infinite-scrollの使い方がよくわからず、色々試行錯誤を繰り返した。元々は全てのデータ取得とレンダリングを全部これに任せようと思ってたが、最初のAPIの実行結果をレンダリングする前に次のAPIの呼び出しが始まってしまい、最初のAPIの実行結果がシカトされ、結果的にレンダリングされる内容が1ページ分過去にズレて開始されてしまう。thresholdとかinitialLoadとか色々使ってみたがどうも期待通りに動かかなかった。というわけで、これ単発で全ての描画を担保するのを辞めて、最初のローディングでは当月分だけを単発で取得して表示する仕様に変え、このコンポーネントはボタンを押されたときに動作開始するように変えた。(つまり自動で動かすのではなく手動で動かすように変えた)このコンポーネントを上手く使いこなせなかったという意味では若干不満は残るが、システム都合により自動で何発もAPIが発行されるケースを防止できるような作りにできたので、結果オーライかなとは思っている。今の作りは個人的には満足している。無限スクロールのコンポーネントは似たようなのが何個も出ているので、そっちを使えば解決したかもしれないと思う部分はあるが、試すのも面倒なので今の状態で落ち着くことにする。
  • tailwind-hambergersは、デモサイトはあるが、「メニューボタンの動き」の確認とそのコードサンプルしかなくて、実際のメニューをどうやって記述するのかのサンプルが、ググって見た感じでも、英語ですら使ったサンプルはどこにも見当たらなくて、困った。今の形は色々試行錯誤して実装にたどり着いた事例だが、これも正直個人的にそこまで気に入ってるわけではない(例えば開いてるときにページ外クリックしたときに自動で閉じる実装できていないことが不満。これは継続確認中)ちょっとググってみた感じだと、こういうOSS使うより、割と自分で実装してる節があったな。そっちの方が良いのかもしれない。いつかそれで作り直すかも。

残課題

  • GAEに乗せたときだけ、ローディングのgif画像が読み込めない現象が起きた。より具体的に言うと、このgif画像に対する _next/image?url=... のリクエストがHTTP500エラーになる。内部的には[Error: ENOENT: no such file or directory, mkdir '/workspace/.next/cache/images'] という謎のエラーが発生している。他の画像は問題なくレスポンスが返ってくるのにgif画像だけ駄目。かつ、この現象はローカルでは起きない(GAEに乗せたときだけ起きる)。Next.jsのバージョン変えたりしても解決しない(11.x系と12.x系を試した)。next.config.jsimages > domains にドメイン名書いてみたけど解決しない。ググってみると、GAEではないが、Azureでも同じようなことが起きたissueがあって、これは「Platform側の問題」ということで片づけられたようだった。GAEでもそうなのかは定かではないが、そういう事例があるならもうそういうことでいいかと思い、残念ながらこのローディングのgif画像だけnext/imageを使わずimgタグでそのまま記述するという実装で回避した。そういう意味で、これは解決していない…issueあげてみようかな…
  • iPhoneで表示した時だけ一部のResponsive用のClassが一切適用されない問題がある。正確に言うと、Tailwindcssがデフォで用意している最小のBreakpoint sm(min-width:640px)「すら」適用されない。つまり、iPhoneで表示した場合、そのdisplayサイズがsmの設定値min-widthの640px以下と(何故か)判断されているということのようだ。もともとこのサイトに関しては、メインコンテンツの横幅に関してはResponsiveを適用していて、それはiPhoneで表示した時も(見ている限りでは)効いてるんだけど、なぜか一部の要素についてはこれが一切効かないらしい。これは試した限りでは端末がiPhoneなら使うブラウザがなんであろうと同じだった(Chrome、Safari、Twitter内臓ブラウザなど全て)PC(Win/Chrome及びMac/Chrome)iPad/Chromeでは適用されるし、iPadに至っては横向き⇔縦向きを変える度にちゃんと変わることすら確認できるんだけど、なぜかiPhoneだけが駄目。例えばPCでブラウザの横幅を縮めながら確認してみたところの録画動画は以下である。widthの変化に応じて「検索結果なし」の文字部分の色が変わっていく(responsiveが適用されている)ことが確認できると思う。

    これはiPad(横向き)

    iPad(縦向き)

    俺の個人iPhone。横幅がMAXになってるので、レイアウト全体に対するmdがあたってるのが確認できるが、「検索結果なし」の文字部分にはmdの要素(水色になるはず)が効いていない。

    この問題は正直どういう理由で起きてるのか、よくわかっていない。コンテンツ全体の横幅に対するmd の指定は生きてるのに対し、コンテンツ内の一要素に対する指定がmdだろうとsmだろうと効かない。min-width 0pxのCustom Breakpoint作ろうとかと思ったが、面倒くさいのでやめた。原因不明なので、あまり納得いってないが、Responsiveはあきらめて、このページのコンテンツ全体に対して一定のclassを適用するように変えた。これは完全なるワークアラウンドと言える。気持ち悪いのも事実なので、できれば時間あるときにもう少し探ってみたい。
  • cache関連が完全に未対応。つまり、Cloudflareを実質DNSとしてしか利用できていない(CDNとしての機能を活用できていない)。リメイク前は、Firebase全体のResponse Headerを設定することができて、それに基づいてキャッシュさせてたが、正直これもちゃんと考えて設定していたわけではないし、あと何より、リメイク前の構成だと前ページ基本的にSSRだったのでキャッシュ指示が出しやすかったのに対し、今回フロント側のページはSSGで、裏のAPIがSSRという構成に変更になっていることもあって、実際どうやってキャッシュ指示を書けばいいかあまりよく自分の中でも整理できていない。ただ一方で、現状のアクセス数と頻度なら、別にCDNを活用するほどのものでもなく、現状は正直そこまで困っていないし、今後もそこまで(CDNに頼るほど)アクセスが来るようにも思えない。なので、これは課題というより、個人的好奇心を満たすための残タスクという色合いが大きい。せっかくCloudflareを使ってるんだし、Next.jsもいい感じに仕上がってるので、cacheとの付き合い方はここでナレッジにしておきたい。そのうち対応する予定。
  • staging環境がおっぴろげになっている。つまりpublic access可能になっている。まあURL知ってるのは俺だけなのでアクセスしてくるのも(基本的には)俺しかいないのだが、出来ればこの環境は隠蔽したい。最初はCloudflareのFirewallルールで弾けばいいと思っていたが、カスタムドメインを設定したときのQiitaに書いた通り、CloudflareをDNSに使う場合、プロキシ設定を使うことが出来なく、CloudflareのFirewallルールはCloudflareのDNSプロキシ設定をして初めて有効になるので、要するにこのケースではCloudflareルールを使えない。じゃあGAEのほうのFirewallルール使えばいいかと思いついたが、こっちはこっちでGAEの「アプリケーション」に対して1つ設定する形になるようで、現状GCPのプロジェクトを1つでやりくりしてる俺はこれが使えないことに今更ながら気づいた(GAEのFirewallルール設定しちゃうとstagingもproductionも両方に適用されてしまう)本当に今更だが、GCPの「プロジェクト」の概念をここでようやく思い知った(要するに「開発」とか「テスト」とか「本番」とかで「プロジェクト」を分けて使うことを想定しているのだねこれ)というわけで、「テスト用のプロジェクト」を作って→そっちにGAEのサービス作って→devブランチ繋げてdeployし→GAEのFirewallルール設定して、という対応が必要になる。既にGAEへのdeployは出来ているので、プロジェクトが違うだけならそこまで苦労はしなそうだが、Firestoreのデータどうするかとか色々考えなきゃいけないことがある。全般的に「テスト環境の構築」という巨大なタスクとして残課題化された。ただまあ、一応テスト環境はテスト環境で現状存在しており問題なく動作するので、急務でもないし、これはこれで面白そうなので、色々試行錯誤してノンビリやっていきたいと思う。
  • このリメイクの最中、Firestoreのデータ保存容量が、無料枠の5GBを超えていることに気づいた。これは別に今回のリメイクに関係した話じゃなく、最初にサイト作ったときからの課題なのだが。課金って意味で言えばそもそもFirebaseやCloud Functionsの利用料で今までも多少なりともお金かかっていたので、課金が発生すること自体は問題ではない。今まで無料だったものが無料じゃなくなってるというのが問題なのだ。データを消してないんだからそりゃいつかはそうなると思っていたが、サイト開設後1年経過して早くも突破してしまったようだ。というわけで、真面目にコレの扱いを考えなければいけなくなってきている。うーーーむ。どうしようかな…

感想

  • 今回、GAEの基本的な使い方や仕様を覚えられたのは良かった。正直、個人的にFirebaseより使い勝手が直感的で分かりやすい。こういうPaaSは好みだ。もっと早めに使っておくべきだった。一番懸念していたのがFirestoreに繋ぐための連携部分で、Firebaseだとその辺を良しなにやってくれるのが利点だと思っていたが、GAEでも同様だったので、気にすることではなかったようだった(GAEの実行ユーザーのService Accountで認証するなら難しいことが必要ないということなんだろう)。少なくともこのサイトに関して言えばもう恐らくFirebaseに戻ることはないだろう。
  • Typescriptを覚えられたのも良かった。最初は「なんだこの面倒くさいのは、自分でjavascript書くほうが絶対早いし確実だぞ」と思ったが、結果的に見ると、確かにこれを使うことによる生産効率は純粋なJavascriptより高いと思った。オブジェクトにきちんと型を付けておけば勝手に補完してくれるし、存在しないプロパティを書いたらその場でエラーを指摘してくれる。無駄にconsole.log(JSON.stringify(obj))とかやらなくて済むのはでかい。その分、自由度が減った部分は確かにあるのだが、それを上回る効率性を手に入れた気がする。まあ、すごい簡単なツールとか動作確認用のアプリとかなら、今でもjavascript使っちゃうと思うけど、ちゃんとした(?)アプリケーションを開発するならTypescriptのほうが結果的には良さそうだと思った。この境地に至れたのは個人的に良い収穫だっと思っている。
  • useStateとかuseEffectとかuseRouterとか、その辺の基本的な(?)React Hook?の使い方を、以前に比べて覚えられたのが良い経験になった。リメイク前は、useStateしか使ったことなかったし、それも正直「なんとなく」使ってる部分が多くて、どういう動きするかを完全に意識して実装に落とし込めていたわけではなかった。リメイク後はuseStateなんか積極的に使ったし(むしろこれがなきゃなんもできんレベル)、useEffectuseRouterは今回初めて使ったけど、それぞれどういう使い方をするものなのか、基本的なところは覚えられた気がする。React Componentに対してこれらを使う実装は、そのコードを動く場所が(基本的に)フロント側になる、という意識が結構重要なようだ。特に俺の使い方だとNext.jsをFullstack的に使ってる関係もあってか、フロントとバックエンドの垣根が自分の中でも曖昧になりがちで、冒頭書いた「なんとなく使っていた」のところは、その曖昧さに基づいてもたらされていたものだと思われる。この辺の整理ができたことと、具体的な実装例をいくつか経験できたのは自分の中でデカイ財産になっている。これは別のアプリケーションを作るときにも活かしていきたい。
  • Tailwindcssを覚えられたのも良かった。これも正直最初は「なんだこの面倒くさいのは、自分でCSS書くほうが全然楽だぞ」と思ったが、すでにそれっぽく初期セットされているCSSがたくさんあるので、いちいち自分でCSS書くよりは、これに頼ったほうが全然良いと気付いた。まだ完全に使いこなせているわけではないし、そもそもデザインセンスがないもんで、あまり活かしきれているとも思えないのだが、これは今後も積極的に活用していきたいと思う。

おわりに

まあ色々大変だった部分もあったけど、好きで始めたリメイクだったし(そもそも誰かに求められているようなものでもない)、今回のリメイクを通じて技術面での収穫は多くあったので、やって良かったと思うし、全体を通してみたら楽しくやれた。
これはこれで新しい技術要素を習得するのに良い題材として使えることがわかったので、そのうちまた気まぐれで作り直すかもしれないw

なお、現行サイトは一応まだ残っているのだが、そう遠くない後に停止し、Firebaseプロジェクトも閉じる予定。