背景
以前 atomic design を採用した React のプロジェクトで、下記のようなディレクトリ構成を採用したことがあった。
(※ Next.js ではない)
src/
└ components/
├ atoms/
├ molecules/
├ organisms/
├ pages/
└ templates/
stories/
└ components/
├ atoms/
├ molecules/
├ organisms/
├ pages/
└ templates/
tests/
└ components/
├ atoms/
├ molecules/
├ organisms/
├ pages/
└ templates/
この構成で開発を進めている時、下記2点の不満があった。
コンポーネントの実装とテスト・storybookの実装の位置が離れている
例えば Button というコンポーネントを実装する時、下記のように各ファイルを配置することになる。
src/
└ components/
└ atoms/
└ buttons/
└ Button.tsx
stories/
└ components/
└ atoms/
└ buttons/
└ Button.stories.tsx
test/
└ components/
└ atoms/
└ buttons/
└ Button-test.tsx
この場合、Button.tsx 本体の実装と Button.stories.tsx, Button-test.tsx の実装がかなり離れたところにあるので、辿るのが面倒に感じていた。
元々は test が chai/mocha で実装されており、確か mocha が src/ と並列に test/ ディレクトリを作って src/ 配下と同じディレクトリ構成を鏡のようにして作っていく思想だったため、後から導入された stories/ もその構成に倣ったと記憶している。
今ならテストランナーは jest が主流なので、 src/components/atoms/buttons/Button.tsx のテストは src/components/atoms/buttons/tests/Button.test.tsx に実装するのが普通かもしれない。
src/
└ components/
└ atoms/
└ buttons/
├ __tests__/
│ └ Button.test.tsx
└ Button.tsx
同じリソースを表すコンポーネントでも粒度が異なると位置が離れてしまう
例えば「お知らせ一覧ページ」を実装する時に、関連コンポーネントを下記のような配置で実装することになる。
src/
└ components/
├ molecules/
│ └ notifications/
│ └ NotificationListItem.tsx
├ organisms/
│ └ notifications/
│ └ NotificationList.tsx
└ organisms/
└ notifications/
└ NotificationListPage.tsx
しかし実際に開発する際、これらは同時に修正する頻度が高いものなので、下記のようにもっと距離が近い方が都合が良いように思う。
src/
└ components/
└ notifications/
├ molecules/
│ └ NotificationListItem.tsx
├ organisms/
│ └ NotificationList.tsx
└ pages/
└ NotificationListPage.tsx
推しのディレクトリ構成
以上の経験から、最近は下記のような配置のディレクトリ構成を推している。
(Next.js 前提なので components/ と並んで pages/ ディレクトリがある)
src/
├ components/
│ ├ common/
│ │ ├ buttons/
│ | │ ├ Button.stories.ts
│ | │ ├ Button.test.ts
│ │ │ └ Button.tsx
│ │ └ templates/
│ │ └ BaseTemplate.tsx
│ └ notifications/
│ ├ hooks/
│ │ ├ useNotifications.test.ts
│ │ └ useNotifications.ts
│ ├ molecules/
│ │ ├ NotificationListItem.stories.tsx
│ │ ├ NotificationListItem.test.tsx
│ │ └ NotificationListItem.tsx
│ ├ organisms/
│ │ ├ NotificationList.stories.tsx
│ │ ├ NotificationList.test.tsx
│ │ └ NotificationList.tsx
│ └ pages/
│ ├ NotificationListPage.stories.tsx
│ ├ NotificationListPage.test.tsx
│ └ NotificationListPage.tsx
├ hooks/
│ ├ useAPI.test.ts
│ └ useAPI.ts
└ pages/
└ notifications/
├ notifications.stories.tsx
├ notifications.test.tsx
└ index.page.tsx
ポイント
- 特定のドメインを扱うコンポーネント群を src/components/{domain]/* 内にまとめて配置する
- ※ {domain}/ 配下に molecules, organisms といったコンポーネント粒度ごとのディレクトリを切るべきかどうかはまだ要議論だが、何かしらで分けないとファイルが増えた時に見づらくなる気がしている
- 特定のドメインに関連しているわけではない共通のコンポーネント(主に atomic design で言うところの atoms と templates)は src/components/common/ 配下に置く
- コンポーネント実装とテスト、storybookの実装は同じディレクトリ内に置く
Next.js の pages/ ディレクトリ配下にページ以外のファイルを置く
Next.js の Custom Page Extensions という機能を利用して *.page.tsx
という suffix が付いたファイルのみページとして扱うように設定できる。
module.exports = {
pageExtensions: ['page.tsx'],
}
これを適用することで、 pages/ 配下に *.stories.tsx
や *.test.tsx
といったファイルがあっても無視してくれる。
storybook 駆動開発
タイトルに storybook 駆動開発と入れたのに、書いてみたら結果あまり関係ない内容になってしまったので、最後に申し訳程度に書いておく。
- chromatic などを使って storybook 上のコンポーネントをキャプチャして visual regression test を行うことができる。
- UI が想定通りレンダリングされることだけでなく storybook の CSF3.0 で導入された play 関数を利用することで、ユーザーとのインタラクションの検証もできる。
- さらに play 関数を jest 上で実行することで、ユーザーインタラクションのロジック部分の自動テストも可能になり、かつ coverage の計測も可能になる。
- これらを駆使して、実装したコンポーネントを全て storybook に登録して CI で自動回帰テストを行うことを storybook 駆動開発と呼んでいる(と想像している)。
- この時、コンポーネント実装と storybook, test を密に連携させながら実装を進めるので、同じディレクトリに配置されていた方が都合が良い。