【Next.js 15入門②】RSC/RCC とレンダリング戦略（CSR/SSR/SSG/ISR）

## はじめに

本記事では、Next.js でのレンダリング手法（**CSR/SSR/SSG/ISR**）の違いや適用方法と、**RSC**（React Server Components）、**RCC**（React Client Components）について解説します。

※こちらの記事の続編になります

https://qiita.com/m-bitterC2/items/68b1c767eb9f499950aa

### 想定読者

- React の基礎知識がある方
- RSC や RCC を活用したパフォーマンス向上に興味がある方
- Next.js のレンダリング戦略を学びたい方

### 本記事の目標

1. **RSC**と**RCC**の使い方を学ぶ
2. **CSR、SSR、SSG、ISR**の違いを理解する

## 1. RSC と RCC の違い

Next.js 15 では、コンポーネントが「サーバーコンポーネント（RSC）」と「クライアントコンポーネント（RCC）」の 2 種類に分かれています。

サーバーコンポーネント（RSC）はサーバー側（例：Vercel 上や Node.js 環境）でレンダリングされるため、データ取得やパフォーマンスに優れています。
一方、クライアントコンポーネント（RCC）はブラウザ（例：Chrome や Safari など）で動作し、ユーザーの操作に応じた動的な処理に適しています。

#### RSC (React Server Components)

- サーバー側で動作するコンポーネント
- Next.js のコンポーネントは、基本的にデフォルトで RSC になる
- RSC では React Hooks（例：`useState`や`useEffect`）を使用するとエラーになる

#### RCC (React Client Components)

- クライアント側（ブラウザ）で動作するコンポーネント
- React Hooks を使いたい場合は、RCC として作成する
- Next.js で RCC を定義するには、ファイル先頭に`'use client'`と書く必要がある

```tsx
"use client";

import { useState } from "react";

const page = () => {
  const [count, setCount] = useState(0);

  console.log("client");

  return (
    <>
      <div>クライアントコンポーネント</div>
      <p>
        <button
          className="hover:cursor-pointer whitespace-pre"
          onClick={() => setCount((prev) => ++prev)}
        >
          count:{" "}
        </button>
        {count}
      </p>
    </>
  );
};

export default page;
```

## 2. RCC と RSC の組み合わせ

RCC と RSC は組み合わせて使用できます。

例えば、画面描画時に RSC でデータフェッチを行い、必要なデータを props で RCC に渡し、ユーザーアクション（クリックなど）の機能は RCC で実装する、といった役割分担が可能です。

なお、RCC 配下のコンポーネントは暗黙的に RCC として扱われます。

#### ex: RCC と RSC を組み合わせた画面構成

```bash
src/
├── app/
│   │── api/
│   │   └── menu/
│   │       └── route.ts
│   └── rsc/
│       └── page.tsx
└── components/
    └── Contents.tsx
```

```ts: api/menu/route.ts
import { NextResponse } from "next/server";

export const GET = () => {
  return NextResponse.json(["blog", "counter", "about", "contact"]);
};
```

```tsx: api/rsc/page.tsx
import Contents from "@/components/Contents";

const page = async () => {
  const response = await fetch("http://localhost:3000/api/menu", {
    cache: "no-store",
  });
  const menuList: string[] = await response.json();

  return (
    <>
      <div className="bg-green-400">サーバーコンポーネント</div>
      <div className="flex flex-col md:flex-row">
        {/* サイドメニュー */}
        <ul className="w-full md:w-3/12 bg-green-200">
          {menuList.map((menu, index) => (
            <li
              key={index}
              className="hover:bg-green-300 cursor-pointer px-2 py-1 rounded"
            >
              {menu}
            </li>
          ))}
        </ul>

        {/* メインコンテンツ（RCC） */}
        <div className="w-full md:w-9/12">
          <Contents initCount={menuList.length} />
        </div>
      </div>
    </>
  );
};

export default page;
```

```tsx: components/Contents.tsx
"use client";

import { useEffect, useState } from "react";

const Contents = ({ initCount }: { initCount: number }) => {
  const [count, setCount] = useState(0);

  useEffect(() => setCount(initCount), [initCount]);

  return (
    <div className="bg-orange-200">
      <p>クライアントコンポーネント</p>
      <br />
      <p className="pl-5">
        <button
          className="hover:cursor-pointer whitespace-pre"
          onClick={() => setCount((prev) => ++prev)}
        >
          Counter:{" "}
        </button>
        {count}
      </p>
    </div>
  );
};

export default Contents;
```

画面はこのようになります。
![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665246/82a03b58-797c-40b8-a770-a4a8af7fa727.png)


## 3. レンダリング

Next.js の App Router では、ページごとに CSR（Client-Side Rendering）、SSR（Server-Side Rendering）、SSG（Static Site Generation）、ISR（Incremental Static Regeneration）を柔軟に使い分けることができます。
ページ要件に応じて、これらのレンダリング手法を選択することで、パフォーマンスや UX を最適化できます。

:::note warn
注意
開発モード（npm run dev）では、すべてのページが SSR として動作します。
SSG や ISR の挙動を確認するには、本番ビルド（npm run build）と本番起動（npm start）が必要です。
:::

#### CSR（Client-Side Rendering）

- クライアント側で HTML を生成する
- 動的にページを更新する
- ユーザーインタラクションが多いページや、SEO が不要なページに適している

#### SSR（Server-Side Rendering）

- サーバ側で HTML を生成する
- 常に最新のデータを取得する
- ユーザーごとに異なるコンテンツを表示するページや、SEO が重要なページに適している

###### ex: SSR で Dog API から犬の画像を取得

:::note info
外部ドメインの画像を使用する場合は、next.config.ts で許可設定が必要です。
:::

```ts: next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    // 許可する外部ドメインのパターンを記載
    remotePatterns: [
      // Dog APIのURL
      {
        protocol: "https",
        hostname: "images.dog.ceo",
      },
    ],
  },
};

export default nextConfig;
```

:::

```tsx: SSRPage
import Image from "next/image";

export const dynamic = "force-dynamic"; // 明示的にSSRを指定

const SSRPage = async () => {
  const res = await fetch("https://dog.ceo/api/breeds/image/random", {
    cache: "no-store",
  });
  if (!res.ok) {
    throw new Error("Failed to fetch Dog data");
  }

  /** 犬の画像データ */
  const image: string = (await res.json()).message;
  /** タイムスタンプ */
  const timestamp = new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });

  return (
    <div>
      <p>SSRで犬の画像を取得</p>
      <p>Timestamp: {timestamp}</p>
      <br />
      <Image src={image} alt="dog" width={500} height={500} />
    </div>
  );
};

export default SSRPage;
```

このページでリロードすると、タイムスタンプが更新され、新しい犬の画像が表示できます。

#### SSG（Static Site Generation）

- ビルド時に静的 HTML を生成する
- 変更頻度が低いページや、初期表示速度を重視するページに適している

###### ex: SSG で PokéAPI からピカチュウのデータ取得

```tsx: SSGPage.tsx
import Image from "next/image";

export const dynamic = "force-static"; // 明示的にSSGを指定

type Pokemon = {
  name: string;
  sprites: {
    front_default: string;
  };
  types: {
    type: {
      name: string;
    };
  }[];
};

const SSGPage = async () => {
  const res = await fetch("https://pokeapi.co/api/v2/pokemon/pikachu");
  if (!res.ok) {
    throw new Error("Failed to fetch Pikachu data");
  }

  /** ポケモンデータ */
  const pokemon: Pokemon = await res.json();
  /** タイムスタンプ */
  const timestamp = new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });

  return (
    <div>
      <p>SSGでピカチュウのデータ取得</p>
      <p>Timestamp: {timestamp}</p>
      <br />
      <p>名前：{pokemon.name}</p>
      <p>タイプ：</p>
      <ol>
        {pokemon.types.map((typeInfo) => (
          <li key={typeInfo.type.name}>{typeInfo.type.name}</li>
        ))}
      </ol>
      <Image
        src={pokemon.sprites.front_default}
        alt={pokemon.name}
        width={300}
      />
    </div>
  );
};

export default SSGPage;
```

ピカチュウのデータが取得できました。
このページで何度ブラウザを更新しても、タイムゾーンが変わらないことが確認できます。

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665246/03e78d9d-395b-48a8-ae15-b20e90ebe36d.png)


#### ISR（Incremental Static Regeneration）

- 一定時間ごとに静的ページを再生成する
- 最新ではないが適度に更新したいページや、大量のコンテンツを扱う場合に適している

###### ex: ISR で 10 秒ごとに国際宇宙ステーション（ISS）の現在位置を取得

```tsx
export type IssNowResponse = {
  timestamp: number;
  iss_position: {
    latitude: string;
    longitude: string;
  };
};

export const revalidate = 10; // 再生成の間隔を指定

const ISRPage = async () => {
  const res = await fetch("http://api.open-notify.org/iss-now.json", {
    next: { revalidate: 10 }, // 10秒ごとに再生成
  });
  if (!res.ok) {
    throw new Error("Failed to fetch ISS data");
  }

  const data: IssNowResponse = await res.json();

  return (
    <div style={{ textAlign: "center", margin: "2rem" }}>
      <h1>国際宇宙ステーションの現在位置</h1>
      <p>緯度: {data.iss_position.latitude}</p>
      <p>経度: {data.iss_position.longitude}</p>
      <p>最終更新日時: {new Date(data.timestamp * 1000).toISOString()}</p>
    </div>
  );
};

export default ISRPage;
```

10 秒後にブラウザを更新すると、ISS の緯度・経度と最終更新日時が更新されることが確認できます。

## まとめ

本記事では、Next.js のレンダリング戦略とコンポーネントについて学びました。

〈コンポーネント〉
- **RSC (React Server Components)**: サーバー側で動作するコンポーネント。React Hooks は使えません。
- **RCC (React Client Components)**: クライアント側で動作するコンポーネント。`'use client'`を記述する必要があります。

〈レンダリング〉
- **CSR (Client-Side Rendering)**: クライアント側で HTML を生成。動的なページ更新に適しています。
- **SSR (Server-Side Rendering)**: サーバー側で HTML を生成。最新データや SEO が重要なページに適しています。
- **SSG (Static Site Generation)**: ビルド時に静的 HTML を生成。初期表示速度が重視されるページに適しています。
- **ISR (Incremental Static Regeneration)**: 定期的に静的ページを再生成。大量コンテンツや適度な更新が求められるページに適しています。

これらの戦略を状況に応じて使い分けることで、パフォーマンスや SEO を最適化できます。