はじめに
この記事は 株式会社エス・エム・エス Advent Calendar 2023 の 10 日目の記事です。
カイポケリニューアルプロジェクトでエンジニアリングマネージャーをしている @hotpepsi です。
フロントエンドのチームが本格的に立ち上がってから一年ほど経過しました。現在では二つのフロントエンドチームが日々活発に開発しており、多くの時間は、アプリケーションコードを書いたり、より良い仕様を議論したりすることに費やしています。並行して、コンポーネント共通で発生する問題を解消したり、開発者体験や生産性を向上させる取り組みも数多く行ってきました。
今回は一年分のまとめとして、アプリケーション固有でない様々な取り組みについて紹介したいと思います。こんな感じのスクリプトで一年分のフロントエンド共通の pull request の title と description を markdown で取得して、その中から 100 個をピックアップしました。カテゴリごとにおおよそ時系列に並んでいます。両方のカテゴリにまたがるものは関連性の高そうな方に分類しました。
利用しているフレームワークやコンポーネント
前提として、カイポケリニューアルプロジェクトのフロントエンドチームでは、以下のフレームワークやコンポーネントを利用しています。この構成になった経緯は「大規模 SaaS 「カイポケ」の未来を支えるフロントエンドの技術選定」にありますので、ぜひご一読ください。
- 言語: TypeScript
- UI framework: Next.js, Chakra UI
- linting: ESlint
- GraphQL client: Apollo Client
- state management: Recoil
- form: React Hook Form
- schema validator: Zod
- component catalog: Storybook
- testing framework: Jest
- testing library: Testing Library
- visual regression testing: Chromatic
やったこと
共通ガイドラインやファイル構成
- コードレビューガイドラインを追加
はじめに、pull request のテンプレートや、大まかなレビュー方針を作成。 - コーディングガイドラインを esa から移設
開発者のドキュメントはesaに書いているが、コーディングガイドラインは GitHub にあるほうがレビューや議論がしやすいので移設した。 - テスト実装に関するガイドラインを追加
なぜテストを書くのかや、Storybook の活用方針などを記述。 - モノレポに移行
プロジェクトチーム全体で議論した結果、モノレポに移行した。サービス毎の GitHub Actions の調整などは必要だったが、GraphQL のスキーマ管理などがしやすくなった。 - Props に
type
を使うことにした
Props にinterface
とtype
が混在していた。たいていはtype
で十分であり、意図しない拡張も防げるので、type
を使うことにした。そのあと、Props 以外でもなるべくtype
を使っていくことにした。 - Node.js のバージョンを固定する
CI や開発者の Node.js のバージョンを揃えることにした。package.json
での記述など、複数の手段を検討したが、ツールの選択肢などを考慮して.node-version
で固定することにした。 - ファイルコロケーション
テストをtests/
、ストーリーをstories/
に置いたりしていたが、関連するファイルが同じディレクトリにあるほうが見通しがよくなるので、コロケーションを採用した。 - named export への移行
default export を行っているコードが少し存在したが、なぜ default export を使うべきではないのか?を参考にして、named export を利用することにした。既存のコードを修正して ESLint のimport/no-default-export
も追加した。 - コードレビューガイドラインにコメント prefix を追記
question:
やsuggestion:
やnits:
など、コードレビューのコメント時に prefix をつけて意図を明確にすることを推奨することにした。 - ポータルドキュメントを GitHub に設置
様々な場所にストック情報が増えてきたので、フロントエンドチームのミッション・ビジョン・チーム構成などへのリンクをポータルドキュメントとして一箇所にまとめた。 - テストガイドラインに、推奨するテストの書き方を追加
Common mistakes with React Testing Libraryで紹介されているようなベストプラクティスを導入していて、ガイドラインにも反映した。 - zod の import 方法の統一
import * as z from 'zod';
とimport { z } from 'zod';
が混在していて、公式ドキュメントなどを参考に、後者に揃えた。 - ファイル名の規則の lint ルール化
コンポーネントの名前やファイル名は PascalCase、ディレクトリ名を lowerCamelCase にしており、それを lint ルール化した。 - eslint-config-airbnb-base を参考に lint 設定を見直し
- ソースコードの src ディレクトリへの移動
ソースコードがsrc/
以下にあるほうが grep や lint に便利なので、移動した。 - PropsWithChildren 利用の lint 化
子要素を受け取るコンポーネントは PropsWithChildren の利用を推奨している。no-restricted-syntax
のselector
を使ったカスタムルールにより、ReactNode
のchildren
を検出してエラーにするようにした。
Design System
- Chakra UIでDesign Systemを構築
- アプリケーションコードからChakra UIのimportを禁止する 基本的な部品を全てデザインシステム上に構築できたので、アプリケーションから直接importしないようにした。
- Design SystemのStorybookの分離 Design SystemのStorybookをアプリケーションから分離した。
import/no-restricted-paths
により検出。
Storybook と Chromatic
- StorybookでHMRを有効化
- GitHub Actions で Chromatic を実行する Chromaticによりvisual regression testを行うようにした。
- Fakerの結果を固定する モックデータをFakerで生成しており、毎回異なるとChromaticの差分として検出されてしまうので、seed値を与えてデータを固定するようにした。(なおその後Fakerを利用するテストを削除したため使わなくなった)
- Chromatic導入に伴うレビューガイドラインの更新 pull requestレビュー時に、見た目の差分がある場合には確認して承認する必要があるので、ガイドラインを整備した。
- mainブランチマージ後にStorybook をビルドする featureブランチだけでなく、
- Chromaticで意図しない差分が出るStoryをUI Testの対象から除外する ポップアップなどのアニメーションを使用しているStoryで、変更がなくても差分として検出されてしまうケースがあり、
- 除外対象の一部修正 レンダリング終了より早くクリックしてしまっていたケースがあったのを修正。
- ChromaticのTurboSnap を有効化し、スナップショットの数を減らした
- Chromaticの差分が生じたときにコメントする Chromaticで差分が検出されたとき、見逃してマージしてしまうことがあるので、差分が発生したというコメントをpull requestに追加するようにした。 create-or-update-comment を使用した。
- フォントのプリロードについて調査 Chromaticの結果が不安定なコンポーネントがあり、公式ドキュメントに従ってフォントのプリロードを試したが、改善しなかったため導入しなかった。
- Storybook 7 へのアップデートとマイグレーション Chakra UIが対応したため、Storybook 7にアップデートした。これだけで1記事にできるくらいの更新があった。 CSF もバージョン3にした。
- 日付に依存したStoryの現在時刻を固定する 現在月を表示する画面で、月が変わるごとに差分が発生していたので、
- ChromaticでModalやPopoverのスナップショットを修正する play関数やアニメーションにより描画領域が変化する要素のスナップショットを安定して取得するために、画面サイズを指定した。
- pull requestのマージにChromaticの結果の承認を必須にする Chromaticの差分があるとき、承認せずにマージしてしまうと、baselineが更新されないため他のpull requestでも差分として表示してしまう。そのため、差分があったらpull requestの必須のアクションの状態をpendingにして、承認するまでマージできないようにした。
- Chromaticの結果のリンク先をChromaticの対象ビルドにする Chromaticの結果(UI Tests)がアプリのトップページにリンクしていて不便だった。pull requestのイベントの
-
main
ブランチでの実行ではautoAcceptChanges
を有効にする
Chromaticの結果の承認を必須にできたので、 - PR時の
reactDocgen
の無効化
Chromaticを高速化するため、pull request作成時にStorybookの - 各ページにStoryを書くことにした コンポーネントだけでなく、ページのStoryも書くことにした。Storyやテストも同じ場所におけるように、ビルド対象であるページのファイル名のsuffixを
main
ブランチにマージされたときにもChromaticでビルドするようにした。 main
ブランチのStorybookはURLが固定されているので、他のチームに共有するときに使用している。
delay
オプションでも解決できなかったので、 disableSnapshot
オプションで除外した。
mockdate
により日付を固定した。
github.event.target_url
にビルド毎のURLが入っているので、それに書き換えた。
autoAcceptChanges
を設定して、baselineを更新するようにした。
reactDocgen
を無効化した。
.page.tsx
とした。
テスト
- JestやStorybookでGraphQLのクエリをテストするためMSWを導入
- MSWのレスポンスに型制約を与える @graphql-codegen/typescript-msw を導入した。
- mockの姓名を 名姓 から 姓名 にする fakerが生成する
- Jestのタイムゾーンを東京に固定
console.error
が出ているテストがあれば CI を失敗させる- CI でtestとbuildを並列で実行する
- テスト実行時間の可視化 10秒以上かかっているテストを洗い出し、分割するかどうか検討した。
- CIのJestのテストを高速化する
- CI にキャッシュを設定して実行時間を速くする main ブランチにpushしてビルドするとき、キャッシュを保存するようにした。
- Jestでwrapperを書かなくて済むようにする custom renderを定義し、
- Jestを3回リトライするようにした テスト実行時間のばらつきがあり、タイムアウトして落ちるテストがあるので、リトライするようにした。
- ローカル開発ではJestをリトライしないようにした
-
fireEvent
よりもuserEvent
の利用を優先する - テストの実行時間が長くなる場合は
fireEvent
の利用を許可する旨をドキュメントに追加 - テストのshardingの改善 テストの実行時間のばらつきがあるため、前回の実行時間を考慮するTestSequencerを実装し、shardingを改善した。
- testing library v14対応 jest.useFakeTimersを使うと
- Larger Runnersの導入 テストの高速化のためLarger Runnersを導入した。Jestの
- mainブランチではテストを1shardで実行する CIのキャッシュの効率化のため、
- JestのエラーをSentryに送る Jestで失敗したテストを、テスト用のSentryプロジェクトに報告するようにした。
- テストの日付を固定する 現在時刻から年齢を計算して表示する画面があり、
main
ブランチのCIが失敗したらSlackに通知する- 大量の文字入力のテストをclick/pasteに変更した 大量の文字入力をチェックする際に、
- lintとデプロイ失敗時にもSlackに通知する
fullName
が 名 姓 の順だが、逆にした。
--shard
と --maxWorkers
を与えて並列実行するようにした。
ApolloProvider
や RecoilRoot
など、共通で必要なwrapperをいちいち書かなくて済むようにした。
userEvent
のほうが実際のユーザーの操作に近いため、推奨することにした。lintルールも追加した。
userEvent
が終わらない問題の対処などを行った。
maxWorkers
は CPUコア数 - 1
にした。
main
ブランチではテストを1shardで実行するようにした。
useFakeTimers
により日付を固定した。
user.type
ではテストに時間がかかるため、大量の文字を扱う場合は user.paste
を利用するようにした。
共通コンポーネント
- 編集中にブラウザの戻るボタンをクリックした確認ダイアログを表示する ブラウザバックを監視するカスタムフックを実装した。
- カレンダー編集UIのパフォーマンスチューニング データ量が多い複雑な編集画面のパフォーマンスを改善するため、オリジナルの仮想スクロールを実装した。
- Kaipochiコンポーネントを追加 カイポケのマスコットキャラクターであるカイポチくんを追加した。なおカイポチくんの身長は133.4cm、体重はイチゴ3個分である。
- ゼロ埋めの内部処理を標準組み込みメソッドに置換 標準メソッドに
- 電話番号の表示にハイフンを付ける libphonenumber-js を導入して、電話番号の表示にハイフンを付けた。バンドルサイズを軽くするため、 Customizing metadata の方法で日本のmetadataだけ使うようにした。
- Feature Flagの導入 開発中の機能を限定された範囲で有効にするため、Feature Flagを導入した。
- デバッグ用に日付を固定する 日付や時刻によって挙動が異なるページがあるため、デバッグ用のデータをlocalStorageに保存して、特定日時にできる機能を実装した。
- Storybook用のFeature Flag判定を追加 Storybookのみで有効になるコンポーネントが実装できるようになった。
String.prototype.padStart
があったのでゼロ埋めのメソッドを置き換えた。
Next.js
- Dynamic Routing で 403 になった際のフォールバックを追加 SPAかつCloudFrontの環境で、Dynamic Routingを使ったページでリロードすると、プレースホルダ置換後のURLをアクセスしようとして403となっていた。CloudFront内で403を404にリダイレクトし、アプリケーションの404のハンドラ内で、routerで元のページに戻すようにした。
- Dynamic Routingで存在しないページをリロードしたときに404にする 存在するページのみでリダイレクトしたいので、ビルド時にページの一覧を生成して、マッチしたページがあるときだけ遷移するようにした。
- アプリヘッダの再レンダリングを抑える アプリのレイアウトをページコンポーネントの
.getLayout
に移し、再レンダリングを抑えるようにした。
フォーム
- 全角の英数字入力を許容する
- フォームのバリデーションのタイミング変更 onChangeでバリデーションを行うと、入力してすぐにエラーとなり体験が悪かったので、バリデーションのタイミングをonTouchedに変更した。
- 入力した文字の前後の空白・タブをtrimする
GraphQL
- Apollo Server でモック用の GraphQL サーバーを起動
- refetchQueriesに関するガイドラインを追記 リスト系のデータの再描画では
- 特定コンポーネントのGraphQLのqueryにprefixをつける GraphQLのクエリ名が衝突しないよう、prefixをつけることにした。@graphql-eslint/naming-convention により判定。
- near-operation-file-preset 設定
- フロントエンドでのsupergraph生成をやめ、バックエンドのファイルを参照するように修正 GraphQLのsupergraphの生成方法を試行錯誤した結果、最終的にバックエンドで合成するようにした。フロントエンド側ではCIのタスクでコピーして、変更点を確認したり壊れていないかどうかを検証するようにした。
- 複数のmutationを呼んだ時も確実にrefetchするようにする
@deprecated
の扱いをwarn
にする
APIに - GraphQL 利用側の推奨ルールを導入 @graphql-eslint/operations-recommendedを導入した。
refetchQueries
を発行する必要があり、いくつかの注意点をガイドラインに追記した。
.graphql
ファイルをコンポーネントと同じディレクトリに生成するために @graphql-codegen/near-operation-file-presetを導入した。
@deprecated
をつけるタイミングをフロントエンド側で決められず、error
だとCIが失敗するので、@graphql-eslint/no-deprecated
のルールを warn
に変更した。
Renovate
- Renovateの導入 パッケージのアップデートのため RenovateをGitHubに導入した。
- Next.js を Renovate から除外する Next.jsのバージョンアップは慎重に行いたいので、Renovateからは除外した。
- Renovateが作成したpull requestではChromaticを実行しないようにする ライブラリのアップデートでは見た目が変わらなかったり、頻繁にrebaseされたりするため、botが作成したpull requestでは自動実行しないようにした。
- ChromaticのCIを手動で回せるようにした 見た目を確認したいこともあるので、手動でも起動できるようにした。
- 特定のライブラリのアップデートでChromaticを実行する Renovateが作ったpull requestはChromaticを実行しないようにしたが、Storybookなど見た目に関わるライブラリのアップデートではChromaticを実行するようにした。
- 手作業で閉じたRenovateのpull requestが再度作られないようにする Renovateが
chore(deps):
をつけたりつけなかったりして閉じたものを再度作るケースがあったので、 semanticCommits
を enabled
にして常に付与されるようにした。
GitHub actions
- デプロイ通知を良い感じにする デプロイ成功時のアイコンを機嫌の良いカイポチくんに、失敗時にはしょんぼりしたカイポチくんにした。
- pull requestを作るbotは自前のトークンを使う
- ドキュメントだけ更新したときにコード系のactionsを起動しないようにする
- dev環境へのデプロイを直列にする
GITHUB_TOKEN
でpull requestを作成するとCIなどのワークフローが起動しないため、自前のトークンに置き換えた。
main
ブランチにマージするとdev環境にデプロイするようになっていて、近いタイミングでマージするとリソースが壊れることがあったため、直列にした。
その他
- pre-commit フックに時間がかかるので pre-push でチェックする Huskyのpre-commitを導入していたが、小さい単位でコミットしようとすると地味にlint待ちが発生していた。基本的にVSCodeで整形&lintされているので、pre-pushに変更した。
- git pull時に
package-lock.json
に差分があればnpm install
する
Huskyのpost-mergeで実装。
- フロントエンドのhooksを希望者のみで発動するようにする モノレポで開発しており、必要な開発者だけhuskyのhooksを動作させたかったが、
- Hygen 導入
- 循環参照の調査と修正
- pre-push の処理を並列化 https://github.com/open-cli-tools/concurrently を導入してpre-push の処理を並列化した。
- バージョン番号の生成スクリプトを作成 commit hashなどをJSONファイルとして公開し、どのバージョンがデプロイされているのか取得できるようにした。
- ローカルデータ追加用のE2Eテストの設定追加 Playwrightを導入して、ローカル開発環境でユーザー登録から初期データ投入までできるようにした。
npm install
を実行したバックエンドの開発者でも発動してしまっていた。 .env.local
ファイルに特別な記述をしたときだけ発動するようにして解決した。
eslint-plugin-import
の no-cycle
で検出した。ルールを追加するとlintの時間が大幅に増加し開発効率が著しく下がるため、発見のみに使用した。
おわりに
フロントエンドの技術は年々進化して複雑化しており、モダンな開発を普通に実践しようとすると、多岐にわたる取り組みが必要になることが確認できました。今後も、中長期のプロダクトを想像しつつ、より良い技術や手法をバランス良く取り込んでいきたいと思っています。 なおエス・エム・エスでは、フロントエンドだけでなく、様々なポジションで開発メンバーを募集しています。興味を持った方は、ぜひお気軽にお声がけください。