C
Next.js/Basics/Lesson 02

Client vs Server Boundary — 'use client' Changes Everything

35 min·theory
This chapter
2/3
TypeScript

Client vs Server Boundary — 'use client' Changes Everything

💡 Why Does This Matter? — The Server/Client Boundary Is the Boundary of Bundle Size and Security

🎯 Every component in the App Router is a Server Component by default. Where you place 'use client' determines the size of your client bundle.
💼 Importing DB, secrets, or the `fs` module inside a Client Component causes a build error — drawing the boundary incorrectly means your service won't even build.
Conversely, marking everything 'use client' throws away all the advantages of Server Components (0 KB bundle, direct DB access) — that's just the old SPA model.
🔗 The most important pattern: **composition where the Server wraps a Client as children** — without this, you cannot use server-fetched data inside a Client Component.
🏢 실무에서는
The most common real-world mistake: carelessly writing 'use client' at the top of a page component and then calling `await db.user.findMany()` inside it — since the browser can't call the database, you get a build error. The fix is to put 'use client' only on small leaf components that genuinely need interactivity (buttons, forms, anything using useState), and leave everything else as Server Components.

3 Core Rules of the Boundary

1. What 'use client' Really Means

tsx
'use client';
  • This file and every component this file imports become Client Components (it's contagious).
  • In other words, 'use client' marks the boundary of "the tree starting from here" — not just "this one component."
  • A single page can have multiple 'use client' boundaries, and that's perfectly normal.

2. Five Signals That You Need 'use client'

SignalExample
State hooksuseState, useReducer
Effect hooksuseEffect, useLayoutEffect
Browser APIswindow, localStorage, navigator
Event handlersonClick, onChange, onSubmit
Context Providers<MyContext.Provider> (in most cases)

If any one of these is needed → 'use client'. Otherwise, keep it a Server Component.

3. Server → Client is OK, Client → Server is ❌

tsx
// ✅ Server imports and embeds a Client Component — allowed
// app/page.tsx (Server)
import { Button } from './Button';  // Button has 'use client'
export default async function Page() {
  const data = await db.posts.findMany();
  return <div>{data.length} items<Button /></div>;
}
tsx
// ❌ Client directly importing and embedding a Server Component causes a build error
// Header.tsx (Client)
'use client';
import { ServerWidget } from './ServerWidget'; // no 'use client'
export function Header() {
  return <ServerWidget />; // ❌ build error
}

4. The Solution — Composition via the children Prop

tsx
// ClientShell.tsx (Client)
'use client';
import { useState } from 'react';
export function ClientShell({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && children}  {/* Server content goes here */}
    </div>
  );
}
tsx
// app/page.tsx (Server)
import { ClientShell } from './ClientShell';
import { ServerWidget } from './ServerWidget'; // Server
export default function Page() {
  return (
    <ClientShell>
      <ServerWidget />  {/* Server passed as children to the Client — OK */}
    </ClientShell>
  );
}

Key insight: A Client Component cannot directly import a Server Component, but it's perfectly fine for the Server to pass Server-rendered JSX as a children prop to a Client Component. This is the most important pattern in App Router.

5. Common Pitfalls Inside Server Components

tsx
// ❌ onClick in a Server Component — build error
export default function Page() {
  return <button onClick={() => alert('hi')}>X</button>;
}

// ✅ Extract interactive behavior into a Client leaf
// app/page.tsx
import { AlertButton } from './AlertButton';
export default function Page() {
  return <AlertButton />;
}
// AlertButton.tsx
'use client';
export function AlertButton() {
  return <button onClick={() => alert('hi')}>X</button>;
}
💻 🅰️ Common Mistake — Putting 'use client' on the Entire Page
// ❌ Anti-pattern: Making the entire page 'use client'

// 📁 app/posts/page.tsx
'use client';
import { useState, useEffect } from 'react';

interface Post { id: number; title: string; body: string; }

export default function PostsPage() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [liked, setLiked] = useState<Set<number>>(new Set());

  // Fetch data on the client — flicker, SEO loss, heavy bundle
  useEffect(() => {
    fetch('/api/posts').then(r => r.json()).then(setPosts);
  }, []);

  const toggle = (id: number) => {
    const n = new Set(liked);
    n.has(id) ? n.delete(id) : n.add(id);
    setLiked(n);
  };

  return (
    <ul>
      {posts.map(p => (
        <li key={p.id}>
          <h2>{p.title}</h2>
          <p>{p.body}</p>
          <button onClick={() => toggle(p.id)}>
            {liked.has(p.id) ? '❤️' : '🤍'}
          </button>
        </li>
      ))}
    </ul>
  );
}

// Disadvantages:
// - All posts data and render code included in client bundle
// - Initial page = blank screen → flicker after useEffect → display
// - SEO loss (search engines only see empty HTML)
// - Cannot directly access DB → must go through /api/posts
💻 🅱️ Well-Drawn Boundary Pattern — Server Handles Data, Client Handles Only the Like Button
// ✅ Push boundary down to the leaf — Server for data, Client for toggle only

// 📁 app/posts/page.tsx (Server)
import { LikeButton } from './LikeButton';

interface Post { id: number; title: string; body: string; }

// Component itself is async — direct DB access
export default async function PostsPage() {
  const posts: Post[] = await db.post.findMany();

  return (
    <ul>
      {posts.map(p => (
        <li key={p.id}>
          <h2>{p.title}</h2>
          <p>{p.body}</p>
          <LikeButton postId={p.id} />  {/* Client leaf */}
        </li>
      ))}
    </ul>
  );
}

// 📁 app/posts/LikeButton.tsx (Client)
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: number }) {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}

// 📁 app/dashboard/page.tsx — Inject Server content as children
import { ClientShell } from './ClientShell';
import { ServerStats } from './ServerStats';

export default function Dashboard() {
  return (
    <ClientShell>
      <ServerStats />  {/* Server component as children — OK */}
    </ClientShell>
  );
}

// 📁 app/dashboard/ClientShell.tsx
'use client';
import { useState } from 'react';

export function ClientShell({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && children}  {/* Server JSX rendered as is */}
    </div>
  );
}

// Advantages:
// - PostsPage code is NOT in client bundle — only HTML sent
// - Only LikeButton is split into a small chunk for hydration
// - Perfect SEO (all posts rendered on server)
// - Direct DB access possible

💡 💡 Five Rules for Drawing the Boundary Well

1. Push 'use client' as close to the leaf as possible
Adding 'use client' to large components like pages or layouts is almost always the wrong call. Reserve it for the small components that genuinely need interactivity (buttons, inputs, dropdowns).

2. Remember that 'use client' is contagious
Everything imported by a file marked 'use client' becomes a Client Component. Even a Server Component gets treated as a Client Component once it falls inside that import tree.

3. When you need Server content inside a Client, receive it via children

tsx
<ClientLayout><ServerContent /></ClientLayout>
// ClientLayout only accepts children as a prop

4. You can't add onClick in a Server Component — extract it into a Client leaf
The compiler will catch it as a build error, but knowing the pattern upfront saves time.

5. Even with 'use client', the component still renders on the server before hydration
The initial HTML is rendered on the server; the browser brings it to life via hydration. SSR always happens. 'use client' does not mean 'browser only' — it means 'also alive in the browser after hydration.'

⚡ Try It Yourself — Boundary Violation Check Simulation

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

Check Your Understanding

Which of the following can be used directly inside a Server Component without needing to extract it into a Client Component?
💡 Accessing the database, environment variables, and the filesystem are **Server-only** capabilities. `await db.user.findMany()` can be used directly inside a Server Component — in fact, it is one of the key advantages of Server Components. useState, onClick, and useEffect all run exclusively in the browser, so they must be moved into a Client Component. This is the most important decision criterion for Server Components.
Client vs Server Boundary - Next.js