Next.js の始め方
トムは
create-react-appを使ってブログサイトを構築しました。公開後、彼は最初の画面の読み込みに3秒かかっていること、検索エンジンがコンテンツをクロールできないこと、そして投稿を更新するたびに手動でCDNのキャッシュを更新しなければならないことに気づきました。彼は、クライアントサイドレンダリングだけでは不十分であり、サーバーサイドレンダリング、静的生成、自動最適化が可能なフレームワークが必要だと悟りました。そこで登場したのがNext.jsでした。
Vercel が開発・保守を行っている Next.js は、現在最も人気のあるフルスタック React フレームワークです。これは、純粋なクライアントサイド UI ライブラリである React では対応できない中核的な課題、すなわちルーティングシステム、サーバーサイドレンダリング、静的サイト生成、API ルーティング、画像の最適化、フォントの最適化、ミドルウェアなどに対応しています。ブログの構築、ECサイトの開発、SaaSアプリケーション、あるいはエンタープライズレベルのアプリケーションのいずれであっても、Next.jsは導入直後からベストプラクティスを提供します。
このレッスンでは、まずNext.jsの中核となる概念とアーキテクチャ設計について解説します。4つのレンダリングモードの違い、App Routerの仕組み、そしてサーバーサイドコンポーネントとクライアントサイドコンポーネントの役割分担について学びます。これらの基礎概念は、データフェッチ、デプロイ、そして包括的な実践プロジェクトへの取り組みをさらに深めるための土台となります。Tomによる実例に基づく移行ケーススタディを通じて、CRAからNext.jsへの完全な技術的な移行プロセスを明確に理解できるようになります。
1. 学習内容
- CSR、SSR、SSG、ISR:4つのレンダリングモードの違いと選択基準
- Next.js アプリ・ルーターのファイルベースのルーティング機構
- ネストされたレイアウトシステムの設計と再利用
- サーバーコンポーネントとクライアントコンポーネント間の役割分担と連携
- Pages Router および App Router の移行パス
- ダイナミックルート、キャッチオールルート、およびAPIルートの設定方法
- 「読み込み中」、「エラー」、「見つかりません」などのルートステータスコンポーネントの使用
- CRAプロジェクトからNext.jsへの移行に関する実践ガイド
- レンダリングモード選択の決定木:ページの種類やデータの特性に基づいて最適なソリューションを選択する
- パフォーマンス指標:さまざまなレンダリングモードにおけるファーストスクリーン読み込み時間、SEOスコア、およびTTFB(Time to First Byte)の比較
2. 概念図
Next.js を学習する中で、トムはリクエストの処理経路を理解するためにフローチャートを作成しました。ユーザーからのリクエストが届くと、Next.js はそのページの設定(静的、動的、または増分)に基づいて異なるレンダリング経路を選択し、最終的に HTML を返して、ブラウザ側での操作を可能にします。
この図は、4つの重要な決定ポイントを示しています。まず、ページの種類(静的/動的/増分/クライアントサイド)を決定します。次に、それに対応するレンダリングエンジンを選択します。その後、HTMLを生成してブラウザに返します。そして最後に、ハイドレーション処理を用いて、ページ上のクライアントコンポーネントがインタラクティブになるようにします。
flowchart LR
A[User Request] --> B{Next.js Route Matching}
B -->|Static Page| C[SSG<br/>Generated during build]
B -->|Dynamic Pages| D[SSR<br/>Render on Request]
B -->|Incremental Update| E[ISR<br/>Re-verify on Demand]
B -->|Client Interaction| F[CSR<br/>Browser Execution]
C --> G[Back HTML]
D --> G
E --> G
F --> G
G --> H[Hydration<br/>Activate Interaction]
H --> I[Client Component<br/>Run JS]
3. 実際の事例
トムのブログは当初、create-react-appを使用して開発されており、すべてのページがクライアント側でレンダリングされていました。ユーザーが記事を開くと、まず空のHTMLテンプレートをダウンロードし、JavaScriptの読み込みが完了するのを待ってから、APIからデータを取得してコンテンツをレンダリングしていました。これはコンテンツ中心のウェブサイトにとっては大問題でした。検索エンジンのクローラーには空白のページしか表示されず、コンテンツの初回表示までの時間(TFC)は最大で3秒にも及んでいました。
調査を行った結果、彼は「ハイブリッドレンダリング」というアプローチが必要であると判断しました。具体的には、ブログ記事は静的に生成され(ビルドプロセス中にHTMLが生成され、CDNを介して配信される)、ユーザープロフィールページはサーバーサイドでレンダリングされ(リクエストごとに最新のデータが生成される)、コメント欄はクライアントサイドでレンダリングされる(インタラクションはブラウザ内で処理される)というものです。Next.jsは、これらすべてを実現できるフレームワークです。
彼は1週間かけて、自身のブログをCRAからNext.jsへ移行し、具体的には以下の3つの作業を行いました。まず、投稿ページをSSGに切り替え、ビルドプロセス中にgenerateStaticParamsを使用してすべての投稿ページのHTMLを生成しました。Vercelへのデプロイ後、CDNがプリレンダリングされたページを直接配信することで、初回コンテンツ表示時間(TFC)を0.3秒に短縮しました。次に、ユーザープロフィールページをSSRに切り替え、リクエストごとにデータベースから最新のデータを取得するようにしました。3つ目は、コメントセクションをクライアントサイドコンポーネントを使用して実装し、ブラウザ側でインタラクティブなJavaScriptのみを読み込むようにしました。移行後、Google LighthouseにおけるサイトのSEOスコアは45から98へと向上し、ファーストコンテンツ表示時間(TFC)は90%短縮されました。
トムも移行の過程でかなりの数の落とし穴に遭遇しました。例えば、当初はpages/ディレクトリ内のPages Routerを使用していましたが、後にApp Routerの方がうまく機能することがわかったため、それへの移行に余分な時間を費やしました。彼は、二度目の移行を避けるために、新規プロジェクトでは最初からApp Routerを直接使用することを推奨しています。また、useRouterやusePathnameはサーバーコンポーネント内で直接使用できず、代わりにこのルーティングロジックをクライアントコンポーネントに抽出する必要があることも発見しました。これらの経験を通じて、彼はNext.jsの設計哲学である「サーバーファースト、クライアントオンデマンド」を深く理解するようになりました。
(1) レンダリングモードの比較
Next.js には 4 つのレンダリングモードがあり、それぞれが異なるユースケースに合わせて設計されています。適切なアーキテクチャを選択するには、これらの違いを理解することが重要です。
CSR(クライアントサイドレンダリング):ブラウザが空のHTMLシェルをダウンロードし、JavaScriptを実行してコンテンツをレンダリングします。トムのブログは当初、このモデルを採用していました。メリットは、スムーズなインタラクティブ性とサーバーへの負荷軽減です。デメリットは、ファーストスクリーンの読み込みが遅く、SEO対策が不十分であることです。ダッシュボードや管理用バックエンドなど、ユーザーログインを必要とするインタラクティブなアプリケーションに適しています。
SSR(サーバーサイドレンダリング):ユーザーがリクエストを行うたびに、サーバーが完全なHTMLを動的に生成して返します。トムはこの手法を用いて自身のブログのSEO上の問題を解決しましたが、各リクエストではサーバーによるレンダリングが完了するまでレスポンスが返されないため、サーバーに大きな負荷がかかります。ニュースサイトやECサイトの商品ページなど、リアルタイムのデータが必要なページに適しています。
SSG(静的サイト生成):ビルド段階で全ページのHTMLファイルを生成し、CDNへのデプロイ後は、ユーザーが静的ファイルに直接アクセスします。これにより、最速の読み込み速度と最高のSEO効果が得られますが、コンテンツを更新するには完全な再ビルドが必要となります。トムのブログ記事ページはこのモデルの好例です。記事が一度公開されると、その内容は変更されません。ブログ、ドキュメントサイト、マーケティングページに適しています。
ISR(Incremental Static Generation):SSGの拡張版であり、ビルド後にサイト全体の再ビルドを行うことなく、特定のページをオンデマンドで再検証・更新することができます。例えば、トムの商品カタログページが60秒ごとに再検証されるように設定されている場合、新商品は追加されてから60秒以内に表示されます。これは、ECサイトの商品ページや、コンテンツが頻繁に更新されるサイトに最適です。
選択戦略は、次のように要約できます。コンテンツを重視するシナリオではSSGを、リアルタイムデータにはSSRを、インタラクションが頻繁に行われるシナリオではCSRを、そしてサイト全体を再構築したくない頻繁な更新が行われるシナリオではISRを使用します。
レンダリングモードのパフォーマンス比較表
| 指標 | SSG | ISR | SSR | CSR |
|---|---|---|---|---|
| 初回画面読み込み速度 | 最速 | 速い | 平均 | 遅い |
| SEO対応度 | 高 | 高 | 高 | 低 |
| データの最新性 | ビルド時に固定 | オンデマンドで更新 | リクエストごとに最新のデータ | ブラウザの読み込み完了後 |
| サーバー負荷 | なし | 低 | 高 | 低 |
| ユースケース | ブログ・ドキュメント | Eコマース・ニュース | カスタムページ | バックエンド管理 |
| 一般的なTTFB | <50ms | <100ms | 200~500ms | 200~500ms |
(1) ▶ サンプル:Next.js での 4 つのレンダリングモードの実装
以下のコードは、トムのブログにおける3つのレンダリングモードの実装例です。なお、generateStaticParamsはビルドプロセス中に呼び出され、考えられるすべてのパラメータの組み合わせを返します。Next.jsはこの結果を基に、すべてのページの静的HTMLを生成します。一方、dynamic = 'force-dynamic'は、リクエストがあるたびにページの再レンダリングを強制します。
// === Static Generation SSG(Default behavior) ===
// app/blog/[slug]/page.tsx
// Automatically scan all articles during the build process slug,Generate the corresponding HTML Page
// After generation CDN Return static files directly,No server-side rendering required
export async function generateStaticParams() {
// Called during build:Get a list of all articles
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// Back slug Array,Next.js It will do this for each slug Generate a page
return posts.map((post: any) => ({ slug: post.slug }))
}
async function BlogPost({ params }: { params: { slug: string } }) {
// Each page retrieves data independently when it is rendered.
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
export default BlogPost
// === Server-Side Rendering SSR(Dynamic Rendering) ===
// app/dashboard/page.tsx
// Every user request starts from API Get the latest data,Ensure the data is up to date
export const dynamic = 'force-dynamic' // Force the static cache to close,Leaving every time SSR
async function DashboardPage() {
// This will be executed with every request fetch
const stats = await fetch('https://api.example.com/dashboard/stats').then(r => r.json())
return (
<div>
<h1>Real-Time Dashboard</h1>
<p>Current Online Users:{stats.onlineUsers}</p>
<p>Today's Orders:{stats.todayOrders}</p>
<p>Income for This Month:${stats.monthlyRevenue}</p>
</div>
)
}
export default DashboardPage
// === Incremental Static Generation ISR ===
// app/products/[id]/page.tsx
// Generate static pages during the build process,But every 60 The first visit after X seconds will trigger a regenerate.
async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 } // ISR:60 Please try again in a few seconds.
}).then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>Price:${product.price}</p>
<p>Inventory:{product.stock}</p>
<p>Description:{product.description}</p>
</div>
)
}
export default ProductPage
トムは自身のブログで、SSGとISRを組み合わせたハイブリッドなアプローチを採用しました。具体的には、トップページの読み込みを高速化するために記事ページにはSSGを使用し、価格や在庫情報を60秒ごとに更新する商品一覧ページにはISRを使用しています。このアプローチにより、SEOスコアを良好に保ちつつ、データの最新性も維持することが可能になっています。
(2) アプリルーターによるファイルルーティング
Next.js 13以降では、ファイルシステムに基づいてルートを自動的に生成する「App Router」が導入されました。app/ という名前のフォルダとその中に page.tsx という名前のファイルを作成するだけで、Next.js が自動的に対応する URL パスをマッピングします。これにより、ルート設定ファイルを手動で管理する必要がなくなりました。ディレクトリ構造そのものがルート構造となるのです。
主なファイル形式
page.tsx— ルートに対応する UI ページを定義します。フォルダごとにこのタイプのファイルは 1 つしか許可されませんlayout.tsx— このルート区間およびそのすべてのサブルートに共通するレイアウトを定義します。ネスト構造に対応しています。loading.tsx— React Suspense に基づいて、ルートセグメントの読み込み中に UI を表示error.tsx— ルーティングセグメントエラーが発生した際に表示されるUI。error.tsx内のリセット機能を使用して再試行可能not-found.tsx— 404ページ。各ルート区間単位まで詳細に表示route.ts— APIエンドポイントを定義し、GET、POST、PUT、DELETEなどのメソッドをサポートしています
動的ルーティングと高度なモード
動的なルートを作成するには、[param] という構文を使用します。たとえば、app/blog/[slug]/page.tsx は /blog/hello-world に一致します。多階層のパスに一致させるには、包括的な構文 [...param] を使用します。たとえば、app/docs/[...slug]/page.tsx は /docs/guide/getting-started/installation に一致します。
レイアウトファイルは、親レイアウトが子ページのコンテンツを囲むような、深くネストされたレイアウトに対応しています。ナビゲーションバー、サイドバー、フッターなどの共通セクションは一度記述するだけで済みます。Next.js がレイアウトの永続的な状態を自動的に管理します。ページを切り替える際、レイアウトは再レンダリングされず、子ページのみが更新されます。
(2) ▶ サンプル:ブログルーティング構造の全体像
これは、トムのブログの App Router の完全なディレクトリ構造を示しています。各フォルダ内の特殊なファイル名(page、layout、loading、error、not-found)は Next.js の規約に従っていることに注意してください。このフレームワークはこれらのファイルを自動的に認識し、特定のルーティング動作を割り当てます。dashboard/layout.tsxはルートディレクトリのlayout.tsx内にネストされており、2階層のレイアウトを形成しています。最外層はグローバルナビゲーションとフッターで構成され、内側の層にはダッシュボード専用のサイドバーが含まれています。
app/
├── page.tsx # → / Home
├── layout.tsx # → Global Layout(Navigation + Footer)
├── loading.tsx # → Global Loading State(Skeleton screen during page transitions)
├── error.tsx # → Global Error Page(500 A Flawed Fallback UI)
├── not-found.tsx # → 404 Page
├── about/
│ └── page.tsx # → /about About This Page
├── blog/
│ ├── page.tsx # → /blog Article List Page
│ └── [slug]/ # → Dynamic routing: matches /blog/any-value
│ ├── page.tsx # → /blog/hello-world Article Details
│ └── loading.tsx # → Custom Loading State for Article Detail Pages
├── dashboard/
│ ├── page.tsx # → /dashboard Dashboard Overview
│ ├── layout.tsx # → Dashboard-Exclusive Layout(With sidebar navigation)
│ └── settings/
│ └── page.tsx # → /dashboard/settings Settings Page
└── api/
└── hello/
└── route.ts # → /api/hello API Endpoint
ルートマッピング参照表
| ファイルパス | 対応するURL | 説明 |
|---|---|---|
app/page.tsx |
/ |
ホーム |
app/about/page.tsx |
/about |
静的ルート |
app/blog/[slug]/page.tsx |
/blog/:slug |
ダイナミックルーティング |
app/blog/[slug]/loading.tsx |
/blog/:slug |
同一ルート上の状態の読み込み |
app/dashboard/layout.tsx |
/dashboard/* |
ネストされたレイアウト |
app/api/hello/route.ts |
/api/hello |
APIエンドポイント |
// app/layout.tsx - Global Layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav>
</header>
<main>{children}</main>
<footer>© 2026 Tom 's blog. All rights reserved.</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx - Dashboard-Exclusive Layout(Nested within the global layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: 240, background: '#f5f5f5', padding: 16 }}>
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<section style={{ flex: 1, padding: 16 }}>{children}</section>
</div>
)
}
// app/blog/[slug]/loading.tsx - Article Details Loading...
export default function Loading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4, marginBottom: 16 }} />
<div style={{ height: 16, width: '30%', background: '#eee', borderRadius: 4, marginBottom: 24 }} />
<div style={{ height: 200, background: '#eee', borderRadius: 4 }} />
</div>
)
}
(3) サーバーコンポーネントとクライアントコンポーネント
Next.js の革新的な設計の一つが「サーバーコンポーネント」です。これは、React エコシステムにおいて初めて、コンポーネントがサーバーサイドとクライアントサイドという 2 つの実行環境に分割されたことを意味します。デフォルトでは、App Router 内のすべてのコンポーネントはサーバーコンポーネントとなります。これらはサーバー上で実行され、データベースやファイルシステム、機密性の高い環境変数に直接アクセスできるほか、最も重要な点として、ブラウザに JavaScript を送信することはありません。
インタラクティブな機能(ボタンのクリック、テキストの入力、useState や useEffect の使用、ウィンドウイベントの監視など)が必要な場合、サーバーサイドコンポーネントだけでは不十分です。この場合、ファイルの先頭に 'use client' 宣言を追加する必要があります。これにより、Next.js はそのコンポーネントおよびすべての子コンポーネントを「クライアントコンポーネント」としてマークし、ブラウザ側で実行されるようにバンドルします。
サーバーコンポーネントの特徴
async/awaitを使用すれば、useEffect や SWR を使わずに直接データを取得できます- データベース、ファイルシステム、およびバックエンドサービスへの直接アクセス
- パッケージサイズを小さくするため、ブラウザにJavaScriptを送信しないでください
- 環境変数への安全なアクセス(NEXT_PUBLIC_ で始まらないもの)
- useState、useEffect、onClick などのブラウザ API は使用できません
クライアントコンポーネントの特徴
- ブラウザとの完全な相互運用性(状態、イベント、DOM API)
- サーバーコンポーネントと自由に組み合わせることができます
- SSR(初期HTMLのサーバーサイドレンダリング)を引き続きサポートしています
- ファイルが大きければ大きいほど、ブラウザに送信されるJavaScriptの量も増える
ベストプラクティスモデル
このアーキテクチャは階層構造を採用しており、「サーバーコンポーネント」がコンテナとして機能し、「クライアントコンポーネント」がリーフとして機能します。外側のサーバーコンポーネントはデータの取得とレイアウトの処理を担当し、内側のクライアントコンポーネントはインタラクションを必要とするウィジェットの処理のみを担当します。このようにして、ロジックやデータ取得の大部分はサーバー側で処理され、ブラウザでは必要最小限のJavaScriptのみが実行されます。
(3) ▶ サンプル:サーバーコンポーネントとクライアントコンポーネント間の最適な役割分担
以下のコードは、トムのブログで説明されている「サーバー側でのデータ取得、ブラウザ側での操作」という階層モデルを示しています。PostsPageはサーバーコンポーネントであり、サーバー上でデータを取得し、HTMLとしてレンダリングして、ブラウザに直接送信します。ユーザーは、JavaScriptの読み込みを待つことなく、完成したページを閲覧できます。PostListはクライアントコンポーネントであり、サーバーで準備されたデータを受け取り、ブラウザ側での検索、フィルタリング、削除といった操作のみを担当します。なお、'use client'は、操作を必要とする「リーフコンポーネント」にのみ適用され、ページ全体には適用されない点に注意してください。
// app/posts/page.tsx - Server Component(Default)
// This component runs on the server.,Do not send any JS Go to the browser
import PostList from './PostList'
async function PostsPage() {
// Directly on the server fetch,Data is being compiled HTML It was already ready at that time
// Users see the full page,None loading Status
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
<h1>All Articles</h1>
<PostList posts={posts} />
</div>
)
}
export default PostsPage
// app/posts/PostList.tsx - Annotation 'use client' Become Client Component
// Only this file will be sent JS Go to the browser,The parent component that contains it(Server Component)No
'use client'
import { useState } from 'react'
interface Post {
id: number
title: string
body: string
}
function PostList({ posts: initialPosts }: { posts: Post[] }) {
// useState Only at 'use client' Available in the component
const [posts, setPosts] = useState(initialPosts)
const [search, setSearch] = useState('')
const filtered = posts.filter(p =>
p.title.toLowerCase().includes(search.toLowerCase())
)
function handleDelete(id: number) {
setPosts(prev => prev.filter(p => p.id !== id))
}
return (
<div>
<input
type="text"
placeholder="Search Articles..."
value={search}
onChange={e => setSearch(e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 16, border: '1px solid #ddd', borderRadius: 4 }}
/>
{filtered.map(post => (
<div key={post.id} style={{
border: '1px solid #ddd', padding: 16, marginBottom: 8, borderRadius: 8,
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<div>
<h2 style={{ margin: 0 }}>{post.title}</h2>
<p style={{ margin: '8px 0', color: '#666' }}>{post.body}</p>
</div>
<button
onClick={() => handleDelete(post.id)}
style={{ padding: '4px 12px', background: '#ff4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
Delete
</button>
</div>
))}
{filtered.length === 0 && <p>No matching articles were found.</p>}
</div>
)
}
export default PostList
// pages/old-posts.js - Pages Router Pattern(Compatibility with Legacy Projects)
// If you are maintaining Next.js 12 or earlier projects,Pages Router Usage getStaticProps/getServerSideProps
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return { props: { posts }, revalidate: 60 }
}
export default function OldPostsPage({ posts }: { posts: Post[] }) {
return (
<div>
<h1>Pages Router Pattern</h1>
{posts.map(p => <p key={p.id}>{p.title}</p>)}
</div>
)
}
サーバーコンポーネントとクライアントコンポーネントの制限
| 機能 | サーバーコンポーネント | クライアントコンポーネント |
|---|---|---|
| useState/useEffect を使用する | ❌ | ✅ |
| onClick/onChange を使用 | ❌ | ✅ |
| データベースへの直接アクセス | ✅ | ❌ |
| 環境変数へのアクセス(接頭辞なし) | ✅ | ❌ |
| JSをブラウザに送信 | ❌ | ✅ |
| async/await を使用する | ✅ | ❌ |
| useRouter/usePathname を使用 | ❌ | ✅ |
| requestAnimationFrame を使用する | ❌ | ✅ |
フローチャート:サーバーコンポーネントか、クライアントコンポーネントか?
Components require interaction(Click、Input、Scrolling, etc.)?
├── is → Required useState/useEffect?
│ ├── Yes → add 'use client' → Client Component
│ └── No → only accept props for rendering?
│ ├── is → Retain Server Component
│ └── No → add 'use client'
└── No → keep Server Component
4. トムの移行ロードマップ
これで、Next.js の主要な概念をすべて解説しました。次に、これらの知識をまとめて、トムが CRA ブログを Next.js に移行した手順を段階を追って見ていきましょう。このプロセスは、ご自身のプロジェクトで Next.js を導入する際の標準的なアプローチとしても役立ちます。
トムは、ブログをCRAからNext.jsへ移行する一連のプロセスを5つのステップに分解しています。各ステップは、このレッスンで取り上げられる重要な概念に対応しています:
- ステップ1:プロジェクトの初期化 —
create-next-appを使用して新しいプロジェクトを作成し、「App Router」を選択して、古いsrc/のコードをapp/ディレクトリに移行します。このステップの重要なポイントは、「App Router」と「Pages Router」のディレクトリ構成の違いを理解することです。 - ステップ 2: ルーティングのリファクタリング — CRA の
react-router-domルーティング設定を、App Router のファイルベースのルーティングに置き換えます。以前の手動によるルーティング設定 (<Routes><Route path="..." /></Routes>) は、直感的なフォルダ構造へと変更されました。動的ルーティングでは、:param構文の代わりに[param]構文が使用されるようになりました。 - ステップ 3: ページの種類に応じたレンダリングモードの選択 — ページの種類に応じて適切なレンダリング方式を選択します。ブログ記事には SSG(事前生成には
generateStaticParamsを使用)、ユーザープロフィールには SSR(dynamic = 'force-dynamic'を使用)、コメント欄には CSR + クライアントコンポーネント('use client'を使用)を使用します。 - ステップ4:レイアウトの統合 — 以前はすべてのページに重複して記述されていたナビゲーションバーとフッターのコードをグローバルレイアウトに抽出することで、コードの重複を60%削減しました。ダッシュボード領域ではネストされたレイアウトを使用してサイドバーナビゲーションを追加し、レイアウトにおける関心の分離を実現しました。
- ステップ 5: 最適化とテスト —
loading.tsxを追加して読み込み中のスケルトン画面を実装し、error.tsxを追加してエラー時のフォールバック UI を実装します。Chrome Lighthouse を使用して SEO およびパフォーマンスの指標を確認し、移行後の SEO スコアが 90 以上になることを確認します。
(1) ▶ サンプル: App Router の loading.tsx と error.tsx
// app/dashboard/loading.tsx - Automatically Displayed Loading Status
export default function DashboardLoading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: 200, background: '#f0f0f0', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{[1, 2, 3].map(i => (
<div key={i} style={{
height: 120, background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%', borderRadius: 8, animation: 'shimmer 1.5s infinite',
}} />
))}
</div>
</div>
)
}
// app/dashboard/error.tsx - Automatically Displayed Error Status
'use client'
export default function DashboardError({ error, reset }) {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<h2>Something went wrong!</h2>
<p style={{ color: '#999' }}>{error.message}</p>
<button onClick={reset} style={{ padding: '8px 24px', cursor: 'pointer' }}>Try again</button>
</div>
)
}
// app/dashboard/page.tsx - It will automatically use the one above loading and error
async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard').then(r => r.json())
return (
<div>
<h1>Dashboard</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{data.stats.map(stat => (
<div key={stat.label} style={{ padding: 16, border: '1px solid #eee', borderRadius: 8 }}>
<p style={{ color: '#999', margin: 0 }}>{stat.label}</p>
<p style={{ fontSize: 24, fontWeight: 'bold', margin: '4px 0 0' }}>{stat.value}</p>
</div>
))}
</div>
</div>
)
}
export default DashboardPage
(1) ▶ サンプル:ネストされたレイアウトとテンプレート
// app/layout.tsx - Root Layout(Must have html and body)
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body style={{ margin: 0, fontFamily: 'sans-serif' }}>
{children}
</body>
</html>
)
}
// app/(dashboard)/layout.tsx - Dashboard Layout(Route groups have no effect URL)
import Sidebar from '@/components/Sidebar'
export default function DashboardLayout({ children }) {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar />
<main style={{ flex: 1, padding: 24 }}>
{children}
</main>
</div>
)
}
// app/(auth)/layout.tsx - Authentication Page Layout(Centered Card)
export default function AuthLayout({ children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f5f5f5' }}>
<div style={{ background: 'white', padding: 32, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.1)', width: 400 }}>
{children}
</div>
</div>
)
}
// app/(auth)/login/page.tsx - Login Page
function LoginPage() {
return (
<>
<h2 style={{ textAlign: 'center' }}>Sign In</h2>
<form style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input type="email" placeholder="Email" style={{ padding: 8, borderRadius: 4 }} />
<input type="password" placeholder="Password" style={{ padding: 8, borderRadius: 4 }} />
<button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
Sign In
</button>
</form>
</>
)
}
export default LoginPage
❓ よくある質問
useState、useEffect、onClick、またはブラウザ API を使用する必要がある場合にのみ、'use client' を追加してください。一般的なパターンとして、サーバーコンポーネントがデータを取得し、それを内部のクライアントコンポーネントに渡してインタラクションを処理させることが挙げられます。こうすることで、データ取得はサーバー側で処理され、インタラクションロジックはブラウザ側で実行されるため、両方の長所を活かした最適な構成となります。初心者にありがちな間違いとして、ページ全体に 'use client' を追加してしまうことが挙げられます。これは、最も小さなインタラクティブなリーフノードにのみ適用すべきです。インタラクションも状態も必要としないコンポーネントが見つかった場合は、'use client' を追加しないでください。pages/ディレクトリを使用し、ファイル名に基づいてルーティングをマッピングします。App Routerは、Next.js 13で導入された新しいルーティングシステムです。app/ディレクトリを使用し、ネストされたレイアウト、サーバーコンポーネント、ストリーミングレンダリングなどの新機能をサポートしています。新規プロジェクトでは、最初から App Router を使用することを推奨します。既存のプロジェクトについては、段階的に移行することができます。Pages Router の getStaticProps/getServerSideProps 構造は、App Router では async component + fetch に置き換えられています。2つのルーティングシステムは共存可能であり、移行中は Pages Router から App Router へページを段階的に移行することができます。next build && next start を使用すれば、任意の Node.js サーバーにデプロイできますし、Docker を使用してコンテナ化されたデプロイを行うことも可能です。Netlify、AWS Amplify、Cloudflare Pages も Next.js のデプロイに対応しています。ただし、ISR やミドルウェアなどの高度な機能を使用する場合は、Vercel が最高の互換性を提供します。pages/からapp/へページを段階的に移行し、移行するたびに各ページを確認していけばよいので、一度にすべてを移行する必要はありません。移行の際は、以下の点に注意してください。getStaticProps をサーバーコンポーネント内で直接 fetch に変更し、レイアウトを app/ ディレクトリの下に作成した layout.tsx に移動し、ロジックを pages/_app.tsx からルートディレクトリの layout.tsx へ移行してください。まずは最もシンプルなページから始め、経験を積んでからより複雑なページに取り組むことをお勧めします。params プロパティを介して、クエリ文字列のパラメータは searchParams プロパティを介して取得します。例えば、function Page({ params, searchParams }: { params: { slug: string }, searchParams: { q: string } }) などです。ただし、サーバーコンポーネントでは useRouter や usePathname を使用できない点に注意してください。これらのフックはクライアントコンポーネントでのみ利用可能です。📖 まとめ
- Next.js は、ルーティング、レンダリングの最適化、API ルーティング、画像の最適化、ミドルウェアなどの機能が標準で備わっているフルスタック React フレームワークです。
- 4つのレンダリングモード:SSGは最も高速で、コンテンツ量の多いサイトに最適です。SSRはリアルタイムのパフォーマンスを提供し、データ量の多いサイトに最適です。ISRは両者の折衷案(オンデマンドでの再検証)です。CSRは、インタラクションの多いアプリケーションに最適です。
- パフォーマンス比較:SSGはサーバー側の負荷がなく、最初のページの読み込みが最も速い。SSRはリアルタイムデータを提供するが、サーバーに大きな負荷がかかる。ISRは速度とリアルタイム性能のバランスが取れている。CSRはバックグラウンドでのインタラクションに適している。
- App Router はファイルシステムベースのルーティングを採用しており、ディレクトリ
app/、page.tsx、layout.tsx、loading.tsx、error.tsx、およびnot-found.tsxは、それぞれ個別のルーティング機能に対応しています。 - 動的ルートは
[param]構文を使用し、包括的ルートは[...param]構文を使用します。API ルートはroute.tsファイルを使用します。 - レイアウトは深いネスト構造と状態の保持に対応しています。サブページに切り替えても、現在読み込まれているレイアウトは破棄されません。
- サーバーコンポーネントはデフォルトでサーバー上で実行され、ブラウザにJavaScriptを送信しません。ユーザーとのやり取りが必要なコンポーネントは、
'use client'を使用してクライアントコンポーネントとして宣言する必要があります。 - 階層型アーキテクチャ:サーバーコンポーネントはデータコンテナとして機能し、クライアントコンポーネントは対話型のリーフノードとして機能することで、ブラウザ側のJavaScriptのサイズを可能な限り最小限に抑えます
- App Router 内の Pages Router の
getStaticProps/getServerSidePropsが、async コンポーネント + fetch に置き換えられました - レンダリングモードを選択する際の判断基準:ページのコンテンツは頻繁に変更されますか?ユーザーはリアルタイムのデータを必要としていますか?そのページにはSEO対策が必要ですか?SSG、ISR、SSR、CSRのいずれを選択する際も、これらの要素をすべて考慮してください。
- 移行手順:CRA →
create-next-app作成 → コンポーネントをapp/ディレクトリに移行 → ページごとにレンダリングモードを選択 → レイアウトを抽出 → 読み込み中/エラー状態を追加 - このコースは、Next.js を用いたデータ取得、CI/CD デプロイ、コンポーネントライブラリの統合、および包括的なプロジェクトの実践演習に関する今後のコースの基礎となります。
- トムの教訓を心に留めておきましょう。新しいプロジェクトでは、最初からApp Routerを選択し、2度目の移行にかかるコストを回避しましょう。
📝 練習問題
-
npx create-next-app@latest my-blogを使用して新しいプロジェクトを作成し、App Router のディレクトリ構造が使用されていることを確認してください。app/の下に、/about、/blog、/blog/[slug]の 3 つのページルートを作成してください。各ページには、少なくとも 1 つのタイトルと説明文を含めてください。npm run devを実行し、各ルート (/about、/blog、/blog/test-article) にアクセスして、マッピングが正しいことを確認してください。 -
上部ナビゲーションバー(「ホーム」、「ブログ」、「About」の3つのリンクを含む)とフッターを含むグローバルレイアウト(
app/layout.tsx)を実装します。次に、記事詳細ページの左側に記事の目次を表示するため、/blog/*ルート用のネストされたレイアウトを作成します。ブラウザで React DevTools を開き、レイアウトのネスト階層が正しいこと、およびページを切り替えた際にナビゲーションバーとサイドバーが再レンダリングされずに状態を維持していることを確認してください。 -
ブログのホームページを、JSONPlaceholder API (
https://jsonplaceholder.typicode.com/posts) から直接記事の一覧を取得してレンダリングするサーバーコンポーネントに変更します。次に、キーワード検索およびフィルタリング機能を実装するクライアントコンポーネントを作成し、ユーザーがブラウザ側でリアルタイムに記事を検索・フィルタリングできるようにします。サーバーコンポーネントがデータ取得を処理し、クライアントコンポーネントはインタラクティブなフィルタリングロジックのみを処理するようにしてください。これが完了したら、loading.tsx(プレースホルダー画面を表示するため)とerror.tsx(エラーメッセージと再試行ボタンを表示するため)を追加し、ルーティングセグメントの設定全体を体験してください。 -
オプション:ブログを Vercel(無料)にデプロイし、Vercel コンソールで ISR の再検証プロセスを確認します。API から返されるデータを変更した後、
revalidateの時間が経過するとページが自動的に更新されるかどうかを確認してください。



