4年分の負債を解消するために React ディレクトリ構成について真剣に考えてみた

この記事は株式会社ビットキー Advent Calendar 2023 5日目の記事です。

# はじめに
この記事では React を用いたフロントエンドアプリケーションのディレクトリ構成について検討した内容を紹介します。
現在フロントエンド開発を行っていて、ディレクトリ構成にお悩みの方の参考になれば幸いです。

※ State 管理についての良し悪しやその他 React 向けのフレームワークライブラリについては本記事では触れません。

# 今回対象とするプロダクト
ビットキーのHome事業では、不動産管理会社向けのB2B2Cプロダクトを展開しています。
その中でも不動産管理会社の方が利用する管理画面について、リリース後から様々な機能や画面が実装されシステムが巨大化してきたので、ディレクトリ構成を見直す機会が訪れました。
参考値として、現在のプロダクトは100画面を超えており、ソースファイルも1500ファイル強もあるほど膨らんでいました。

※ 要件としてSEOは不要だったのでクライアントサイドレンダリングを選定しています

# 課題
対象プロダクトの機能開発を進める上で、画面によっては開発効率が著しく悪い状態のソースがありました。
具体例を挙げると、1コンポーネント内で View / ロジック / API呼び出しが全て詰め込まれていて約5000行近くあるファイルも存在していました。

元々コーディングルールは存在していたのですが、
- Lint や husky など静的チェックの仕組みを完全にメンテナンスできていなかった
- オフショアメンバーはブリッジエンジニアを通してコミュニケーションを取っていたため、入れ替わりも激しく個々のメンバーにルールを浸透させることができていなかった
- 新規の画面を実装する時に既存の実装に合わせてコーディングするなど個人の判断に委ねられていた
- レビュー体制を手厚くできていなかった

など複数の要因が重なり、無秩序なソースで構成された画面が生まれてしまっていました。

その結果、
- 各コンポーネントの実装内容に統一性がないので、どこで何をしているかわからない。
- 巨大なファイルの場合、修正を加えるときはジェンガをしている気分になって修正コストが普段の2,3倍にも膨れ上がる

といった課題がありました。

これらの課題に対する解決手段として、ディレクトリ構成を見直しファイル毎の責務を明文化することから始めました。

# 先人達の知恵のご紹介
ディレクトリ構成を考える前に、世の中で一般的に良いとされているパターンについて調査を行いました。
下記の記事が非常に良くまとまっていたので参考にしています。

[Top 6 Best Folder Structures for React: Ultimate Comparison](https://itnext.io/top-6-best-folder-structures-for-react-ultimate-comparison-effc29ae5045)

- The Feature-Based Pattern: The Cornerstone of Scalable Folder Structure
- Fractal-Based Pattern: The Winner Among Folder Structure Patterns?
- Page-Based Pattern: Scalable Next JS Folder Structure
- Library-Based Pattern: Scalable NX mono repo Folder Structure
- Atomic Design Folder Structure: For Scalable React Applications
- Ducks Design Folder Structure: For organizing Redux-related code

# 成果物
議論を重ねた結果、今回は The Feature Based Pattern をベースにディレクトリ構成を考えました。

The Feature Based Pattern を選択している理由は次の通りです。
- モジュールや機能単位で関連するファイルを管理したい
- モジュール内に配置されたファイルが他モジュールに依存しないことを明示的に表現できる
- 将来チーム規模が拡大した際に、モジュール単位でのリポジトリ切り出しなどを行いやすくなる

## 実際のディレクトリ構成案
```
.
├── assets
│   ├── icon
│   └── img
│
├── components
│   ├── composite
│   │   └── action-card
│   ├── layout
│   │   ├── detail
│   │   │   ├── header.tsx
│   │   │   └── index.tsx
│   │   └── list
│   │       └── header.tsx
│   └── mui
│       ├── button
│       ├── label
│       ├── switch
│       └── text
│
├── features
│   └── account
│       ├── _shared
│       │   ├── components
│       │   ├── dict
│       │   ├── hooks
│       │   ├── logics
│       │   ├── models
│       │   └── repositories
│       │       ├── apis
│       │       │   ├── apiFetchAccountDetail.ts
│       │       │   └── apiUpdateAccountDetail.ts
│       │       ├── datasources
│       │       └── storage
│       ├── account-details
│       │   ├── components
│       │   │   └── edit-account-dialog
│       │   │       ├── index.tsx
│       │   │       └── view.tsx
│       │   ├── dict
│       │   ├── hooks
│       │   │   ├── effect.ts
│       │   │   ├── useAccountDetail.ts
│       │   │   └── useAccountDetailEdit.ts
│       │   ├── logics
│       │   │   ├── accountDetailEditLogic.ts
│       │   │   └── accountDetailSharedLogic.ts
│       │   ├── stores
│       │   │   ├── accountDetailContext.ts
│       │   │   └── accountDetailEditContext.ts
│       │   ├── types
│       │   │   └── types.ts
│       │   ├── index.tsx
│       │   └── view.tsx
│       └── account-list
│
├── infrastructures
│   ├── api
│   │   └─── accountApi.ts
│   ├── elasticsearch
│   │   ├── client.ts
│   │   └── types.ts
│   └── firebase
│       ├── auth.ts
│       ├── firestore.ts
│       └── storage.ts
│
├── pages
│   └── account
│       ├── detail
│       │   └── index.tsx
│       └── list
│           └── index.tsx
│
├── stores
│   ├── account.ts
│   ├── provider.tsx
│   └── snacbar.ts
│
├── utils
│   └── arrayUtils.ts
│
├── App.tsx
└── index.tsx
```

それぞれのディレクトリの責務は以下の通りです。

## assets
アセットファイルを配置します。
例） アイコンや画像、フォントファイル

## components
ページを跨いで利用される共通コンポーネントを配置します。
特定のページ内でしか利用しないコンポーネントについては後述する features に配置します。
また、components　に関しては下記のルールを設けています。
- コンポーネントは Containar / Presentation 層に分ける
- Presentation 層は View のレンダリングに関わるソースのみを管理する
- Containar 層は hooks や stores にアクセスし、 Presentation ファイルへと渡す

```mermaid
classDiagram
Container ..> "hooks の取得" hooks
Container ..> "context の取得" stores
Container ..> "リソースへの<br/>アクセスロジックの取得" repositories
Container ..> "context の値や<br/>コールバック関数を渡す" Presentation
```

components 内のサブディレクトリの責務は次の通りです。
- components/mui
  - material-ui コンポーネントを wrap したシンプルなコンポーネントを配置します。
  - atomic デザインの atoms に近いです。
  - 例）　テキスト、ボタン
- components/composite
  - material-ui コンポーネントを組み合わせた共通コンポーネントを配置します。
  - 例）　確認ダイアログ、ユーザーのプロフィールカード
- components/layout
  - page を構成するためのデザインフレームを提供します。
  - 例）　詳細ページ用レイアウト、一覧ページ用レイアウト

## features
各モジュールや機能毎のファイルを配置します。

機能毎に `/features/feature_a/` や `/features/feature_b/` のようにディレクトリ階層が切られ、更にその下に `/features/feature_a/sub_feature_c` のようにサブモジュールのディレクトリで管理します。
モジュール内で共通利用されるファイルは `/features/feature_a/_shared` 内に配置します。

feature 内のサブディレクトリの責務は次の通りです。
- components
    - feature 内で利用するコンポーネントを配置
- dict
    - feature 内で利用する辞書ファイルを配置
- hooks
    - feature 内で利用するカスタムフックを配置
- logics
    - feature 内で利用する共通ロジック
    - React のカスタムフックに依存しない処理を配置
- models
    - feature 内で利用するモデルクラス
    - モデル特有のドメインロジックを内包する
- repositories
    - feature 内で利用するリソースへのアクセスクラスを配置
    - 内部では infrastructures の実装ファイルを呼び出す
- stores
    - feature 内で利用する状態を持つファイルを管理
    - 本プロダクトでは主に context を利用
- types
    - feature 内で利用する型定義ファイルを管理

## infrastructures
repository ファイルの実態を配置します。
例） API定義ファイル、外部リソースへのアクセスクラス

```mermaid
classDiagram
repositories ..> "リソースへのアクセスクラスの実装クラスを呼び出し" infrastructures
```

## pages
routing された画面ファイルを配置します。
ディレクトリは URL のパスと一致させ、features 以下の対応するコンポーネントファイルとの繋ぎ込みを行います。
例） アカウント一覧ページ、アカウント詳細ページ

```mermaid
classDiagram
pages ..> "feature_components の container 呼び出し" sub_feature_a_components
```

## stores
アプリケーション全体でグローバル管理される情報を管理します。
例） ログインアカウント情報

※ 特定の機能やページ内でグローバル管理したい情報は features 内の stores で管理します。

## utils
言語特有のユーティリティを提供します。
例） 配列処理、日付処理

※ 特定の機能の共通処理は features 内の logics で管理します。

# 結果
今回の整理によって、
> - オフショアメンバーはブリッジエンジニアを通してコミュニケーションを取っていたため、入れ替わりも激しく個々のメンバーにルールを浸透させることができていなかった
> - 新規の画面を実装する時に既存の実装に合わせてコーディングするなど個人の判断に委ねられていた

という課題に対して、メンバーからも下記の意見をもらうことができました。

- どこに何が記載されているかが明確なので問題の特定が容易になった
- 責務ごとにファイルを分けているので修正による影響範囲が限定的になった
- ルールが定まっているので新規開発時にどこに何を書けば良いか迷う時間が減った

その結果、
- メンバー内の共通認識が高まった
- 新規入社や業務委託など新しく参画するメンバーのオンボーディングコストが削減できた
- 保守、新規の開発効率が上がった

などの効果を得ることができました。

今回は特にディレクトリ構成の整理に焦点を当てましたが、静的チェックの仕組みやレビュー体制については整備仕切れていない部分もあるので継続的に改善策を考えていきます。

一貫性のあるディレクトリ構成にすることで機械的に静的チェックを行いやすくなるため、今後の導入は容易になったかと思います。

# おわりに
今回は React を利用したフロントエンドアプリケーションのディレクトリ構成案についてお話しさせていただきました。
ディレクトリ構成について社内のメンバーと議論をすることで現状の課題や理想のプロダクトへの共通認識を持ち改善の計画を立てることができただけでなく、世の中の一般事例についても学ぶ良い機会にもなりました。

また、この記事内で触れることができなかった課題も社内の議論では挙がっており、Next.js 化などリファクタ計画を立てています。
上記の改善内容についてもどこかでお話しできればと思います。

<br/>
<br/>
6日目の株式会社ビットキー Advent Calendar 2023は、 @ksk-taka が担当します！