🙄

Reactのディレクトリ構成について

2023/03/18に公開

Screaming Architecture - Evolution of a React folder structure

ディレクトリ構成でScreaming Architectureを個人開発で採用しています。
この記事ではScreaming Architectureを採用して良かったことや困っていることを書いていきます。

Screaming Architectureを採用した背景

下記のようにファイルタイプ毎にディレクトリを分ける構成が辛かったためです。

.
└── src/
    ├── components
    ├── hooks
    ├── layouts
    ├── utils
    └── pages

このファイル構成は一般的だと思いますし、どのファイルをどのディレクトリに置けばいいのかが明確なのでファイルの置き場所自体に困ることはあまりないのかなと思っています。(ディレクトリの中でどう置くのかというのは一旦なしで)
また、プロダクトが小さいうちはこの構成で機能すると思っているのですが、プロダクトが大きくなると次第に扱いづらくなってきます。

例えば、componentsの中に特定の箇所でしか使わないcomponentが入ってるということが起き始めます。
hooksもutilsも同様です。
そうするとディレクトリの中には下記の2点のファイルが混ざることになり、ディレクトリの肥大化や不要だと思って消したファイルが実は他の場所で使われていたなどの問題が発生するように思います。

  • 全体で使用するファイル
  • 特定の箇所でしか使用しないファイル

また、ファイルが点在してしまうのも個人的には辛いポイントです。

.
└── src/
    ├── component/
    │   └── some-component.tsx
    ├── hooks/
    │   └── some-hook.ts
    ├── layouts
    ├── utils/
    │   └── some-util.ts
    └── pages/
        ├── top
        └── mypage

some-component.tsxsome-hook.ts | some-util.tsを使用していた場合、コードを読む際に対象となるディレクトリを全て開く必要が出てきます。
前述の通りプロダクトが小さい内は問題ないのですが、大きくなってくるとディレクトリ内が肥大化し見つけづらくもなってきます。

このような背景からScreaming Architectureを採用してみようと思いました。

実際のディレクトリ構造

.
└── src/
    ├── components/
    │   ├── button
    │   ├── container
    │   ├── text-field
    │   └── check-bok
    ├── features/
    │   └── some-feature/
    │       ├── hooks
    │       ├── utils
    │       └── component
    ├── hooks
    ├── layouts
    ├── utils
    └── pages

「いや、componentsもhooksもutilsも残ってるじゃん」
その通りなんですが、featuresができたので特定のページ(機能)でしか使用しないcomponentやhooksが混ざらなくなりました。

良かったこと

各ディレクトリの見通しが良くなった

特定のページ(機能)で使用するものについてはsome-feature内に置くことにより各ディレクトリの見通しがよくなりました。

ファイルのscopeが分かりやすくなった

  • src/componentsには全体で使用するファイルしか置かない
  • 特定のページ(機能)で使用するcomponentはfeaturesの中に置く
    というルールができるためファイルのscopeが分かりやすくなりました。

コードを追いやすくなった

some-feature/componentのコードを読むとなったときにutilsもhooksも同じディレクトリ内にあるため肥大化したディレクトリの中から対象ファイルを探し出すという行動が少なくなったように思います。

困っていること

apiの置き場所

バックエンドとの通信にはreqct-queryを使っているためfeature内のhooksに置いています。

.
└── src/
    └── features/
        └── some-feature/
            ├── hooks(ここにapiを置いている)
            ├── util
            └── component

ただ、apiにはAの機能でもBの機能でも使用するということがよくあると思っていて、その場合にsome-feature/hooksに置いてることによる違和感が出てきます。

現在は「some-feature/hooksは他のfeatureでも使用可能とする」というルールを設けていますが、この先運用してみないとちょっとわからないです。
(もし、良い考え方があれば教えてください🙇‍♂️)

featureをどのscopeで区切るのか

個人開発のプロダクトではユーザーの設定画面があるのですが、そこでは以下のようなことが行えます。

  • ユーザー名の編集
  • アバターの変更
  • メールアドレスの編集
  • お知らせの受け取り
  • etc...
    これらを「設定」という機能として捉えるのか、各項目を一つの機能として捉えるのかに悩んでいます。
    ディレクトリ構造にするとsettingsの中にnestさせるかどうかだけなのですが、下記のようになります。

「設定」という機能として捉えた場合のディレクトリ構造

.
└── src/
    └── features/
        └── settings/
            ├── user-name
            ├── avatar
            ├── email
            └── notification

各項目を一つの機能として捉えた場合のディレクトリ構造

.
└── src/
    └── features/
        ├── user-name
        ├── avatar
        ├── email
        └── notification

今は「各項目を一つの機能として捉えた場合のディレクトリ構造」を採用しています。
理由にはpathが関わってくるのですが、settingsの中にnestした場合pathと同期することになり、pathが変わった場合にディレクトリ構造も変更する必要が出てきそうだからです。

機能名 path
ユーザー名の編集 /settings/name/
アバターの変更 /settings/avatar/
メールアドレスの編集 /settings/email/
お知らせの受け取り /settings/notification/

そのため、「設定」というscopeで捉えるのではなく一段目線を下げて各項目を1つの機能として捉えています。
(この辺りも良い考え方があれば教えてください🙇‍♂️)

まとめ

簡単ではありましたが、Screaming Architectureを採用して良かったことや困っていることを書きました。
ディレクトリ構造は正解も不正解もないと思っていますし、プロダクトの規模によって変わってくるものだと思っているので、現時点でどの構造が違和感なく開発体験を良くできるのかを個人的には念頭に置いています。

この先運用してみて気づいた点などあればまた記事にしようと思います。
閲覧ありがとうございました。

参考資料

Discussion