フロントエンドチームで今年やったこと100

はじめに

この記事は 株式会社エス・エム・エス Advent Calendar 2023 の 10 日目の記事です。 カイポケリニューアルプロジェクトでエンジニアリングマネージャーをしている @hotpepsi です。
フロントエンドのチームが本格的に立ち上がってから一年ほど経過しました。現在では二つのフロントエンドチームが日々活発に開発しており、多くの時間は、アプリケーションコードを書いたり、より良い仕様を議論したりすることに費やしています。並行して、コンポーネント共通で発生する問題を解消したり、開発者体験や生産性を向上させる取り組みも数多く行ってきました。

今回は一年分のまとめとして、アプリケーション固有でない様々な取り組みについて紹介したいと思います。こんな感じのスクリプトで一年分のフロントエンド共通の pull request の title と description を markdown で取得して、その中から 100 個をピックアップしました。カテゴリごとにおおよそ時系列に並んでいます。両方のカテゴリにまたがるものは関連性の高そうな方に分類しました。

利用しているフレームワークやコンポーネント

前提として、カイポケリニューアルプロジェクトのフロントエンドチームでは、以下のフレームワークやコンポーネントを利用しています。この構成になった経緯は「大規模 SaaS 「カイポケ」の未来を支えるフロントエンドの技術選定」にありますので、ぜひご一読ください。

やったこと

共通ガイドラインやファイル構成

  1. コードレビューガイドラインを追加
    はじめに、pull request のテンプレートや、大まかなレビュー方針を作成。
  2. コーディングガイドラインを esa から移設
    開発者のドキュメントはesaに書いているが、コーディングガイドラインは GitHub にあるほうがレビューや議論がしやすいので移設した。
  3. テスト実装に関するガイドラインを追加
    なぜテストを書くのかや、Storybook の活用方針などを記述。
  4. モノレポに移行
    プロジェクトチーム全体で議論した結果、モノレポに移行した。サービス毎の GitHub Actions の調整などは必要だったが、GraphQL のスキーマ管理などがしやすくなった。
  5. Props に type を使うことにした
    Props に interface と type が混在していた。たいていは type で十分であり、意図しない拡張も防げるので、 type を使うことにした。そのあと、Props 以外でもなるべく type を使っていくことにした。
  6. Node.js のバージョンを固定する
    CI や開発者の Node.js のバージョンを揃えることにした。 package.json での記述など、複数の手段を検討したが、ツールの選択肢などを考慮して .node-version で固定することにした。
  7. ファイルコロケーション
    テストを tests/ 、ストーリーを stories/ に置いたりしていたが、関連するファイルが同じディレクトリにあるほうが見通しがよくなるので、コロケーションを採用した。
  8. named export への移行
    default export を行っているコードが少し存在したが、なぜ default export を使うべきではないのか?を参考にして、named export を利用することにした。既存のコードを修正して ESLint のimport/no-default-exportも追加した。
  9. コードレビューガイドラインにコメント prefix を追記
    question: や suggestion: や nits: など、コードレビューのコメント時に prefix をつけて意図を明確にすることを推奨することにした。
  10. ポータルドキュメントを GitHub に設置
    様々な場所にストック情報が増えてきたので、フロントエンドチームのミッション・ビジョン・チーム構成などへのリンクをポータルドキュメントとして一箇所にまとめた。
  11. テストガイドラインに、推奨するテストの書き方を追加
    Common mistakes with React Testing Libraryで紹介されているようなベストプラクティスを導入していて、ガイドラインにも反映した。
  12. zod の import 方法の統一
    import * as z from 'zod'; と import { z } from 'zod'; が混在していて、公式ドキュメントなどを参考に、後者に揃えた。
  13. ファイル名の規則の lint ルール化
    コンポーネントの名前やファイル名は PascalCase、ディレクトリ名を lowerCamelCase にしており、それを lint ルール化した。
  14. eslint-config-airbnb-base を参考に lint 設定を見直し
  15. ソースコードの src ディレクトリへの移動
    ソースコードが src/ 以下にあるほうが grep や lint に便利なので、移動した。
  16. PropsWithChildren 利用の lint 化
    子要素を受け取るコンポーネントは PropsWithChildren の利用を推奨している。 no-restricted-syntax の selector を使ったカスタムルールにより、 ReactNode の children を検出してエラーにするようにした。

Design System

  1. Chakra UIでDesign Systemを構築
  2. アプリケーションコードからChakra UIのimportを禁止する
  3. 基本的な部品を全てデザインシステム上に構築できたので、アプリケーションから直接importしないようにした。 import/no-restricted-pathsにより検出。
  4. Design SystemのStorybookの分離
  5. Design SystemのStorybookをアプリケーションから分離した。

Storybook と Chromatic

  1. StorybookでHMRを有効化
  2. GitHub Actions で Chromatic を実行する
  3. Chromaticによりvisual regression testを行うようにした。
  4. Fakerの結果を固定する
  5. モックデータをFakerで生成しており、毎回異なるとChromaticの差分として検出されてしまうので、seed値を与えてデータを固定するようにした。(なおその後Fakerを利用するテストを削除したため使わなくなった)
  6. Chromatic導入に伴うレビューガイドラインの更新
  7. pull requestレビュー時に、見た目の差分がある場合には確認して承認する必要があるので、ガイドラインを整備した。
  8. mainブランチマージ後にStorybook をビルドする
  9. featureブランチだけでなく、 main ブランチにマージされたときにもChromaticでビルドするようにした。 main ブランチのStorybookはURLが固定されているので、他のチームに共有するときに使用している。
  10. Chromaticで意図しない差分が出るStoryをUI Testの対象から除外する
  11. ポップアップなどのアニメーションを使用しているStoryで、変更がなくても差分として検出されてしまうケースがあり、 delay オプションでも解決できなかったので、 disableSnapshot オプションで除外した。
  12. 除外対象の一部修正
  13. レンダリング終了より早くクリックしてしまっていたケースがあったのを修正。
  14. ChromaticのTurboSnap を有効化し、スナップショットの数を減らした
  15. Chromaticの差分が生じたときにコメントする
  16. Chromaticで差分が検出されたとき、見逃してマージしてしまうことがあるので、差分が発生したというコメントをpull requestに追加するようにした。 create-or-update-comment を使用した。
  17. フォントのプリロードについて調査
  18. Chromaticの結果が不安定なコンポーネントがあり、公式ドキュメントに従ってフォントのプリロードを試したが、改善しなかったため導入しなかった。
  19. Storybook 7 へのアップデートとマイグレーション
  20. Chakra UIが対応したため、Storybook 7にアップデートした。これだけで1記事にできるくらいの更新があった。 CSF もバージョン3にした。
  21. 日付に依存したStoryの現在時刻を固定する
  22. 現在月を表示する画面で、月が変わるごとに差分が発生していたので、mockdate により日付を固定した。
  23. ChromaticでModalやPopoverのスナップショットを修正する
  24. play関数やアニメーションにより描画領域が変化する要素のスナップショットを安定して取得するために、画面サイズを指定した。
  25. pull requestのマージにChromaticの結果の承認を必須にする
  26. Chromaticの差分があるとき、承認せずにマージしてしまうと、baselineが更新されないため他のpull requestでも差分として表示してしまう。そのため、差分があったらpull requestの必須のアクションの状態をpendingにして、承認するまでマージできないようにした。
  27. Chromaticの結果のリンク先をChromaticの対象ビルドにする
  28. Chromaticの結果(UI Tests)がアプリのトップページにリンクしていて不便だった。pull requestのイベントの github.event.target_url にビルド毎のURLが入っているので、それに書き換えた。
  29. main ブランチでの実行では autoAcceptChanges を有効にする
  30. Chromaticの結果の承認を必須にできたので、autoAcceptChanges を設定して、baselineを更新するようにした。
  31. PR時の reactDocgen の無効化
  32. Chromaticを高速化するため、pull request作成時にStorybookの reactDocgen を無効化した。
  33. 各ページにStoryを書くことにした
  34. コンポーネントだけでなく、ページのStoryも書くことにした。Storyやテストも同じ場所におけるように、ビルド対象であるページのファイル名のsuffixを .page.tsx とした。

テスト

  1. JestやStorybookでGraphQLのクエリをテストするためMSWを導入
  2. MSWのレスポンスに型制約を与える
  3. @graphql-codegen/typescript-msw を導入した。
  4. mockの姓名を 名姓 から 姓名 にする
  5. fakerが生成する fullName が 名 姓 の順だが、逆にした。
  6. Jestのタイムゾーンを東京に固定
  7. console.error が出ているテストがあれば CI を失敗させる
  8. CI でtestとbuildを並列で実行する
  9. テスト実行時間の可視化
  10. 10秒以上かかっているテストを洗い出し、分割するかどうか検討した。
  11. CIのJestのテストを高速化する
  12. --shard と --maxWorkers を与えて並列実行するようにした。
  13. CI にキャッシュを設定して実行時間を速くする
  14. main ブランチにpushしてビルドするとき、キャッシュを保存するようにした。
  15. Jestでwrapperを書かなくて済むようにする
  16. custom renderを定義し、ApolloProvider や RecoilRoot など、共通で必要なwrapperをいちいち書かなくて済むようにした。
  17. Jestを3回リトライするようにした
  18. テスト実行時間のばらつきがあり、タイムアウトして落ちるテストがあるので、リトライするようにした。
  19. ローカル開発ではJestをリトライしないようにした
  20. fireEvent よりも userEvent の利用を優先する
  21. userEvent のほうが実際のユーザーの操作に近いため、推奨することにした。lintルールも追加した。
  22. テストの実行時間が長くなる場合は fireEvent の利用を許可する旨をドキュメントに追加
  23. テストのshardingの改善
  24. テストの実行時間のばらつきがあるため、前回の実行時間を考慮するTestSequencerを実装し、shardingを改善した。
  25. testing library v14対応
  26. jest.useFakeTimersを使うと userEvent が終わらない問題の対処などを行った。
  27. Larger Runnersの導入
  28. テストの高速化のためLarger Runnersを導入した。Jestの maxWorkers は CPUコア数 - 1 にした。
  29. mainブランチではテストを1shardで実行する
  30. CIのキャッシュの効率化のため、main ブランチではテストを1shardで実行するようにした。
  31. JestのエラーをSentryに送る
  32. Jestで失敗したテストを、テスト用のSentryプロジェクトに報告するようにした。
  33. テストの日付を固定する
  34. 現在時刻から年齢を計算して表示する画面があり、useFakeTimers により日付を固定した。
  35. main ブランチのCIが失敗したらSlackに通知する
  36. 大量の文字入力のテストをclick/pasteに変更した
  37. 大量の文字入力をチェックする際に、user.type ではテストに時間がかかるため、大量の文字を扱う場合は user.paste を利用するようにした。
  38. lintとデプロイ失敗時にもSlackに通知する

共通コンポーネント

  1. 編集中にブラウザの戻るボタンをクリックした確認ダイアログを表示する
  2. ブラウザバックを監視するカスタムフックを実装した。
  3. カレンダー編集UIのパフォーマンスチューニング
  4. データ量が多い複雑な編集画面のパフォーマンスを改善するため、オリジナルの仮想スクロールを実装した。
  5. Kaipochiコンポーネントを追加
  6. カイポチくん カイポケのマスコットキャラクターであるカイポチくんを追加した。なおカイポチくんの身長は133.4cm、体重はイチゴ3個分である。
  7. ゼロ埋めの内部処理を標準組み込みメソッドに置換
  8. 標準メソッドにString.prototype.padStartがあったのでゼロ埋めのメソッドを置き換えた。
  9. 電話番号の表示にハイフンを付ける
  10. libphonenumber-js を導入して、電話番号の表示にハイフンを付けた。バンドルサイズを軽くするため、 Customizing metadata の方法で日本のmetadataだけ使うようにした。
  11. Feature Flagの導入
  12. 開発中の機能を限定された範囲で有効にするため、Feature Flagを導入した。
  13. デバッグ用に日付を固定する
  14. 日付や時刻によって挙動が異なるページがあるため、デバッグ用のデータをlocalStorageに保存して、特定日時にできる機能を実装した。
  15. Storybook用のFeature Flag判定を追加
  16. Storybookのみで有効になるコンポーネントが実装できるようになった。

Next.js

  1. Dynamic Routing で 403 になった際のフォールバックを追加
  2. SPAかつCloudFrontの環境で、Dynamic Routingを使ったページでリロードすると、プレースホルダ置換後のURLをアクセスしようとして403となっていた。CloudFront内で403を404にリダイレクトし、アプリケーションの404のハンドラ内で、routerで元のページに戻すようにした。
  3. Dynamic Routingで存在しないページをリロードしたときに404にする
  4. 存在するページのみでリダイレクトしたいので、ビルド時にページの一覧を生成して、マッチしたページがあるときだけ遷移するようにした。
  5. アプリヘッダの再レンダリングを抑える
  6. アプリのレイアウトをページコンポーネントの .getLayout に移し、再レンダリングを抑えるようにした。

フォーム

  1. 全角の英数字入力を許容する
  2. フォームのバリデーションのタイミング変更
  3. onChangeでバリデーションを行うと、入力してすぐにエラーとなり体験が悪かったので、バリデーションのタイミングをonTouchedに変更した。
  4. 入力した文字の前後の空白・タブをtrimする

GraphQL

  1. Apollo Server でモック用の GraphQL サーバーを起動
  2. refetchQueriesに関するガイドラインを追記
  3. リスト系のデータの再描画では refetchQueries を発行する必要があり、いくつかの注意点をガイドラインに追記した。
  4. 特定コンポーネントのGraphQLのqueryにprefixをつける
  5. GraphQLのクエリ名が衝突しないよう、prefixをつけることにした。@graphql-eslint/naming-convention により判定。
  6. near-operation-file-preset 設定
  7. .graphql ファイルをコンポーネントと同じディレクトリに生成するために @graphql-codegen/near-operation-file-presetを導入した。
  8. フロントエンドでのsupergraph生成をやめ、バックエンドのファイルを参照するように修正
  9. GraphQLのsupergraphの生成方法を試行錯誤した結果、最終的にバックエンドで合成するようにした。フロントエンド側ではCIのタスクでコピーして、変更点を確認したり壊れていないかどうかを検証するようにした。
  10. 複数のmutationを呼んだ時も確実にrefetchするようにする
  11. @deprecated の扱いを warn にする
  12. APIに @deprecated をつけるタイミングをフロントエンド側で決められず、error だとCIが失敗するので、@graphql-eslint/no-deprecated のルールを warn に変更した。
  13. GraphQL 利用側の推奨ルールを導入
  14. @graphql-eslint/operations-recommendedを導入した。

Renovate

  1. Renovateの導入
  2. パッケージのアップデートのため RenovateをGitHubに導入した。
  3. Next.js を Renovate から除外する
  4. Next.jsのバージョンアップは慎重に行いたいので、Renovateからは除外した。
  5. Renovateが作成したpull requestではChromaticを実行しないようにする
  6. ライブラリのアップデートでは見た目が変わらなかったり、頻繁にrebaseされたりするため、botが作成したpull requestでは自動実行しないようにした。
  7. ChromaticのCIを手動で回せるようにした
  8. 見た目を確認したいこともあるので、手動でも起動できるようにした。
  9. 特定のライブラリのアップデートでChromaticを実行する
  10. Renovateが作ったpull requestはChromaticを実行しないようにしたが、Storybookなど見た目に関わるライブラリのアップデートではChromaticを実行するようにした。
  11. 手作業で閉じたRenovateのpull requestが再度作られないようにする
  12. Renovateが chore(deps): をつけたりつけなかったりして閉じたものを再度作るケースがあったので、 semanticCommits を enabled にして常に付与されるようにした。

GitHub actions

  1. デプロイ通知を良い感じにする
  2. しょんぼりしたカイポチくん デプロイ成功時のアイコンを機嫌の良いカイポチくんに、失敗時にはしょんぼりしたカイポチくんにした。
  3. pull requestを作るbotは自前のトークンを使う
  4. GITHUB_TOKEN でpull requestを作成するとCIなどのワークフローが起動しないため、自前のトークンに置き換えた。
  5. ドキュメントだけ更新したときにコード系のactionsを起動しないようにする
  6. dev環境へのデプロイを直列にする
  7. main ブランチにマージするとdev環境にデプロイするようになっていて、近いタイミングでマージするとリソースが壊れることがあったため、直列にした。

その他

  1. pre-commit フックに時間がかかるので pre-push でチェックする
  2. Huskyのpre-commitを導入していたが、小さい単位でコミットしようとすると地味にlint待ちが発生していた。基本的にVSCodeで整形&lintされているので、pre-pushに変更した。
  3. git pull時に package-lock.json に差分があれば npm install する
  4. Huskyのpost-mergeで実装。
  5. フロントエンドのhooksを希望者のみで発動するようにする
  6. モノレポで開発しており、必要な開発者だけhuskyのhooksを動作させたかったが、 npm install を実行したバックエンドの開発者でも発動してしまっていた。 .env.local ファイルに特別な記述をしたときだけ発動するようにして解決した。
  7. Hygen 導入
  8. 循環参照の調査と修正
  9. eslint-plugin-import の no-cycle で検出した。ルールを追加するとlintの時間が大幅に増加し開発効率が著しく下がるため、発見のみに使用した。
  10. pre-push の処理を並列化
  11. https://github.com/open-cli-tools/concurrently を導入してpre-push の処理を並列化した。
  12. バージョン番号の生成スクリプトを作成
  13. commit hashなどをJSONファイルとして公開し、どのバージョンがデプロイされているのか取得できるようにした。
  14. ローカルデータ追加用のE2Eテストの設定追加
  15. Playwrightを導入して、ローカル開発環境でユーザー登録から初期データ投入までできるようにした。

おわりに

フロントエンドの技術は年々進化して複雑化しており、モダンな開発を普通に実践しようとすると、多岐にわたる取り組みが必要になることが確認できました。今後も、中長期のプロダクトを想像しつつ、より良い技術や手法をバランス良く取り込んでいきたいと思っています。 なおエス・エム・エスでは、フロントエンドだけでなく、様々なポジションで開発メンバーを募集しています。興味を持った方は、ぜひお気軽にお声がけください。