こんにちは。プロジェクトで採用したNext.js(App Router)のフォルダ構成を紹介します。
一例として参考にしていただければ幸いです。
今回はフルスタック構成ではありません。
1. プロジェクトの概要
まずは、プロジェクトの前提条件を共有します。
- サービス形態: 一般消費者向け(toC)サービス
- 主な機能: イベント申し込みなど、独立した機能が複数ある
- 開発スタイル: ユーザーからのフィードバックに基づき、繰り返し新機能の追加や改善を行う
- MVPローンチまで:1年弱
- 画面数:30ほど
-
システム構成:
- フロントエンド: AWS Amplify (Next.js App Router)
- バックエンド: AWS
- データベース: AWS
いわゆる「変化が激しく、繰り返し機能追加・改善を行っていくWebサービス」です。
今回私は、フロントエンドのコードレビュー・モジュール設計を担当しました。
AmplifyにはSDKが何個かあるのですが、今回はAmplifyがフルスタックではないのと
学習コスト、べンダーロックイン、サプライチェーン攻撃を考慮して、SDKに
依存するメリットが少なかったのでフルスクラッチ寄りで実装しました。
2. モジュール構成の全体像
src/
├── app/ # ルーティング定義に特化(薄く保つ)
│ ├── layout.tsx
│ ├── page.tsx #TOPページ
│ ├── events/
│ │ ├── [id]/
│ │ │ └── page.tsx # 実際の処理は features/ から呼び出す
│ │ └── page.tsx
│
├── features/ # ドメインごとのモジュール(featureと言いつつ、今回はドメインで区切りました
│ ├── event/ # イベント関連機能
│ │ ├── components/ # この機能だけで使うコンポーネント
| | | ├── container/ # 【こだわり1】UIコンポーネントの相互制御・ロジック担当。スタイルのレイアウトも兼ねる
| | | └── presenter/ # 【こだわり2】共通UI(components)にドメインを注入したコンポーネント
│ │ ├── hooks/ # カスタムフック *あまり使いませんでした
│ │ ├── types.ts # interface型定義
│ │ ├── provider/ # 固有のコンテキスト
│ │ ├── services/ # 固有のfetchメソッド
│ │
│ └── .../
│
├── components/ # プロジェクト全体で共通のドメイン注入前のUI(Button, Modalなど)
│ └──.../ # 横断的なCompound Patternもここかなと思います(使いませんでしたが)
│
├── hooks/ # プロジェクト全体で共通の汎用フック
├── lib/ # 共通ユーティリティ(fetchのラッパーなど)
├── provider/ # 認証、LINE連携などのコンテキスト
└── services/ # 横断的なfetchメソッド
2.1 構成の採用意図
在籍していた会社では、React関連のプロジェクトの炎上率が高く、新人の頃に参画したプロジェクトでそれを理解していたので、初期のモジュール設計は時間をかけました。まだ私の経験は浅いですが、どのプロジェクトでも初期設計を妥協すると後々負債が膨れ上がるイメージです。見てみると、コードが極端に泥団子化していたり、初期設計の段階から関心の分離が適切にされていないことが多いように思います(この辺りでとても分かりやすかった記事はこちらです)
モジュール設計にあたり、読んだ書籍・サイトドキュメントは下記です。
- 『実践Next.js』:Next.jsを知らなかったので、読みました。「page.tsxでデータfetchする」というシンプルで強力な流儀はここで学びました(データフローが予測しやすい)
- 『Atomic Design』 :タイトルの通りatomic designについてクラスベースのReactで学べます。私はこの本からcontainer・presenterというシンプルで強力な関心の分離を学びました。
- Feature-Sliced Design (FSD):単方向の依存(上から下に流れる)なので、予測可能性が高く、フロントエンド版のClean Architectureみたいなイメージです。各モジュールの役割が厳格で良いと思います。採用しなかったのは難易度・学習コストが高い、かつプロジェクトの性質を考えると、多少オーバーエンジニアリングな気がしたためです。仮にフルスタックでNext.js使うとして、ORM(prisma)を使うなら、FSDに少し変更加えると、理にかなったアーキテクチャになりそうだなと今ふと思いました
- BulletProof React:ベースはこれを採用しました。コロケーション(強凝集)がメインで理解しやすく、保守性もあるアーキテクチャです。プロジェクトのフロントエンドメンバーはジュニアクラスの方がメインで、プロジェクトの性質(直感的)も考慮してこちらにしました
featureベースなのは間違いないなと思いました。ユーザのフィードバックに基づいて機能を追加したり改善するので、コロケーションで固めて、いらなくなったらごっそり消せる(疎結合)みたいな運用ができればと考えました。
2.2 改善点
特定のドメインの機能が多くなってくるとcontainer・presenterが肥大化してきます(containerが特に。presenterは再利用することが多いので比較的増えにくい)
今回の構成だと、まずは機能ごとに一貫性のあるプレフィックスでファイルを命名することが考えられました。
3. 採用したライブラリ
採用した汎用的なライブラリを紹介します。
-
Tailwind: cssで採用。コンポーネント(tsx)に直接スタイル情報を書くので、cssの設計(BEM、PRECSSなど)をせずに済みます。cssファイルを行き来する必要がなく、tsxで全部完結するので、認知負荷が軽減される点も大きなメリットです。学習コストは高くないので、納期が厳しめなプロジェクトにもお勧めです
*ユーティリティーファーストという考え方(パラダイム)が採用されています - Swiper: カルーセルの実装で採用。ドキュメントが豊富で直感的なAPIインタフェースです。ライセンスはMITです。
汎用的なライブラリはこれくらいです。Jotaiとかも見てみましたが、プロジェクトのドメインの難易度から考えると、ほとんどはネイティブのcontext providerで十分でした。ですが、メイン機能の内の一つのドメインの状態遷移が比較的複雑だったので、そこはJotaiを使うか、パターンを使う必要がありました。私の考えとしては、基本的にライブラリを使わなくても、保守性の高い設計・実装ができるのであれば、その方向性でいきます。直近のaxiosの件を考えると、明確なメリットがない限り依存関係は増やさないほうが良いと思います。
4.最後に
NextJsのモジュール構成は自由なので、オリジナルが作れるところも魅力の一つです(先ほど紹介した書籍のgithubのモジュール構成は本記事と異なります)。
次回は、レビュワーとして学んだこと・AppRouter(Amplify)で詰まったところ、それか、とても勉強になった書籍『人月の神話』の紹介のどちらかを記事にしようかと思います。