フロントエンドのコンポーネントの責務分離のための方針とその効果について

目次

はじめに

こんにちは!ハイヤールーの共同創業者の谷合です。

ハイヤールー初のアドベントカレンダー初日を担当します!

現在弊社では Web フロントエンドのリアーキテクチャプロジェクトが行われています。

既存のフロントエンドのアーキテクチャを一新するために、様々な取り組みを行っています。

その中から今回の記事は、責務の分離について詳しく言及したいと思います。

どのようなアーキテクチャを選んだのか、またそれによって得られたメリット・デメリットについて説明いたします。

フロントエンドに限らず、プログラミングにおける関心事項の一つでもあり、シンプルですがかなり大きな方針転換の一つだと考えています。

※文字数が多くなってしまうので、今回の記事では具体的なコードには触れず、別記事にて実際に遭遇したケースについて解説予定です。

リアーキテクチャについての詳しい背景などは、以前僕が書いたWeb フロントエンドのリアーキテクチャに向けた課題整理と解決の道筋をご参照下さい。

TL;DR

  • UI の表示ロジックとビジネスロジックの責務分離することによって、「テスタビリティ」と「再利用性」が高まった
  • それによって**「開発スピードの改善」「PR レビューの時間短縮」**などの生産性の向上にも繋がった
  • ただしそれらの恩恵を享受するためには、「ルール化とドキュメントの整備」「レビュー時の指摘」、**「自動化できるものは自動化する」**などを徹底する必要がある

背景

改めて UI の表示ロジックとビジネスロジックの責務分離を検討するに至った課題感を上述の記事から列挙しました。

  • UI の表示ロジックとビジネスロジックがコンポーネント内に混在してしまい、テスタビリティが下がっていた
  • コンポーネントが最低限しか使いまわせないため、機能実装のたびに作り直していた
  • それにより一つの PR が肥大化する傾向があり、レビュー時に時間がかかるようになった

これらを踏まえて、弊社ではどのようなアプローチを取ったのか、その結果どうなったのかというのがこの記事の趣旨となります。

アーキテクチャと方針

方針についてそれぞれ説明させて頂く前に、まず簡単に全体のアーキテクチャがどのようになっているのかについて言及します。

弊社ではpnpmを利用したモノレポのアーキテクチャを利用しているため、以下はすべて Package 単位で分割されています。

├─ app
├─ app-store
├─ backend
├─ ui
…
  • appUI の呼び出しもこの Package 経由で行うビジネスロジックをまとめた Container 層。State ライブラリの利用や、Backend(GraphQL)の呼び出しなどはここで行う
  • app-storeState ライブラリの action や hooks などがまとまっている Package
  • backend ブラウザとバックエンドの通信境界と他のマイクロサービスとの通信境界。GraphQL の処理がまとまっている Package
  • uiUI のコンポーネントを持つ Presentational 層

この記事では特に**app(Container 層)ui(Presentational 層)**についての説明がメインとなります。

ではさっそく方針について説明していきます。

1. Presentational 層と Container 層を分離する

Presentational 層と Container 層に分離するという考え方は、元 Facebook で Redux や Create React App を開発した Dan Abramov さんのPresentational and Container Componentsが有名ですが、弊社でもその考え方を取り入れたアーキテクチャを選択しました。

かなり基本的なことですが、再利用性やテスタビリティを向上させるために責務を完全に分離するということを徹底しました。

以前のアーキテクチャでは、UI の表示ロジックとそれを操作するビジネスロジックが一つのコンポーネントとしてまとまっていたため、ひどいケースだと 1 ファイル 1500 行以上の巨大コンポーネントが存在しており、中身を読み解くのにかなりの時間が費やされたり、専任で保守する人が必要になるなどの問題が発生していました。

そのようなコンポーネントは、テスタビリティが低く基本的には共通で利用するコンポーネントや内部で利用されているピュアな関数にのみテストが書かれている状態でした。

そのため変更を行うとデグレが起こる可能性が高く、かなり扱いにくいものとなっていました。

そうしたコードを新しいアーキテクチャでは、2 つのファイルに分割することができます。

Presentational 層は受け取った Props をただ表示する程度に留まり、その Props は Container 層で生成されたものを利用します。

Presentational 層はどのようなデータを受け取って(インターフェース)何を画面に表示するのか。という部分に集中ができ、Container 層ではその UI に渡すデータをどう取得するのか、取得したデータをどこでどう管理するのかに集中することができるようになりました。

また、これによって UI とビジネスロジックの分離が行われるため、それぞれのコンポーネントでテストが書きやすくなり、結果として先程の可読性やデグレの問題を軽減できるようになりました。

加え今まで一つのページを作る際に、UI とビジネスロジックという形で作業者の分離や PR を分割することは難しかったですがこのアーキテクチャにしたことで、副次的にそういったことも可能になったのも大きなメリットの一つです。

今までのアーキテクチャでは、どうしても PR を分割しにくかったため PR が肥大化する傾向があったというのは背景でも述べたとおりでしたが、PR を分割できることによってレビューが長引くこうした課題にもアプローチできるようになりました。

ただ新規コンポーネントにおいての、作業者の分離に関しては課題もあって、どこで状態管理を行うか、どの粒度でコンポーネントを切るかなどは、作業者に委ねられる部分が大きいため、UI を作成した作業者がロジックをつなぎこむというのがスムーズなケースが多い印象です。

2. Presentational 層は階層を細分化して再利用可能性を高める

Presentaion 層は更に以下の粒度で分解することで、更に再利用可能性を高める工夫をしています。

いわゆるアトミックデザイン的な考え方に近いですが、アトミックデザインの概念にはドメインの情報が含まれないため、弊社では独自に以下の分け方でコンポーネントを切っています。

  • Primitive最小限の単位。Page、Widge、Usecase などから参照されても利用できるように、ユースケースやドメインが入り込まない単位に分割例) ボタン、 ダイアログ、 リンク など
  • UsecasePremitive より詳細度が高く、複数のページから参照できるユースケースが含まれたコンポーネント例) フォームの提出ボタン、 言語選択用のセレクター、 検索エリアなど
  • Widget後述する Container 層からの接続を前提として、他の Page にそのまま差し込むことができるコンポーネント(自己完結するコンポーネント)複数の Usecase を含むこともできるフッター、 スナックバー、 ナビゲーション など
  • Page各ページのコンポーネント。それぞれのルーティングごとにマッピングされる例) テスト作成画面、テスト一覧画面、ホーム画面 など
  • **(Layout)**ページから参照されるレイアウト。フッターやスナックバーなどページグループごとに共通して持っているべきコンポーネントを持っている

過去のアーキテクチャでも共通化されていた部分はありましたが、あくまでも共通で利用できるもの(Primitive くらいの粒度)がほとんどだったため、そこまで恩恵を受けられておりませんでした。

また、特定のページでのみ利用されているコンポーネントが共通化されていたケースもありましたが、内部にビジネスロジックが含まれていることがほとんどだったため、例えば grpc の特定のドメインのインターフェースを Props として要求するが故に、UI としては共通だがインターフェースが異なるため利用できないといったケースも存在しました。

そういった再利用性が低い状態を取り除いたおかげで、画面に表示される UI ベースで共通化が可能になりました。

特に Primitive や Usecase などの使い回しの効くコンポーネントを抽出することができるようになったことで、最初こそ時間はかかりましたが共通化されたコンポーネントをインポートするだけである程度 UI が組み立てられるといったことが可能になりました。

これにより、今までページ作成時にほぼすべてのコンポーネントを一から作成し、そのたびにテストを書いたりしていましたが、それらの時間が短縮されました。

実際に計測をしたわけではないため、体感になりますが以前のアーキテ f クチャと比べて作業時間の 10%-15%程は UI 作成の時間が軽減されたと思います。

3. Container 層の責務分離

Container 層に関しても責務ごとにいくつかのコンポーネントに切り分けることで、それぞれの役割を明確に定め、責任の所在を明らかにしています。

  • FetchContainer 初期値が必要なページの場合の、初期 fetch を行うコンポーネント初期値が取得できなかった場合(やエラーが発生した場合)はこのコンポーネントの ErrorBoundary で Catch する
  • ContainerPresentational 層の Page を返すだけのコンポーネント必要な Props は以下の GenerateProps から受け取る
  • useGeneratePropsPage が必要な Props を生成、取得する処理を分離したカスタムフック Props ごとにファイルを分けたり、カスタムフックを分離することも可能

弊社プロジェクトのディレクトリ構造を一部抜粋すると以下のイメージです。

弊社プロジェクトのディレクトリ構造を一部抜粋また Container 層からは、State ライブラリの Package を参照する形で利用していますが、action や hooks はただの TypeScript のコードなので、こちらもテスタビリティが高い状態で切り出されています。

4. 参照の向き先を制限させる

最後に上記の方針で切り分けたコンポーネントの参照の向き先を制限することによって、アーキテクチャの腐敗を防ぐようにしています。

具体的には、循環参照を避けるために Page コンポーネントから Page コンポーネントの参照を禁止したり、Presentational 層のコンポーネントの切り分け時に、Usecase には必ずドメイン名あるいは、ユースケース名が含まれているといったレベルの制限というものをルールとして設けています。

基本的にはレビューで指摘したり、弊社のスタイルガイドにルール化する程度の制限のものもあれば、dependency-cruiseを利用して循環参照のチェックをしたり、コンポーネントの粒度と内容が一致しているかなどをファイル名やディレクトリ名に含まれているかなどをテストするなど、一部自動化されているものもあります。

弊社内部で利用しているルールの一部を以下に示します。

単に方針を定め、共通化やコンポーネントを細分化するとなると、意図しない参照が至るところに発生するといった弊害が発生することもあると思いますので、そうした制限を同時に行うことをおすすめします。

また、上記ルールを実施したおかげで副次的にですが、Page 下に配置されるコンポーネントが他の Page や Widget から参照されることがないという共通認識が取れるようになったため、「このディレクトリ以下のコンポーネントはしっかりレビューしなくてはいけない。」逆に「このコンポーネントは他からも参照される可能性があるため、しっかりレビューしなくてはいけない」などの共通認識も生まれました。

実際やってみてどうだったか

最後に上記の方針でプロジェクトを進めてみてどうだったかについて言及したいと思います。

(※ただし、現状リアーキテクチャの途中であるため開発時に限った話になります。実際に運用された際の比較については、リリース後別途記事を書く予定です。)

責務分離と再利用性の高いコンポーネントによって開発スピードが向上した

コンポーネントの役割が明確になり、更に共通で利用できるコンポーネントが増えたことによって、既存のアーキテクチャに比べて開発スピードが上がりました。

これはリアーキテクチャの初期フェーズと今の進捗とを比べると明確で、作業者数は変わらないにもかかわらず、スプリント中にマージできる PR の数や移行される画面数が明確に増えていることを実感しています。

今後荒い粒度で分離されているコンポーネントを更に整えると、新規ページにおいて既存のパーツを組み合わせるだけで UI が完成するといったことが可能になることが期待されます。

そうすることにより、更に開発のスピード向上に寄与できるのではないかと考えています。

チームのコンポーネントの分解能力が上がった

チーム全体のフロントエンドのレベルが上がったことによって、どのレベルでコンポーネントを切り出せば他で利用しやすいかなどを考えながら作業する能力が上がりました。

それによって各コンポーネントの責務が明確になることにより、PR レビューの際も今までと比べて議論する余地がほとんどないくらいシンプルになりました。

とはいえいきなりレベルを上げるのはしんどい

今回フロントエンドにかなり詳しいメンバーの協力を得て、アーキテクチャを定めてそれを実行に移すといった形で作業を進めてきましたが、やはり経験者がある程度方針を定め、そのルールをチーム内で合意するということを徹底して初めて価値があるものだと実感しました。

また、コンポーネントの分離の粒度や、処理分割の方法、アーキテクチャを定めて終わりということはなく、それ元にをチームメンバーが実装できるようにドキュメントを用意したり、依存関係が発生しないように自動テストを組み込むなどの工夫があって初めてワークするフロントエンドの開発体制が整えられるものだと思います。

現状リアーキテクチャに関わるメンバーのみで作業をしていますが、リリース後に他のメンバーが開発スピードを落とすことなく実装できるために、まだまだ工夫の余地がある部分でもあります。

おわりに

本記事では、フロントエンドの UI とビジネスロジックの責務の分離についての方針と、それを実際にやってみた際の感想について述べました。

責務を明確に分離することによって「テスタビリティの向上」、「再利用性の向上」といった技術的なメリットと、それによって生じた「PR レビュー時間の短縮」、「開発スピードの改善」といったチーム全体のメリットについても言及いたしました。

今後チーム全体にこのアーキテクチャを広げる際に生じる課題や、効果についても別途記事にしたいと思います。

明日は @Himenonの「Storybook の自動生成による DX 改善」についての記事です。