안동민 개발노트

안동민 개발노트

서버 컴포넌트클라이언트 컴포넌트 사용법서버·클라이언트 컴포넌트 조합use client 지시어 활용
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 7장 : 서버, 클라이언트 컴포넌트
  5. use client 지시어 활용
  1. Next.js
  2. use client 지시어 활용

use client 지시어 활용

use client가 만드는 모듈 경계와 하위 의존성의 번들 포함 범위를 이해하고 선언 위치를 최소화합니다.

Next.js App Router에서 컴포넌트의 렌더링 환경(서버/클라이언트)을 정하는 가장 중요한 기준은 "use client" 지시어입니다.

이 문자열을 파일 맨 위에 선언하면 해당 파일과 정적 import 의존성이 클라이언트 번들 경계에 들어갑니다.

Effect와 이벤트는 브라우저에서 실행되지만 클라이언트 컴포넌트의 첫 HTML 생성에는 서버도 참여합니다. 따라서 모듈 평가·렌더 중 브라우저 전용 API를 바로 읽어도 된다는 뜻은 아닙니다.

이 절에서는 'use client'의 정확한 역할과 사용법, 그리고 내부 동작 메커니즘을 실무 관점에서 살펴봅니다.


'use client' 지시어의 역할과 중요성

App Router의 페이지와 레이아웃은 기본적으로 서버 컴포넌트입니다. 하위 모듈의 평가는 어느 경계에서 import되는지에 따라 달라집니다.

이 방식은 초기 성능, SEO, 보안 측면에서 강점이 있습니다.

다만 상호작용, 브라우저 API 접근, 클라이언트 상태 관리처럼 React의 핵심 기능 일부는 브라우저 환경에서만 가능합니다.

'use client' 지시어는 이 경계를 명확히 하여 필요한 컴포넌트만 클라이언트 번들에 포함되게 만듭니다.

'use client'의 핵심 역할
  • 컴파일러 지시어: Next.js 컴파일러와 번들러(Webpack, Turbopack)에게 해당 파일이 클라이언트 컴포넌트 그래프의 시작점임을 알립니다. 지시어가 없는 모듈도 클라이언트 경계에서 import되면 그 그래프에서 평가될 수 있습니다.
  • JavaScript 번들 포함: 'use client'가 선언된 파일은 클라이언트 컴포넌트 그래프의 진입점이 되며, 그 파일이 임포트하는 모듈은 클라이언트 번들 경계 안으로 들어갑니다. 서버에서 렌더링한 UI를 클라이언트 컴포넌트 안에 끼워 넣어야 한다면 직접 임포트가 아니라 children이나 slot 형태로 전달합니다.
  • 하이드레이션(Hydration) 트리거: 서버에서 렌더링된 HTML이 클라이언트에 도착하면, 'use client'로 표시된 컴포넌트들은 클라이언트 측 JavaScript로 하이드레이션되어 상호작용 가능한 상태가 됩니다.
  • React 훅 및 브라우저 API 사용 가능: 해당 파일 내에서 useState, useEffect, window, document 등의 클라이언트 전용 기능들을 사용할 수 있게 합니다.

'use client' 사용법

'use client' 지시어는 매우 간단하게, 컴포넌트 파일의 가장 상단에 위치해야 합니다.

다른 임포트 문이나 코드보다 먼저 와야 합니다.

src/app/my-component/InteractiveButton.tsx
"use client"; // 🚨 이 지시어는 항상 파일 맨 위에 와야 합니다.

import React, { useState } from 'react';

export default function InteractiveButton() {
  const [clicked, setClicked] = useState(false);

  const handleClick = () => {
    setClicked(true);
    alert('버튼이 클릭되었습니다!');
  };

  return (
    <button
      onClick={handleClick}
      style={{
        padding: '10px 20px',
        fontSize: '1em',
        backgroundColor: clicked ? '#28a745' : '#007bff',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        transition: 'background-color 0.3s'
      }}
    >
      {clicked ? '클릭됨!' : '클릭하세요'}
    </button>
  );
}
중요 사항
  • 정확한 위치: 'use client'는 파일의 첫 번째 비-주석(non-comment) 라인이어야 합니다.
  • 따옴표: 반드시 작은따옴표나 큰따옴표로 감싸야 합니다. ('use client', "use client")
  • 한 번만 선언: 한 번 선언되면 해당 파일이 서버와 클라이언트 모듈 그래프의 경계가 됩니다. 이 파일에서 임포트하는 하위 모듈은 클라이언트 번들 대상이 되므로, 서버 컴포넌트로 남길 UI는 부모 서버 컴포넌트에서 렌더링해 children이나 props로 넘기는 구조를 사용합니다.

모듈 경계와 실행 시점 구분

사용 위치판단
서버 그래프서버 컴포넌트는 빌드 또는 요청에서 실행됩니다. 컴포넌트 자체 코드는 브라우저 번들에 들어가지 않습니다.
클라이언트 그래프use client 파일과 import 의존성은 브라우저 코드 대상입니다. 첫 HTML은 서버에서 프리렌더될 수 있습니다.
공유 모듈지시어가 없어도 양쪽 그래프에서 import하면 각 환경에서 평가될 수 있습니다.
브라우저 APIEffect·이벤트 등 브라우저 실행 시점에서 사용합니다. 지시어가 초기 서버 렌더를 없애지는 않습니다.
전달값서버 부모는 React 직렬화 계약에 맞는 공개 데이터·React 노드를 props로 전달합니다.

'use client' 사용 시 고려사항 및 최적화

'use client'를 남용하면 Next.js App Router의 성능 이점을 잃을 수 있습니다.

가능한 한 최소한의 컴포넌트에만 'use client'를 선언하여 클라이언트 번들 크기를 작게 유지하는 것이 중요합니다.

최적화 전략

리프까지 내려가기 (Move Client Components to the Leaves): 상호작용이 필요한 클라이언트 컴포넌트를 가능한 한 컴포넌트 트리의 가장 깊은 곳(리프 노드) 으로 옮기세요.

예를 들어 헤더의 토글 버튼만 상태가 필요하면 토글 버튼에 클라이언트 경계를 둡니다.

아래 한 코드 블록에는 변경 전·후와 별도 파일을 모은 발췌가 들어 있습니다. 그대로 한 파일에 붙이면 중복 default export가 생깁니다. 각 파일을 나누고 생략된 ToggleButton·useState import 등을 준비해야 합니다.

components/Header.tsx
// Bad (전체 헤더가 클라이언트)
"use client";
export default function Header() {
  // ... 복잡한 정적 콘텐츠와 작은 토글 버튼
  return <header>... <ToggleButton /> ...</header>;
}

// Good (토글 버튼만 클라이언트)
// components/Header.tsx (서버 컴포넌트)
import ToggleButton from './ToggleButton';
export default function Header() {
  return <header>... <ToggleButton /> ...</header>;
}

// components/ToggleButton.tsx
"use client";
export default function ToggleButton() {
  const [open, setOpen] = useState(false);
  return <button onClick={() => setOpen(!open)}>Toggle</button>;
}

이렇게 하면 헤더의 대부분의 정적 HTML은 서버에서 렌더링되고, Header 자체의 코드는 서버에 남고 ToggleButton과 그 의존성이 클라이언트 그래프에 들어갑니다. Next.js 런타임 등 다른 JavaScript까지 없어지는 것은 아닙니다.

children prop 활용: 서버 부모는 서버 컴포넌트의 렌더 결과를 나타내는 React 노드를 children으로 합성합니다. RSC Payload가 이 결과를 전송하며, 서버 컴포넌트 모듈 자체를 클라이언트 번들로 바꾸지 않습니다.

이 패턴을 활용하면 클라이언트 컴포넌트가 서버 컴포넌트를 직접 임포트할 수 없는 제약을 우회하고, 서버에서 생성된 콘텐츠를 클라이언트에서 동적으로 조작할 수 있습니다.

layout.tsx (서버 컴포넌트)
import ClientWrapper from './ClientWrapper';
import ServerOnlyContent from './ServerOnlyContent';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <ClientWrapper>
      <ServerOnlyContent /> {/* 서버에서 렌더링되어 children으로 전달됨 */}
      {children} {/* 페이지 콘텐츠도 children으로 전달될 수 있음 */}
    </ClientWrapper>
  );
}
ClientWrapper.tsx
"use client";
// 이 파일은 클라이언트 컴포넌트입니다.

export default function ClientWrapper({ children }: { children: React.ReactNode }) {
  // children은 서버 렌더 결과나 다른 React 노드를 나타냅니다.
  return (
    <div>
      {/* 여기서 children을 조건부 렌더링하거나 다른 상호작용 추가 */}
      {children}
    </div>
  );
}

서버 전용 코드 분리: 클라이언트 컴포넌트 내에서 서버 전용 코드를 실수로 포함하지 않도록 주의하세요.

예를 들어, 클라이언트 컴포넌트가 민감한 API 키를 직접 포함하거나, 데이터베이스 연결 로직을 포함해서는 안 됩니다.

데이터 페칭이 필요하다면, 클라이언트에서 사용할 수 있는 /api 라우트 핸들러를 호출하거나, 서버 컴포넌트에서 데이터를 받아 props로 전달하는 방식을 사용해야 합니다.

아래는 원문의 Header와 ToggleButton이 각각 어느 클라이언트 경계에 들어가는지 비교한 import 관계입니다. 화살표는 import이며 화면의 DOM 포함 관계와 구분합니다.

Header에서 ToggleButton으로 내린 클라이언트 경계

원문의 두 대안을 import 관계로 비교합니다. Header에 use client를 두면 ToggleButton도 클라이언트 그래프에 들어갑니다. Header를 서버에 두고 ToggleButton에 경계를 두면 Header 자체의 코드는 서버에 남습니다. 실제 전체 번들 크기는 의존성과 빌드 결과로 확인해야 합니다.

Header에서 ToggleButton으로 내린 클라이언트 경계원문의 두 대안을 import 관계로 비교합니다. Header에 use client를 두면 ToggleButton도 클라이언트 그래프에 들어갑니다. Header를 서버에 두고 ToggleButton에 경계를 두면 Header 자체의 코드는 서버에 남습니다. 실제 전체 번들 크기는 의존성과 빌드 결과로 확인해야 합니다.Header에 선언버튼에 선언HeaderClient 경계HeaderServerToggleButtonClient 의존성ToggleButtonClient 경계importimport
Header에서 ToggleButton으로 내린 클라이언트 경계원문의 두 대안을 import 관계로 비교합니다. Header에 use client를 두면 ToggleButton도 클라이언트 그래프에 들어갑니다. Header를 서버에 두고 ToggleButton에 경계를 두면 Header 자체의 코드는 서버에 남습니다. 실제 전체 번들 크기는 의존성과 빌드 결과로 확인해야 합니다.Header에 선언버튼에 선언HeaderClient 경계HeaderServerToggleButtonClient 의존성ToggleButtonClient 경계importimport

서버·클라이언트 컴포넌트 조합

이전 페이지

React 상태 관리 기초

다음 페이지

이 페이지의 목차

'use client' 지시어의 역할과 중요성'use client' 사용법모듈 경계와 실행 시점 구분'use client' 사용 시 고려사항 및 최적화