Next.js App Router에서 Server Component와 Client Component

2025년 2월 10일


Next.js App Router에서 모든 컴포넌트는 기본적으로 Server Component다. 브라우저가 아닌 서버에서 실행되고, 결과물인 HTML만 클라이언트로 내려온다. 'use client'를 선언하면 Client Component가 된다.


Server Component

서버에서만 실행된다. 번들에 포함되지 않기 때문에 아무리 무거운 라이브러리를 써도 클라이언트 번들 사이즈에 영향을 주지 않는다.

// app/page.tsx — 기본적으로 Server Component
async function Page() {
  const data = await fetch("https://api.example.com/posts");
  const posts = await data.json();

  return <PostList posts={posts} />;
}

async/await로 데이터를 직접 가져올 수 있다. useEffect나 별도 상태가 필요 없다.

사용할 수 없는 것들:

  • useState, useEffect 등 React 훅
  • 브라우저 API(window, document)
  • 이벤트 핸들러(onClick, onChange)

Client Component

'use client'를 파일 상단에 선언하면 Client Component가 된다. 브라우저에서 실행되며 상태와 상호작용이 필요한 곳에 쓴다.

"use client";

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Client Component라고 해서 서버에서 실행되지 않는 건 아니다. 초기 HTML은 서버에서 렌더링(SSR)되고, 이후 브라우저에서 hydration된다.


나누는 기준

대부분의 컴포넌트는 Server Component로 두는 게 기본이다. 아래 경우에만 Client Component로 바꾼다.

  • useState, useReducer 등 상태가 필요할 때
  • useEffect로 사이드이펙트를 처리할 때
  • onClick, onChange 등 이벤트 핸들러가 있을 때
  • 브라우저 API를 사용할 때

Server Component 안에 Client Component

Server Component가 Client Component를 포함하는 건 자연스럽다. 반대로 Client Component 안에 Server Component를 직접 import하면 동작하지 않는다.

// 가능 — Server가 Client를 포함
function Page() {
  // Server Component
  return <Counter />; // Client Component
}
// 불가능 — Client가 Server를 직접 import
"use client";
import { ServerOnly } from "./ServerOnly"; // 이 순간 Client Component로 취급

Client Component 안에서 Server Component를 쓰고 싶다면 children으로 넘겨야 한다.

// 가능 — children으로 전달
"use client";

function Wrapper({ children }) {
  return <div>{children}</div>;
}

// Server Component에서
function Page() {
  return (
    <Wrapper>
      <ServerOnly /> {/* Server Component */}
    </Wrapper>
  );
}