C
Next.js/レンダリング/Lesson 04

Server Component — DB 直接アクセス、0KB クライアントバンドル

40分·theory
このチャプター
1/5
TypeScript

Server Component — DB 直接アクセス、0KB クライアントバンドル

💡 なぜ学ぶ必要があるのか? — 「サーバーで完結して HTML だけ送る」がデフォルト

🎯 Server Component の関数自体は**クライアントバンドルに含まれません** — レンダリングされた HTML のみが送信されます。ページの重さが劇的に削減されます。
💼 DB・ファイルシステム・環境変数に直接アクセスできます。ブラウザには一切漏れないため、API キーやシークレットを安全に使用できます。
同一レンダリング内の `fetch` は自動的に重複排除・キャッシュされます — 同じ URL が複数回呼ばれても、実際のネットワークリクエストは 1 回だけです。
🔗 Pages Router の `getServerSideProps`・`getStaticProps`・`SWR` といったデータ取得の抽象化がすべて不要になります。シンプルに `await fetch()` を使うだけです。
📈 デメリット: `useState`・`onClick`・`useEffect` などのブラウザフックは使用できません。それらは Client Component の領域です。
🏢 실무에서는
codemaster40 の `src/app/study/[category]/page.tsx` も Server Component である可能性が高いです。学習データの JSON をサーバーで読み込んでそのまま HTML としてレンダリングするため初期ロードが速く、クライアントに送られる JS はインタラクティブな部分(`useState` を持つコンポーネント)のみです。

Server vs Client — 境界とルール

1. デフォルトは Server Component

ファイルの先頭に 'use client' がなければ、自動的に Server Component になります。

tsx
// app/users/page.tsx — 'use client' なし → Server
export default async function UsersPage() {
  const users = await db.user.findMany(); // DBに直接アクセスOK
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

2. Server Component の機能

機能ServerClient
async function コンポーネント
await fetch() の直接使用△ (useEffect 内のみ)
DB・ファイルシステム・シークレットへのアクセス
useState / useEffect
onClick / onChange
ブラウザ API (window, localStorage)
Context Provider

3. fetch の自動重複排除 + キャッシング

同じレンダリング内で複数のコンポーネントが同じ URL を呼び出しても、実際の fetch は一度だけ実行されます:

tsx
async function Header() {
  const user = await fetch('/api/me').then(r => r.json()); // 最初の呼び出し
  return <div>{user.name}</div>;
}
async function Sidebar() {
  const user = await fetch('/api/me').then(r => r.json()); // 同じfetch — 自動重複排除
  return <div>{user.email}</div>;
}

キャッシュポリシー:

tsx
fetch(url);                              // デフォルトSSG (force-cache)
fetch(url, { cache: 'no-store' });       // 毎回新しく
fetch(url, { next: { revalidate: 60 } });// 60秒ISR
fetch(url, { next: { tags: ['user'] } });// revalidateTag('user') で無効化

4. Server → Client の境界

tsx
// app/page.tsx (Server) — Client コンポーネントをimportして組み込むことができる
import { Counter } from './Counter';

export default async function Home() {
  const users = await db.user.findMany();
  return (
    <div>
      <h1>ユーザー: {users.length}名</h1>
      <Counter />  {/* Client Component */}
    </div>
  );
}
tsx
// app/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}
  • Server が Client を import して組み込むのは OK。
  • Client が Server を import して直接組み込むのは ❌ (ブラウザから DB を呼び出せないため)。
  • ただし、Server Component を children として Client Component に渡すのは OK (コンポジション)。
💻 🅰️ Pages Router — getServerSideProps のボイラープレート
// ❌ Pages Router — データ取得とレンダリングが分離される

// 📁 pages/users/[id].tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';

interface User { id: number; name: string; email: string; }

// 1. サーバーデータ取得 — 別関数
export const getServerSideProps: GetServerSideProps<{ user: User }> = async (ctx) => {
  const id = ctx.params?.id as string;
  const user = await db.user.findUnique({ where: { id: Number(id) } });
  if (!user) return { notFound: true };
  return { props: { user } };
};

// 2. コンポーネントはpropsを受け取る器
export default function UserPage({
  user,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// クライアントに送信されるもの:
//  - ページコンポーネントのJS (propsマッピングコードを含む)
//  - getServerSideProps の結果のシリアライズされたJSON
//  - Reactのハイドレーションコード
// → 静的コンテンツであるにもかかわらず、クライアントバンドルにページ関数が含まれる
💻 🅱️ App Router — async Server Component をひとまとめに
// ✅ App Router — Server Component が直接fetch・DB

// 📁 app/users/[id]/page.tsx
import { notFound } from 'next/navigation';
import { Counter } from './Counter';

interface User { id: number; name: string; email: string; }

// コンポーネントがasync — 内部でawait
export default async function UserPage({
  params,
}: {
  params: { id: string };
}) {
  const user: User | null = await db.user.findUnique({
    where: { id: Number(params.id) },
  });

  if (!user) notFound(); // 404ページに切り替え

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>

      {/* Client Component を組み込む — 相互作用が必要な部分だけ */}
      <Counter />
    </div>
  );
}

// 📁 app/users/[id]/Counter.tsx
'use client';
import { useState } from 'react';

export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>いいね: {n}</button>;
}

// クライアントに送信されるもの:
//  - UserPage のHTMLのみ (関数本体はX)
//  - Counter コンポーネントのJS (これが本当に相互作用が必要な部分)
//  - Reactランタイム
// → 静的な部分はHTMLのみ、相互作用は別のチャンク

💡 💡 Server Component 実践 Tips 5 選

1. async コンポーネントは Server のみClient に async を付けるとビルドエラーになります。データ取得は Server で行い、結果を props として Client に渡してください。

2. 「use client」は上位に伝染しないServer が Client を import しても、親は依然として Server のままです。境界は明確です。

3. Client → Server の import は不可Client Component が Server Component を import して直接組み込むとビルドエラーになります。代わりに children で渡してください:

tsx
// ✅ composition パターン
<ClientLayout><ServerContent /></ClientLayout>
// ClientLayout の props で children のみを受け取る

4. fetch キャッシュモード 4 種類

tsx
fetch(url);                                    // force-cache (基本 SSG)
fetch(url, { cache: 'no-store' });             // SSR (毎回新規)
fetch(url, { next: { revalidate: 60 } });      // ISR (60秒)
fetch(url, { next: { tags: ['user'] } });      // タグ無効化

5. 環境変数 — Server はすべて参照可、Client は NEXT_PUBLIC_ のみ

ts
process.env.DATABASE_URL          // Server: OK, Client: undefined
process.env.NEXT_PUBLIC_API_URL   // 両方 OK

Server Component に埋め込んだシークレットはブラウザに漏れません。

⚡ 実際に試してみよう — Server/Client 境界シミュレーション

各コンポーネントがどこで実行されるか、fetch がどこで発生するかをシミュレーションします。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

App Router において、Client Component が Server Component を直接 `import` して JSX として組み込むとどうなりますか?
💡 Client Component のコードはブラウザで実行されるため、Server Component の本体(DB 呼び出しなど)を直接 import して埋め込むことはできません。代わりに、Server の親が Client Component をラップしながら Server Component を `children` として渡す**コンポジションパターン**を使用します。このルールが Server/Client の境界を明確に保つための核心です。
Server Component — DB直接アクセス、0KBバンドル - Next.js