ROUTE06 でソフトウェアエンジニアをしている @MH4GF です。
ROUTE06 ではエンタープライズ向けビジネスプラットフォーム「Plain」を開発しています。この記事では 2023 年 8 月に Plain クラウド EDI の Web フロントエンドで採用している技術について、その選定理由をまとめました。
現代の Web フロントエンド技術は領域ごとに選択肢が多く、プロダクトに最適な技術選定をする上で検討事項が多いと感じます。この記事がフロントエンド技術選定において参考になれば幸いです。
前提
プロダクトの特徴
技術選定に影響するプロダクトの特徴を箇条書きでまとめます。
- エンタープライズ向け SaaS
- 現在開発中のプロダクトは商取引におけるクラウド EDI のドメインにフォーカス
- Plain が解決する課題は、元々フルスクラッチで開発すると 1 年かかるプロダクトの開発期間を極限まで圧縮するためのプラットフォーム
- Plain 上に複数のプロダクトが共存する想定
- EDI ドメインのプロダクトは開発・運用経験もあるためドメインに対する知見がすでに蓄積されている
- フロントエンドの特徴は、入力内容の多い複雑なフォームとデータ量の多いテーブル表示が主な機能でページ数が多い
- モバイル対応は視野に入れておきつつ、主にデスクトップでの利用を想定
フロントエンドチームの規模
ドメインに対する機能を実装する Feature チームと、フロントエンド基盤の実装をする Platform チームに分かれており、フルタイムの正社員・業務委託含め合計で 8 人ほどの開発者が関わっています。私は Platform チームのメンバーです。
開発者以外で関わるステークホルダーとして、プロダクトオーナーとデザインチームがいます。デザイナーやデザインエンジニアのメンバーがスタイリングの実装をする場合もあります。
モノレポ
Ruby on Rails によるバックエンド・Terraform によるインフラ・仕様書なども含めて 1 つの GitHub リポジトリにまとめています。そのため今回の記事はリポジトリの /frontend
ディレクトリ配下の内容となります。
ROUTE06 では全社的に GitHub をワークスペースとして利用しており、PdM や デザイナーメンバーとのコミュニケーションも Issues, Discussions, Pull Requests で行っています。
GraphQL の採用
フロントエンド / バックエンド間のインターフェースとして GraphQL を採用しています。
gRPC と比較した場合はクライアント側ライブラリが豊富であること、REST と比較した場合は 1 回のネットワークリクエストでクライアント側が欲しい情報を柔軟に取得できることが選定理由に挙げられます。社内の別のプロダクトでも採用事例があり、チームにノウハウが蓄積されていたのも大きいです。
採用した技術や方針
TypeScript, React
フロントエンド開発をする上でのベースの技術として、TypeScript, React を採用しました。
TypeScript, React は社内で知見をもつメンバーが多く、また世界的な採用実績もあるためエンジニア採用観点でのアドバンテージが大きいと判断し採用しました。
Vite の採用
前述した一つ前のプロダクトでは Next.js を採用していましたが、そこでの経験を踏まえつつプロダクトの特性を考慮し今回は Vite による SPA を採用しました。主な理由は以下です。
- サーバーサイドでの実行フェーズがなくなることで開発中に考慮すべきポイントが減り、またインフラコストの削減ができた
- GraphQL のキャッシュの扱いや認証・認可のロジックなど、SSR を介すことによる複雑さと比べてメリットを感じ辛かった
- ユーザーインタラクションが多いプロダクトで、ブラウザ上で動作する JS が主となるため SSR の恩恵を受けることが少ない
- React の今後の志向性としては Server Component のようなサーバーサイドでのレンダリングに向かっていくことは理解しつつ、エコシステムの状況を踏まえてまだ今ではないと判断
ブラウザでの動作のみを考慮すれば良いことによる開発速度の向上を享受しつつ、React Server Component や Next.js App Router の動向を追いながら、状況が変わったタイミングでいつでも切り替えられるよう準備を進めています。
詳細記事: Coming soon
Yarn Workspace によるマルチパッケージ構成
パッケージマネージャが提供する Workspace 機能を利用してアプリケーションを複数のパッケージに分割しています。また、それぞれのチームが独立して開発を進められるようにするために各パッケージごとに担当チームを割り当てる運用としています。
達成したいこととして、以下の論点がありました。
- 「カスタマイズ可能な共通基盤」というプロダクトで、個々のお客様のニーズを個別実装するか共通化するかの判断がプロダクトマネジメントとしてもアーキテクチャとしても難しい
- 全てをカスタマイズ前提の抽象化された仕様にするのはモデリングと実装の難易度が跳ね上がり、一方個別実装が増えすぎると保守の難易度が跳ね上がるため、チームで学習を進めながら進められるちょうど良いバランスを探りたい
- 現時点でも多くのチームやメンバーが関わることがわかっているため、それぞれのチームが独立して開発を進められるようにしたい
- とはいえ立ち上げ期であり、スピードを保った状態で開発を進めたい
他の選択肢として 1 つの package.json で全てのコードを管理する方法、基盤になる機能を npm private package として切り出す方法がありましたが、Workspace 機能でパッケージを分割することによってほどよく境界を区切りながら依存関係を明確にしつつ、各チームが独立して開発を進められる状態が実現しやすいと判断しました。チームやプロダクトのスケールに合わせて将来的に基盤系パッケージを npm private package として切り出す余地も残されています。
パッケージの担当チームは GitHub の CODEOWNERS として表現し、レビューがチームに自動でアサインされるようにしています。複数のチームがレビューしなければならない状況は境界が曖昧になっている黄色信号と考えることができ、パッケージの責務を調整する動きにつながります。
Workspace 機能による分割を支えるために、以下のツールも導入しています。これらの詳細についても別途記事を書くつもりです。
- Turborepo ... パッケージ間のタスクの依存関係を定義・タスクの並列実行・差分ビルド
- dependency-cruiser ... JS / TS コードの依存関係の lint
- syncpack ... 複数パッケージ内で依存する外部パッケージのバージョンがずれていないことをチェックする
余談: 現在は Yarn v1 を利用していますが、 近いうちに pnpm への移行を予定しています。依存関係インストールの高速化・厳密なパッケージの参照ができるのがモチベーションです。Yarn v1 での Workspace 機能はルートの node_modules に巻き上げ (hoisting) を行うため実質的に全てのパッケージをどこからでも参照できてしまうのと、同じパッケージの複数バージョンが node_modules に含まれた場合での予期しない不具合が発生する可能性があるのが気になっています。
詳細記事: Coming soon
パッケージ分割の方針
大きく分けて以下の 3 つのレイヤーに分割し、依存関係が上位レイヤーから下位レイヤーに向かうようにしています。
- projects ... エントリーポイントとなる vite によるアプリケーション 可能な限り薄く保つ
- features ... ドメインの各機能を提供する
- platform ... ドメインに依存しない機能や共通設定などを提供する 例: ui, router, storybook
projects には複数のアプリケーションが共存し、それぞれ Vite アプリケーションが動作します。ただ projects のコードは最小限に保ち、可能な限り features 以下のロジックとして提供します。これによってアプリケーションごとのコード重複を抑えつつ、新規アプリケーションの追加・保守が容易になります。
GraphQL クライアント
GraphQL クライアントは urql を採用しました。
React Suspense に対応していることと、GraphQL Code Generator によるコード生成に対応していることが理由です。
またキャッシュアルゴリズムとしては当初は Graphcache を採用しましたが、デフォルトである Document Caching に戻すことを検討しています。 Graphcache は Apollo Client と同様の正規化されたキャッシュで、 urql デフォルトの Document Caching に比べてネットワークリクエストをさらに減らせることが魅力ですが、トレードオフとしてキャッシュ操作を開発者が手動で行うことによるバグの発生を懸念しています。
B to B アプリケーションのためネットワークリクエストを多少富豪的に行っても API への負荷は少ないため、キャッシュ操作は基本的にせず安全側に倒したいと考えています。
GraphQL Code Generator では新しい Client preset を利用し、Fragment Masking を利用した Fragment Colocation を運用しています。 Fragment Masking の制約はキツすぎるのでは、と導入当初は懸念がありましたが、今のところは問題なく運用できています。
詳細記事: Coming soon
ルーティング
ルーティングは React Router + react-router-typesafe-routes を採用しました。
ページ数が多いため、Link コンポーネントに渡す href
などのナビゲーションを型安全に行うことでミスを減らしたいというモチベーションを前提にいくつかのライブラリを比較しました。利用者数の多い React Router が最初は有力でしたが、React Router 単体では型安全なナビゲーションは実現できないためそこを解決する他のルーターライブラリがいくつか名乗りを上げている状況です。
react-router-typesafe-routes は React Router と組み合わせて利用するライブラリで、オブジェクトベースでパスを定義し、そのオブジェクトを React Router のパス指定・コンポーネントの href
の両方で使うことができます。 /users/:userId?name=foo
の userId, name を型安全に取り出すことができ、また React Router の挙動に干渉することがないため、こちらを採用することにしました。
詳細記事: Coming soon
スタイリング
スタイリングについては Chakra UI をベースに行うこととしました。理由は以下です。
- B to B 業務アプリケーションにおいて必要とされるコンポーネントが一通り揃っており、コンポーネントを利用するだけで一定程度のアクセシビリティが担保されること
- Figma UI Kit が提供されており、デザイナーチームが Chakra UI をベースとしてデザインすることに慣れていること
- Chakra UI としてのデザインガイドラインがあるわけではないので、デザイン上の制約が UI コンポーネントライブラリとしては少ないこと
とはいえ Chakra UI から別のライブラリへの移行をするなどの状況の変化に耐えられるよう、可能な限り Chakra UI への過度な依存はしないようにしたいです。そのため @chakra-ui/react への依存は ui パッケージ内に閉じ、他のパッケージは ui パッケージを利用するようにしています。これにより、デザイナーメンバーがスタイリングの実装・修正をする際も ui パッケージだけの修正に閉じることができ、認知負荷の低減にも役立っています。
Plain はカスタマイズ可能な共通基盤であり、そこには UI デザインのカスタマイズも含まれています。プロダクトごとのカスタマイズを実現するために Tokens Studio for Figma を利用したデザイントークンの仕組みを導入しており、このトークンを利用して Figma 上でデザインし、フロントエンド実装に反映するようにしています。
余談: Chakra UI が依存する emotion が React Server Component に対応していないため、別の UI コンポーネントライブラリへ移行しなければならない日も来るかもしれません。zero runtime な UI コンポーネントライブラリもいくつか出てきていますが、現時点では様子見をしています。
同様に Figma Variables もまだ Tokens Studio for Figma の代替にはならないと考えていますが、Figma Variables によって今後のフロントエンド開発が大きく変わる可能性もあるため動向を追っています。
何にしても変化に適応し続けるための柔軟性を保つことは重要だなと改めて実感します。
詳細記事: Coming soon
テスト
基本方針としてTesting Trophyに従い、アプリケーション利用者目線によるテストを優先させることとしています。以下の技術を採用しています。
- Jest ... 機能付きコンポーネントのテスト・ユーティリティ関数のユニットテスト
- Playwright ... ユーザーインタラクションの E2E テスト
- Mock Service Worker ... テストや Storybook でネットワークリクエストのモックに利用。 GraphQL のオブジェクトをモックすることが多く、graphql-codegen-typescript-mock-data のモックデータも利用している
Linter / Formatter
ESLint, Prettier, 前述した dependency-cruiser, syncpack を利用しています。
マルチパッケージ運用を採用していたり、プロダクト要件が複雑なため初見での学習コストは高いと感じています。静的解析はその認知負荷の低減に寄与すると考えており、積極的に投資をしたいです。プロジェクト内でのベストプラクティスは可能な限り Linter として表現できるようにしたり、実装方法がいくつかあり悩む状況下では Linter のルールがある方を選択します。
hygen によるコード生成
開発者間での実装方法のブレを減らすために、 hygen によるテンプレート生成の仕組みを用意しています。現在は React コンポーネントと Workspace 機能のパッケージの雛形を用意しています。
ADR の運用
上記の各種選定内容は、リポジトリに蓄積された ADR を元に記載しています。今回はサマリーのような記事のためそれぞれは概要を触れるだけとなっていますが、それぞれの ADR には詳細なコンテキストや選定理由・検討した別の選択肢などが記載されています。
以下はフロントエンドの ADR の一覧のスクリーンショットです。
フロントエンドの ADR だけで現在 35 個あり、バックエンドや全体の ADR もあります(今回は割愛しています)。
新たな意思決定時に記載するのはもちろん、新メンバーのオンボーディングの一環で一通り読んでもらった上で議論を進めながら暗黙的な意思決定に気づくこともあり、その際にも記載しています。
ADR を書くことは一定のコストがかかりますが、ADR があることで意思決定のプロセスが洗練されていき、また後から意思決定の経緯を振り返ることができることに大きな価値を感じています。当時の意思決定のコンテキストを追いながら、未来に向けた意思決定に繋げることができます。
ADR については、ROUTE06 メンバーが別の記事も書いています。よければこちらも参考にしてください。
終わりに
この記事では 2023 年 8 月現在の Plain のフロントエンドアーキテクチャについて、リポジトリに蓄積された ADR を元に技術選定の経緯や採用した技術の特徴などをまとめました。
それぞれの領域は概要に触れるだけとなっていますが、Coming soon としている各領域についても別途記事を書く予定です。順序は決めていないため、「これは特に知りたい」といった項目があればぜひツイートなどで反応いただけると嬉しいです。
合わせて宣伝です。ROUTE06 では変化の速い Web フロントエンド領域の動向を追いながら、ユーザーに価値を届け続けることに注力するソフトウェアエンジニアを募集しています。カジュアル面談からでも歓迎なので、興味がある方はぜひご連絡ください。