Next.jsディレクトリ構成・設計再考(featuresが何を解決するか)
初めに
最近自分がNext.jsをやる案件がとにかく増え、ディレクトリ構成が固まってきたのでここに置いておきます。
何か意見あれば是非いただけると嬉しいです。
個人的に最近流行りのfeaturesはやりすぎだと思っていたのですが、結局開発していくとfeaturesのようなディレクトリを切った方が合理的な場面が多々出てきたのでこうなった次第です。
設計時に参考にした記事
こちらのReact TypeScript Cheatsheetに、Recommendationとして載っているbulletproof-reactを参考にしました。
構成
例として飲食店のウェブサイトで、コース(course)という機能を持たせるとする。
├─ components/
│ ├─ elements/
│ │ └─ Button
│ │ ├─ Button.stories.tsx
│ │ └─ Button.tsx
│ └─ layouts/
│ └─ Header
│ └─ Header.tsx
├─ pages/
├─ features/
│ └─ /course
│ ├─ api/
│ │ └─ getCourse.ts
│ ├─ stores/
│ ├─ const/
│ ├─ styles/
│ ├─ components/
│ ├─ Course.tsx
│ ├─ Courses.tsx
│ ├─ Courses.stories.tsx
│ └─ Course.stories.tsx
│ ├─ hooks/
│ └─ useCourse.ts
│ └─ types/
│ └─ index.ts
├─ stores/
├─ config/
├─ const/
├─ hooks/
├─ libs/
│ └─ queryClient.ts
├─ styles/
└─ types/
/components
アプリケーション全体で使うコンポーネントを入れる。
正直この辺りはアプリの規模と相談で、elementsが肥大化しそうなら分けた方がいいかなーくらい。
あとelementsはおそらく変更追加が多いのに対して、layoutsはelementsほどはないと思うので分けておくと少し安心できる。
/components/elementsについて
アプリケーション全体で使う共通コンポーネントを置く。
例えばボタンなど。
ボタンを使うためのuseButtonなどがあればこのディレクトリの中に入れる。
components/layoutsについて
アプリケーション全体で使うレイアウトコンポーネントを置く。
例えば、HeaderやFooter,管理者や一般ユーザーによってレイアウトを変えるならそのLayoutコンポーネントなどもここに置く。
/pages
Nextのページコンポーネントを入れる。割愛。
/features
ある特定の機能、ドメインでしか使わないapiへのアクセサや定数、型、hooks、コンポーネントなど全てを詰め込む。
/features/courseについて
飲食店のウェブサイトで、コース(course)というコースを表示するという機能が欲しいと考える。
表示したいのでCourse.tsxというコンポーネントを切り/course/componentsに置く。
apiからデータを取得したいので/api/getCourse.tsを作る。(getCourseがCourseコンポーネントから参照されていなくてもここに置いて良い。あくまでfeaturesディレクトリは「機能」なので。)
コンポーネントが増えてバケツリレーが嫌になったら、/storesに好きな状態管理ライブラリを使ったファイルを入れれば良い。
型は/types/index.tsに、コースを表示するためのロジックは/hooks/useCourse.tsに。
定数が必要なら/configsに。
スタイルを当てる必要があるならstyles/に。(TailwindやCharkra UIなどの場合は必要ないのであまり考えてなかった。。)
僕はフロントのテストに疎いので可能かどうかはわからないが、もしテストのディレクトリを自由に決められるなら/testsとしてテストのコードをそこに書く。
/store
アプリケーション全体のグローバルステートの管理に使う。
僕はrecoilを使うことが多いが、最近はもうめんどくさくなってreactQueryを状態管理ツールとしてそのまま使ってたりする。
ちなみに、アプリケーション全体のグローバルステートというのがほぼないのでほぼ使わない。
例えばログイン情報は/features/auth/storesというディレクトリに切る。
ダークモードとかくらいかな??
/config
アプリケーション全体の設定を置く。
といってもほぼfeaturesに持っていけると思うので、主にライブラリの設定が入ったりする。
僕はここにはTailwind CSSやChakra UIのthemeのconfigを置いている。
逆に言うと他には何も置いていないので必要ないかも。
/const
アプリケーション全体の定数を置く。
僕は都道府県を入れている。(都道府県はいろんな機能から参照される可能性のある定数のため。)
/hooks(もしくは/utilsなど)
アプリケーション全体で使う共通ロジックを置く。
主に便利ロジックが入るだろう。
単純な関数ならutilsとかに切り出しても良いかも。
/libs
ライブラリのラッパーや設定済みのインスタンスをexportするファイルなどを置く。
axiosやreactQueryの設定をここに置いている。
/styles,/types
アプリケーション全体で使う〜、以下略。
featuresディレクトリが何を解決するか
関心毎にコードをまとめられる
コードを書くとき僕らが対象とすべきは「アプリの機能、関心の単位」であり、「どのライブラリ、言語の機能を使ってコードを書くか」ではない。
たとえばプロジェクト直下の/hooksディレクトリの中のファイルだけをいじって特定の機能が完成することはない。
でも飲食店のコースを表示する機能を作るとき/courseというディレクトリを切って、そこに必要な全てを入れればそのディレクトリだけをいじって完成させられる。
改修の時もここを起点に開発すれば済むようになる。
もし、これがライブラリ、言語の機能ごとにディレクトリが切られていた(プロジェクト直下にしか/hooks,/api,/typesなどがない)場合、僕らはまずひとつひとつ散らばった/hooksなどディレクトリの中から、その特定の機能に関するコードだけを探し出す作業から始まることになるだろう。
コンポーネント駆動開発からの脱却
昨今要求されるフロントエンドはただのCRUDだけではなくかなり複雑なロジックや、リッチなUI、ときにはドメインロジックのようなものまで持つようになってきている。
そういった場合、コンポーネント(UI)に関係のないコードをどこに置くのかが悩ましい問題となる。
ロジック自体や、APIへのアクセサ、型、テスト、などなど。
時にはUIより先にロジックから書きたい場合もあるかもしれない。
テストから、型から書きたい場合もあるかもしれない。
それをコードを、関心を散らばらせずにまとめられることがfeaturesディレクトリの良いところであると考える。
featuresディレクトリの難しさ
機能、関心を分け、名前をつけるというのが難しいかもしれない。
ある種ドメインモデリングに近いかもしれない。
でも僕は実装するとき結構気軽に決めてしまう。
実際、Reactもディレクトリ構成はそんなに悩むなと公式で言っている。
ドメインモデリングに近いとしたら、ドメインモデルというのは流動的で育てていくものなので、featuresディレクトリが変動するのも仕方のないことだと言える。
ただ、バックエンドのドメインモデルと唯一違うのが、コードに落とし込まれたドメインモデルが抽象化されておらず詳細がコードに含まれてしまっているのでバックエンドほど変更しやすくないといったところだろうか。
おまけ
pages再考
こういうのもありかなと思っている。
├─ components/
│ ├─ pages/
│ │ └─ Index
│ │ ├─ Index.tsx
│ │ └─ useIndex.ts
Next.jsのプロジェクト直下の/pagesディレクトリにはページコンポーネントのファイルを置かなければならないが、そのコンポーネントはあくまでルーティングのためだけにとどめておき、実態は/components/pagesに入れるという考え方。
こうすると、そのページコンポーネントが肥大化したのでロジックだけ別ファイルに切り出したい(この例だとuseIndex.ts)となったときに便利。
/hooks,/utilsディレクトリを切ってもいいが、ディレクトリを切らなきゃいけないほど/components/pagesにファイルが増えているのは多分何かがおかしい。
あくまでここに入るのはページ固有の(機能に関係のない)ロジックやコンポーネントだけ。
hooks,utils再考
そもそもuseStateやuseEffectを使用しない純粋な関数はカスタムフックに切り出すべきなのか、それとも純粋な関数としてexportすべきなのか。
このあたり割と適当にやってしまっていたので意見がある方はぜひ書いてください。
僕は関連するロジックをまとめたいのと、あとから状態をもつ可能性(useState,useEffectが追加される可能性)も考慮し、カスタムフックにしてしまうことが多い。
あまりにも単純で他のロジックと関連を持たず、アプリ全体で使われるであろうようなロジック(numberを受け取って、三桁区切りでカンマをつけたstringを返す関数など)はそのまま関数としてexportするかも。
useCallbackでメモ化は忘れずに。
最後に
自分が最近いそがしくなってしまって、あまり盛り上がってないのですがReactのツイッターコミュニティを作りましたので、ご興味ある方は入って盛り上げて行っていただけると幸いです!
ツイッターのフォローもお願いします!
Discussion