404 Not Found

404 Not Found


nginx

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. 学習内容


2. 概念図

Next.js を学習する中で、トムはリクエストの処理経路を理解するためにフローチャートを作成しました。ユーザーからのリクエストが届くと、Next.js はそのページの設定(静的、動的、または増分)に基づいて異なるレンダリング経路を選択し、最終的に HTML を返して、ブラウザ側での操作を可能にします。

この図は、4つの重要な決定ポイントを示しています。まず、ページの種類(静的/動的/増分/クライアントサイド)を決定します。次に、それに対応するレンダリングエンジンを選択します。その後、HTMLを生成してブラウザに返します。そして最後に、ハイドレーション処理を用いて、ページ上のクライアントコンポーネントがインタラクティブになるようにします。

100%
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を直接使用することを推奨しています。また、useRouterusePathnameはサーバーコンポーネント内で直接使用できず、代わりにこのルーティングロジックをクライアントコンポーネントに抽出する必要があることも発見しました。これらの経験を通じて、彼は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'は、リクエストがあるたびにページの再レンダリングを強制します。

TSX
// === 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 パスをマッピングします。これにより、ルート設定ファイルを手動で管理する必要がなくなりました。ディレクトリ構造そのものがルート構造となるのです。

主なファイル形式

動的ルーティングと高度なモード

動的なルートを作成するには、[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 の完全なディレクトリ構造を示しています。各フォルダ内の特殊なファイル名(pagelayoutloadingerrornot-found)は Next.js の規約に従っていることに注意してください。このフレームワークはこれらのファイルを自動的に認識し、特定のルーティング動作を割り当てます。dashboard/layout.tsxはルートディレクトリのlayout.tsx内にネストされており、2階層のレイアウトを形成しています。最外層はグローバルナビゲーションとフッターで構成され、内側の層にはダッシュボード専用のサイドバーが含まれています。

BASH
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エンドポイント
TSX
// 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>&copy; 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 を送信することはありません。

インタラクティブな機能(ボタンのクリック、テキストの入力、useStateuseEffect の使用、ウィンドウイベントの監視など)が必要な場合、サーバーサイドコンポーネントだけでは不十分です。この場合、ファイルの先頭に 'use client' 宣言を追加する必要があります。これにより、Next.js はそのコンポーネントおよびすべての子コンポーネントを「クライアントコンポーネント」としてマークし、ブラウザ側で実行されるようにバンドルします。

サーバーコンポーネントの特徴

クライアントコンポーネントの特徴

ベストプラクティスモデル

このアーキテクチャは階層構造を採用しており、「サーバーコンポーネント」がコンテナとして機能し、「クライアントコンポーネント」がリーフとして機能します。外側のサーバーコンポーネントはデータの取得とレイアウトの処理を担当し、内側のクライアントコンポーネントはインタラクションを必要とするウィジェットの処理のみを担当します。このようにして、ロジックやデータ取得の大部分はサーバー側で処理され、ブラウザでは必要最小限のJavaScriptのみが実行されます。

(3) ▶ サンプル:サーバーコンポーネントとクライアントコンポーネント間の最適な役割分担

以下のコードは、トムのブログで説明されている「サーバー側でのデータ取得、ブラウザ側での操作」という階層モデルを示しています。PostsPageはサーバーコンポーネントであり、サーバー上でデータを取得し、HTMLとしてレンダリングして、ブラウザに直接送信します。ユーザーは、JavaScriptの読み込みを待つことなく、完成したページを閲覧できます。PostListはクライアントコンポーネントであり、サーバーで準備されたデータを受け取り、ブラウザ側での検索、フィルタリング、削除といった操作のみを担当します。なお、'use client'は、操作を必要とする「リーフコンポーネント」にのみ適用され、ページ全体には適用されない点に注意してください。

TSX
// 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 を使用する

フローチャート:サーバーコンポーネントか、クライアントコンポーネントか?

TEXT
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) ▶ サンプル: App Router の loading.tsx と error.tsx

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) ▶ サンプル:ネストされたレイアウトとテンプレート

TSX
// 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
▶ 試してみよう

❓ よくある質問

Q Next.jsとReactの関係はどのようなものですか?
A Reactは、コンポーネントのレンダリングや状態管理を担うUIライブラリです。一方、Next.jsはReactを基盤として構築されたフルスタックフレームワークであり、ルーティング、SSR/SSG/ISR、APIルーティング、画像最適化、フォント最適化、ミドルウェアなどの機能が組み込まれています。次のように考えることができます。Reactは構成要素を提供し、Next.jsはそれらの構成要素を本番環境向けのアプリケーションに組み立てるための完全なツールチェーンを提供します。Reactは「UIをどのようにレンダリングするか」に焦点を当てているのに対し、Next.jsは「完全なWebアプリケーションをどのように構築するか」に取り組んでいます。
Q サーバーコンポーネントとクライアントコンポーネントのどちらを選ぶべきですか?
A サーバーコンポーネント(デフォルト)の使用を優先してください。コンポーネントが useStateuseEffectonClick、またはブラウザ API を使用する必要がある場合にのみ、'use client' を追加してください。一般的なパターンとして、サーバーコンポーネントがデータを取得し、それを内部のクライアントコンポーネントに渡してインタラクションを処理させることが挙げられます。こうすることで、データ取得はサーバー側で処理され、インタラクションロジックはブラウザ側で実行されるため、両方の長所を活かした最適な構成となります。初心者にありがちな間違いとして、ページ全体に 'use client' を追加してしまうことが挙げられます。これは、最も小さなインタラクティブなリーフノードにのみ適用すべきです。インタラクションも状態も必要としないコンポーネントが見つかった場合は、'use client' を追加しないでください。
Q SSRとSSG、どちらのパフォーマンスが優れていますか?
A SSGは、ビルドプロセス中にHTMLが生成され、CDNがサーバーサイドでの計算オーバーヘッドなしに静的ファイルを直接配信するため、パフォーマンスに優れています。ただし、SSGはコンテンツが頻繁に更新されるページには適していません。SSRはリクエストごとに再レンダリングを行うため、レイテンシは高くなりますが、データは常に最新の状態に保たれます。その中間的なアプローチとしてISRがあります。これは、ほとんどの場合SSGと同じくらい高速であり、更新が必要な場合にオンデマンドでコンテンツを再生成します。実際のプロジェクトでは、通常、ハイブリッドなアプローチが採用されます。具体的には、マーケティングページにはSSG、製品ページにはISR、ユーザーダッシュボードにはSSRを使用します。
Q App RouterとPages Routerの違いは何ですか?
A Pages Routerは、Next.js 12およびそれ以前のバージョンで使用されていたルーティング方式です。これはpages/ディレクトリを使用し、ファイル名に基づいてルーティングをマッピングします。App Routerは、Next.js 13で導入された新しいルーティングシステムです。app/ディレクトリを使用し、ネストされたレイアウト、サーバーコンポーネント、ストリーミングレンダリングなどの新機能をサポートしています。新規プロジェクトでは、最初から App Router を使用することを推奨します。既存のプロジェクトについては、段階的に移行することができます。Pages Router の getStaticProps/getServerSideProps 構造は、App Router では async component + fetch に置き換えられています。2つのルーティングシステムは共存可能であり、移行中は Pages Router から App Router へページを段階的に移行することができます。
Q Next.js プロジェクトは Vercel にデプロイしなければならないのでしょうか?
A 必ずしもそうではありません。Vercel は Next.js の開発元であり、最もシームレスなデプロイ体験(ワンクリックデプロイ、自動スケーリング、エッジネットワーク)を提供していますが、Next.js は他のプラットフォームにもデプロイ可能です。next build && next start を使用すれば、任意の Node.js サーバーにデプロイできますし、Docker を使用してコンテナ化されたデプロイを行うことも可能です。Netlify、AWS Amplify、Cloudflare Pages も Next.js のデプロイに対応しています。ただし、ISR やミドルウェアなどの高度な機能を使用する場合は、Vercel が最高の互換性を提供します。
Q 最初にPages Routerを使用し、後でApp Routerに移行したい場合はどうすればよいですか?
A 2つのルーティングシステムは同じプロジェクト内で共存可能です。pages/からapp/へページを段階的に移行し、移行するたびに各ページを確認していけばよいので、一度にすべてを移行する必要はありません。移行の際は、以下の点に注意してください。getStaticProps をサーバーコンポーネント内で直接 fetch に変更し、レイアウトを app/ ディレクトリの下に作成した layout.tsx に移動し、ロジックを pages/_app.tsx からルートディレクトリの layout.tsx へ移行してください。まずは最もシンプルなページから始め、経験を積んでからより複雑なページに取り組むことをお勧めします。
Q サーバーコンポーネントでURLパラメータを取得するにはどうすればよいですか?
A サーバーコンポーネントでは、動的なルートパラメータは params プロパティを介して、クエリ文字列のパラメータは searchParams プロパティを介して取得します。例えば、function Page({ params, searchParams }: { params: { slug: string }, searchParams: { q: string } }) などです。ただし、サーバーコンポーネントでは useRouterusePathname を使用できない点に注意してください。これらのフックはクライアントコンポーネントでのみ利用可能です。

📖 まとめ


📝 練習問題

  1. npx create-next-app@latest my-blog を使用して新しいプロジェクトを作成し、App Router のディレクトリ構造が使用されていることを確認してください。app/ の下に、/about/blog/blog/[slug] の 3 つのページルートを作成してください。各ページには、少なくとも 1 つのタイトルと説明文を含めてください。npm run dev を実行し、各ルート (/about/blog/blog/test-article) にアクセスして、マッピングが正しいことを確認してください。

  2. 上部ナビゲーションバー(「ホーム」、「ブログ」、「About」の3つのリンクを含む)とフッターを含むグローバルレイアウト(app/layout.tsx)を実装します。次に、記事詳細ページの左側に記事の目次を表示するため、/blog/* ルート用のネストされたレイアウトを作成します。ブラウザで React DevTools を開き、レイアウトのネスト階層が正しいこと、およびページを切り替えた際にナビゲーションバーとサイドバーが再レンダリングされずに状態を維持していることを確認してください。

  3. ブログのホームページを、JSONPlaceholder API (https://jsonplaceholder.typicode.com/posts) から直接記事の一覧を取得してレンダリングするサーバーコンポーネントに変更します。次に、キーワード検索およびフィルタリング機能を実装するクライアントコンポーネントを作成し、ユーザーがブラウザ側でリアルタイムに記事を検索・フィルタリングできるようにします。サーバーコンポーネントがデータ取得を処理し、クライアントコンポーネントはインタラクティブなフィルタリングロジックのみを処理するようにしてください。これが完了したら、loading.tsx(プレースホルダー画面を表示するため)とerror.tsx(エラーメッセージと再試行ボタンを表示するため)を追加し、ルーティングセグメントの設定全体を体験してください。

  4. オプション:ブログを Vercel(無料)にデプロイし、Vercel コンソールで ISR の再検証プロセスを確認します。API から返されるデータを変更した後、revalidate の時間が経過するとページが自動的に更新されるかどうかを確認してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%