Server Component의 경계 — use client를 어디에 붙여야 하는가
기본은 Server, 인터랙션 필요한 잎에만 use client. children 패턴, Server Actions 폼, 직렬화 가능한 prop 규칙과 자주 헷갈리는 케이스 정리.
Next.js App Router에서 모든 컴포넌트는 기본 Server Component입니다. 'use client'를 붙이면 그 컴포넌트와 그 자식들이 Client Component가 됩니다. 단순한 규칙이지만, "어디에 붙여야 가장 좋은가"는 처음에 헷갈립니다. 핵심은 잎(leaf)에 가깝게입니다.
1. 잎에 가깝게 붙인다
페이지 전체에 'use client'를 붙이면 정적 마크업까지 클라이언트로 보내집니다. 인터랙션이 필요한 가장 작은 단위에만 붙입니다.
1// app/blog/page.tsx — Server Component 2import { BlogList } from './BlogList'; 3import { LikeButton } from './LikeButton'; 4 5export default async function BlogPage() { 6 const posts = await getPosts(); // 서버에서 fetch 7 return ( 8 <main> 9 <BlogList posts={posts} /> {/* 정적 렌더 */} 10 <LikeButton /> {/* 클라이언트 */} 11 </main> 12 ); 13}
1// app/blog/LikeButton.tsx 2'use client'; 3import { useState } from 'react'; 4 5export function LikeButton() { 6 const [count, setCount] = useState(0); 7 return <button onClick={() => setCount(c => c + 1)}>♡ {count}</button>; 8}
BlogList는 Server, LikeButton만 Client. 클라이언트로 가는 JS 양이 최소화됩니다.
2. Client에서 Server를 자식으로 받기
'use client' 파일은 import를 통해 다른 Server Component를 들여올 수 없습니다. 단, 자식으로 받으면 OK입니다.
1// Layout.tsx 2'use client'; 3export function Layout({ children }) { 4 const [open, setOpen] = useState(false); 5 return ( 6 <div> 7 <Sidebar open={open} setOpen={setOpen} /> 8 {children} {/* Server Component 가능 */} 9 </div> 10 ); 11}
1// page.tsx — Server 2import { Layout } from './Layout'; 3import { ArticleList } from './ArticleList'; // Server 4 5export default function Page() { 6 return ( 7 <Layout> 8 <ArticleList /> {/* Server 컴포넌트 */} 9 </Layout> 10 ); 11}
children은 부모가 Client여도 Server Component를 받을 수 있습니다. "Client가 Server를 import하지 못한다"의 회피 패턴입니다.
3. 상태가 필요 없는 인터랙션은 Server에서
폼 제출은 Server Actions로 처리하면 'use client' 없이도 됩니다.
1// app/feedback/page.tsx — Server 2async function submit(formData: FormData) { 3 'use server'; 4 await db.insert({ message: formData.get('message') }); 5} 6 7export default function Page() { 8 return ( 9 <form action={submit}> 10 <input name="message" /> 11 <button>전송</button> 12 </form> 13 ); 14}
useState, onChange 같은 클라이언트 hook이 필요 없으면 Server에 두는 게 가장 단순합니다.
4. 자주 헷갈리는 케이스
- 이벤트 핸들러는 Client만.
onClick같은 prop을 Server Component에서 인라인으로 못 씁니다. - Hook은 Client만.
useState,useEffect,useContext사용 시'use client'필요. - 외부 라이브러리 —
'use client'가 안 적힌 라이브러리도 hook을 쓰면 Client 컴포넌트로 wrap해야 합니다.
1// MotionDiv.tsx 2'use client'; 3export { motion as MotionDiv } from 'framer-motion';
5. 직렬화 가능한 prop만 넘기기
Server → Client로 전달되는 prop은 직렬화 가능해야 합니다 (JSON-가능).
| OK | NG |
|---|---|
| string, number, boolean | function (event handler) |
| 배열, 객체 | Date 인스턴스, Map, Set |
| ReactElement (children) | class 인스턴스 |
이벤트 핸들러를 prop으로 넘기려면 그 컴포넌트 자체가 Client여야 합니다.
6. 정리
| 결정 | 기준 |
|---|---|
'use client' 붙이기 | hook 사용 / 이벤트 핸들러 / 브라우저 API |
| Server 유지 | 데이터 fetch / 정적 마크업 / Server Actions |
| children 패턴 | Client wrapper에 Server 자식 넣기 |
기본은 Server, 인터랙션이 필요한 가장 작은 잎에만 Client. 페이지의 정적 마크업이 클라이언트 JS로 변하지 않게 하는 게 핵심입니다.
참고: Next.js 공식 문서 — Server and Client Components.