【Next.js】実務でapp routerに移行した所感

まずはサンプルコードから
以下のコードを見ていただきたい。この記事では、この巨大なソースをリファクタリングします。
! 細かな実装については、本題とズレるので、今回はファイルの構成について、注力してください。
要素技術
  • React/Next.Js(App Router)
  • tailwindcss
  • GraphQL/Postgraphile
  • Mantine UI
/app/item/page.tsx
ソースの説明をしておきます。
  1. 商品を検索/登録できる
  1. 商品は、商品名、カテゴリ、金額で構成される
  1. 検索条件を入力すると、リアルタイムで検索一覧が更新される
  1. 登録はモーダルが起動する
古典的に、「動けばそれで OK」な開発をするところは、現在も多数ありますし、私自身少し前までこういう実装をしていました。
ただ、これを見て思うことは、「どこから見ようか」「どうなっているのか」「壊れやすそう」など、ネガティブな感想が多いのではないかと思います。何が問題かを考えると、一番は長すぎること です。今回は、フロントエンドを取り上げていますが、バックエンドでも同じことで、長すぎる場合は、疑った方がいいです。

Method 1 ページコンポーネントの分離

このページには、検索と登録の 2 つのページ・機能があるので、それらを分離できないか考えます。検索ページから登録ページを呼び出しているので、登録ページを分離してみます。
/app/item/page.tsx
/app/item/RegisterItem.tsx
ページ・機能ごとにファイルが分かれるだけでも、だいぶ見通しはよくなります。

Method 2 App Router の活用

Next.js v13 から App Router が実装されました。
この機能は、より直感的な構成に実装できる点、汎用性の高さが魅力的だと感じています。これを活用することで、以下のようにリファクタリングできます。
/app/item/layout.tsx
/app/item/RegisterItem.tsx -> /app/item/@register/RegisterItem.tsx
/app/item/@register/page.tsx
これは、Parallel Routes という機能で、コンポーネントを並列で呼び出すことができます。フォルダ名の先頭に をつけることで実現できます。ここでは解説しないので、詳しくは公式をご覧ください。
もう一方の  についても同様の実装をすればよいのですが、ここで気づくことがあります。 は、中身を呼び出しているだけになりました。App Router における page.tsx とはルーティングを責務とするファイルです。どの言語においても、それぞれのファイル・コンポーネントは責務外をしないようにすることで、可読性向上、testable なコードを実現できます。
/app/item/@search/page.tsx
/app/item/page.tsx -> /app/item/@search/SearchItem.tsx
Parallel Routes を使うことで、検索ページから登録ページの呼び出しをすることがなくなりました。互いに依存関係にあるコンポーネントを別 page.tsx にするのは、また手間要りますが、今回の場合だと依存関係がないもの同士だったので、このような分離が可能です。

Method 3 外部ライブラリの隠蔽

今回のメインといってもいい内容です。次の資料を見てください。
UI ライブラリの Mantine は、リリース頻度が高く、バージョンアップのたびに変更の手間が大きいです。他の各ライブラリに関しても(特にフロントエンドは)バージョンアップ頻度が多く、開発生産性に大きく影響します。
UI ライブラリの例を出しましたが、他にも API Gateway 機構やフレームワーク、グローバルステート管理など、ほとんどのライブラリのバージョンアップが頻発されているようです。
脆弱性対応が含まれることもあるので、バージョンアップしないという選択肢は採りたくないので、最小限に手間を省いた方法を考えます。
先に取り上げたソースを見てください。
/app/item/@register/RegisterItem.tsx
このソースには、外部ライブラリとして、 が import されています。
仮にですが、バージョンアップにより、TextInput の I/F が変わったり、コンポーネントの名称が変わったとすると、修正箇所は、3 箇所も修正することになります。当然ながら、他にも使用している箇所がありそうなので、かなり膨大な修正になることがわかります。
core なコンポーネントから直接外部ライブラリを参照しないようにすることで、修正コストを軽減することが可能です。これは、Clean Architecture でもよく聞く「関心の分離」に基づく考え方です。忠実に採用するならば、「依存関係逆転の原則」を使うのですが、そこまでする必要もないので、以下のようにします。
/components/ui/index.ts
/components/ui/TextInput.tsx
/components/ui/Button.tsx
/app/item/@register/RegisterItem.tsx
外部ライブラリを隠蔽し、案件用にカスタマイズすることのメリットは以下だと考えます。以下では、UI コンポ というようにします。
  1. バージョンアップによる、改修が発生した場合は、呼び出しているページではなく UI コンポの修正で済む。
  1. StoryBook 等の UI テストは、UI コンポの確認で済む。
  1. Mantine でない別のライブラリに置き換えが容易である。
  1. エンジニア間のばらつきを防ぐことができる。
3 について補足があります。
階層はしっかりと意識する必要があります。今回の場合は、各 UI コンポで、Mantine のコンポーネントを使用しています。だからといって、 のような外部ライブラリがあらわになる名称を使っては台無しです。Mantine からライブラリを変更する場合、または複数の UI ライブラリを使う場合など、呼び出し元であるページコンポーネントも変更する必要性が出てきます。変えなくてもいいものを変えずに済む工夫も必要になります。
さらに、4 については、効果が大きく、使えるものが整理されているだけで、エンジニアが考える分量を減らしてくれます。統制を取るために、ドキュメントを用意したりすることもあるでしょうが、そういった手間を減らします。
これは、hooks にも言える話で、例えばアプリ内で使用される ID があるとします。 としましょう。これは、zenn の記事を特定するもので、Client に保存して使うものとします。これを保存するとき、取り出すときの実装は、上記の応用で以下のようにできます。
/hooks/zenn/useZennHolder.ts
/app/zenn/Sample.tsx
この実装では、 を使用した保存をしていますが、別のライブラリ  に変更になっても、 になっても、 だけの修正であるのはもちろんのこと、さらには各ページで、保存方法を実装しないようにすることで、エンジニアによる  の保存方法がばらつくのを防いでいます。
以上を受けて、ソースはこのようになります。
/hooks/item/useGetItemList
/app/item/@search/SearchItem.tsx
/hooks/item/useRegisterItem
/app/item/@register/RegisterItem.tsx

Method4 さらなる分割

ページコンポーネントをロジック部分とビュー部分に分離する Container/Presenter デザインパターン というものが存在します。やむなく巨大になったページコンポーネントの可読性をよくすることができます。今回は、割愛しますが興味のある方は、試してみてください。

Method5 地味なテクニック

コメント文

以下の例であれば、前方一致であることが変数名だけで判断できるものがよいでしょう。実装を見てすぐに判断できるのであれば、コメントはない方が良いです。実装を見ても判断できない場合は、コメントがあっても良いかもしれないですが、そもそもの実装・構造がよくない可能性があります。私は、特別な事情がある場合にのみ、コメントするようにしています。
1 行ごとに、コメントをつけるように指導する文化もありますが、大概置いてけぼりになり、誤解を生み、不具合に繋がるので、私は推奨しません。
/app/item/@search/SearchItem.tsx
こちらも、ぱっと見でわかるので、なくて十分です。
/hooks/item/useRegisterItem

細かな改行

構造体の要素が 1 行ごとに改行されているのは、レビューの観点で効果を発揮します。この記事でも使用していますが、変更差分が一目でわかるようにすることで、可読性の向上が計れます。可読性はレビュー品質に影響するので、知っておいて損はないでしょう。
あまり、意識しなくても、フォーマッターライブラリはあるので、活用してみてください。
ちなみに、ほぼ変更入らないであろうところは、無理に改行する必要はないです。
/hooks/item/useRegisterItem

まとめ

App Router を使った、仕様変更に強い構成を考えましたが、それなりに、実装コスト、学習コストを伴うものなので、すべてを駆使する必要はないと思っています。重要なのは、その実装が変更されやすいか、そうでないかの見極め です。現在私が参画する案件では、PostGraphile を採用していますが、このライブラリ前提の案件であるため、ページコンポーネントに露出して実装しています。逆に、グルーバルステートは、なかなか定まり兼ねているのもあり、hooks に隠蔽しています。
案件には個性があるので、それぞれにあった方針をエンジニア間で共有し、事故の少ない、最高なプロダクトを目指しましょう。