フロントエンド開発を安全かつ素早くできるように取り組んでいること

エンジニアの田川です。
インフルエンサーマーケティングを支援するCast Me!というプロダクトでフロントエンド開発を担当しています。

アプリケーション開発において、スケールすることを想定した設計は非常に大切です。
私が入社した当初のCast Me!は、フロントエンドの設計方針などが定まっておらず、機能改修などをした際に意図せず他の機能が壊れてしまう問題が多々ありました。

直近では、新規開発と並行して新アーキテクチャへの刷新を行ってフロントエンドの設計の見直しやルールの策定などをしています。
今回は安全かつ素早く開発をできる状態にするために、Cast Me!で行っている取り組みを紹介します。

機能ごとにディレクトリを作成する

コードを変更する際、特定の機能に関連するコードが散乱していると、変更の影響を完全に理解するのが困難になり、結果としてバグを生む可能性が高まります。

まず、全ての機能が同一の階層に配置される構成について考えてみましょう。これは以下のようなディレクトリ構成を指します。
この構造だと、全ての機能(例えば、「案件を作成する機能」や「ユーザーの新規登録機能」、「ユーザーのプロフィールを変更する機能」など)のコードが同一の階層(ここでは「hooks」など)に配置されます。
この方式では、アプリケーションが大規模になるほど、各機能のコードが一箇所に混在してしまうため、どのコードがどの機能に対応しているのかを把握するのが難しくなるという問題があります。
 
そこで、それぞれの機能ごとに独自のディレクトリを持つという考え方があり、Cast Me!のフロントエンドでは、開発する機能ごとにディレクトリを作成し、関連するファイルを配置しています。
例えば、「インフルエンサーに依頼する案件を作成する」という機能の場合、以下のようなディレクトリ構造になります。

この構造にすることで、「案件作成機能」に関連するコードを変更したいと思った時には、「project/register」(及び「project/core」)を見るだけで、該当機能の全体像を把握できます。
また、機能ごとにディレクトリを分けているため、特定の機能を削除する際にはディレクトリごと削除することで、不要なコードの大部分を一括で削除できるなど、コードの管理がしやすくなります。

ドメインモデルを操作するロジックは、極力コンポーネント内に記述しない

Cast Me!では、ドメインモデルを操作するロジックはEntityやValue Objectのファイルを別途作成し、そちらに記述するようにしています。
例えば、案件の手数料を計算するロジックは、下記のように、ProjectRewardのValue Objectに記載します。

このアプローチにより、ロジックとUIが密結合になることを防ぎ、再利用可能でテストしやすいコードを実現できます。
また、案件の手数料に関連するロジックを探す際は、ProjectReward内を確認するだけで明確になります。

レイヤーごとにコードを分割する

モジュールごとにAPI呼び出しや、ドメインモデルの定義、コンポーネントUIの定義などをそれぞれの層に分割するようにしています。

案件作成機能を例に挙げてみます。

この場合、処理の依存関係はrepositories → hooks → componentsとなります。

データの取得にはReact Queryを使用しています。

repository層はデータの取得に関心を持ち、データの加工やEntityへのマッピングはhooks層で行うようにします。domain層は前述の通りです。

このようにすることで、コンポーネントではUIに関連する処理のみを記述できるようになります。

より安全に開発するためのテクニック

TypeScriptで開発する際、以下の2つを使用することで型安全性を担保できるようにしています。

BrandedTypes

BrandedTypesを使用してプリミティブ型に固有の型を付与し、他のプリミティブ型の代入を防ぐようにしています。

BrandedTypes自体の実装自体は、下記のようにします。

Value Objectをclassで定義することでも代用可能ですが、valueへのアクセスが面倒なため、Cast Me!ではBrandedTypesでValue Objectを定義して利便性と型安全性を両立しています。

enumではなくconst assertionを使用する

TypeScriptでは、enumを定義する機能があります。

しかし、enumは意図しない値にアクセスできてしまう、ビルド時にTree-shakingが効かないなどいくつかの問題点があります。
そのためCast Me!では、代用としてconst assertionを使用しています。

ただし、この場合でも1か2であればどんな値でも代入できてしまうため、前述したBrandedTypesと組み合わせることでより安全にしています。

このようにすると、EmailStatusを引数に受け取るような関数などで、他のプリミティブ値が入る可能性を防ぐことが可能になります。

まとめ

フロントエンド開発においては、ベストプラクティスや新しい実装パターンが常に進化しています。しかし、一貫した設計手法を採用することで、リファクタリングや機能追加が容易になります。
常に学習と改善に取り組みながら、より良いフロントエンド開発を追求していきたいと思います。

PLAN-Bでは、この記事の内容以外にもフロントエンド技術の調査や導入、週に1度のフロントエンドに関する勉強会など、さまざまな取り組みを行っています。
もしモダンなフロントエンド開発に興味をお持ちの方がいらっしゃれば、ぜひ以下のリンクからご応募ください。

株式会社PLAN-B 採用情報