フロントエンドの実装について考えたメモ
フロントエンドのモデル
フロントエンドにとってのモデル = UIをデータ構造で表したもの。
つまり、フロントエンドの関心事とUIは本質的に密結合である。
その場合の問題点
上記の観点で実装を進めると、ページや機能単位での実装になりがち。トランザクションスクリプト的なコードになってしまう。それを改善するにはどうするべきか?
改善方法
ロジックをモジュールに閉じ込める = React hooks
その場合、どの程度のレベルで抽象化するべきなのか。
→過度な共通化を避ける+場合に応じてすぐにモジュールを剥がせるような設計を心掛けるべき。
フロントは頻繁に修正が入る箇所であり、似たようなUIでも細部が微妙に異なる場合も多い。
それらの差異を共通のモジュールで吸収しようとすると、コードが過度に複雑化する。
本当に抽象化できるのか( = ロジックの関心事が同じなのか)、それとも単に同じコードが登場しただけなのか、それを見極めた上でモジュール化することが重要。
(ただし、その見極めはエンジニアの個人技に依存しがち。コードレビューやペアプロで標準化を図れればいいが…)
APIから取得するデータをどう表現するべきか
APIから取得するデータはフロント側では制御できない。つまり、期待するデータ構造では無いという可能性を排除できない。
TypeScript上でラディカルに扱うならば、取得するデータの型はすべてunknown
になるのか?
現実的な手段を探る
APIから取得するデータをunknown
として扱いつつ、ユーザー定義型ガードで型を絞り込むという方法がある。ただし、取得するデータすべてにそれを適応するのは現実的では無い。
実装する上で最適な表現方法は何なのか。
1. APIのデータの型を定義する
「このようなデータが確実に返ってくる」と確信を持てるのであれば、もっともシンプルな方法。
チーム内での開発であれば概ね問題なさそう。
ただし、サーバー側の修正によってフロントの型定義およびコード中のプロパティ名等も修正が必要というデメリットがある
2. フロントが必要とする型を定義+APIのデータは抽象的な型を付ける
APIから返ってくるデータを直接使用せず、フロント用に変換しているのであれば、この方法も使える。
具体的には、
- フロント用の型:
{id:string; name:string;} 等
- 取得するデータ:
{[key:string]:unknown} や {[key:string]:unknown[]} 等
と表現した上で、データをフロント用に変換するレイヤーでそれらを用いる。
メリットとしては、APIの修正を変換用のレイヤーで吸収できる。
デメリットとしては、変換用のレイヤーでunknown
をstring
等に解釈させるため、型アサーションが必要になる。型安全では無いものの、「取得するデータを制御できない」という点を踏まえれば、致し方ないか…?
3. 上記2つのハイブリット
APIから取得するデータの型を定義する+フロント用のデータの型を定義する。
それらを準備した上で、取得したデータをフロント用に変換する。
1と比べて工数は増えるものの、API側の修正に対する影響範囲を最小限に留められる。
データの取得失敗はどう表現する?
BeforeFetch
, FetchSuccess
, FetchFailed
等の型を準備しておけば、それらを用いてデータ取得時の各状態を表現できる。
各データ構造を{type:'beforeFetch' | 'success' | 'failed' ; body:求める返り値}
のようなオブジェクトにしておけば、型ガードによる絞り込みも容易になる。
粒度の大きな汎用コンポーネントは避ける
具体的には、AtomicDesignでOrganismと呼ばれる粒度での汎用コンポーネントは避ける方が良い。
理由
- コンポーネントは大小様々な修正を繰り返す箇所。共通コンポーネントに見えても、微妙に仕様が異なることも起こりうる
- 粒度が大きくなるほど、当然内部の実装も複雑化する。それを一まとめに共通化すると、保守性が悪化する
- 上記2つが合わさると、複雑な実装+ページ固有の条件分岐による複雑怪奇なコンポーネントが爆誕する。
ではどの単位で共通化するべきか
いわゆる、AtomsやMolecules単位で共通化するのが良さそう。
具体的には、ボタンや見出し、フォームの入力欄など、UIの中のパーツ単位。
その程度の粒度であれば実装も複雑化しにくい。また、ページ固有のパーツが必要になった際の差し替えも容易。
APIとフロントエンドの接点
基本的に、API側に関連した物事がフロントに漏れ出るのは避けておきたい。
なぜなら、APIとフロントを疎結合にしておけば、APIの変更がフロントまで波及することを避けられるはずだから。
しかし、現実的には、必ずどこかでAPI側の知識(POST / GET するデータ構造など)を持つ必要がある。
APIの知識を持つレイヤーを制限する
APIの知識を持つことが避けられないのならば、その知識を持つレイヤーを限定すれば良い。
Repository等のレイヤーを設け、そこをAPIとの接点としてデータの変換、POST / GET を行えば、APIとフロントを疎結合に保つことができる。
POSTとGETでレイヤーを分けるべきか?
分けたほうが柔軟に対応できる(取得したデータを画面に応じて別の構造に変換する、など)
しかし、ソースコードが冗長になるため、そこまでする必要があるかどうかは要検討。
また、画面に応じたデータ変換という役割はSelectorに担わせることもできる。
Reduxを導入する場合は、むしろその方がRedux Wayに沿った形になる。
以上を考えると、個人的には「そこまで分けるのはやり過ぎかも…」という印象。