icon
7장 : 서버, 클라이언트 컴포넌트

서버 컴포넌트 이해하기

Next.js 15 App Router의 가장 혁신적이고 근본적인 변화는 바로 서버 컴포넌트(Server Components) 의 도입입니다. 이 새로운 아키텍처는 React 생태계의 미래를 제시하며, 애플리케이션의 성능, 보안, 그리고 개발 경험을 대폭 개선했습니다. 클라이언트 컴포넌트 중심의 기존 React 애플리케이션 개발 방식에 익숙하다면, 서버 컴포넌트의 개념과 작동 원리를 정확히 이해하는 것이 Next.js App Router를 효과적으로 사용하는 데 필수적입니다.

이 절에서는 서버 컴포넌트가 무엇이며, 왜 필요한지, 그리고 어떻게 동작하는지에 대해 심도 있게 알아보겠습니다.


서버 컴포넌트란 무엇인가요?

서버 컴포넌트(Server Components) 는 이름 그대로 서버 환경에서 렌더링되고 실행되는 React 컴포넌트입니다. 기존의 React 컴포넌트(현재 Next.js App Router에서는 클라이언트 컴포넌트라고 명명)는 항상 브라우저(클라이언트)에서 실행되는 것과 대조적입니다.

서버 컴포넌트의 핵심 특징

  • 기본값: Next.js App Router에서 .tsx, .jsx 등 모든 컴포넌트 파일은 명시적으로 "use client" 지시어를 사용하지 않는 한 기본적으로 서버 컴포넌트로 간주됩니다.
  • 서버에서만 실행: 컴포넌트 내부의 코드가 클라이언트 번들에 포함되지 않고 서버에서만 실행됩니다.
  • 제로(0) 번들 사이즈: 서버 컴포넌트 코드는 클라이언트에 전송되지 않으므로, 클라이언트 측 JavaScript 번들 크기를 획기적으로 줄일 수 있습니다. 이는 특히 모바일 환경에서 초기 로딩 속도에 큰 영향을 줍니다.
  • 직접적인 백엔드 접근: 데이터베이스, 파일 시스템, 내부 API 등 서버 전용 자원에 직접 접근하고 데이터를 가져올 수 있습니다. API 라우트를 만들 필요 없이 컴포넌트 내에서 바로 데이터를 페칭할 수 있다는 의미입니다.
  • async/await 지원: 비동기 함수로 정의할 수 있어 async/await를 사용하여 데이터 페칭을 매우 직관적으로 작성할 수 있습니다.
  • 상태 및 효과 없음: useState, useEffect와 같은 React 훅을 사용할 수 없습니다. (이는 클라이언트 컴포넌트의 역할입니다.)
  • 이벤트 핸들러 없음: onClick, onChange와 같은 클라이언트 사이드 이벤트 핸들러를 직접 추가할 수 없습니다.
  • React Context 사용 불가: React Context를 직접 사용할 수 없습니다. (하지만 서버 컴포넌트 간에는 prop 드릴링 또는 서버 전용 캐싱 패턴을 사용할 수 있습니다.)

서버 컴포넌트가 필요한 이유

서버 컴포넌트는 기존의 클라이언트 중심 렌더링 방식이 가졌던 여러 문제점을 해결하고 다음과 같은 이점을 제공합니다.

  1. 성능 최적화 (번들 크기 감소 및 초기 로딩 속도 향상)

    • JavaScript 번들 크기 감소: 서버 컴포넌트의 코드는 클라이언트에 전송되지 않으므로, 사용자가 다운로드해야 하는 JavaScript 양이 줄어듭니다. 이는 특히 초기 페이지 로딩 속도에 결정적인 영향을 미칩니다.
    • 데이터 페칭 워터폴(Waterfall) 방지: 서버에서 필요한 모든 데이터를 병렬로 미리 가져온 후 HTML을 생성하므로, 클라이언트에서 여러 번의 데이터 요청이 순차적으로 발생하는 '워터폴' 현상을 줄여줍니다.
    • 스트리밍 및 점진적 로딩: Next.js는 서버 컴포넌트의 HTML을 청크(Chunk) 단위로 클라이언트에 스트리밍할 수 있습니다. 이는 페이지의 일부가 먼저 보인 후 데이터 로딩이 완료되는 대로 점진적으로 콘텐츠가 나타나게 하여 사용자 경험을 향상시킵니다.
  2. 보안 강화

    • 민감 정보 노출 방지: 데이터베이스 자격 증명, API 키 등 서버 전용 정보가 클라이언트 번들에 포함될 위험이 없습니다.
    • 직접적인 백엔드 접근: API 라우트를 따로 만들 필요 없이 서버 컴포넌트에서 직접 백엔드 로직을 실행할 수 있어 개발 복잡성을 줄이고 안전하게 데이터를 처리할 수 있습니다.
  3. 개발 경험 향상

    • 단순화된 데이터 페칭: async/await를 사용하여 useStateuseEffect 없이도 직관적으로 데이터를 가져올 수 있습니다. 이는 getServerSidePropsgetStaticProps보다 유연합니다.
    • 클라이언트-서버 간 경계 추상화: 개발자는 어떤 컴포넌트가 서버에서 실행될지, 어떤 컴포넌트가 클라이언트에서 실행될지 명시적으로 제어할 수 있습니다.
    • SSR(Server-Side Rendering)의 기본화: 모든 페이지와 레이아웃이 기본적으로 서버에서 렌더링되므로, SSR의 이점(빠른 초기 로딩, SEO)을 별도의 설정 없이 누릴 수 있습니다.

서버 컴포넌트 작동 원리

Next.js 앱이 서버 컴포넌트를 사용하여 페이지를 렌더링하는 과정은 다음과 같습니다.

  1. 사용자 요청: 사용자가 Next.js 애플리케이션의 URL을 요청합니다.
  2. 서버에서 렌더링 시작: Next.js 서버는 해당 URL에 매핑되는 라우트 세그먼트의 서버 컴포넌트들을 실행합니다.
  3. 데이터 페칭 및 직렬화: 서버 컴포넌트 내부에서 async/await를 사용하여 데이터 페칭이 이루어집니다. 이 데이터는 JSON으로 직렬화됩니다.
  4. HTML 및 React Server Component Payload (RSC Payload) 생성
    • 서버 컴포넌트들은 UI를 HTML로 렌더링합니다.
    • 동시에, 클라이언트 컴포넌트가 포함된 부분은 HTML 대신 특별한 데이터 형식인 React Server Component Payload (RSC Payload) 로 변환됩니다. 이 페이로드는 클라이언트 컴포넌트를 렌더링하는 데 필요한 정보(프롭스, 참조할 클라이언트 컴포넌트 파일 경로 등)를 담고 있습니다.
  5. 클라이언트로 전송: 생성된 HTML과 RSC Payload가 클라이언트(브라우저)로 전송됩니다.
  6. 클라이언트에서 하이드레이션 및 인터랙션
    • 클라이언트는 서버에서 받은 HTML을 즉시 표시합니다.
    • 이후 RSC Payload를 기반으로 클라이언트 컴포넌트들이 다운로드되고, 이들이 HTML에 "하이드레이션(Hydration)"되어 상호작용 가능한 애플리케이션이 됩니다.
    • 사용자가 클릭이나 입력과 같은 상호작용을 하면, 클라이언트 컴포넌트 내의 JavaScript가 실행됩니다.

서버 컴포넌트 예시 (복습)

이전 장에서 다뤘던 많은 페이지 컴포넌트와 레이아웃 컴포넌트가 이미 서버 컴포넌트로 작성되었습니다.

src/app/dashboard/layout.tsx
// src/app/dashboard/layout.tsx (서버 컴포넌트 예시)
// "use client" 지시어가 없으므로 서버 컴포넌트입니다.

import Link from 'next/link';

// 이 함수는 서버에서 실행됩니다.
async function getUserInfo() {
  console.log('Fetching user info on server...');
  // 실제 DB 접근 또는 내부 API 호출
  await new Promise(resolve => setTimeout(resolve, 500)); // 지연 시뮬레이션
  return { username: '서버 사용자', role: 'admin' };
}

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const userInfo = await getUserInfo(); // 서버에서 데이터 페칭

  return (
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      <aside style={{ width: '250px', padding: '20px', backgroundColor: '#f0f4f8', borderRight: '1px solid #e0e0e0' }}>
        <h2>관리자 대시보드</h2>
        <p>환영합니다, {userInfo.username}님!</p> {/* 서버에서 가져온 데이터 사용 */}
        <p>권한: {userInfo.role}</p>
        <nav style={{ marginTop: '20px' }}>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ marginBottom: '10px' }}><Link href="/dashboard"></Link></li>
            <li style={{ marginBottom: '10px' }}><Link href="/dashboard/settings">설정</Link></li>
            {/* 여기에 다른 서버 컴포넌트나 클라이언트 컴포넌트를 자식으로 렌더링 */}
          </ul>
        </nav>
      </aside>
      <main style={{ flexGrow: 1, padding: '20px' }}>
        {children} {/* 하위 페이지나 레이아웃이 여기에 렌더링됩니다. */}
      </main>
    </div>
  );
}

DashboardLayoutasync 함수로 정의되어 getUserInfo를 호출하고, 이 함수는 서버에서 실행되어 데이터를 가져옵니다. 이 모든 과정은 클라이언트의 JavaScript 번들과는 무관하게 서버에서 처리됩니다.

서버 컴포넌트는 Next.js App Router의 성능, 보안, 개발 편의성을 극대화하는 핵심 개념입니다. 이를 통해 개발자는 사용자에게 더 빠르고 효율적인 웹 경험을 제공하는 동시에, 백엔드 로직과 프론트엔드 UI를 통합하여 개발 복잡성을 줄일 수 있습니다.