ハンバーガーメニュー

Menu

【質問箱】お手伝いサークル公式サイトをNext.jsにリプレイスしました

お手伝いサークル公式サイト開発者のやたです。
今回、今年の3月から9ヶ月ほど稼働しているお手伝いサークル公式サイトのリプレイスを行いました。
それに伴ってどのように変更したのか、どんなところを改善したのかなどを紹介していこうと思います。

お手伝いサークル公式サイトって何?

横浜国立大学に存在するお手伝いサークルの活動を支援するためのWebアプリケーションです。学生生活や授業を始めとした大学に関する質問をX(Twitter)を通して回答することができます。

サイトURL

https://otetsudai-circle.com

X URL

https://x.com/PALDOW2022

GitHub

https://github.com/arfes0e2b3c/q-box-next

技術スタック

以前まではNuxt.jsで書かれていたものをNext.jsにリプレイスしました。詳細な技術スタックの変化は以下の通りです。

種類 Before After
ホスティング Google App Engine Vercel
フロントエンド Nuxt.js v2.15.8 Next.js v13.5.6
CSS Sass vanilla-extract
Fetch axios react-query
状態管理 - Zustand
認証 Firebase Authentication Firebase Authentication
ストレージ microCMS S3, microCMS
CDN - Cloudfront
CI - GitHubActions

システム構成図

ざっくりとしたシステム全体の流れです。
複雑な処理も多くないためかなりシンプルな構成になっています。


システム構成図です

修正点

旧お手伝いサークル公式サイト(以下旧サイト)ではいくつかの問題が発生しており、今回のリプレイスに伴っていくつかの修正を行いました。

よく検索されるキーワードを追加

検索ボックスにフォーカスした際に「よく検索されるワード」が出るのですが、旧サイトでは僕が開発初期に適当に入れたものがそのままになっていました。
image

これらのワードは実態と乖離していたので、それに合わせるようにリストの修正を行いました。

具体的には過去の質問1000件の質問文を取得し、単語の頻度分析を行った上で妥当なものをお手伝いサークルさん側に選択してもらいました。

image

サムネイルの文字とtwitterのリンクが被っている

最近のX(Twitter)の改悪アップデートによってサムネイルの表示方法に変更がありました。それによって既存のサムネイルでは文字とリンクが被ってしまっていたのでサムネイルの文字を右側に寄せることにしました。

Before After
image image

apiのリクエスト回数が分からない

現在Twitter APIを使用してサイトからツイートを行なっているのですが、これも改悪アップデートによってFreeプランではツイートが1500件/月、50件/24時間までと制限されています。

平常時では問題にはならないのですが、新入生入学前後では質問箱の投稿は非常に多く可能性としては50件を超えることは十分考えられます。その対策として管理者画面に24時間以内のリクエスト数の表示を追加しました。

内部的にはtwitter apiにリクエストを送るたびにmicroCMSにログが蓄積され、それをここでカウントしている形になります。
image

情報提供欄から新規質問への移動

Xでリンクを押した際に遷移する詳細ページには、その質問に対する情報提供を送信できるテキストボックスがあります。

これは完全にこちらの設計ミスではあるのですが、この情報提供欄から新規で質問を投稿される方がいらっしゃるそうで、旧サイトではそれを手作業で新規質問として投稿し直し回答するということをお手伝いサークルさん側に強いている状況でした。

情報提供欄

それを管理画面から移動できる機能として追加しました。

スマホだと検索できないことがある

未だに原因はわかっていないのですが、旧サイトではスマホで検索しようとすると正常に動作しないというものがありました。

推測ですが、旧サイトでは検索結果を表示する画面でmount時にURLのクエリから検索ワードを取得していたため、検索ワードをもとにしたapiのリクエストとの実行の兼ね合いでエラーが発生していたのではないかと考えています。

現サイトでは画面遷移時にuseSearchParamsを使用して検索語句を取得し、その後react-queryで取得し問題なく検索ができる様になっています。

工夫したこと

実際の開発に寄せた開発環境の構築

今回は全体的に実務を意識したものにしようという気持ちで開発を行いました。
その中でも何点かを紹介します。

ステージング環境の構築

以前までは開発環境と本番環境のみで開発を行なっていたのですが、機能改善の際に本番環境でテストする必要があったりと怖さや不便さがあるタイミングが多く、改善が必要だと感じていました。
そこで今回はステージング環境を用意し手軽で安全に継続開発を行えるようにしました。
具体的には以下のことを行いました

  • mainブランチと作業ブランチに加えてdevelopブランチの作成
  • Vercelでmainブランチをproduction環境、developブランチをpreview環境に設定
  • microCMSを本番用とステージング用の2種類用意
  • (以前から)twitter apiの検証用アカウントを利用

これにより開発者体験がかなり向上しました。

GitHubActionsを使用したCI

Pull Requestを出した際に自動でlintなどを確認できるよう、GitHubActionsを利用したCIの構築を行いました。Vercelが自動で行なってくれるテストも含めて常に3つが動作しています

  1. Vercelの自動テスト
  2. lintやbuildに問題がないか確認するテスト
  3. lighthouseによるパフォーマンスの監視テスト

3.に関しては後述します。

Pull Requestのdescription

今までインターン、特に就業型のインターンに参加させていただいた際に教えていただいたようにdescriptionを書くことを意識しました。

正直に言うと最後の方やGitHubActionsに苦戦している時はかなり適当(というか書いていない)ことも多かったので全てのPull Requestを書ければよかったなぁと反省しています。

思ったよりも書いてなかった...誰も見る人がいないと書くモチベ下がりますね

パフォーマンスの考慮

今までの一番大きな課題はパフォーマンスでした。
このアプリケーションの特徴として「詳細ページのみの遷移がとても多い」というものがあるのですが、その詳細ページのパフォーマンスが低く画面が表示されるまでに時間がかかっていました。というのも、ページを訪れるたびにmicroCMSにリクエストが走る上、取得したデータからクライアント側でcanvasに画像を描画するという非常に重い処理をしていたためです。
これらパフォーマンス関連の課題を解決するために以下の2つを行いました。

SSG

詳細ページに関しては基本的に静的なページだったのでSSGであらかじめ生成しておいたHTML(とその他ファイル)を配信する形に変更しました。
これによりapiリクエストをせずにページを高速に表示できる様になりました。

画像配信の最適化

前述の通り、旧サイト内の画像は基本的にmicroCMSから受け取ったデータと背景画像をクライアントサイドでcanvasに合成して表示していました。しかしそれではクライアントサイドに負荷がかかりすぎてしまうことに加え何よりパフォーマンスがデバイス依存になってしまう部分が大きいので、クラウドのストレージから取得する形に変更しました。

ストレージとして使用しているのはS3で、imgixを利用して生成した画像データをS3に格納し、Cloudfrontを経由して画像を取得しています。これによりキャッシュを効かせて画像を取得できるため早ければ1桁ミリ秒(平均20~30ミリ秒)で画像を取得しそのまま表示ということができる様になりました。

最終的なパフォーマンスは以下の通りです。

条件:/x7qg4ftjvrのページをlighthouseで計測

種類 旧サイト 新サイト
モバイル
デスクトップ

APIの切り出し

旧サイトの大問題として「クライアントサイドで外部APIを叩いている」というものがありました。ですので言ってしまえば開発者ツールからAPIキーが完全に見える状態だったということです。やばいですね。

ということで今回のリプレイスに伴って通信を隠蔽することにしました。

Next.jsのApp RouterではRoute Handlerという機能を使用して通信をバックエンドに切り出すことができます。これを使用して外部APIをバックエンド側に切り出しました。

バックエンド側には全く詳しくないのでざっくりとした構成ではありますが以下のようになっています。

命名は適当です...

ViewとModelは関数の呼び出しだけを行い、具体的なロジックはViewClientとControllerに寄せる形にしています。

依存性などにはあまり関心を払えていないんですがコード全体として割とスッキリしたと思います。特にView部分がシンプルになっているので満足です。

大変だったこと

lighthouseをGitHubActionsで自動実行

今回初めてGitHubActionsを使ったんですが、試行錯誤が大変でした。というのも下の記事にもある通り、issue_commentのトリガーが動くのがデフォルトブランチにあるコードのみで作業ブランチのコードは実行されない様でした。

https://www.arfes.jp/article/8slf8dwtufpn

つまり動作確認の際に毎回mainマージをする必要がありました。動作確認のたびにPull Requestを出すのが煩雑でしたし何よりトリガーや動作が不明確で何度も何度も繰り返してエラーメッセージも十分に出ないのでかなり苦労しました。

ツイート用に文字を分割すること

Twitter APIを使用してサイト上からXにポストを行なっているのですが、140字を超えた場合にツイートを分割してリプライとして繋げなければいけません。その際に必要な文字の分割で苦労しました。

というのもポストの文字数というのは一律1文字1カウントというわけではなく、アルファベットや記号であれば0.5カウント、日本語では1カウントなどと文字種別ごとにカウント数が決まっています。

それに加えてツイートの最も困難だったのはURLのカウント方式です。URLに関しては一律23カウントと決まっており実際の文字数に依りません。つまり実際は文字数制限内に収まっているにも関わらずURL全体を表示することができず、途中で次のポストに切り替わってしまうという問題が発生していました。

その改善のためにtwitter-textというX用の文字数カウントが可能なライブラリを新たに使用することに加え、URLを含めたとしても1ポストに最大まで文字を含められるように以下の様な実装を行いました。

let currentTweet = ''
if (countTweetLength(testTweet) <= MAX_LENGTH && !willSplitLink) {
  currentTweet += char
} else {
  if (isInsideLink && willSplitLink) {
    let link = links.find((link) => i >= link.start && i < link.end) as {
      start: number
      end: number
    }
    currentTweet += text.slice(i, link.end)
    i = link.end - 1
  }
  tweets.push(currentTweet)
  currentTweet = char
}

1文字ずつ判定していって、その文字が140カウントを超えてかつリンク内にある場合はリンクの最後の文字まで自動追加するという処理をしています。

これによって正確に文字数をカウントすることができる様になりましたし、リンクが途中で切れる問題も無くなりました。

課題点、今後修正したいこと

一旦開発は終了したのですが、問題がある部分が何点かあるので最後にそれをまとめてこの記事を終わりにしたいと思います。

キャッシュの問題を解決する

新サイトでは全体的にreact-queryを使用して非同期処理に関する状態管理を行なっているのですが、一覧画面全体で画面遷移をした際にキャッシュを保持できないという課題が残っています。

例として一覧ページから詳細ページに遷移し、その後改めて一覧ページに戻るとなぜかキャッシュが保持されず、それに伴って再度APIリクエストが走りスクロール位置も上に戻されてしまいます。これではあまりにUXが悪く一覧性に欠けすぎているので、しっかりとキャッシュを保持できるように修正を行いたいです。

試してみたこととしてはreact-query-devtoolsを使用してキャッシュを監視したり、キャッシュキーを確認してみたりuseInfiniteQueryからuseQueryに変更してみて挙動を確認してみたいしたのですが、なぜか開発環境(next dev)では正常にキャッシュが保持されて、本番環境(next build && next start)の時には保持されなくなっているという状況です。

現状では原因がよく分からないのでApp Routerやreact-queryのバージョン周りの何か何だろうなとは考えているものの一旦保留にしています。

画像の文字サイズや行間隔を調整する

前述した通り、このサイトでは画像の生成はimgixで行なっています。
これはAPI経由で画像を生成できるというもので、以下の様な形式で生成しています。

https://images.microcms-assets.io/assets/ca0c41f03efd472a910782fea07dff31/690434409f8a4b2f9e53fe9f8dd23102/answered-right.png
?w=1200&h=630&blend-mode=normal&blend-align=middle,center
&blend=https%3A%2F%2Fassets.imgix.net%2F%7Etext%3Fw%3D1000%26txt-color%3D333%26txt-align%3Dcenter%26txt-size%3D44%26txtfont%3DZenMaruGothic-Regular%26txt64%3D

概要としては1行目の部分がmicroCMSに格納されている背景画像のURLで2行目が画像合成時の設定が続きます。
重要なのは3行目で、背景画像に合成するオブジェクトの指定をしています。

ここでは文字サイズも設定できるのですが、現状は一律で44になっています。しかしこれでは合成する文字数や行数が多くなると画像をはみ出して見切れてしまうという問題があります。

画像以外で質問を確認する手段がないのでこれは早急に解消する必要があると感じています。

画像生成の際に文字数と行数をカウントして画像内に収まるように修正したいと考えていますが、あまりに長文になると最終的には視認できなくなるほど文字数を小さくするかはみ出すかの2択になってしまうので、検討の余地があると思います。

最後に

初めてフルリプレイスというものを経験したのですが、過去の自分から成長した点を感じられて感慨深かったです。

ただ今回の変更がユーザー側の目に見える要素がほとんどないせいで全く伝わらないのが悲しい限りですね...

まぁ楽しかったからいっか!