minstudio

서버 컴포넌트(RSC) vs 클라이언트 컴포넌트

Next.js 13부터 도입된 App Router 환경에서는 기본적으로 모든 컴포넌트가 서버 컴포넌트(React Server Components)입니다. 이 혁신적인 패러다임은 서버와 클라이언트가 각자 가장 잘하는 일에 집중하게 하여, 페이지 로딩 속도를 극대화하고 보안을 강화합니다.

⚖️ Server Components vs Client Components 🌐 Server Component (기본) ✅ 데이터베이스 직접 접근 (안전함) ✅ 무거운 라이브러리 서버에서만 실행 ✅ 브라우저로 전송되는 JS 용량 0 Bytes ✅ 초기 페이지 로딩 속도 극대화 (SEO) ❌ useState, onClick 등 상호작용 불가 💻 Client Component 🚨 최상단에 "use client" 명시 필수 ✅ useState, useEffect 사용 가능 ✅ onClick, onChange 등 이벤트 처리 ✅ 브라우저 전용 API (window, document) ⚠️ 불필요한 사용 시 JS 번들 크기 증가

1. 서버 컴포넌트 (Server Components) 란?

Next.js의 모든 컴포넌트는 아무런 선언을 하지 않으면 기본적으로 서버 컴포넌트로 동작합니다. 말 그대로 브라우저가 아닌 서버에서만 실행되는 컴포넌트입니다.

  • 뛰어난 성능 (Zero Bundle Size): 렌더링된 HTML 껍데기만 클라이언트로 보내므로 클라이언트가 다운로드해야 할 JavaScript 코드가 획기적으로 줄어듭니다.
  • 안전한 백엔드 접근: 컴포넌트 내에서 데이터베이스 비밀번호나 API Key를 직접 다루더라도 브라우저에 노출되지 않으므로 매우 안전합니다.
  • 검색 엔진 최적화 (SEO): 완성된 HTML을 즉시 제공하므로 구글과 같은 크롤러 봇이 내용을 완벽하게 수집할 수 있습니다.

2. 클라이언트 컴포넌트 (Client Components) 와 "use client"

사용자와의 동적인 상호작용(Interaction)이 필요할 때는 클라이언트 컴포넌트를 사용해야 합니다. 파일의 가장 첫 번째 줄에 "use client"; 라고 적어주면 클라이언트 컴포넌트로 전환됩니다.

  • 버튼 클릭(onClick), 입력창 변경(onChange) 등 이벤트 리스너가 필요할 때
  • React의 상태 관리(useState, useReducer)나 생명주기 훅(useEffect)이 필요할 때
  • window, document 등 브라우저 전용 API를 써야 할 때

주의점: "use client"를 남발하면 Next.js App Router의 성능적 이점을 잃게 됩니다. 전체 페이지를 "use client"로 만들기보다는, 꼭 필요한 버튼이나 폼(Form) 요소만 잘게 쪼개서 클라이언트 컴포넌트로 분리하는 것이 핵심 최적화 기법입니다.

3. 어떻게 조합해야 할까? (Composition Pattern)

현대적인 Next.js 개발의 핵심은 서버 컴포넌트를 베이스로 깔고, 그 안의 인터랙티브한 작은 조각들을 클라이언트 컴포넌트로 끼워 넣는 것(Colocation)입니다.

올바른 패턴: <페이지(서버)> 안에서 <좋아요 버튼(클라이언트)> 컴포넌트를 import 해서 사용하기.

피해야 할 패턴: 클라이언트 컴포넌트 내부에서 서버 컴포넌트를 import 하기. (클라이언트 컴포넌트 하위에 있는 모든 컴포넌트는 자동으로 클라이언트 환경에서 실행되어 버립니다.)

// ==========================================
// 📂 app/page.tsx (서버 컴포넌트)
// ==========================================
import LikeButton from './LikeButton';

export default async function BlogPostPage() {
  // DB에서 데이터 직접 조회 (서버단에서 안전하고 빠르게 실행됨)
  // 서버 컴포넌트이므로 "use client"가 필요 없습니다.
  const post = await db.post.findUnique({ id: 1 });
  
  return (
    <article className="max-w-2xl mx-auto p-8">
      {/* 정적인 콘텐츠는 서버에서 렌더링되어 완성된 HTML로 전송됨 (SEO 완벽) */}
      <h1 className="text-3xl font-bold">{post.title}</h1>
      <div className="mt-4 text-gray-700">{post.content}</div>
      
      <div className="mt-8 border-t pt-4">
        {/* 사용자와 상호작용하는 부분만 클라이언트 컴포넌트로 분리하여 삽입 */}
        <LikeButton initialCount={post.likes} />
      </div>
    </article>
  );
}

// ==========================================
// 📂 app/LikeButton.tsx (클라이언트 컴포넌트)
// ==========================================
"use client"; // 파일의 최상단에 선언하여 클라이언트 컴포넌트임을 명시!

import { useState } from 'react';

export default function LikeButton({ initialCount }: { initialCount: number }) {
  // useState와 onClick 같은 이벤트는 클라이언트 컴포넌트에서만 사용 가능합니다.
  const [likes, setLikes] = useState(initialCount);

  return (
    <button 
      onClick={() => setLikes(likes + 1)}
      className="px-4 py-2 bg-pink-500 text-white rounded-lg hover:bg-pink-600 transition"
    >
      ❤️ 좋아요 {likes}
    </button>
  );
}
서버 컴포넌트(RSC) vs 클라이언트 컴포넌트 | Minstudio