Reactのディレクトリ構造パターン例

Next.jsをベースにWEBサイトを構築したのでどんな構成で構築したのかを備忘録として

Nextjs

React

Structure

2021年に、Next.jsをベースにWEBサイトを構築したので、どんな構成で構築したのかメモしておきます。
ディレクトリ構成についてはいろんな意見があり、それぞれが強い意見を持っていると思います。僕もこれが一番いい、という趣旨ではなく、あくまで一つのパターンとして参考にしてもらえたらと思います。
今回の背景は以下の要素です。
  • 数人のエンジニアしかいない小さなスタートアップ、スピードの速い環境
  • PMFしていないため、捨てる可能性・変更する可能性がある

ディレクトリ構造と方針

今回、前提として極力アーキテクチャを入れない決定をしました。
その目的は「新規メンバーの参入障壁を下げる」ことです。プロジェクト初期に様々な方にアドバイスをもらったのですが、そのときに聞いた考えをまるっと参考にしました。
具体的には、DDDのようなアーキテクチャ論は用いず、ディレクトリ構造は一般的な構成+featureディレクトリ(+storybook)のような構成にしました。
Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件
Reactアプリケーションのアーキテクチャの一例として公開されているGitHubリポジトリ「bulletproof-react」が大変勉強になるので、私自身の見解を交えつつシェアします。 まずはプロジェクトごとにバラつきがちなディレクトリ構造について。 bulletproof-reactでは、Reactに関するソースコードは srcディレクトリ以下に格納されています。逆に言えば、ルートディレクトリに componentsや utils といったディレクトリはありません。 たとえば Create Next Appで作成されるアプリケーションは、デフォルトではルートディレクトリに pagesといったソースコードのディレクトリが並びますから、 src 以下に入れるのは本リポジトリが意図的に行っているディレクトリ構造といえます。 実プロジェクトのルートには、マークダウンで書かれたドキュメント群( docs)や、GitHub Actions等のCI設定(.github)、もしコンテナベースで扱っているアプリケーションであればDockerの設定( docker)などが混在するでしょうから、ルートレベルに直接 components などを配置するとアプリケーションのソースコードとそうでないものが同一の階層に混在してしまいます。 単純に紛らわしいだけでなく、たとえばCI設定を書くときにもソースコードは src 以下に統一しておいたほうが適用する範囲を明示しやすくて便利です。 本リポジトリにおけるディレクトリ構造で面白いなと感じた点は、 features というディレクトリです。 src | +-- assets # assets folder can contain all the static files such as images, fonts, etc.
Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件
Reactの公式ドキュメントと近いのですが、feature(機能)は概念的に強く定義しませんでした。その代わり、そのチーム内でユビキタス言語を必ず定義し、そのワード(英単語)を共通認識としていたので、結果的にfeatureにはそのワードが頻出するようになりました。(このあたりはDDDの姿勢と近いので、≒DDDともいえるかもしれません。)
 
チームメンバーが非同期的かつ、フルリモート前提だったこともあるため、ルールは暗黙知とせずに極力eslintなどで縛るようにしました。
それ以外は極力ゆるく、何よりもスピード重視で都度やっていこ、のスタイルで組んでみました。
ディレクトリ構造のような答えの無いものは考え出すと時間が溶けてしまうので仮で進行し、問題が生まれてから考えるスタイルにしました。(WebStormのリファクタリング機能なら、後から変更してもほぼバグ無しでリファクタリングできるので・・・笑)

ディレクトリ

ちなみに公式では「機能・ルートによるグルーピング」と「ファイルタイプによるグルーピング」の2パターンの構成が紹介されています。
ファイル構成 - React
React はファイルをどのようにフォルダ分けするかについての意見を持っていません。とはいえ、あなたが検討したいかもしれないエコシステム内でよく用いられる共通の方法があります。 機能ないしルート別にグループ化する プロジェクトを構成する一般的な方法の 1 つは、CSS や JS やテストをまとめて、機能別ないしルート別のフォルダにグループ化するというものです。 common/ Avatar.js Avatar.css APIUtils.js APIUtils.test.js feed/ index.js Feed.js Feed.css FeedStory.js FeedStory.test.js FeedAPI.js profile/ index.js Profile.js ProfileHeader.js ProfileHeader.css ProfileAPI.js ここでの「機能」の定義は普遍的なものではないので、粒度の選択はあなた次第です。トップレベルのフォルダの名前が思いつかない場合は、ユーザに「この製品の主な構成部品は何か」と聞いてみて、ユーザの思考モデルを青写真として使いましょう。 ファイルタイプ別にグループ化する プロジェクトを構築する別の人気の方法は、例えば以下のようにして類似ファイルをグループ分けするというものです。 api/ APIUtils.js APIUtils.test.js ProfileAPI.js UserAPI.js components/ Avatar.js Avatar.css Feed.js Feed.css FeedStory.js FeedStory.test.js Profile.js ProfileHeader.js ProfileHeader.css 人によってはこの方法をさらに推し進め、コンポーネントをアプリケーション内の役割に応じてフォルダ分けすることを好みます。例として、 Atomic Design はこのような原則の下に作られたデザインの方法論です。ただこのような方法論は、従わなければならない厳格なルールとして扱うよりも、役に立つ見本として扱う方が多くの場合生産的であるということを忘れないでください。 ネストのしすぎを避ける 深くネストされた JavaScript プロジェクトには様々な痛みを伴います。相対パスを使ったインポートが面倒になりますし、ファイルが移動したときにそれらを更新するのも大変です。よほど強い理由があって深いファルダ構造を使う場合を除き、1 つのプロジェクト内では 3 段か 4 段程度のフォルダ階層に留めることを考慮してください。もちろんこれはお勧めにすぎず、あなたのプロジェクトには当てはまらないかもしれません。 考えすぎない まだプロジェクトを始めたばかりなら、ファイル構成を決めるのに 5 分以上かけない ようにしましょう。上述の方法の 1 つを選ぶか、自分自身の方法を考えて、コードを書き始めましょう! おそらく実際のコードをいくらか書けば、なんにせよ考え直したくなる可能性が高いでしょう。 もしも完全に詰まった場合は、すべて 1 フォルダに入れるところから始めましょう。そのうち十分に数が増えれば、いくつかのファイルを分離したくなってくるでしょう。そのころには、どのファイルを一緒に編集している頻度が高いのか、十分わかるようになっているでしょう。一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。この原則は、「コロケーション」と呼ばれます。 プロジェクトが大きくなるにつれ、実際にはしばしば上記両方の方法が組み合わされて使用されます。ですので、「正しい」方法を最初から選択することはさほど重要ではありません。
ファイル構成 - React

src

今回のsrcディレクトリは bulletproof-react と近い形になりました。
基本的にsrc直下はアプリケーション全体に依存するもの、それ以外はfeatures(機能に依存するもの)の分けです。
src
|
+-- components        # 共通コンポーネント
|
+-- constants         # URLなどのアプリケーション全体
|
+-- features          # feature based modules
|
+-- hooks             # 汎用的なhooks
|
+-- lib               # NPMライブラリに依存する汎用的な関数. wrapperなど
|
+-- pages             # Next.jsを採用したため. route
|
+-- routes            # routes configuration
|
+-- scheme            # zod schemeなど
|
+-- styles            # global style, colorなど。chakraのthemeなども
|
+-- types             # 共通のtype.d.tsなどをここに
|
+-- utils             # 汎用的なユーティリティ関数.
notion image
ストア管理は
  • 全体管理:Context
  • 部分管理:Recoil
にしました。
当初、Contextのみを使用して管理していましたが、機能単位でstate管理をしたくなり、都度Contextの実装をすることに手間を感じたので、コード量の少なさ・取り回しの良さを目的にRecoilを導入しました。
Contextは src/components/contextsに配置していますが、root(src)からの見通しが良いともいえないので、src/storesに配置してもよかったかもしれません。

features

src/features/awesome-feature
|
+-- api         # APIアクセスする主にPromise関数
|
+-- hooks       # feature依存hooks: apiファイルやformのstate管理など
|
+-- state       # 機能に依存するstate(Recoil)
|
+-- (etc)       # その他、機能に関するものすべて. converterなど
機能に関するものすべての感覚で入れていますが、主に api / hooks を中心に、store(state)を入れたりしてます。
components にfeaturesディレクトリが配置されているので、機能特有のパーツはこちらに配置されてます。

components

src/components
|
+-- context                  # 全体store. Context
|
+-- features/awesome-feature # 機能依存のComponents: 主にForm.tsxなどでfeaturesのhooksを呼び出す
|
+-- pages                    # Next.js(~v12)でpageコンポーネントを分割するため
|
+-- ui                       # UIパーツを配置。基本的にstorybookで構築できるようなプリミティブな値を引数にする。
複数の機能にまたがるようなグレーゾーンのものや、機能に依存するけど共通で呼び出しそうなものは機能名っぽくなくてもsrc/compoents/features/xxx/ に配置して疎結合なディレクトリを汚染しないようチームで心がけていました。
また、firebaseを採用しており、実装スピード面やパフォーマンス面から react-firebase-hooks をComponentから直接読んでいるものも許容し、同じく src/compoents/features に配置しました。

components/ui

src/components/ui
|
+-- (parts name)       # 各UIパーツ
|
+-- layouts            # FooterやHeaderなどパーツを組み合わせたもの
Atomic Designは実践的なやり方がいいんじゃないかと思っているクチで、Atomsのようなパーツだけしっかり分けておいてstoryも書いておいて、それを組み合わせたMolecules以上はparts以外、と分けておけばいいんじゃないかと考えています。
今回のデザインシステム上もパーツの定義ははじめにしっかり決まっており、それ以上は画面デザイン時に決める方針(あの画面と同じ構成で、etc…)だったので、チーム運用の兼ね合いとしてもベターだったと思います。
 

 
こんな感じで、1ページを構成するファイルの主な依存関係は以下の感じになりました。
notion image
eslint-plugin-strict-dependencies でその依存関係のルールはこんな感じです(tsconfigでpath設定している前提です)
// .eslintrc.json
{
	"strict-dependencies/strict-dependencies": [
      "error",
      [
        /**
         * Example:
         * Limit the dependencies in the following directions
         * pages -> components/page -> components/ui
         */
        {
          "module": "@/components/page",
          "allowReferenceFrom": ["@/pages"],
          "allowSameModule": false
        },
        {
          "module": "@/components/ui",
          "allowReferenceFrom": ["@/components/pages", "@/components/features"],
          "allowSameModule": false
        },

        /**
         * example:
         * Disallow to import `next/router` directly. it should always be imported using `libs/router.ts`.
         */
        {
          "module": "next/router",
          "allowReferenceFrom": ["src/lib/router.ts"],
          "allowSameModule": false
        },
        {
          "module": "@/*",
          "allowReferenceFrom": ["src/*"],
          "allowSameModule": false
        }
      ],
      // options
      {
        "resolveRelativeImport": false
      }
    ]
}
// tsconfig.json
{
	...
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
	...
}
 
考え方としては、末端の部分はしっかりロジック分離しておいて、中間層の置き場所だけ決めておこう、という感じです。中間層は階層わかんないし端っこ固めておけばどうにかなるっしょ、的な。
 
notion image
 

 
こんな感じです。
 
サンプルリポジトリまで用意できたらよかったのですが、あとで気が向いたら作って追記しておきます。
 
参考程度にどうぞ!
Satoshi Nitawaki

Application Engineer. I’m Japanese. Detail for 👆 about page.