C
Next.js/Rendering/Lesson 04

Server Component — Direct DB Access, 0KB Client Bundle

40 min·theory
This chapter
1/5
TypeScript

Server Component — Direct DB Access, 0KB Client Bundle

💡 Why Should You Learn This? — 'Finish on the Server and Send Only HTML' Is the Default

🎯 The Server Component function itself is **not included in the client bundle** — only the rendered HTML is sent. Page weight is dramatically reduced.
💼 You can directly access databases, the filesystem, and environment variables. Since they never leak to the browser, API keys and secrets can be used safely.
`fetch` calls within the same render pass are automatically deduplicated and cached — even if the same URL is called multiple times, only one actual network request is made.
🔗 All data-fetching abstractions from the Pages Router — `getServerSideProps`, `getStaticProps`, `SWR` — disappear entirely. Just use `await fetch()`.
📈 Downside: browser hooks such as `useState`, `onClick`, and `useEffect` cannot be used. Those belong to the Client Component world.
🏢 실무에서는
`src/app/study/[category]/page.tsx` in codemaster40 is likely a Server Component. It reads the learning data JSON on the server and renders it directly as HTML, so initial load is fast and the only JS sent to the client is for interactive parts (components that use `useState`).

Server vs Client — Boundaries and Rules

1. Server Component Is the DefaultIf there is no 'use client' at the top of a file, it is automatically a Server Component.``tsx// app/users/page.tsx — no 'use client' → Serverexport default async function UsersPage() { const users = await db.user.findMany(); // Direct DB access OK return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}`## 2. Capabilities of Server Components| Feature | Server | Client ||---|---|---|| async function component | ✅ | ❌ || Direct await fetch() | ✅ | △ (only inside useEffect) || DB, file system, secret access | ✅ | ❌ || useState / useEffect | ❌ | ✅ || onClick / onChange | ❌ | ✅ || Browser APIs (window, localStorage) | ❌ | ✅ || Context Provider | △ | ✅ |## 3. Automatic fetch Deduplication + CachingEven if multiple components call the same URL within the same render, the actual fetch only happens once:`tsxasync function Header() { const user = await fetch('/api/me').then(r => r.json()); // First call return <div>{user.name}</div>;}async function Sidebar() { const user = await fetch('/api/me').then(r => r.json()); // Same fetch — automatic dedupe return <div>{user.email}</div>;}`Caching policies:`tsxfetch(url); // Default SSG (force-cache)fetch(url, { cache: 'no-store' }); // Fresh every timefetch(url, { next: { revalidate: 60 } });// 60-second ISRfetch(url, { next: { tags: ['user'] } });// Invalidate with revalidateTag('user')`## 4. Server → Client Boundary`tsx// app/page.tsx (Server) — Can import and embed Client componentsimport { Counter } from './Counter';export default async function Home() { const users = await db.user.findMany(); return ( <div> <h1>Users: {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>;}`- A Server Component importing and embedding a Client Component is OK.- A Client Component importing and embedding a Server Component is ❌ (you can't call a DB from the browser).- However, passing a Server Component as children` into a Client Component is OK (composition).

💻 🅰️ Pages Router — getServerSideProps Boilerplate
// ❌ Pages Router — Data fetching and rendering are separated

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

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

// 1. Server data fetching — separate function
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. Component is a prop receiver
export default function UserPage({
  user,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Sent to client:
//  - Page component JS (including prop mapping code)
//  - Serialized JSON of getServerSideProps result
//  - React hydration code
// → Even for static content, the page function is included in the client bundle
💻 🅱️ App Router — One async Server Component Block
// ✅ App Router — Server Component directly fetches/accesses DB

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

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

// Component is async — await inside
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(); // Redirect to 404 page

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

      {/* Embed Client Component — only for interactive parts */}
      <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)}>Likes: {n}</button>;
}

// Sent to client:
//  - Only the HTML of UserPage (not the function body)
//  - Counter component JS (this is what truly needs interaction)
//  - React runtime
// → Static parts are only HTML, interaction is a separate chunk

💡 💡 5 Server Component Tips for Production

1. Only Server Components Can Be asyncAdding async to a Client Component causes a build error. Fetch data on the Server and pass the result down to the Client as props.2. 'use client' Does Not Spread UpwardEven if a Server imports a Client, the parent remains a Server. The boundary is clear.3. Client → Server Import Is Not AllowedIf a Client Component imports a Server Component and embeds it directly, you will get a build error. Use children instead:``tsx// ✅ composition pattern<ClientLayout><ServerContent /></ClientLayout>// ClientLayout only receives children as props`4. Four fetch Cache Modes`tsxfetch(url); // force-cache (default SSG)fetch(url, { cache: 'no-store' }); // SSR (fresh every time)fetch(url, { next: { revalidate: 60 } }); // ISR (60 seconds)fetch(url, { next: { tags: ['user'] } }); // Tag invalidation`5. Environment Variables — Server Sees Everything, Client Only Sees NEXT_PUBLIC_`tsprocess.env.DATABASE_URL // Server: OK, Client: undefinedprocess.env.NEXT_PUBLIC_API_URL // Both OK``Secrets embedded in a Server Component never leak to the browser.

⚡ Try It Yourself — Server/Client Boundary Simulation

Simulate where each component executes and where the fetch calls occur.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Quiz

In App Router, what happens when a Client Component directly `import`s a Server Component and embeds it as JSX?
💡 Because Client Component code runs in the browser, you cannot directly import and embed a Server Component's body (e.g., database calls). Instead, use the **composition pattern**: a Server parent wraps the Client Component and passes the Server Component as `children`. This rule is the key to maintaining a clear Server/Client boundary.
Server Component — Direct DB Access, 0KB Bundle - Next.js