[Next.js / React] 小中規模プロジェクトの、保守可能なクリーンなアーキテクチャを考える
前提
- 今までVue.jsで作っていたCMSをNext.tsに置き換えることにしました。
- 筆者はReactを初めて触ったのですが、世の中のドキュメントが小規模単純系<->複雑大規模系に二分化していたように見受けました。
- クリーンアーキテクチャなどを取り入れるほど重厚にはしたくないのですが、いわゆるサンプルプロジェクトやテックスクールが出している記事を参考に作るとコードが肥大化してしまいそうな要件。
- 頑張りすぎない、小中規模アーキテクチャを調べて考えてみたことの備忘録。
- これからNext(React)を触る多言語経験者にとって有益な情報になると良いなと思って公開しますが、拙い内容となる可能性もあるためコメントなど頂戴できれば幸いです。
検討した事項
- Feature Drivenな構成
- Presenter/Containerパターン・FLOCSS等
- Provide/Injectパターン
- クリーンアーキテクチャ・DDD・リポジトリパターン
- Store(Redux, Pinia等)
※ 本記事では各パターンについての詳解は割愛します。
Feature-Drivenなディレクトリ構成
アーキテクチャはディレクトリ構成が色濃く象徴するものであるので、ディレクトリ・ファイルの構成を中心に記述します。
今回ドキュメントの少なさからApp Routerの採用を見送ってしまったのですが、NextだとFeature-Driven Structureが主流であり(つまりFeature, Domainを適切な粒度で切れるかが肝である)、App Routerはこの考えに根ざす仕組みのように思えたので、後から導入すれば良かったと思いました。
初段のディレクトリ構成はこうしました。
src
├── components # 汎用コンポーネント(ドメイン知識を含まない)
├── features # 機能ベースモジュール
│ ├── featureA # 機能Aのディレクトリ
│ │ ├── apis # 機能AのAPI利用部分(レポジトリ)
│ │ ├── components # 機能Aのコンポーネント(ドメイン)
│ │ ├── hooks # 機能Aのhook
│ │ └── types # 機能Aの型
├── hooks # 共通・全体のhook
├── pages # ページコンポーネント
├── types # 共通・全体の型
└── utils # 共通・全体の関数(あまり使わない方針)
参考記事のアーキテクチャを大いに参考にさせていただきました。ありがとうございます。
(参考記事)
page-component層について
pages/配下のファイルが肥大化したり、高凝集になることを防ぐため、上記の記事ではpage-component層を差し込んでいますが、ファイルの数が大きくなりすぎることを嫌い(かつ、私のプロジェクトではpages/配下が中間レイヤー的な役割しか持ち得ないため)、page-component/の役割はpages/配下に押しつけました。
Presenter/Containerパターンなどコンポーネント分割について
Presenter/Containerパターンは、描画を担うコンポーネントと、ロジックやデータのやり取りを担うコンポーネントを分けましょう、というフロントエンド・アーキテクチャです。
Atomic DesignやFLOCSSも本質的には同じ考え方で、プレセンテーション・コンテナを「ドメイン知識を含むか」の観点や、さらにパーツの粒度で分けていく、というものですね。ここについても同時に検討します。
結論としては、
- コンテナ・プレゼンテーションパターンは採用しませんでした。
- React Hooksがロジックを切り出すことを一つの目的としているため、Hooksを適切に運用すれば不要であると考えたためです。
- Atomic DesignやFLOCSS的なアーキテクチャとしては、「ドメイン知識を含むもの」と「それ以外」で分けました。前者がfeature/components、後者がsrc/componentsにあたります。
理由や判断軸は、下記の記事(特に上の2つ)を読んでいただければ記載があります。
(参考記事)
Provide/Injectパターン
Vueを使っていた筆者としては、中規模なプロジェクトが密結合にならないよう、Props/Emitsのバケツリレーをどうシンプルにするか、が一つの課題でありました。
Vue3でprovide()/inject()
という機能ができ、子孫の全てのコンポーネントに親コンポーネントから変数を渡せます、という機能ができましたが、個人的には処理が隠蔽されやすくなる点で、解決的でなく一長一短な機能だなと考えていました。
ReactでもuseContext
という関数がProvide/Injectパターンを体現しているわけですが、上述のデメリットも鑑みて、「コンポーネントの層をHooksの粒度(≒ドメインモデル的なFeatureの粒度)に抑えることで、多層となることを防ぐ」という前提のもと、Providerの採用を見送りました。
ただし、プロジェクトが大きくなってくると、Hooksで行うことがパフォーマンス面、コード面でかえって冗長になる可能性もあるため、特に子孫を多層に抱えるケースだと再考の余地がありそうです。この記事は、Provider自体がマイナスだと主張するものではありません。
(参考記事)
クリーンアーキテクチャ・DDD・リポジトリパターン
実装に求められる速度、プロジェクトの規模を考え、今回は採用を見送りました。
React Hooksの思想がクリーンアーキテクチャと真逆を行っているという意見も見られましたが、関数型だからということでしょうか。(個人的には、useStateは関数的にオブジェクト指向を再現したものだななどと思ったのですが...。)
いずれにせよ、「意味的に疎であるべきモジュールは抽象化・カプセル化し、Pluggable(付け替え可能)にする」というオブジェクト指向の基本はもちろんプロジェクトの全ての箇所に踏襲すべきです。よくある上位記事のように、小規模プロジェクトだからといって全ての処理を、具象的なままユースケースそのままに書くことは避けるべきです。
(参考記事)
リポジトリパターン
RepositoryInterface, Repository (implements RepositoryInterface)を作って、Repository間をPluggableにしようというものです。
今回は素でAPIを叩きに行く機構としましたが、いつ置き変わっても良いようにモジュールは細かく分けています。ここまでは中小規模のプロジェクトでも行うべきだと思います。また、ディレクトリ名は長くなるのを嫌ってapi/としましたが深い意味はありません。
RSC(React Server Components)について
上記と同じような理由で採用をしていません。
(参考記事)
Store(Redux, Pinia等)
採用していません。参考記事にある理由でHooksで整理する方向で実装を行いました。共存するパターンも少なくはないようです。
(参考記事)
改めて、React Hooksについての新参者である筆者の理解
ということで、多くのアーキテクチャを検討した結果、特に中小規模のプロジェクトでは、React Hooksを使うことが鍵であるという理解に至りました。
[WIP......!!!]
Discussion