Next.js
Next.js에서 Client Component 범위를 최소화하는 방법
'use client'를 파일 상단에 붙이면 그 파일 전체와 하위 트리가 클라이언트 번들에 포함된다. 컴포넌트를 작게 쪼개야 하는 이유가 여기에 있다.
Next.js App Router에서 'use client'를 선언하면 해당 파일부터 그 아래 트리 전체가 클라이언트 번들에 들어간다. 서버에서 렌더링되어야 할 정적인 마크업까지 함께 묶이면 초기 JS 번들이 불필요하게 커지고, hydration 비용도 함께 올라간다.
흔히 저지르는 실수
가장 흔한 패턴은 이렇다. 페이지 파일 상단에 'use client'를 붙이고 그 안에 헤더, 본문, 인터랙티브 버튼을 모두 넣는 것이다.
// ❌ 페이지 전체가 클라이언트 번들에 포함된다
'use client'
export default function PostPage({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={post.id} />
</article>
)
}
<h1>과 <p>는 상태나 이벤트가 없으니 서버 컴포넌트로 렌더링해도 된다. LikeButton 하나 때문에 페이지 전체를 클라이언트로 내리는 셈이다.
인터랙티브 부분만 분리한다
해결책은 간단하다. 'use client'가 필요한 최소 단위로만 분리하면 된다.
// ✅ 서버 컴포넌트 (app/posts/[id]/page.tsx)
export default async function PostPage({ params }) {
const post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={post.id} />
</article>
)
}
// ✅ 클라이언트 컴포넌트 (components/LikeButton.tsx)
'use client'
import { useState } from 'react'
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
)
}
이렇게 하면 PostPage 자체는 서버에서 렌더링되고, LikeButton만 클라이언트 번들에 들어간다.
번들 크기를 실제로 확인한다
빌드 후 번들 분석으로 효과를 직접 확인할 수 있다.
npm run build
# .next/analyze/ 폴더가 생기도록 설정하거나
# ANALYZE=true npm run build (next-bundle-analyzer 사용 시)
next build 출력에서 ƒ 표시는 서버 컴포넌트, ○는 정적, λ는 동적 서버 렌더링을 뜻한다. 의도치 않게 클라이언트 번들이 커졌다면 @next/bundle-analyzer로 어느 컴포넌트가 문제인지 찾는다.
판단 기준
Client Component가 필요한 경우는 명확하다: useState, useEffect, 브라우저 이벤트 핸들러(onClick, onChange), 브라우저 전용 API(localStorage, window)가 필요할 때다. 이 중 하나도 해당하지 않는다면 'use client'를 붙일 이유가 없다.
컴포넌트를 처음 만들 때부터 작게 나누는 습관을 들이면, 나중에 리팩토링하며 범위를 줄이는 수고를 피할 수 있다.