Open4

フロントエンドの実装について考えたメモ

KAMYKAMY

フロントエンドのモデル

フロントエンドにとってのモデル = UIをデータ構造で表したもの。
つまり、フロントエンドの関心事とUIは本質的に密結合である。

その場合の問題点

上記の観点で実装を進めると、ページや機能単位での実装になりがち。トランザクションスクリプト的なコードになってしまう。それを改善するにはどうするべきか?

改善方法

ロジックをモジュールに閉じ込める = React hooks
その場合、どの程度のレベルで抽象化するべきなのか。

→過度な共通化を避ける+場合に応じてすぐにモジュールを剥がせるような設計を心掛けるべき。
フロントは頻繁に修正が入る箇所であり、似たようなUIでも細部が微妙に異なる場合も多い。
それらの差異を共通のモジュールで吸収しようとすると、コードが過度に複雑化する。

本当に抽象化できるのか( = ロジックの関心事が同じなのか)、それとも単に同じコードが登場しただけなのか、それを見極めた上でモジュール化することが重要。

(ただし、その見極めはエンジニアの個人技に依存しがち。コードレビューやペアプロで標準化を図れればいいが…)

KAMYKAMY

APIから取得するデータをどう表現するべきか

APIから取得するデータはフロント側では制御できない。つまり、期待するデータ構造では無いという可能性を排除できない。
TypeScript上でラディカルに扱うならば、取得するデータの型はすべてunknownになるのか?

現実的な手段を探る

APIから取得するデータをunknownとして扱いつつ、ユーザー定義型ガードで型を絞り込むという方法がある。ただし、取得するデータすべてにそれを適応するのは現実的では無い。
実装する上で最適な表現方法は何なのか。

1. APIのデータの型を定義する

「このようなデータが確実に返ってくる」と確信を持てるのであれば、もっともシンプルな方法。
チーム内での開発であれば概ね問題なさそう。
ただし、サーバー側の修正によってフロントの型定義およびコード中のプロパティ名等も修正が必要というデメリットがある

2. フロントが必要とする型を定義+APIのデータは抽象的な型を付ける

APIから返ってくるデータを直接使用せず、フロント用に変換しているのであれば、この方法も使える。
具体的には、

  • フロント用の型:{id:string; name:string;} 等
  • 取得するデータ:{[key:string]:unknown} や {[key:string]:unknown[]} 等

と表現した上で、データをフロント用に変換するレイヤーでそれらを用いる。
メリットとしては、APIの修正を変換用のレイヤーで吸収できる。
デメリットとしては、変換用のレイヤーでunknownstring等に解釈させるため、型アサーションが必要になる。型安全では無いものの、「取得するデータを制御できない」という点を踏まえれば、致し方ないか…?

3. 上記2つのハイブリット

APIから取得するデータの型を定義する+フロント用のデータの型を定義する。
それらを準備した上で、取得したデータをフロント用に変換する。
1と比べて工数は増えるものの、API側の修正に対する影響範囲を最小限に留められる。

データの取得失敗はどう表現する?

BeforeFetch, FetchSuccess, FetchFailed等の型を準備しておけば、それらを用いてデータ取得時の各状態を表現できる。
各データ構造を{type:'beforeFetch' | 'success' | 'failed' ; body:求める返り値}のようなオブジェクトにしておけば、型ガードによる絞り込みも容易になる。

KAMYKAMY

粒度の大きな汎用コンポーネントは避ける

具体的には、AtomicDesignでOrganismと呼ばれる粒度での汎用コンポーネントは避ける方が良い。

理由

  • コンポーネントは大小様々な修正を繰り返す箇所。共通コンポーネントに見えても、微妙に仕様が異なることも起こりうる
  • 粒度が大きくなるほど、当然内部の実装も複雑化する。それを一まとめに共通化すると、保守性が悪化する
  • 上記2つが合わさると、複雑な実装+ページ固有の条件分岐による複雑怪奇なコンポーネントが爆誕する。

ではどの単位で共通化するべきか

いわゆる、AtomsやMolecules単位で共通化するのが良さそう。
具体的には、ボタンや見出し、フォームの入力欄など、UIの中のパーツ単位。
その程度の粒度であれば実装も複雑化しにくい。また、ページ固有のパーツが必要になった際の差し替えも容易。

KAMYKAMY

APIとフロントエンドの接点

基本的に、API側に関連した物事がフロントに漏れ出るのは避けておきたい。
なぜなら、APIとフロントを疎結合にしておけば、APIの変更がフロントまで波及することを避けられるはずだから。
しかし、現実的には、必ずどこかでAPI側の知識(POST / GET するデータ構造など)を持つ必要がある。

APIの知識を持つレイヤーを制限する

APIの知識を持つことが避けられないのならば、その知識を持つレイヤーを限定すれば良い。
Repository等のレイヤーを設け、そこをAPIとの接点としてデータの変換、POST / GET を行えば、APIとフロントを疎結合に保つことができる。

POSTとGETでレイヤーを分けるべきか?

分けたほうが柔軟に対応できる(取得したデータを画面に応じて別の構造に変換する、など)
しかし、ソースコードが冗長になるため、そこまでする必要があるかどうかは要検討。

また、画面に応じたデータ変換という役割はSelectorに担わせることもできる。
Reduxを導入する場合は、むしろその方がRedux Wayに沿った形になる。

以上を考えると、個人的には「そこまで分けるのはやり過ぎかも…」という印象。