はじめに
最近チーム開発でNext.jsのApp routerというルーティング方法を使ったフロントエンド開発をする機会があり、フロントエンドを少し勉強しました。フロントエンドの開発はReactで少ししたことがあるくらいでちゃんと理解できておらず、今回のチーム開発で基礎に触れられたので、本記事では学んだ中で重要に感じた部分を中心に備忘録として整理していくことにしました。
一つだけ気になることが、フロントエンドの内容が体系的にまとめられた学習リソースで勉強せず、chatGPTでの壁打ちとネット上の記事を読み漁っていくという方法で勉強していったため、断片的かつ偏ったインプット内容である可能性があることをご了承ください。そのため今後は一度体系的に学ぶ機会をつくり、学び次第本記事を随時更新していく予定です。
主にNext.js(App Router)のディレクトリ構成、サーバーコンポーネントとクライアントコンポーネントの使い分け、レンダリング手法の意味について整理してあるため、この3つの概要を把握したい方の参考になれば幸いです。
ディレクトリ構成
/app
以下の内容は推奨されているというだけであり、プロジェクトによってディレクトリ構成のルールは変わってくる。
-
page.tsx
があるフォルダのフォルダ名が全てルーティングになる。 - フォルダ名を
[フォルダ名]
のようにしてその配下にpage.tsx
を置くことで、動的に増減するコンポーネントの表示を制御できる。- 例:アプリの仕様として、サービス運用中にカテゴリー数が増減しホームページに表示されるカテゴリーの数(=アプリ内に存在するURLの数)が変動する場合、App routerでは
[フォルダ]
というフォルダを用意して実現する。(ECサイトのようなサービスで、商品毎にURLを作るやつもこれ使う)
- 例:アプリの仕様として、サービス運用中にカテゴリー数が増減しホームページに表示されるカテゴリーの数(=アプリ内に存在するURLの数)が変動する場合、App routerでは
- layout.tsxで画面全体の構成を決める(ヘッダー、ボディ、フッター)
- 基本的にデータの取得はapp配下のpage.tsxで行うことが推奨されている(component配下では行わず、Propsを受け取るだけ)。
- アプリ全体で使うコンポーネントはapp配下に作成することが推奨されている(/components、/hooks、/typees等)。
- /api配下の
route.ts
ではapiも書ける。
ルート直下
- 特定のページでしか使わないようなコンポーネントはルートディレクトリ直下に置くことが推奨されている。
サーバーコンポーネントとクライアントコンポーネント(関係性)
- Next.jsではサーバーコンポーネントとクライアントコンポーネントの2種類が存在し、基本的にサーバーコンポーネントで実装することが推奨される。
- 実装の流れとしては、可能な限りサーバーコンポーネント主体に実装していき、「ローディング中」やリロードなしで画面の情報を変化させるなどといった状態管理や副作用系などの実装が必要になったら、適宜クライアントコンポーネントとして切り出すのが推奨される。このようにすれば、クライアントコンポーネントが最小限に押されられ、更にクライアントコンポーネントとサーバーコンポーネントの分離が明確なので開発時にクライアントに公開される範囲も把握しやすい。
-
メリット:
- JSバンドルサイズが最小限に抑えられ、クライアント側での読み込み速度を速くできる。
- なるべくサーバーコンポーネントでJSバンドルを生成した方がクローリング時にインデックスにひっかかるのが速くなるのでSEOに強い。
-
デメリット:
- サーバーコンポーネントに詰め込みすぎると当然サーバーへの負荷がかかる。そのためキャッシュ戦略等もしっかり考えないといけない。
-
メリット:
-
`use client`
でcsrのクライアントコンポーネントになる。デフォルトではssrのサーバーコンポーネントになっている(明示的に`use server`
と書くことも可能)。 - データ取得はコンテナ層、データの表示はプレゼンテーション層という風に責任を分けるのが推奨されている。
サーバーコンポーネント
- ステートフルな処理(状態管理や副作用系)はできない。
- クライアントに公開されないので、安全にデータフェッチできる。
クライアントコンポーネント
- ステートフルな処理(状態管理や副作用系)ができる
- クライアントコンポーネントに定義されクライアントに露出しているデータフェッチの処理はapiを叩けば誰でも使えてしまうので、クライアントコンポーネントではユーザー全員が見ても良いようなデータのフェッチのみにとどめる。ただし環境変数やAPIキーのような機密情報を伴うデータフェッチをクライアントコンポーネントでしなければいけない場合は、必ず
Server Action
かサーバー経由のAPI(fetch + API route)
で認証認可もして取得するようにする。- セキュリティに関しては基本的に
Server Action
を使えば問題なさそうだが、Server Action
はGETメソッドは非対応のようなので取得系は後者のやり方(app/apiに定義されているエンドポイントにリクエストを投げる)を使う。 - Next.js(App router)では
app/api/〇〇
がそのままエンドポイントになる。 - サーバーコンポーネントなら
await fetch
でOK。
- セキュリティに関しては基本的に
サーバーコンポーネントとクライアントコンポーネントの関係、コンテナ層とプレゼンテーション層の関係の違い
この2種類のペアは似ているようで違うので、ここの違いを明確にしておきたい。
まずはそれぞれの概要を整理。
サーバーコンポーネント:
・サーバー上でレンダリングされ、主にデータフェッチを担当する。
・動的な動作や状態管理のないUI描画であればサーバーコンポーネントで実装できる。
クライアントコンポーネント:
・動的な動作や状態管理が必要な場合に使い、ブラウザ上で実行される。
------------------------------------------------------------------
コンテナ層:
・ビジネスロジック、状態管理、データフェッチを担当する。
プレゼンテーション層:
・UIの描画やレイアウトを担当(データはコンテナ層からpropsとして受け取る)
ここからわかること
Q. コンテナ層とプレゼンテーション層全てをクライアントコンポーネントで実装することはできる?
A. 部分的にYes(ただし非推奨)
コンテナ層のビジネスロジックとデータフェッチは基本的にサーバーコンポーネントとして実装されることが多いが、必ずしもそうする必要はなく極端な話コンテナ層の全てをクライアントコンポーネントで実装することもできる。ただし、全てがJSバンドルに含まれてしまうため、ssr、ssg、isrを使ったキャッシュ戦略や情報の隠蔽等はできず、パフォーマンスやセキュリティ的なメリットは享受できないため部分的にYes。
Q. プレゼンテーション層の全ての機能はサーバーコンポーネントで実装できる?
A. No
プレゼンテーション層にはクライアントコンポーネントのみが扱える状態管理が存在する場合があるため、状態管理が存在するプロジェクトでは全てをサーバーコンポーネントで実装することはできない。
つまり、サーバーコンポーネントとクライアントコンポーネントの関係、コンテナ層とプレゼンテーション層の関係は1対1的なものではなく、「互いに入り混じっているもの」というのが今の認識。
改めて両者の違いを整理すると、
サーバーコンポーネントとクライアントコンポーネントの関係:
サーバー側かブラウザ側かという実行環境の関係と、処理の得意不得意と可能か不可能かの関係というような「技術的な区分」の話。
コンテナ層とプレゼンテーション層の関係の違い:
担当する分野(処理か描画か)やデータを整理し渡す側か渡されてただ描画する側かという、「どういう風に責任を分離するか」という話。
コンテナ層とプレゼンテーション層全てをサーバーコンポーネントで実装することもできるし(状態がなければ)、全てをクライアントコンポーネントで実装することもできる(パフォーマンスとセキュリティ度外視するなら)。
一方で、コンテナ層とプレゼンテーション層は↑のように互いに一部を補完するということはなく、役割としてそれぞれ独立している。
レンダリング手法
SSR
ウェブサイトへのアクセス時(ユーザーがアクセス、もしくはクローラーがクローリングする時)に、サーバー側でその時点での最新データでHTMLが生成される。
動的コンテンツの提供が重要な時に使う。
メリット:
- ユーザーは常に最新の情報を得られる。
- SEOに強い。ただし、アクセス時にHTMLを生成するためSSGよりは弱い。
デメリット:
- リクエスト毎にレンダリングするのでサーバー負荷高い。トラフィックが多いサービスだとレスポンスが遅くなることがある。
SSG
ウェブサイトへのアクセス時(ユーザーがアクセスする時)、ビルド時(デプロイ時)にサーバー側でHTMLが生成される。
静的コンテンツで、高速性を重視するコンテンツに使う。(静的コンテンツならできるだけSSGにしてSEO有利にする)。
メリット:
- 事前に静的なHTMLが生成されているので、高速(UX良い)。サーバー負荷低い。
デメリット:
- 内容を変更したら再ビルドしないといけない。そのため頻繁に更新のあるコンテンツには不向き。
ISR
ウェブサイトへのアクセス時(ユーザーがアクセスする時)、ビルド時(デプロイ時)にサーバー側でHTMLが生成される。
revalidate: 60
のようにして、初回HTML生成時から60秒以降にリクエストが合った場合、一度古い期限切れのキャッシュを返し、同時にバックグラウンドで新しいHTMLを再生成する。次回以降のアクセスでその更新されたHTMLが表示される。(アクセスがなければHTMLの再生成はしない)。
メリット:
- SSGのメリットを受け継ぎつつ、再ビルドが不要で、ページ単位での動的な更新ができる。
デメリット: - キャッシュの期限内に発生したデータの更新は、キャッシュ期限が一度切れないとユーザーは最新情報にアクセスすることができない。
CSR
初回レンダリング時(Javascript実行後のレンダリング時)に、クライアント側でHTMLが生成される。
メリット:
- 初回ロード時のサーバー負荷低い。
- UIの動的な更新が簡単。
デメリット:
- クライアント側でJavascriptの実行が必要なのでレンダリングまでラグが起きる。
- SEOに弱い(Javascript実行に非対応のクローラーや処理が遅いクローラーだと、インデックス化に不利になることがある)。
参考
Next.jsのディレクトリ構成のベストプラクティスを知っていますか?
知らないとあぶない?Next.jsセキュリティの話
【完全保存版】Next.js App Routerのベストプラクティスを解説
Next.jsの考え方
今後
今回のチーム開発では、目標がとりあえず動く物を作ろう的な感じだったためコンテナ層とプレゼンテーション層の責任分離もできておらず、煩雑なディレクトリ構成だったりテスト手法や型安全な記述方法などその他の勉強なども頭から抜けていました。その代わりにどういうデータフェッチをしたらセキュリティ的に危険なのかや、そもそものフロントエンドの開発の基礎には触れられたのでとても良い経験になりました。開発したアプリ自体は今後も継続開発をしていく予定なので、これからは今回学びはしたものの反映できなかった部分やUIのデコりの部分、キャッシュ戦略なども考えていきたいです。
また、レンダリングの手法に関しても違いはなんとなく理解できたものの、今回の開発ではサービス内で各ページの特性に合わせてレンダリング手法を使い分けるということもしっかりできていなかったので、今後はそれも意識できたら良いかなと思いました。
いまのところ記事が構成的にも見た目的にも読みづらいので読みやすい記事を書けるようにしたいです。