にわかプラス

にわかが玄人になることを夢見るサイトです。社会や国際のトレンド、プログラミングや電子工作のことについて勉強していきたいです。

フロントエンドのディレクトリのベストプラクティス

sponsor

フロントエンドのディレクトリ構成のベストプラクティス

この記事を参考にこのブログを書いた。 https://profy.dev/article/react-folder-structure

Webフロントエンドのプロジェクトに途中参画したが、ディレクトリ構造が少しカオスめだったので、どのようなディレクトリ構造がベストか模索していた。
例えばフロントエンドは画面に関わるファイルが多くなるため、クリーンアーキテクチャみたいなフォルダ構成はイマイチ合わない感じがすると思っていた。
そんなときに上記のサイトは回答に近い構造を詳しい解説付きで提示してくれていたので大変感謝し、メモとしてここに残すことにした。

参考記事はReactを使うことが前提になっているため、contextやhooksといったReact独自のフォルダ・ファイルが登場する。 自分はあまりReactには詳しくなかったが記事内容は十分理解できた。
参考サイトも別にReactに詳しいわけではなく、逆に「contextってなんだ?よくわからないけどReactででてくるからとりあえず作っておいた」みたいなコメントが残っており、フレームワークによらない結論となっている。

結論

機能ごとのディレクトリ構成にすること。

└── src/
    ├── features/ # 機能ごとに分割し、これ自体を共通コンポーネントとしても扱う
    │   ├── todos/
    │   ├── projects/
    │   ├── ui/
    │   └── users/
    └── pages/ # features配下のファイルをインポートしてページコンテンツを作成
        ├── create-todo.js
        ├── index.js
        ├── login.js
        └── terms.js

ディレクトリ構造によって、プロジェクト全体の見通しを良くすることと、フォルダ間の依存関係を定義できることが大事だと個人的に思っている。

プロジェクト全体の見通しを良くすることで、同じような機能の再実装を防げたり、修正の際の影響範囲を簡単に見積もることができるようになる。
フォルダ間の依存関係を定義することで、複雑な依存を発生させづらくできる。また、これも修正の際の影響範囲の見積もりを簡単にできる。
ただしフォルダ自体の粒度が適切ではないとあまり意味をなさないルールになるのでその塩梅が難しい。

参考ブログの結論をよく見ると、pagesを見ることで、このアプリケーションがどのようなページをもっているのかがひと目でわかる。
また、機能をfeatureフォルダに集め、ここにビジネスロジックやそのロジックを使ったコンポーネントをまとめている。機能単位でフォルダを分けることで、機能を実現するために必要なファイルがどこにあるかをすぐに把握できる。

└── src/
    ├── features/ # 機能ごとに分割し、これ自体を共通コンポーネントとしても扱う
    │   ├── todos/
    │   │   └── todo-list/
    │   │       ├── index.js
    │   │       ├── todo-item.component.js # todoのアイテムを表示するコンポーネント
    │   │       ├── todo-list.component.js # todoのリストを表示するコンポーネント
    │   │       ├── todo-list.context.js   # todoのコンテクスト(React用に存在するかもしれない)
    │   │       ├── todo-list.test.js      # テストコード
    │   │       └── use-todo-list.js       # todo-listのためのビジネスロジック

依存関係はブログには言及があまりなかったがおそらくこのようになっているはず。

  • pages配下の個別ページ -> features配下の各機能ファイル
  • fetrues配下の各機能ファイル -> features配下の各機能ファイル
    • 機能は他の機能を使う場合があると考えられる
  • features -> pagesという依存は存在しない。

(-> : 使用する)

常にpagesからfeaturesに依存があることがわかるため、pagesの変更は他のページと機能に影響を与えないことがすぐに把握できる。

features配下で依存が生じるのが少し気になったので、これが問題かを考えてみた。

  1. 機能追加
    1. 機能追加自体はfeturesフォルダ配下に機能単位でフォルダ作成して追加すればいいのでほか機能に影響を与えない
    2. 機能追加時に他の機能を使用する場合 -> ほか機能をimportすれば良いだけなので特に問題はなさそう
  2. 機能修正・削除 1.ファイルのimportを見て依存するファイルを探す必要がある 2.相互依存になっている場合、修正が大変になる

2-1、2-2はやや問題がありそうなのでここを考えてみる。 2-1について、依存するファイルをディレクトリ構造で明確化するには2通りが考えられる。

  • ディレクトリをネストして、依存されるものを親フォルダに配置する

  • 1つ以上の機能から参照される機能は、共通機能フォルダにを作りそこに配置する

ディレクトリのネストについては、機能が複数の機能から参照される場合、いちいちディレクトリ構造を変化させる必要が出てきそうなので、あまり現実的ではなさそう。

共通フォルダ化について、必ず機能->共通機能の依存とする。しかし結局共通機能フォルダ内の依存が3-1と同じように発生する可能性がある。加えて本質的にファイル間の依存が減るわけでもないためimport文を見てどこに依存があるのかを調べる必要があるのは変わらない。

2-2の相互依存の発生については、どうしてもそれが解消できない場合は一つの機能としてフォルダを分けてしまうことが考えられる。 これはfeaturesフォルダの理念に反しないので許容と思われる。

そもそも、機能の依存が発生するときは、例えばtodo-list機能 -> auth機能といった依存が考えられ、この依存が相互になるケース自体が稀だという気もする。

結論としては、featuresフォルダ内の依存は仕方のないところだと思って諦める。しかしそこまで大きな問題にもならなそうな気がする。

機能ごと以外のディレクトリ分けを比較

  1. ファイルタイプによるディレクトリ分け
  2. componentをcomponentフォルダ
  3. hooksをhookdフォルダ
  4. contextをcontextフォルダに分割
└── src/
    ├── components/
    │   ├── edit-todo-modal/
    │   ├── todo-list/ # uiと違い、ビジネスロジックをもっている
    │   └── ui/ # button,check boxといった単純なコンポーネント
    ├── contexts/
    └── hooks/

問題点: 例えばページを追加していく場合、component配下にフォルダとファイルが増えていく。
component配下内にはページや純粋なUIコンポーネント、共有されるコンポーネント、共有されないコンポーネントなど様々な種類のファイルがあるため、お互いの参照関係が複雑になる。

-> pagesフォルダを作り、ページ関係するコンポーネントを切り出す

ページとグローバルなファイルによるディレクトリ分け

  • ページに関わるものをpagesフォルダにまとめる
    • ex) pages/home/home-page.js, todo-list/todo-item.component.js
  • 複数のページから参照されるcompnentはcomponetフォルダにおく
  • hooks, contextsはそのまま
└── src/
    ├── components/
    │   ├── todo-form/ # このコンポーネントは複数のページから参照されるとする
    │   └── ui/
    ├── contexts/ # contextはこのフォルダにまとめたままにする
    ├── hooks/ # hooksはこのフォルダにまとめたままにする
    └── pages/
        ├── create-todo/
        ├── home/ # homeページでのみ使用されるコンポーネントをまとめる
        │   ├── home-page.js
        │   ├── edit-todo-modal/
        │   └── todo-list/
        │       ├── todo-item.component.js
        │       ├── todo-list.component.js
        │       └── todo-list.test.js
        ├── login/
        └── terms/

問題点: component, hooks, context, pagesを使って一つの機能を作っているため、フォルダの見通しが悪い。

-> hooksとcontextも関連するpagesフォルダに配置する

hooksとcontextsをpageフォルダ配下に分割する

  • pagesフォルダ配下のページと同じ階層に、そこで使われるhooksやcontextsを移動
    • ex) pages/home/home-page.js, use-todo-list.js
└── src/
    ├── components/
    │   ├── todo-form/
    │   └── ui/
    ├── hooks/
    │   └── use-auth.js # 複数のページから参照されるの共有hooksなのでここにおいておいく
    └── pages/
        ├── create-todo/
        ├── home/
        │   ├── home-page.js
        │   ├── edit-todo-modal/
        │   └── todo-list/
        │       ├── todo-item.component.js
        │       ├── todo-list.component.js
        │       ├── todo-list.context.js # 2. で別フォルダにおいていたcontextをここに移動
        │       ├── todo-list.test.js
        │       └── use-todo-list.js # 2.で別フォルダにおいていたhooksをここに移動
        ├── login/
        └── terms/

問題点:

  • Todoに関する機能がcomponentsとpages配下に分散しており、機能の見通しが悪い
  • ディレクトリ構造からだけでは、todo関連機能がpages/home配下にあることが簡単にわからない
  • Todo機能を他のページが使用することになった場合、そのファイルはcomponentに移動されるが、もしtodoに関連する他ファイルがhome配下にある場合はhome配下に残されたままになり、より見通しが悪くなる。

ファイル名はケバブケースを激推し

ケバブケースというのはmy-file-name.jsのように-で単語を区切っていくスタイル。
Mac、Windowsではファイル名の大文字小文字を区別しないが、Linuxは区別する。これのせいで開発環境と、CI環境・実行環境で思ったとおりに動かない場合がある。
そのため、この著者様はケバブケースを推奨していた。

まとめ

  • ディレクトリはページと機能単位で分ける。
    • ページ: 1ページ1jsファイル
    • 機能: コンポーネントや単純なロジックなど。すべての他機能フォルダから参照可能。
  • ファイル名はケバブケース