🕌

【React】フォルダ構成の考え方

に公開

はじめに

Reactのフォルダ構成は難しい

Reactは、フォルダ構成に"意見を持ちません"
この柔軟性が、フォルダ構成の難しさに繋がっていると思います。
また、フォルダ構成について体系的に書かれている情報が少なく、特に用語の説明がなかったりするため、理解が難しいと感じました。
そこで、フォルダ構成を体系的に理解し、作成できるようになるため、フォルダ構成の考え方についてまとめました。

この記事の目的

フォルダ構成について:

  • 意見を持てるようになる
  • 調べやすくなる
  • そして、リポジトリを見た時に構造化して見えるようになる

ことを目的としています。

フォルダ構成の種類

フォルダ構成の種類は、大きく分けて3つに分類できます:

  • type-based
  • feature-based
  • layer-based

by 〇〇」という呼び方も存在しますが、意味は同じです。
詳細は、〇〇-based以外の呼ばれ方に記載しました。

type/ feature/ layerとは?

type

技術的な種類」のことです。
例えば、hookscomponentstypesフォルダが該当します。

Reactは関係ありませんが、深く理解するために、MVCパターンで類比してみます。
MVCは技術的な関心の分離を行っています。従って、ModelViewControllerもtypeに分類されます。

feature

機能」(≒ビジネスドメイン)のことです。
例えば、userpostauthなどが該当します。
これはアプリケーションによって異なります。

layer

このlayerは、「Layered Architecture」のlayerです。
そのためプレゼンテーション層、アプリケーション層、ドメイン層、インフラ層のように分類できます。

〇〇-basedってなに?

basedとは、「〜を中心にした、〜を基にした」という意味です。
by 〇〇と呼ばれることもあります。
つまり、「何をベースに分類したフォルダ構成か」ということです。
これは、src(またはプロジェクト)直下のフォルダ構成で判断できます。

type-based

src(またはプロジェクト)直下のフォルダ構成が、「技術的種類」によって分類されています。
つまり、type-basedとは、「技術的な分類をベースにフォルダが構成されている」構成です。
Reactでの例:

src/
├── api/                # APIフック、API定義
├── assets/             # 画像やフォントなど
├── components/         # UIコンポーネント
├── contexts/           # React Context
├── hooks/              # カスタムフック
├── stores/             # グローバルステート
├── styles/             # CSS
├── types/              # 型定義
├── utils/              # ユーティリティ関数
...

MVCでの例:

src/
├── Model/              # ビジネスロジック/データ
├── View/               # UI
├── Controller/         # コントローラ
├── ViewModel/          # プレゼンテーションロジック/表示用データ
...

feature-based

機能的な分類をベースにしたフォルダ構成です。

先ほどのtype-basedに、featuresフォルダを追加してみます。
すると暗黙的に、src直下のfeatures以外のフォルダは「共有で使用するもの」となります。
有名な「bulletproof-react」はfeature-basedを採用しています:

src/
├── api/            
├── assets/            
├── components/      
├── contexts/          
├── features/           # 機能
│   ├── post/           # 投稿機能
│   └── user/           # ユーザー機能
├── hooks/              
├── stores/             
├── styles/             
├── types/     
├── utils/              
...

sharedなどのフォルダを作成し、明示的に分けているリポジトリもよく見かけます:

src/
├── features/           # 機能
│   ├── users/          # ユーザー機能
│   └── posts/          # 投稿機能
├── shared/             # アプリケーション全体で使用するもの
│   ├── api/
│   ├── assets/
│   ├── components/
│   ├── hooks/
    ...
...

もし、posts内でのみusersを使用するのであれば、ネストすることもできます:

src/
├── features/           # 機能
│   └── posts/          # 投稿機能
│   │   └── users/          # ユーザー機能
├── shared/             # アプリケーション全体で使用するもの
│   ├── api/
│   ├── assets/
│   ├── components/
│   ├── hooks/
    ...
...

layer-based

(レイヤードアーキテクチャの)レイヤーの分類をベースにしたフォルダ構成です。
src直下がlayerで分類されていることを確認してください:

src/
├── presentation/       # プレゼンテーション層
├── application/        # アプリケーション層
├── domain/             # ドメイン層
├── infrastructure/     # インフラ層
...

以下の記事は、フロントエンドにlayer-basedを適用した例として参考になります。インフラ層を除いた、プレゼンテーション層、アプリケーション層、ドメイン層、アダプタ層の4層で構成されています。:
https://bespoyasov.me/blog/clean-architecture-on-frontend/

組み合わせる

type,feature,layerは基本的に、組み合わせて使う「ハイブリッドなアプローチ」が多いと思います。

例えば、feature-basedの場合、各featureの中はtype-basedで分類されていることが多いです:

src
├── features
    ├── users           # feature
    │   ├── api         # type
    │   ├── components  # type
    │   └── hooks       # type
    └── posts           # feature
        ├── api         # type
        ├── components  # type
        └── hooks       # type
...

フォルダ構成の考え方

フォルダ構成を考える上で、重要なポイントが6つあると考えています:

  1. ネストは3〜4階層に抑える
  2. テストしやすさを意識する
  3. コロケーション
  4. 叫ぶアーキテクチャ
  5. 依存関係を考慮する
  6. 目的を意識した名前をつける

ネストは3〜4階層に抑える

フォルダ構成はネストしすぎると、見通しが悪くなってしまいます。

src
├── 1
    ├── 2
    │   ├── 3
    │   │   ├── 4
    │   │   │   ├── 5
    │   │   │   │   └── 6

Reactの旧ドキュメントでは、3〜4階層までに抑えることを推奨しています。

[...]consider limiting yourself to a maximum of three or four nested folders within a single project. Of course, this is only a recommendation, and it may not be relevant to your project.
1つのプロジェクト内でネストされたフォルダは3~4個までに抑えることを検討してください。もちろん、これはあくまで推奨事項であり、あなたのプロジェクトには当てはまらないかもしれません。

File Structure

テストしやすさを意識する

テストしやすいフォルダ構成を意識すべきです。
以下のフォルダ構成は、何をテストすればいいのかパッと見てわかりません。

src/
├── features/
    └── users/
        ├── avatar.ts
        ├── use-user.ts
        └── user-card.tsx

そこで、type-basedを取り入れ、技術的な関心の分離を行います。:

src/
├── features/
    ├── users/
        ├── components/
        │   ├── avatar.ts
        │   └── user-card.tsx
        └── hooks/
            └── use-user.ts
...

ロジックとUIが分離されたことで、hooksが単体テストの対象とすぐに分かり、テストしやすくなりました。

コロケーション(Colocation)

関連性が高いものは、物理的(ファイル構造的)に近くに置く」という考え方です。
「関連性が高いもの」とは、「一緒に変化するもの」です。

Kent C. Dodds氏はコロケーションを以下のように説明しています。

Place code as close to where it's relevant as possible
You might also say: "Things that change together should be located as close as reasonable."[...]
コードはできるだけ関連する場所の近くに配置します
「一緒に変化するものは、可能な限り近くに配置する必要があります」とも言えるでしょう。

Colocation

type-basedではコロケーションがうまくいっていません。
なぜなら、技術的な種類をベースに分類しているため、関連性が高いものを近くに置けていないからです。
type-based:

src/
├── api/
│   ├── create-post.ts
│   ├── delete-post.ts
│   ├── get-posts.ts
│   ├── get-users.ts
│   ├── update-post.ts
│   └── update-user.ts
├── components/
│   ├── post-card.tsx
│   ├── user-card.tsx
│   └── user-list.tsx
├── hooks/
│   └── use-user.ts
...

type-basedのデメリットは、user関連の機能があちこちにあり、低凝集になっていることです。
具体的なデメリット:

  • ある機能を変更するときに、ファイルを探しづらい
  • 見通しが悪い

これをコロケーションで解決します。
feature-basedに変更し、user関連、post関連にまとめます:

src/
├── features/
│   ├── users/
│   │   ├── get-users.ts
│   │   ├── update-user.ts
│   │   ├── user-card.tsx
│   │   └── use-user.ts
│   └── posts/             # 省略
...

コロケーションすることで、以下のメリットが生まれます:

  • 関係しているファイルが探しやすい。
  • 機能ごとに分類されているため、変更しやすい。

さらに各feature内で技術的な分類(type-based)を行います(type-basedもコロケーションと考えています):

src/
├── features/
    ├── users/
    │   ├── api/
    │   │   ├── get-users.ts
    │   │   └── update-user.ts
    │   ├── components/
    │   │   └── user-card.tsx
    │   └── hooks/
    │       └── use-user.ts
    └── posts/
        ├── api/                # 省略
        └── components/         # 省略
...

type-basedはコロケーションか?

私の見解では、「type-basedはコロケーション」です。

ソフトウェアの性質上、UIは変更に敏感(変更されやすい)で、ドメインは変更に鈍感(変更されにくい)です。

type-basedでは、UI(components)とロジック(hooks)が分離されているため、この点でコロケーションができていると言えます。
つまり技術的な分類によって、例えば、UIのみの変更がしやすくなります

ソフトウェアの性質

UIが変更に敏感で、ドメインが変更に鈍感な理由について、詳しくは「ソフトウェア原則[2] - IOP(Inside-Out Principle)」)を見てください。
そしてこの思想は、クリーンアーキテクチャの「安定依存の原則(SDP)[1]」にも反映されています。

コロケーションのトレードオフ

storybookの例を見てみます。
コロケーションしない場合:

src/
├── features/
├── shared/
│   └── components/
│       ├── avatar.tsx
│       ├── button.tsx
│       └── card.tsx
├── stories/
│       ├── avatar.stories.tsx
│       ├── button.stories.tsx
│       └── card.stories.tsx
...

コロケーションしない場合、storybookを作り忘れたり、変更し忘れたりするかもしれません。
探すのもめんどくさいです。
そこでコロケーションを行います。
コロケーションする場合:

src/
├── features/
├── shared/
│   └── components/
│       ├── avatar.tsx
│       ├── avatar.stories.tsx
│       ├── button.tsx
│       ├── button.stories.tsx
│       ├── card.tsx
│       ├── card.stories.tsx
        ...

コロケーションを行った結果、componentsフォルダの見通しが悪くなってしまいました。
これを解決するために、フォルダを追加して、さらにコロケーションを行います:

src/
├── features/
├── shared/
│   └── components/
│   │   └── button/
│   │       ├── button.tsx
│   │       ├── button.stories.tsx
│   │       └── index.ts
│   │   ├── avatar/
│   │   ├── card/
        ...

こうすると、ネストが深くなってしまいました。
つまり、ここにはトレードオフが存在します。
今回の場合は、3階層なので問題ありませんが、場合によっては5階層、6階層と深くなってしまうかもしれません。

ただし、Kent C. Dodds氏はコロケーションの例外としてE2Eテストのみを挙げています(実際には例外ではないと言っていますが)。なのでできるだけコロケーションしてバランスを取るのがいいかと思います。

For end-to-end tests, those generally make more sense to go at the root of the project.
エンドツーエンドテストの場合、一般的にはプロジェクトのルートディレクトリに配置する方が理にかなっています。

Colocation

叫ぶアーキテクチャ(screaming architecture)

Johannes Kettmann氏は、「Screaming Architecture - Evolution of a React folder structure」で叫ぶアーキテクチャをフロントエンドに適用して言及しています。

叫ぶアーキテクチャとは、「フォルダ構成は、フレームワークを伝えるのではなく、何をするアプリケーションなのかを伝えるべき」という考えです。

Robert C.Martinは、「叫ぶアーキテクチャ」で以下のように説明しています:

When you look at the top level directory structure, and the source files in the highest level package; do they scream: Health Care System, or Accounting System, or Inventory Management System? Or do they scream: Rails, or Spring/Hibernate, or ASP?[...]
If you are building a health-care system, then when new programmers look at the source repository, their first impression should be: “Oh, this is a heath-care system”. Those new programmers should be able to learn all the use cases of the system, and still not know how the system is delivered. They may come to you and say: “We see some things that look sorta like models, but where are the views and controllers”, and you should say: “Oh, those are details that needn’t concern you at the moment, we’ll show them to you later.”
トップレベルのディレクトリ構造や最上位パッケージのソースファイルを見ると、医療システム、会計システム、在庫管理システムといった主張をしているでしょうか?それとも、 Rails、Spring/Hibernate、ASPといった主張をしているでしょうか?[...]
医療システムを構築している場合、新しいプログラマーがソースリポジトリを見たときの第一印象は「ああ、これは医療システムか」となるべきです。新しいプログラマーは、システムのユースケースをすべて理解できる必要がありますが、それでもシステムがどのように提供されるのかは理解していないでしょう。彼らは「モデルらしきものが見えますが、ビューやコントローラーはどこにあるのですか?」と尋ねてくるかもしれません。その場合、あなたは「ああ、それは今は気にする必要がない詳細です。後で説明します」と答えるべきです。

Screaming Architecture

つまり、技術的詳細はフォルダ構成上、重要度が低いということです。
重要なのは、:

  • 開発者が初めて見たときに、どこのフォルダを見ればシステムの外観が理解できるかが分かること
  • 技術的詳細よりも、ドメインが分かる構成になっていること

です。

また、「叫ぶアーキテクチャ」を言い換えると、
「開発者は ①どんなシステムか? → ②ユースケース → ③技術的詳細 の順でシステムを理解している。そのようなフォルダ構成にする必要がある。
と言えます。

以下のfeature-basedの例では、features内を見れば「ショッピングのアプリケーションなんだな」と理解できます:

src/
├── features/
    ├── auth/
    ├── cart/
    ├── product/
    └── user/
...

しかしtype-basedの場合、技術的詳細がトップレベルに来ています。
そのためhookscomponentsといった技術的な名前から、「これはReactである」と開発者に伝えていて、「何のアプリケーションか?」は伝えていません。:

src/
├── api/
│   ├── auth/
│   ├── cart/
│   ├── product/
│   └── user/
├── components/
│   ├── auth-form/
│   ├── cart-item-list/
│   ├── product-card/
│   └── user-profile/
├── hooks/
│   ├── use-auth.ts
│   ├── use-cart.ts
│   ├── use-product.ts
│   └── use-user.ts
...

これは開発者の認知負荷を上げてしまいます。
開発者は ①どんなシステムか? → ②ユースケース → ③技術的詳細 の順で理解する必要があるにも関わらず、フォルダ構成は技術的詳細 → ドメインの順になってしまっているからです。

従って、フォルダ構成はドメイン → 技術的詳細の順になるべきと考えられます。

依存関係を考慮する

依存関係が複雑になると管理が大変になってしまいます。
この依存関係の問題は、type-basedにもfeature-basedにも存在しています。

layer-based

layer-basedでは依存関係が最もわかりやすい構造となっています。

矢印の向きは依存の向きです。例えば、presentation層はapplication層に依存しています。

type-based

以下の例では、sharedusersに依存し、循環依存が発生しています(赤い矢印)。:

矢印の向きは依存の向きです

feature-based

feature-basedでは、トップレベルでfeaturessharedに分けられているため、よりわかりやすく分離できます。
これによってsharedfeatureに依存することを避けやすくなると思います。:

矢印の向きは依存の向きです
完全にsharedとの循環依存を避けるには、リンターなどでフォルダ間の依存関係にルールを設け、参照を制限する必要があります。
bulletproof-reactがその一例です(参照)。
FSDでも同様に制限を設けています(参照)。

features内は複雑化する

例えば、記事を投稿するアプリがあったとします。

  • 投稿機能:ユーザーは記事を作成し、投稿できる。
  • コメント機能:他のユーザは、公開された投稿にコメントできる。
  • 通知機能:投稿にコメントが付くと、投稿者にコメント内容と、コメントしたユーザが通知される。

すると、features内で依存関係が複雑化してしまいます。:

FSDではfeatures内での依存の複雑化を防ぐためのルールが設けられています。

You cannot import one feature from another feature, this is prohibited by the import rule on layers.
あるfeatureから別のfeatureをインポートすることはできません。これは、レイヤーのインポート ルールによって禁止されています。

FSD

目的を意識した名前をつける

Name your packages after what they provide, not what they contain.
パッケージには何が 含まれているかではなく、何を 提供するかに基づいて名前を付けます。

Avoid package names like base, util, or common

And from the rules of clean code we remember, the name should be unambiguous and reflect the purpose.
クリーンコードのルールで覚えておくべきことは、名前は明確で目的を反映したものであるべきだということです。

Why utils & helpers is a dump

libや、utilshelpersフォルダなどは、名前が抽象的なため、何でも入れられそうな気がしてしまいます。
言い換えれば、フォルダ名から具体的な目的がわからないのです。

Feature-Sliced Designでは、libフォルダを、以下のように説明しています。

このフォルダはヘルパーやユーティリティとして扱うべきではありません(これらのフォルダがしばしばダンプになってしまう理由については、こちらをご覧ください)。代わりに、このフォルダ内の各ライブラリは、日付、色、テキスト操作など、特定の領域に特化する必要があります。その領域はREADMEファイルに記述する必要があります。チーム内の開発者は、これらのライブラリに追加できるものと追加できないものを把握しておく必要があります。

FSD

つまり、FSDでは:

  • libというフォルダ内にutilshelpersという曖昧なフォルダ名を避けることを推奨。
  • libフォルダの中は、何でも入れられるゴミ箱のようなものになりがちなので、
    • lib/具体的な目的名/として目的を伝える
    • READMEを作成し、管理(制限)する

必要があると言っています。

例:

src/
├── features/
├── shared/
    └── lib/
        └── datetime/         # 日付(目的)
            ├── format.ts     # フォーマット
            └── tz.ts         # タイムゾーン変換
        └── url/              # URL(目的)
            └── index.ts      # URL操作
...

type-basedは非推奨

私は、type-basedは使用しない方がいいと考えています。
なぜなら、type-basedは、これまでの説明で分かる通り、以下の点で問題を抱えています。

  1. コロケーション(feature-basedより弱い)
  2. 叫ぶアーキテクチャ
  3. 依存関係

規模が大きくなるとtype-basedは問題が出やすいため、初期はOKでも、早めにfeature-basedに寄せるのがいいと思われます。

ただし、feature-basedで構成する場合、各feature内にはtype-basedで構成すべきと考えています。技術的な関心の分離によって、ロジックとUIが分離され、テストしやすくなるからです。

調べるときのTips

フォルダ構成 or ディレクトリ構成

日本ではディレクトリ構成と呼ばれ、英語圏ではフォルダ構成と呼ばれることが多い印象です。
「folder structure」で検索するほうが、英語圏では多くの記事が出てくると思います。

〇〇-based以外の呼ばれ方

フォルダ構成の種類は、「〇〇-based」以外に、「by 〇〇(〇〇別)」といったりします。
つまり、決まった呼び方は存在しません。:

  • 〇〇-based
  • Package by 〇〇
  • grouping by 〇〇
  • group by 〇〇
  • folder-by-〇〇
    ※〇〇 = type/ feature/ layer

フォルダ構成には様々な呼ばれ方をしますが、私が調べた上で、フロントエンドでは「〇〇-based」、
バックエンドでは「package by 〇〇」と呼ばれることが多い印象です。

最後に

Reactのフォルダ構成ってめちゃくちゃ難しいなって思って(そもそもフォルダ構成自体が難しい)、かなり調べ、この記事の執筆に至りました。
そして、Reactを使用した開発者だけでなく、フォルダ構成についてわからないって方にも、この記事が参考になればいいなと思います。

参考文献にも貼りましたが、フォルダ構成を考える上で、最も参考になった記事が「Screaming Architecture - Evolution of a React folder structure」です。叫ぶアーキテクチャをフロントエンドに適用して言及している点は目から鱗でした。

私の考えでは、中規模ならfeature-basedで十分だと考えています(各feature内にtype-basedのハイブリッド構成を前提)。
しかし、大規模になった際に、feature-basedを捨てる必要が出てくるのかなと思っています。
私はfeature-basedの問題を解決するには、「FSD(Feature-Sliced Design)」か、クリーンアーキテクチャをフロントエンドに適用したlayer-based構成のどちらかが良い選択なのでは?と考えています(とりあえず現在はFSDを学習中です)。

最後に、主に参考にした文献をまとめておきます。
ここまで読んでいただきありがとうございました。
フィードバック等いただけると幸いです。

参考文献

https://feature-sliced.design/
https://dev.to/profydev/screaming-architecture-evolution-of-a-react-folder-structure-4g25
https://www.robinwieruch.de/react-folder-structure/
https://dev.to/sergeysova/why-utils-helpers-is-a-dump-45fo
https://kentcdodds.com/blog/colocation
https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html
https://github.com/alan2207/bulletproof-react
https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md

脚注
  1. 『クリーンアーキテクチャ』,Robert C.Martin,132頁. ↩︎

Discussion