Kaizen Adのフロントエンドアーキテクチャの遷移について

Kaizen Platformで主にフロントエンドを開発しているyuki-yanoです。
TypeScriptが好きで、最近はZennにDenoでzshのプラグインを作った記事を投稿しました。

今回はKaizen Adというプロダクトにおけるフロントエンドのアーキテクチャの遷移について紹介します。
Kaizen Platformでは2019年に React + GraphQL から成る Kaizen Ad のフロントエンド - Kaizen Platform 開発者ブログ という記事を書いています。
その後、プロダクトが成長するにつれて課題なども出てきており、現在の実装方針は変わってきています。

この記事では現在のアーキテクチャと、どういう経緯があって変遷してきたかについて紹介します。

これまでのKaizen Adでのフロント開発

これまでの実装は 前回の記事 に書いている方針で進めてきました。
基本的にレイヤードアーキテクチャを前提としており、Viewの世界の中だけではなくアプリケーション全体としてView, Usecase, Entityというレイヤを切る規約で強めに縛った設計となっていました。

レイヤードアーキテクチャによる開発は当然様々なメリットもありました。例えば

  • アーキテクチャが共有されていれば誰が書いても近いコードになる
  • ドメインに関わるStateを扱う処理をどこで実装するかが明確(Viewは基本的に関与しなくていい)
  • Serviceのレイヤが抽象化されているのでViewではEntityを取り回すだけで、内部実装などは一切知らなくてよい

などといったものです。

以下が前回の記事で概要の説明に使っていた画像です。
それぞれについての詳細は 前回の記事 を参考にしてください。

20210609115405 20210609115420

現状の課題

プロダクトが成長するにつれて、当時の設計では解決の難しい問題がいくつかでてきました。
具体的に書くと以下になります。

  • レイヤードアーキテクチャの実装手法によるGlobal Stateの肥大化
  • コンポーネントのレイヤリングによる巨大なProps Drilling
  • エンティティの肥大化に伴うGraphQLクエリのサイズ増加

それでは、それぞれの課題の詳細と現在の方針について説明します。

レイヤードアーキテクチャの実装手法によるGlobal Stateの肥大化

これまでの課題

前述の通り、Kaizen Adではレイヤードアーキテクチャを用いて開発しています。

これは大雑把に言うとRedux Thunkを用いた状態管理に近いものとなっています。
UsecaseはThunk Actionと対応しており、Usecase内のビジネスロジックから各種ActionがdispatchされてGlobal Stateが更新されます。

これにより要件が複雑なUIによる入力・影響範囲がUsecaseで処理されることによるGlobal Stateの複雑化が問題になりつつあります。
UsecaseはGlobal Stateと密結合しており、内部ではState全体へアクセスできるインターフェイスが多用されています。
Viewの1つの操作が大量のActionをdispatchしてしまい、Global Stateのどこに影響を与えるか分かりづらくなりがちです。
そのため、再レンダリングの制御も難しくなってしまっています。

また、本来であれば特定のフォームに閉じるようなStateであっても、エンティティに関わる状態管理には基本的に全てUsecaseを通すという制約の影響で実装には多くのファイルに変更を加える必要があります。

現在の方針

これは最近のReactのプラクティスにも繋がりますが、Global Stateだけではなく適切な粒度でLocal Stateを作成し、その中でStateを管理していきたいと思っています。
具体的には大きな方針としてはGlobal Stateが必要ない場面ではUsecaseを使わないようにしていく、ということを考えています。
今までUsecaseに記述していたビジネスロジックについてはCustom Hooksへと移行を進めています。

また、Global Stateの肥大化については「コンポーネントのレイヤリングによる巨大なProps Drilling」にも関わってきます。

コンポーネントのレイヤリングによる巨大なProps Drilling

これまでの課題

useContextで途中からStateを使うのはよくないパターンで、極力Propsを使うべきという意見も見かけますが、Kaizen AdではProps Drillingが問題となっています。

前回の記事には書かれていない内容なのですが、Global StateとUsecaseの扱い方としてRouter直下のURLを管理するレイヤ(pages)でコンポーネントに注入する必要がある、という規約がありました。
例を挙げると pages -> organisms -> atom, molecules というレイヤリングでViewが構築されています。

どれだけ複雑なViewだったとしても、pagesという最上位のレイヤでしかStateとUsecaseを注入していませんでした。
それによって、例えばページ全体の末端で制御するモーダルについてもStateとHandlerを最上位のコンポーネントから全てバケツリレーするという構造になっています。

多いところでは50個前後のPropsが何重にも渡されており、そのうち30個がHandler、20個がState、のような状態となっています。
このような状態では処理を追加する際に大量のファイルに変更を加える必要があります。また、Handlerがどのような処理と対応しているかについて何重もコンポーネントを遡る必要もあります。

現在の方針

解決法としては大きく分けて2つ考えています。

1つは前述したGlobal Stateからの脱却と並行してLocal Stateを適切に用いる事で、そもそもある程度任意の単位でStateを管理するようにする、というものです。
もう1つは単純にpagesレイヤ以外からの注入を許容します。

後者についてですが、何も考えず無秩序にGlobal Stateへのアクセスを濫用すると構造が破綻するという問題があると思います。
なので、全てのコンポーネントでStateへアクセスするのは許可しません。

しかし、Viewの末端のモーダルで使うStateやUsecaseを最上位からPropsとして渡すのは明らかに無駄が多いです。
そういった部分ではモーダル全体を囲うコンポーネントのレイヤでのState, Usecaseの注入を許容するように方針の変更を進めています。

エンティティの肥大化に伴うGraphQLクエリのサイズ増加

これまでの課題

既存のアーキテクチャではどのViewでも使えるように全てのプロパティを持っていることが前提のエンティティという巨大なオブジェクトをStateで取り回しています。
Kaizen AdではバックエンドのAPIにGraphQLを使っているのですが、その際にこのエンティティを生成するコストが問題になります。

複雑なドメインを持つオブジェクトの全てのプロパティを取得するという設計上、GraphQLのクエリ及びレスポンスが肥大化し、バックエンドの処理も時間がかかっています。
この使い方だと毎回同じFieldを送るという実装のため、本来のGraphQLの目的からも乖離してしまっています。

具体的に一部の数字を出すと

  • GraphQLのField: 700行前後
  • レスポンスが返ってくるまでの時間: 1秒強
  • レスポンスのサイズ: 40kb前後
  • prettierをかけたレスポンスのJSONの行数: 1万行前後

となっていました。

これがページ遷移の度に発生しているのでUXについても問題が出てきています。

現在の方針

一部については自分が改修を進めており、予想通り単純に不要なFieldの削除が最も効果的でした。
しかし、既存の実装については全てのプロパティが存在する前提となっており、一部を削除した際どこに影響が波及するか調査する必要があるの慎重に進める必要があります。

明らかに不要な部分を削除することで、画面遷移を1-2秒高速化できた部分も既に見つかっています。

また、自分が試した改善案として

  • ファーストビューに必要な要素だけを取得するFieldを作成して最初に取得
  • Viewのレンダリングをブロックせずに裏で別のFieldから取得したデータをエンティティに注入する

というものがあります。

これについても影響範囲の調査は必要なのですが、エンティティの構造自体には大きな影響を与えずに実装できる可能性があり、引き続き検討していきたいと思っています。

フロントエンド勉強会について

Kaizen Platformでは去年の8-9月から2週間に1度フロントエンド勉強会を開催し、知見を共有しています。
当初は読書会から始まり、現在は得た知見を持ち寄って話し合う時間になっています。
開催前はバックエンドエンジニアの方などがReactについてあまり知見がないという状況だったのですが、現在はフロントエンドのBetterな書き方についてお互いに議論できるような状態となってきています。

この記事のアーキテクチャの改善についてもこの勉強会から生まれたものであり、多くのエンジニアとReactの知見を共有することで問題点の共有・発見へと繋がりました。
この取り組みについては今後も続けていきたいと考えています。

最後に

当時のアーキテクチャから生まれたいくつかの課題はありますが、Kaizen Platformでは積極的にそれらの課題に対応しつつフロントエンドの開発を進めています。
プロダクトの成長につれて新たな課題が生まれていくかもしれませんが、それらについて議論しつつ改善していく環境は整いつつあり、今後も積極的にそれらの知見について発信していければなと考えています。

We're hiring!

Kaizen Platformで一緒に働いてくれる方を絶賛募集中です!

話を聞いてみるだけでもいいので、ご興味ある方はぜひ!

hrmos.co https://hrmos.co/pages/kaizenplatform/jobs/0510ftefehrmos.co hrmos.co