현재 URL: /feed
렌더 경로: 클라이언트에서 즉시 전환
By default, layouts and pages are Server Components, which lets you fetch data and render parts of your UI on the server, optionally cache the result, and stream it to the client. When you need interactivity or browser APIs, you can use Client Components to layer in functionality.
This page explains how Server and Client Components work in Next.js and when to use them, with examples of how to compose them together in your application.
The client and server environments have different capabilities. Server and Client components allow you to run logic in each environment depending on your use case.
Use Client Components when you need:
onClick, onChange.useEffect.localStorage, window, Navigator.geolocation, etc.Use Server Components when you need:
For example, the <Page> component is a Server Component that fetches data about a post, and passes it as props to the <LikeButton> which handles client-side interactivity.
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
);
}
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }) {
const post = await getPost(params.id);
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
);
}
'use client';
import { useState } from 'react';
export default function LikeButton({ likes }: { likes: number }) {
// ...
}
'use client';
import { useState } from 'react';
export default function LikeButton({ likes }) {
// ...
}
On the server, Next.js uses React's APIs to orchestrate rendering. The rendering work is split into chunks, by individual route segments (layouts and pages):
What is the React Server Component Payload (RSC)?
The RSC Payload is a compact binary representation of the rendered React Server Components tree. It's used by React on the client to update the browser's DOM. The RSC Payload contains:
- The rendered result of Server Components
- Placeholders for where Client Components should be rendered and references to their JavaScript files
- Any props passed from a Server Component to a Client Component
Then, on the client:
What is hydration?
Hydration is React's process for attaching event handlers to the DOM, to make the static HTML interactive.
On subsequent navigations:
You can create a Client Component by adding the "use client" directive at the top of the file, above your imports.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count} likes</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count} likes</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
"use client" is used to declare a boundary between the Server and Client module graphs (trees).
Once a file is marked with "use client", all its imports and child components are considered part of the client bundle. This means you don't need to add the directive to every component that is intended for the client.
To reduce the size of your client JavaScript bundles, add 'use client' to specific interactive components instead of marking large parts of your UI as Client Components.
For example, the <Layout> component contains mostly static elements like a logo and navigation links, but includes an interactive search bar. <Search /> is interactive and needs to be a Client Component, however, the rest of the layout can remain a Server Component.
// Client Component
import Search from './search';
// Server Component
import Logo from './logo';
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
);
}
// Client Component
import Search from './search';
// Server Component
import Logo from './logo';
// Layout is a Server Component by default
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
);
}
'use client';
export default function Search() {
// ...
}
'use client';
export default function Search() {
// ...
}
You can pass data from Server Components to Client Components using props.
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return <LikeButton likes={post.likes} />;
}
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }) {
const post = await getPost(params.id);
return <LikeButton likes={post.likes} />;
}
'use client';
export default function LikeButton({ likes }: { likes: number }) {
// ...
}
'use client';
export default function LikeButton({ likes }) {
// ...
}
Alternatively, you can stream data from a Server Component to a Client Component with the use API. See an example.
Good to know: Props passed to Client Components need to be serializable by React.
You can pass Server Components as a prop to a Client Component. This allows you to visually nest server-rendered UI within Client components.
A common pattern is to use children to create a slot in a <ClientComponent>. For example, a <Cart> component that fetches data on the server, inside a <Modal> component that uses client state to toggle visibility.
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
'use client';
export default function Modal({ children }) {
return <div>{children}</div>;
}
Then, in a parent Server Component (e.g.<Page>), you can pass a <Cart> as the child of the <Modal>:
import Modal from './ui/modal';
import Cart from './ui/cart';
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}
import Modal from './ui/modal';
import Cart from './ui/cart';
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}
In this pattern, all Server Components will be rendered on the server ahead of time, including those as props. The resulting RSC payload will contain references of where Client Components should be rendered within the component tree.
React context is commonly used to share global state like the current theme. However, React context is not supported in Server Components.
To use context, create a Client Component that accepts children:
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
Then, import it into a Server Component (e.g. layout):
import ThemeProvider from './theme-provider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
import ThemeProvider from './theme-provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
Your Server Component will now be able to directly render your provider, and all other Client Components throughout your app will be able to consume this context.
Good to know: You should render providers as deep as possible in the tree – notice how
ThemeProvideronly wraps{children}instead of the entire<html>document. This makes it easier for Next.js to optimize the static parts of your Server Components.
You can share fetched data across both Server and Client Components by combining React.cache with context providers.
Create a cached function that fetches data:
import { cache } from 'react';
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user');
return res.json();
});
import { cache } from 'react';
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user');
return res.json();
});
Create a context provider that stores the promise:
'use client';
import { createContext } from 'react';
type User = {
id: string;
name: string;
};
export const UserContext = createContext<Promise<User> | null>(null);
export default function UserProvider({
children,
userPromise,
}: {
children: React.ReactNode;
userPromise: Promise<User>;
}) {
return <UserContext value={userPromise}>{children}</UserContext>;
}
'use client';
import { createContext } from 'react';
export const UserContext = createContext(null);
export default function UserProvider({ children, userPromise }) {
return <UserContext value={userPromise}>{children}</UserContext>;
}
In a layout, pass the promise to the provider without awaiting:
import UserProvider from './user-provider';
import { getUser } from './lib/user';
export default function RootLayout({ children }: { children: React.ReactNode }) {
const userPromise = getUser(); // Don't await
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
);
}
import UserProvider from './user-provider';
import { getUser } from './lib/user';
export default function RootLayout({ children }) {
const userPromise = getUser(); // Don't await
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
);
}
Client Components use use() to resolve the promise from context, wrapped in <Suspense> for fallback UI:
'use client';
import { use, useContext } from 'react';
import { UserContext } from '../user-provider';
export function Profile() {
const userPromise = useContext(UserContext);
if (!userPromise) {
throw new Error('useContext must be used within a UserProvider');
}
const user = use(userPromise);
return <p>Welcome, {user.name}</p>;
}
'use client';
import { use, useContext } from 'react';
import { UserContext } from '../user-provider';
export function Profile() {
const userPromise = useContext(UserContext);
if (!userPromise) {
throw new Error('useContext must be used within a UserProvider');
}
const user = use(userPromise);
return <p>Welcome, {user.name}</p>;
}
import { Suspense } from 'react';
import { Profile } from './ui/profile';
export default function Page() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
);
}
import { Suspense } from 'react';
import { Profile } from './ui/profile';
export default function Page() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
);
}
Server Components can also call getUser() directly:
import { getUser } from '../lib/user';
export default async function DashboardPage() {
const user = await getUser(); // Cached - same request, no duplicate fetch
return <h1>Dashboard for {user.name}</h1>;
}
import { getUser } from '../lib/user';
export default async function DashboardPage() {
const user = await getUser(); // Cached - same request, no duplicate fetch
return <h1>Dashboard for {user.name}</h1>;
}
Since getUser is wrapped with React.cache, multiple calls within the same request return the same memoized result, whether called directly in Server Components or resolved via context in Client Components.
Good to know:
React.cacheis scoped to the current request only. Each request gets its own memoization scope with no sharing between requests.
When using a third-party component that relies on client-only features, you can wrap it in a Client Component to ensure it works as expected.
For example, the <Carousel /> can be imported from the acme-carousel package. This component uses useState, but it doesn't yet have the "use client" directive.
If you use <Carousel /> within a Client Component, it will work as expected:
'use client';
import { useState } from 'react';
import { Carousel } from 'acme-carousel';
export default function Gallery() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
);
}
'use client';
import { useState } from 'react';
import { Carousel } from 'acme-carousel';
export default function Gallery() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
);
}
However, if you try to use it directly within a Server Component, you'll see an error. This is because Next.js doesn't know <Carousel /> is using client-only features.
To fix this, you can wrap third-party components that rely on client-only features in your own Client Components:
'use client';
import { Carousel } from 'acme-carousel';
export default Carousel;
'use client';
import { Carousel } from 'acme-carousel';
export default Carousel;
Now, you can use <Carousel /> directly within a Server Component:
import Carousel from './carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
);
}
import Carousel from './carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
);
}
Advice for Library Authors
If you’re building a component library, add the
"use client"directive to entry points that rely on client-only features. This lets your users import components into Server Components without needing to create wrappers.It's worth noting some bundlers might strip out
"use client"directives. You can find an example of how to configure esbuild to include the"use client"directive in the React Wrap Balancer and Vercel Analytics repositories.
JavaScript modules can be shared between both Server and Client Components modules. This means it's possible to accidentally import server-only code into the client. For example, consider the following function:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
This function contains an API_KEY that should never be exposed to the client.
In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are included in the client bundle. If variables are not prefixed, Next.js replaces them with an empty string.
As a result, even though getData() can be imported and executed on the client, it won't work as expected.
To prevent accidental usage in Client Components, you can use the server-only package.
Then, import the package into a file that contains server-only code:
import 'server-only';
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
Now, if you try to import the module into a Client Component, there will be a build-time error.
The corresponding client-only package can be used to mark modules that contain client-only logic like code that accesses the window object.
In Next.js, installing server-only or client-only is optional. However, if your linting rules flag extraneous dependencies, you may install them to avoid issues.
npm install server-only
yarn add server-only
pnpm add server-only
bun add server-only
Next.js handles server-only and client-only imports internally to provide clearer error messages when a module is used in the wrong environment. The contents of these packages from NPM are not used by Next.js.
Next.js also provides its own type declarations for server-only and client-only, for TypeScript configurations where noUncheckedSideEffectImports is active.
시나리오 버튼을 누르거나 요구사항 체크박스를 바꿔보세요. 그러면 권장 아키텍처가 즉시 Server/Client/Composition으로 달라집니다. 이것이 Server와 Client를 이분법으로 고르는 게 아니라, 요구사항별로 경계를 설계해야 하는 이유입니다.
왜 필요한가: 무조건 Client로 밀어 넣으면 번들 비용이 커지고, 무조건 Server로 고정하면 인터랙션 요구를 충족할 수 없다.
언제 사용하는가: 새 화면 설계 시 어떤 컴포넌트를 Server로 유지하고 어떤 부분만 Client로 분리할지 결정할 때.
문서 예시처럼 app/[id]/page.tsx는 Server로 데이터/보안을 맡기고, app/ui/like-button.tsx는 Client로 상호작용을 담당하게 분리하는 방식이 기본 패턴입니다.
실사용 시나리오
Client 요구
Server 요구
페이지는 Server로 렌더하고, 상호작용 조각만 Client로 분리하세요.
요구사항 충족 상태
한계: 실제 번들 크기 차이와 hydration 비용은 빌드 결과에서 정확히 확인해야 합니다. 이 데모는 문서의 선택 기준을 실사용 의사결정 흐름으로 시각화한 것입니다.
문서 예시 코드 (Server + Client Composition)
// app/[id]/page.tsx (Server)
const post = await getPost(id)
return <LikeButton likes={post.likes} />
// app/ui/like-button.tsx (Client)
'use client'
const [likes, setLikes] = useState(initialLikes)How do Server and Client Components work in Next.js?
세그먼트와 컴포넌트 구성을 바꿔보세요. 그러면 서버가 생성하는 RSC Payload 크기와 HTML 프리렌더 범위가 즉시 달라집니다. 이것이 바로 Next.js가 서버에서 Route Segment 단위로 렌더링을 분할하는 이유입니다.
왜 필요한가: 서버 단계에서 어떤 데이터가 만들어지는지 이해하지 못하면 hydration 문제나 성능 이슈를 정확히 진단하기 어렵다.
언제 사용하는가: 서버 렌더링 성능을 분석하거나, 어떤 UI를 Server Component로 유지할지 결정할 때.
아래 조합은 RSC Payload 생성 단계와 pre-rendered HTML 결과를 근사 시각화합니다.
컴포넌트 구성
1) Server Components 렌더
예상 청크 8개
2) RSC Payload 조합
예상 크기 54KB
3) HTML 프리렌더
정적 미리보기 커버리지 73%
한계: 실제 바이너리 RSC Payload 프레임은 브라우저 DevTools 네트워크 탭에서만 직접 확인할 수 있습니다. 이 데모는 문서의 서버 렌더링 단계를 시각적으로 근사한 모델입니다.
문서 코드 핵심
// app/[id]/page.tsx
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}How do Server and Client Components work in Next.js?
상단 단계 버튼을 눌러 first load 파이프라인을 이동해보세요. 그러면 같은 화면이라도 어떤 시점에 클릭이 막히고, 언제 인터랙션이 활성화되는지 바로 확인할 수 있습니다. 이것이 hydration이 단순 렌더가 아니라 이벤트를 연결하는 과정이라는 의미입니다.
왜 필요한가: 초기 진입 성능과 인터랙션 지연을 분리해서 보지 않으면, 사용자가 느끼는 체감 문제를 잘못 최적화하게 된다.
언제 사용하는가: 첫 진입 화면이 빠르게 보이지만 버튼이 늦게 동작하는 이슈를 분석할 때.
이 데모는 hydration 완료 전후의 차이를 단계별로 체험하도록 구성되었습니다.
네트워크 프로필
first load 단계
브라우저 상태
미리보기 상태현재 단계 설명
브라우저가 서버에서 받은 정적 HTML을 먼저 그린다.
한계: 실제 이벤트 리스너 부착 타이밍과 reconcile 세부 단계는 브라우저/디바이스마다 차이가 있습니다. 정확한 진단은 React DevTools와 Performance 패널에서 확인하세요.
문서 요약 단계
1. HTML: 빠른 비인터랙티브 프리뷰 표시 2. RSC Payload: Client/Server 트리 동기화 3. JavaScript hydration: 이벤트 핸들러 연결
Examples
경계 위치를 바꿔보세요. 그러면 어떤 파일이 client bundle에 포함되는지와 카운터 상호작용 가능 여부가 즉시 달라집니다. 이것이 바로 use client가 단일 컴포넌트가 아니라 모듈 그래프 경계를 선언하는 이유입니다.
왜 필요한가: 경계를 넓게 잡으면 불필요한 JS가 늘고 hydration 비용이 커져 초기 성능이 악화된다.
언제 사용하는가: state, event handler, 브라우저 API가 필요한 컴포넌트를 Server 트리 안에 섞어야 할 때.
app/ui/counter.tsx에만 경계를 두는 방식이 기본 권장 패턴입니다.
경계 위치
Client Bundle 포함 파일
Server Component로 유지되는 파일
한계: 실제 번들 결과는 코드 분할과 트리셰이킹에 따라 달라질 수 있습니다. 이 패널은 문서의 경계 규칙을 이해하기 위한 학습용 시뮬레이션입니다.
문서 코드 핵심
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Click me</button>
}Examples
전략을 바꿔가며 Nav 항목 수를 조절해보세요. 그러면 클라이언트로 내려가는 JavaScript 양과 예상 hydration 시간이 즉시 변합니다. 이것이 정적 레이아웃은 Server Component로 두고 Search 같은 상호작용 부분만 client로 분리해야 하는 이유입니다.
왜 필요한가: UI 대부분이 정적인데 전체를 client로 만들면 네트워크/실행 비용이 불필요하게 커진다.
언제 사용하는가: 상단 네비게이션, 사이드바처럼 정적 영역과 작은 인터랙션 위젯이 섞인 레이아웃을 설계할 때.
문서 예시처럼 app/layout.tsx는 서버에 두고 app/ui/search.tsx만 client로 분리해보세요.
Client 경계 전략
전송 JS 크기
Layout 전체 client: 24KB
Search만 client: 8KB
선택 전략 전송량: 8KB
예상 hydration 비용: 24ms
한계: 실제 번들 수치와 hydration 시간은 앱 코드, 분할 전략, 브라우저 성능에 따라 달라집니다. 이 데모는 전략별 방향성을 이해하기 위한 비교 모델입니다.
문서 코드 핵심
// app/layout.tsx (Server Component)
import Search from './search' // Client Component
import Logo from './logo' // Server Component
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}Examples
게시글과 전달할 prop 타입을 바꿔보세요. 그러면 Client Component가 정상적으로 값을 받는지, 아니면 직렬화 제약으로 막히는지 즉시 확인됩니다. 이것이 Server에서 가져온 데이터를 props로 넘길 때 serializable 규칙이 중요한 이유입니다.
왜 필요한가: 데이터 경계 규칙을 모르고 props를 설계하면 빌드/런타임 에러로 UI 전달 파이프라인이 깨진다.
언제 사용하는가: 서버 데이터(fetch/DB)를 좋아요 버튼, 필터 UI 같은 Client Component로 넘길 때.
app/[id]/page.tsx에서 fetch한 값을 LikeButton에 전달하는 상황을 시뮬레이션합니다.
Client로 전달할 prop 형태
Server fetch 결과: 12 likes
전달 시도 값: { likes: 12 }
문서 코드 핵심
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}Examples
모달을 열고 닫으면서 children 슬롯 콘텐츠를 바꿔보세요. 그러면 같은 Client 모달 안에서도 서버에서 준비된 UI가 어떻게 끼워지는지 바로 보입니다. 이것이 Server Component를 Client Component의 children으로 interleave하는 핵심입니다.
왜 필요한가: 상호작용 UI를 모두 클라이언트로 옮기지 않고도, 서버 데이터 UI를 필요한 위치에 배치할 수 있다.
언제 사용하는가: 모달, 드로어 같은 Client UI 컨테이너 안에 서버에서 준비한 목록/카트를 넣고 싶을 때.
핵심은 children slot입니다. Modal은 client 상태를, Slot 콘텐츠는 server fetch 결과를 담당합니다.
슬롯에 넣을 Server Component
ServerPage
ClientModal (isOpen: false)
Server<Cart /> as children
서버 준비 시간: 140ms
모달 표시 상태: 숨김 (server 결과는 이미 준비됨)
배경 클릭 동작: 닫힘
한계: 실제 RSC payload 내부에서 children 참조가 직렬화되는 방식은 이 데모에서 직접 보여주지 못합니다. Network/React DevTools에서 payload를 함께 확인해야 합니다.
문서 코드 핵심
// app/ui/modal.tsx
'use client'
export default function Modal({ children }) {
return <div>{children}</div>
}
// app/page.tsx (Server)
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}Examples
Provider 래핑 위치를 바꿔보세요. 그러면 어떤 범위가 context 영향권에 들어오는지와 정적 최적화 점수가 즉시 달라집니다. 이것이 ThemeProvider를 가능한 깊게 배치하라고 문서에서 강조하는 이유입니다.
왜 필요한가: Provider 범위를 루트 전체로 넓히면 정적 최적화 범위가 줄어 성능 비용이 커질 수 있다.
언제 사용하는가: 테마/로케일 같은 전역 상태를 도입하되, 서버 렌더 성능을 최대한 유지하고 싶을 때.
ThemeProvider는 client에서 만들고, layout.tsx에서 필요한 깊이만 감싸는 것이 핵심입니다.
Provider 배치 위치
현재 테마 값: dark
영향 범위: 필요한 하위 트리만 provider 범위
정적 최적화 점수(근사): 92/100
한계: 이 점수는 학습용 근사치입니다. 실제 최적화 범위는 정적/동적 세그먼트 구성에 따라 달라집니다.
문서 코드 핵심
// app/theme-provider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
// app/layout.tsx (Server)
import ThemeProvider from './theme-provider'Examples
소비자 조합과 cache 옵션을 바꿔보세요. 그러면 같은 사용자 데이터를 몇 번 네트워크로 가져오는지가 즉시 바뀝니다. 이것이 React.cache와 context를 함께 써서 Server와 Client 소비 경로를 통합하는 핵심 이유입니다.
왜 필요한가: profile, dashboard 등 여러 경로에서 같은 데이터를 각자 fetch하면 중복 요청과 지연이 증가한다.
언제 사용하는가: 레이아웃/페이지/클라이언트 위젯이 공통 사용자 데이터를 함께 사용해야 할 때.
이 데모는 getUser = cache(async () => ...) 패턴을 요청 단위로 시각화합니다.
소비자 조합
layout.tsx (Server)
fetch 실행getUser()로 userPromise 생성
Profile (Client)
재사용use(userPromise)로 값 해석
dashboard/page.tsx (Server)
재사용getUser() 재사용 (memoized)
요청 범위: 동일 요청
cache 상태: 활성
한계: React.cache는 요청 단위라 브라우저 탭 전체에 전역 공유되지 않습니다. 요청이 바뀌면 같은 함수라도 새 fetch가 실행됩니다.
문서 코드 핵심
import { cache } from 'react'
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user')
return res.json()
})
// layout.tsx
const userPromise = getUser() // await 하지 않고 provider로 전달Examples
사용 패턴을 바꿔보세요. 그러면 Carousel이 빌드 단계에서 차단되는지, 혹은 정상적으로 렌더되는지가 즉시 바뀝니다. 이것이 서드파티 client-only 컴포넌트를 wrapper로 감싸는 패턴이 필요한 이유입니다.
왜 필요한가: 경계 없이 서버에서 직접 import하면 빌드 실패나 런타임 오류가 발생해 통합 비용이 커진다.
언제 사용하는가: npm 패키지 UI를 App Router의 Server Component 페이지에 붙일 때.
app/carousel.tsx wrapper는 외부 컴포넌트를 안전한 Client 경계로 고정하는 가장 단순한 방법입니다.
사용 방식
렌더 결과
현재 Carousel 미표시
주의: 일부 번들러 설정은 use client directive를 제거할 수 있습니다. 문서에서 안내한 것처럼 라이브러리 빌드 설정을 함께 점검해야 합니다.
문서 코드 핵심
// app/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
// app/page.tsx (Server)
import Carousel from './carousel'Examples
모듈 import 위치와 env 키를 바꿔보세요. 그러면 값이 비워지는지, 빌드에서 차단되는지, 혹은 노출 위험이 생기는지가 즉시 바뀝니다. 이것이 server-only로 서버 모듈 경계를 명시해야 하는 이유입니다.
왜 필요한가: 서버 전용 토큰을 보호하지 않으면 빌드 실패, 런타임 오류, 민감 정보 노출 위험이 동시에 생긴다.
언제 사용하는가: DB/API 키를 쓰는 유틸 함수를 여러 컴포넌트에서 공유할 때, 안전한 import 경계를 강제하고 싶을 때.
import 'server-only' 한 줄로 서버 전용 모듈이라는 계약을 명시할 수 있습니다.
모듈 사용 위치
사용 env 키
authorization 헤더 값: (blocked)
server-only가 client import를 빌드 시점에 막아 환경 오염을 예방한다.
import 대상: client-component
guard: server-only 적용
한계: 실제 빌드 에러 텍스트는 프로젝트 설정에 따라 다르지만, 핵심은 server-only가 잘못된 client import를 조기에 막는다는 점입니다.
문서 코드 핵심
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: { authorization: process.env.API_KEY },
})
return res.json()
}By default, layouts and pages are Server Components, which lets you fetch data and render parts of your UI on the server, optionally cache the result, and stream it to the client. When you need interactivity or browser APIs, you can use Client Components to layer in functionality.
This page explains how Server and Client Components work in Next.js and when to use them, with examples of how to compose them together in your application.
The client and server environments have different capabilities. Server and Client components allow you to run logic in each environment depending on your use case.
Use Client Components when you need:
onClick, onChange.useEffect.localStorage, window, Navigator.geolocation, etc.Use Server Components when you need:
For example, the <Page> component is a Server Component that fetches data about a post, and passes it as props to the <LikeButton> which handles client-side interactivity.
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
);
}
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }) {
const post = await getPost(params.id);
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
);
}
'use client';
import { useState } from 'react';
export default function LikeButton({ likes }: { likes: number }) {
// ...
}
'use client';
import { useState } from 'react';
export default function LikeButton({ likes }) {
// ...
}
On the server, Next.js uses React's APIs to orchestrate rendering. The rendering work is split into chunks, by individual route segments (layouts and pages):
What is the React Server Component Payload (RSC)?
The RSC Payload is a compact binary representation of the rendered React Server Components tree. It's used by React on the client to update the browser's DOM. The RSC Payload contains:
- The rendered result of Server Components
- Placeholders for where Client Components should be rendered and references to their JavaScript files
- Any props passed from a Server Component to a Client Component
Then, on the client:
What is hydration?
Hydration is React's process for attaching event handlers to the DOM, to make the static HTML interactive.
On subsequent navigations:
You can create a Client Component by adding the "use client" directive at the top of the file, above your imports.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count} likes</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count} likes</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
"use client" is used to declare a boundary between the Server and Client module graphs (trees).
Once a file is marked with "use client", all its imports and child components are considered part of the client bundle. This means you don't need to add the directive to every component that is intended for the client.
To reduce the size of your client JavaScript bundles, add 'use client' to specific interactive components instead of marking large parts of your UI as Client Components.
For example, the <Layout> component contains mostly static elements like a logo and navigation links, but includes an interactive search bar. <Search /> is interactive and needs to be a Client Component, however, the rest of the layout can remain a Server Component.
// Client Component
import Search from './search';
// Server Component
import Logo from './logo';
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
);
}
// Client Component
import Search from './search';
// Server Component
import Logo from './logo';
// Layout is a Server Component by default
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
);
}
'use client';
export default function Search() {
// ...
}
'use client';
export default function Search() {
// ...
}
You can pass data from Server Components to Client Components using props.
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return <LikeButton likes={post.likes} />;
}
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({ params }) {
const post = await getPost(params.id);
return <LikeButton likes={post.likes} />;
}
'use client';
export default function LikeButton({ likes }: { likes: number }) {
// ...
}
'use client';
export default function LikeButton({ likes }) {
// ...
}
Alternatively, you can stream data from a Server Component to a Client Component with the use API. See an example.
Good to know: Props passed to Client Components need to be serializable by React.
You can pass Server Components as a prop to a Client Component. This allows you to visually nest server-rendered UI within Client components.
A common pattern is to use children to create a slot in a <ClientComponent>. For example, a <Cart> component that fetches data on the server, inside a <Modal> component that uses client state to toggle visibility.
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
'use client';
export default function Modal({ children }) {
return <div>{children}</div>;
}
Then, in a parent Server Component (e.g.<Page>), you can pass a <Cart> as the child of the <Modal>:
import Modal from './ui/modal';
import Cart from './ui/cart';
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}
import Modal from './ui/modal';
import Cart from './ui/cart';
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}
In this pattern, all Server Components will be rendered on the server ahead of time, including those as props. The resulting RSC payload will contain references of where Client Components should be rendered within the component tree.
React context is commonly used to share global state like the current theme. However, React context is not supported in Server Components.
To use context, create a Client Component that accepts children:
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
Then, import it into a Server Component (e.g. layout):
import ThemeProvider from './theme-provider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
import ThemeProvider from './theme-provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
Your Server Component will now be able to directly render your provider, and all other Client Components throughout your app will be able to consume this context.
Good to know: You should render providers as deep as possible in the tree – notice how
ThemeProvideronly wraps{children}instead of the entire<html>document. This makes it easier for Next.js to optimize the static parts of your Server Components.
You can share fetched data across both Server and Client Components by combining React.cache with context providers.
Create a cached function that fetches data:
import { cache } from 'react';
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user');
return res.json();
});
import { cache } from 'react';
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user');
return res.json();
});
Create a context provider that stores the promise:
'use client';
import { createContext } from 'react';
type User = {
id: string;
name: string;
};
export const UserContext = createContext<Promise<User> | null>(null);
export default function UserProvider({
children,
userPromise,
}: {
children: React.ReactNode;
userPromise: Promise<User>;
}) {
return <UserContext value={userPromise}>{children}</UserContext>;
}
'use client';
import { createContext } from 'react';
export const UserContext = createContext(null);
export default function UserProvider({ children, userPromise }) {
return <UserContext value={userPromise}>{children}</UserContext>;
}
In a layout, pass the promise to the provider without awaiting:
import UserProvider from './user-provider';
import { getUser } from './lib/user';
export default function RootLayout({ children }: { children: React.ReactNode }) {
const userPromise = getUser(); // Don't await
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
);
}
import UserProvider from './user-provider';
import { getUser } from './lib/user';
export default function RootLayout({ children }) {
const userPromise = getUser(); // Don't await
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
);
}
Client Components use use() to resolve the promise from context, wrapped in <Suspense> for fallback UI:
'use client';
import { use, useContext } from 'react';
import { UserContext } from '../user-provider';
export function Profile() {
const userPromise = useContext(UserContext);
if (!userPromise) {
throw new Error('useContext must be used within a UserProvider');
}
const user = use(userPromise);
return <p>Welcome, {user.name}</p>;
}
'use client';
import { use, useContext } from 'react';
import { UserContext } from '../user-provider';
export function Profile() {
const userPromise = useContext(UserContext);
if (!userPromise) {
throw new Error('useContext must be used within a UserProvider');
}
const user = use(userPromise);
return <p>Welcome, {user.name}</p>;
}
import { Suspense } from 'react';
import { Profile } from './ui/profile';
export default function Page() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
);
}
import { Suspense } from 'react';
import { Profile } from './ui/profile';
export default function Page() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
);
}
Server Components can also call getUser() directly:
import { getUser } from '../lib/user';
export default async function DashboardPage() {
const user = await getUser(); // Cached - same request, no duplicate fetch
return <h1>Dashboard for {user.name}</h1>;
}
import { getUser } from '../lib/user';
export default async function DashboardPage() {
const user = await getUser(); // Cached - same request, no duplicate fetch
return <h1>Dashboard for {user.name}</h1>;
}
Since getUser is wrapped with React.cache, multiple calls within the same request return the same memoized result, whether called directly in Server Components or resolved via context in Client Components.
Good to know:
React.cacheis scoped to the current request only. Each request gets its own memoization scope with no sharing between requests.
When using a third-party component that relies on client-only features, you can wrap it in a Client Component to ensure it works as expected.
For example, the <Carousel /> can be imported from the acme-carousel package. This component uses useState, but it doesn't yet have the "use client" directive.
If you use <Carousel /> within a Client Component, it will work as expected:
'use client';
import { useState } from 'react';
import { Carousel } from 'acme-carousel';
export default function Gallery() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
);
}
'use client';
import { useState } from 'react';
import { Carousel } from 'acme-carousel';
export default function Gallery() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
);
}
However, if you try to use it directly within a Server Component, you'll see an error. This is because Next.js doesn't know <Carousel /> is using client-only features.
To fix this, you can wrap third-party components that rely on client-only features in your own Client Components:
'use client';
import { Carousel } from 'acme-carousel';
export default Carousel;
'use client';
import { Carousel } from 'acme-carousel';
export default Carousel;
Now, you can use <Carousel /> directly within a Server Component:
import Carousel from './carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
);
}
import Carousel from './carousel';
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
);
}
Advice for Library Authors
If you’re building a component library, add the
"use client"directive to entry points that rely on client-only features. This lets your users import components into Server Components without needing to create wrappers.It's worth noting some bundlers might strip out
"use client"directives. You can find an example of how to configure esbuild to include the"use client"directive in the React Wrap Balancer and Vercel Analytics repositories.
JavaScript modules can be shared between both Server and Client Components modules. This means it's possible to accidentally import server-only code into the client. For example, consider the following function:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
This function contains an API_KEY that should never be exposed to the client.
In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are included in the client bundle. If variables are not prefixed, Next.js replaces them with an empty string.
As a result, even though getData() can be imported and executed on the client, it won't work as expected.
To prevent accidental usage in Client Components, you can use the server-only package.
Then, import the package into a file that contains server-only code:
import 'server-only';
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
});
return res.json();
}
Now, if you try to import the module into a Client Component, there will be a build-time error.
The corresponding client-only package can be used to mark modules that contain client-only logic like code that accesses the window object.
In Next.js, installing server-only or client-only is optional. However, if your linting rules flag extraneous dependencies, you may install them to avoid issues.
npm install server-only
yarn add server-only
pnpm add server-only
bun add server-only
Next.js handles server-only and client-only imports internally to provide clearer error messages when a module is used in the wrong environment. The contents of these packages from NPM are not used by Next.js.
Next.js also provides its own type declarations for server-only and client-only, for TypeScript configurations where noUncheckedSideEffectImports is active.
시나리오 버튼을 누르거나 요구사항 체크박스를 바꿔보세요. 그러면 권장 아키텍처가 즉시 Server/Client/Composition으로 달라집니다. 이것이 Server와 Client를 이분법으로 고르는 게 아니라, 요구사항별로 경계를 설계해야 하는 이유입니다.
왜 필요한가: 무조건 Client로 밀어 넣으면 번들 비용이 커지고, 무조건 Server로 고정하면 인터랙션 요구를 충족할 수 없다.
언제 사용하는가: 새 화면 설계 시 어떤 컴포넌트를 Server로 유지하고 어떤 부분만 Client로 분리할지 결정할 때.
문서 예시처럼 app/[id]/page.tsx는 Server로 데이터/보안을 맡기고, app/ui/like-button.tsx는 Client로 상호작용을 담당하게 분리하는 방식이 기본 패턴입니다.
실사용 시나리오
Client 요구
Server 요구
페이지는 Server로 렌더하고, 상호작용 조각만 Client로 분리하세요.
요구사항 충족 상태
한계: 실제 번들 크기 차이와 hydration 비용은 빌드 결과에서 정확히 확인해야 합니다. 이 데모는 문서의 선택 기준을 실사용 의사결정 흐름으로 시각화한 것입니다.
문서 예시 코드 (Server + Client Composition)
// app/[id]/page.tsx (Server)
const post = await getPost(id)
return <LikeButton likes={post.likes} />
// app/ui/like-button.tsx (Client)
'use client'
const [likes, setLikes] = useState(initialLikes)How do Server and Client Components work in Next.js?
세그먼트와 컴포넌트 구성을 바꿔보세요. 그러면 서버가 생성하는 RSC Payload 크기와 HTML 프리렌더 범위가 즉시 달라집니다. 이것이 바로 Next.js가 서버에서 Route Segment 단위로 렌더링을 분할하는 이유입니다.
왜 필요한가: 서버 단계에서 어떤 데이터가 만들어지는지 이해하지 못하면 hydration 문제나 성능 이슈를 정확히 진단하기 어렵다.
언제 사용하는가: 서버 렌더링 성능을 분석하거나, 어떤 UI를 Server Component로 유지할지 결정할 때.
아래 조합은 RSC Payload 생성 단계와 pre-rendered HTML 결과를 근사 시각화합니다.
컴포넌트 구성
1) Server Components 렌더
예상 청크 8개
2) RSC Payload 조합
예상 크기 54KB
3) HTML 프리렌더
정적 미리보기 커버리지 73%
한계: 실제 바이너리 RSC Payload 프레임은 브라우저 DevTools 네트워크 탭에서만 직접 확인할 수 있습니다. 이 데모는 문서의 서버 렌더링 단계를 시각적으로 근사한 모델입니다.
문서 코드 핵심
// app/[id]/page.tsx
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}How do Server and Client Components work in Next.js?
상단 단계 버튼을 눌러 first load 파이프라인을 이동해보세요. 그러면 같은 화면이라도 어떤 시점에 클릭이 막히고, 언제 인터랙션이 활성화되는지 바로 확인할 수 있습니다. 이것이 hydration이 단순 렌더가 아니라 이벤트를 연결하는 과정이라는 의미입니다.
왜 필요한가: 초기 진입 성능과 인터랙션 지연을 분리해서 보지 않으면, 사용자가 느끼는 체감 문제를 잘못 최적화하게 된다.
언제 사용하는가: 첫 진입 화면이 빠르게 보이지만 버튼이 늦게 동작하는 이슈를 분석할 때.
이 데모는 hydration 완료 전후의 차이를 단계별로 체험하도록 구성되었습니다.
네트워크 프로필
first load 단계
브라우저 상태
미리보기 상태현재 단계 설명
브라우저가 서버에서 받은 정적 HTML을 먼저 그린다.
한계: 실제 이벤트 리스너 부착 타이밍과 reconcile 세부 단계는 브라우저/디바이스마다 차이가 있습니다. 정확한 진단은 React DevTools와 Performance 패널에서 확인하세요.
문서 요약 단계
1. HTML: 빠른 비인터랙티브 프리뷰 표시 2. RSC Payload: Client/Server 트리 동기화 3. JavaScript hydration: 이벤트 핸들러 연결
Examples
경계 위치를 바꿔보세요. 그러면 어떤 파일이 client bundle에 포함되는지와 카운터 상호작용 가능 여부가 즉시 달라집니다. 이것이 바로 use client가 단일 컴포넌트가 아니라 모듈 그래프 경계를 선언하는 이유입니다.
왜 필요한가: 경계를 넓게 잡으면 불필요한 JS가 늘고 hydration 비용이 커져 초기 성능이 악화된다.
언제 사용하는가: state, event handler, 브라우저 API가 필요한 컴포넌트를 Server 트리 안에 섞어야 할 때.
app/ui/counter.tsx에만 경계를 두는 방식이 기본 권장 패턴입니다.
경계 위치
Client Bundle 포함 파일
Server Component로 유지되는 파일
한계: 실제 번들 결과는 코드 분할과 트리셰이킹에 따라 달라질 수 있습니다. 이 패널은 문서의 경계 규칙을 이해하기 위한 학습용 시뮬레이션입니다.
문서 코드 핵심
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Click me</button>
}Examples
전략을 바꿔가며 Nav 항목 수를 조절해보세요. 그러면 클라이언트로 내려가는 JavaScript 양과 예상 hydration 시간이 즉시 변합니다. 이것이 정적 레이아웃은 Server Component로 두고 Search 같은 상호작용 부분만 client로 분리해야 하는 이유입니다.
왜 필요한가: UI 대부분이 정적인데 전체를 client로 만들면 네트워크/실행 비용이 불필요하게 커진다.
언제 사용하는가: 상단 네비게이션, 사이드바처럼 정적 영역과 작은 인터랙션 위젯이 섞인 레이아웃을 설계할 때.
문서 예시처럼 app/layout.tsx는 서버에 두고 app/ui/search.tsx만 client로 분리해보세요.
Client 경계 전략
전송 JS 크기
Layout 전체 client: 24KB
Search만 client: 8KB
선택 전략 전송량: 8KB
예상 hydration 비용: 24ms
한계: 실제 번들 수치와 hydration 시간은 앱 코드, 분할 전략, 브라우저 성능에 따라 달라집니다. 이 데모는 전략별 방향성을 이해하기 위한 비교 모델입니다.
문서 코드 핵심
// app/layout.tsx (Server Component)
import Search from './search' // Client Component
import Logo from './logo' // Server Component
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}Examples
게시글과 전달할 prop 타입을 바꿔보세요. 그러면 Client Component가 정상적으로 값을 받는지, 아니면 직렬화 제약으로 막히는지 즉시 확인됩니다. 이것이 Server에서 가져온 데이터를 props로 넘길 때 serializable 규칙이 중요한 이유입니다.
왜 필요한가: 데이터 경계 규칙을 모르고 props를 설계하면 빌드/런타임 에러로 UI 전달 파이프라인이 깨진다.
언제 사용하는가: 서버 데이터(fetch/DB)를 좋아요 버튼, 필터 UI 같은 Client Component로 넘길 때.
app/[id]/page.tsx에서 fetch한 값을 LikeButton에 전달하는 상황을 시뮬레이션합니다.
Client로 전달할 prop 형태
Server fetch 결과: 12 likes
전달 시도 값: { likes: 12 }
문서 코드 핵심
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}Examples
모달을 열고 닫으면서 children 슬롯 콘텐츠를 바꿔보세요. 그러면 같은 Client 모달 안에서도 서버에서 준비된 UI가 어떻게 끼워지는지 바로 보입니다. 이것이 Server Component를 Client Component의 children으로 interleave하는 핵심입니다.
왜 필요한가: 상호작용 UI를 모두 클라이언트로 옮기지 않고도, 서버 데이터 UI를 필요한 위치에 배치할 수 있다.
언제 사용하는가: 모달, 드로어 같은 Client UI 컨테이너 안에 서버에서 준비한 목록/카트를 넣고 싶을 때.
핵심은 children slot입니다. Modal은 client 상태를, Slot 콘텐츠는 server fetch 결과를 담당합니다.
슬롯에 넣을 Server Component
ServerPage
ClientModal (isOpen: false)
Server<Cart /> as children
서버 준비 시간: 140ms
모달 표시 상태: 숨김 (server 결과는 이미 준비됨)
배경 클릭 동작: 닫힘
한계: 실제 RSC payload 내부에서 children 참조가 직렬화되는 방식은 이 데모에서 직접 보여주지 못합니다. Network/React DevTools에서 payload를 함께 확인해야 합니다.
문서 코드 핵심
// app/ui/modal.tsx
'use client'
export default function Modal({ children }) {
return <div>{children}</div>
}
// app/page.tsx (Server)
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}Examples
Provider 래핑 위치를 바꿔보세요. 그러면 어떤 범위가 context 영향권에 들어오는지와 정적 최적화 점수가 즉시 달라집니다. 이것이 ThemeProvider를 가능한 깊게 배치하라고 문서에서 강조하는 이유입니다.
왜 필요한가: Provider 범위를 루트 전체로 넓히면 정적 최적화 범위가 줄어 성능 비용이 커질 수 있다.
언제 사용하는가: 테마/로케일 같은 전역 상태를 도입하되, 서버 렌더 성능을 최대한 유지하고 싶을 때.
ThemeProvider는 client에서 만들고, layout.tsx에서 필요한 깊이만 감싸는 것이 핵심입니다.
Provider 배치 위치
현재 테마 값: dark
영향 범위: 필요한 하위 트리만 provider 범위
정적 최적화 점수(근사): 92/100
한계: 이 점수는 학습용 근사치입니다. 실제 최적화 범위는 정적/동적 세그먼트 구성에 따라 달라집니다.
문서 코드 핵심
// app/theme-provider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
// app/layout.tsx (Server)
import ThemeProvider from './theme-provider'Examples
소비자 조합과 cache 옵션을 바꿔보세요. 그러면 같은 사용자 데이터를 몇 번 네트워크로 가져오는지가 즉시 바뀝니다. 이것이 React.cache와 context를 함께 써서 Server와 Client 소비 경로를 통합하는 핵심 이유입니다.
왜 필요한가: profile, dashboard 등 여러 경로에서 같은 데이터를 각자 fetch하면 중복 요청과 지연이 증가한다.
언제 사용하는가: 레이아웃/페이지/클라이언트 위젯이 공통 사용자 데이터를 함께 사용해야 할 때.
이 데모는 getUser = cache(async () => ...) 패턴을 요청 단위로 시각화합니다.
소비자 조합
layout.tsx (Server)
fetch 실행getUser()로 userPromise 생성
Profile (Client)
재사용use(userPromise)로 값 해석
dashboard/page.tsx (Server)
재사용getUser() 재사용 (memoized)
요청 범위: 동일 요청
cache 상태: 활성
한계: React.cache는 요청 단위라 브라우저 탭 전체에 전역 공유되지 않습니다. 요청이 바뀌면 같은 함수라도 새 fetch가 실행됩니다.
문서 코드 핵심
import { cache } from 'react'
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user')
return res.json()
})
// layout.tsx
const userPromise = getUser() // await 하지 않고 provider로 전달Examples
사용 패턴을 바꿔보세요. 그러면 Carousel이 빌드 단계에서 차단되는지, 혹은 정상적으로 렌더되는지가 즉시 바뀝니다. 이것이 서드파티 client-only 컴포넌트를 wrapper로 감싸는 패턴이 필요한 이유입니다.
왜 필요한가: 경계 없이 서버에서 직접 import하면 빌드 실패나 런타임 오류가 발생해 통합 비용이 커진다.
언제 사용하는가: npm 패키지 UI를 App Router의 Server Component 페이지에 붙일 때.
app/carousel.tsx wrapper는 외부 컴포넌트를 안전한 Client 경계로 고정하는 가장 단순한 방법입니다.
사용 방식
렌더 결과
현재 Carousel 미표시
주의: 일부 번들러 설정은 use client directive를 제거할 수 있습니다. 문서에서 안내한 것처럼 라이브러리 빌드 설정을 함께 점검해야 합니다.
문서 코드 핵심
// app/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
// app/page.tsx (Server)
import Carousel from './carousel'Examples
모듈 import 위치와 env 키를 바꿔보세요. 그러면 값이 비워지는지, 빌드에서 차단되는지, 혹은 노출 위험이 생기는지가 즉시 바뀝니다. 이것이 server-only로 서버 모듈 경계를 명시해야 하는 이유입니다.
왜 필요한가: 서버 전용 토큰을 보호하지 않으면 빌드 실패, 런타임 오류, 민감 정보 노출 위험이 동시에 생긴다.
언제 사용하는가: DB/API 키를 쓰는 유틸 함수를 여러 컴포넌트에서 공유할 때, 안전한 import 경계를 강제하고 싶을 때.
import 'server-only' 한 줄로 서버 전용 모듈이라는 계약을 명시할 수 있습니다.
모듈 사용 위치
사용 env 키
authorization 헤더 값: (blocked)
server-only가 client import를 빌드 시점에 막아 환경 오염을 예방한다.
import 대상: client-component
guard: server-only 적용
한계: 실제 빌드 에러 텍스트는 프로젝트 설정에 따라 다르지만, 핵심은 server-only가 잘못된 client import를 조기에 막는다는 점입니다.
문서 코드 핵심
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: { authorization: process.env.API_KEY },
})
return res.json()
}