반응형

요약: Next.js App Router에서 Server Component와 Client Component를 나누는 기준을 실행 위치, 상호작용, props 직렬화 관점에서 정리합니다. use client를 언제 붙여야 하는지 간단한 카운터 예제로 확인합니다.

Next.js App Router를 쓰다 보면 가장 자주 만나는 질문이 있습니다. “이 컴포넌트에 use client를 붙여야 하나?”입니다. 결론부터 말하면 기준은 파일 위치가 아니라 어느 런타임에서 실행되어야 하는가입니다.

Server Component는 서버에서 데이터와 HTML을 준비하는 역할에 가깝고, Client Component는 브라우저에서 상태와 상호작용을 담당합니다. 이 경계를 이해하면 use client 에러, 이벤트 핸들러 전달 오류, 불필요한 번들 증가를 훨씬 빨리 잡을 수 있습니다.

기본값은 Server Component다

Next.js App Router에서는 컴포넌트가 기본적으로 Server Component로 동작합니다. 서버 컴포넌트는 브라우저로 보내기 전에 서버에서 렌더링됩니다. 그래서 서버 전용 데이터 패칭, 파일 시스템 접근, 비밀 키를 사용하는 작업처럼 브라우저에서 직접 처리하면 안 되는 일을 맡기 좋습니다.

반대로 Server Component는 브라우저 이벤트를 직접 처리하지 못합니다. onClick, useState, useEffect처럼 사용자 입력이나 브라우저 상태에 반응해야 하는 코드는 Client Component에서 다뤄야 합니다.

use client는 클라이언트 경계를 만든다

Client Component로 만들고 싶다면 파일 맨 위에 'use client'를 선언합니다.

'use client';

import { useState } from 'react';

type CounterProps = {
  initialCount: number;
};

export default function Counter({ initialCount }: CounterProps) {
  const [count, setCount] = useState(initialCount);

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

이 컴포넌트는 useState를 사용하고 버튼 클릭 이벤트를 처리합니다. 따라서 브라우저에서 실행되어야 하며, Client Component가 맞습니다.

반면 페이지 컴포넌트는 단순히 초기값을 준비해서 넘기기만 합니다.

import Counter from './Counter';

export default function Page() {
  const initialCount = 3;

  return <Counter initialCount={initialCount} />;
}

이 구조에서는 서버가 숫자 3을 준비하고, 클라이언트가 클릭 이후의 상태 변화를 처리합니다. 역할이 깔끔하게 나뉩니다.

Server에서 Client로 넘길 수 있는 값

Server Component에서 Client Component로 props를 넘길 수는 있습니다. 다만 React가 복원할 수 있는 직렬화 가능한 값이어야 합니다.

문자열, 숫자, boolean, 배열, 평범한 객체 같은 값은 괜찮습니다. 위 예제의 initialCount도 숫자이므로 문제없이 전달됩니다.

하지만 일반 함수, DOM 노드, class instance처럼 실행 환경에 묶인 값은 넘길 수 없습니다. 예를 들어 Server Component에서 onClick 함수를 만들어 Client Component에 props로 넘기려 하면 문제가 됩니다. 서버에서 만든 함수는 브라우저 이벤트 핸들러로 그대로 살아 있을 수 없기 때문입니다.

헷갈릴 때 던질 질문

실무에서는 “이 컴포넌트가 인터랙션이 있나?”보다 조금 더 정확한 질문이 필요합니다.

  • 클릭, 입력, 스크롤, localStorage, window, React 상태와 effect가 필요한가?
  • DB 조회, 서버 전용 API 호출, 인증 토큰 사용처럼 브라우저로 보내면 안 되는 로직인가?
  • Server Component에서 Client Component로 넘기는 props가 직렬화 가능한 값인가?

서버는 UI에 필요한 데이터를 준비하고, 클라이언트는 그 데이터에 행동을 붙인다고 생각하면 경계가 훨씬 선명해집니다.

Server Action은 예외처럼 보이지만 별도 규칙이다

use server로 선언한 Server Action은 일반 함수와 다릅니다. React와 Next.js가 이 함수를 서버 호출로 해석할 수 있도록 별도의 계약을 제공합니다.

그래서 “함수는 절대 못 넘긴다”라고 외우기보다는, 일반 함수와 Server Action을 구분하는 것이 좋습니다. 일반 이벤트 핸들러는 Client Component 안에서 정의하고, 서버에서 실행되어야 하는 변경 작업은 Server Action으로 분리하는 식입니다.

정리

Next.js App Router에서 Server Component와 Client Component를 나누는 핵심은 실행 위치입니다. 서버는 데이터와 렌더링 결과를 준비하고, 클라이언트는 사용자의 행동과 상태 변화를 처리합니다.

use client는 필요한 곳에만 붙이는 것이 좋습니다. 무작정 클라이언트 컴포넌트로 올리면 브라우저 번들이 커지고, 반대로 서버 컴포넌트에 상호작용 코드를 두면 훅과 이벤트 핸들러에서 막힙니다.

오늘 예제처럼 서버에서 initialCount 같은 직렬화 가능한 값만 넘기고, 클릭 로직은 Counter 안에 두면 경계가 자연스럽게 정리됩니다.

참고

반응형