← 전체 글로 돌아가기

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'를 붙일 이유가 없다.

컴포넌트를 처음 만들 때부터 작게 나누는 습관을 들이면, 나중에 리팩토링하며 범위를 줄이는 수고를 피할 수 있다.