본문으로 건너뛰기

안동민 개발노트

본문 시작

서버 컴포넌트

서버 컴포넌트가 브라우저 번들과 비밀 정보 노출을 줄이며 서버에서 데이터와 UI를 만드는 과정을 이해합니다.

Next.js 16 App Router의 가장 큰 변화는 서버 컴포넌트(Server Components) 도입입니다.

이 아키텍처는 React 애플리케이션의 성능, 보안, 개발 경험을 함께 개선하는 방향을 제시했습니다.

클라이언트 컴포넌트 중심 개발에 익숙하다면, 서버 컴포넌트의 개념과 작동 원리를 정확히 이해하는 것이 App Router를 제대로 활용하는 첫걸음입니다.

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

서버 컴포넌트는 코드가 아니라 결과를 보냅니다

데이터 조회와 UI 계산은 서버에서 끝내고, 브라우저에는 화면을 그릴 결과와 상호작용에 필요한 최소 JavaScript만 전달합니다.

  1. 요청 경로 선택

    · ROUTE App Router가 URL에 맞는 page와 layout 트리를 찾습니다.

  2. 데이터와 UI 계산

    · RENDER DB, secret, 서버 의존성을 가까이 두고 React 결과를 만듭니다.

  3. 전송 형식으로 변환

    · STREAM RSC payload와 초기 HTML을 준비해 가능한 부분부터 보냅니다.


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

서버 컴포넌트(Server Components)는 이름 그대로 서버 환경에서 렌더링되고 실행되는 React 컴포넌트입니다.

기존의 React 컴포넌트(현재 Next.js App Router에서는 클라이언트 컴포넌트라고 명명)는 항상 브라우저(클라이언트)에서 실행되는 것과 대조적입니다.

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

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

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

성능 최적화 (번들 크기 감소 및 초기 로딩 속도 향상)
  • JavaScript 번들 크기 감소: 서버 컴포넌트의 코드는 클라이언트에 전송되지 않으므로, 사용자가 다운로드해야 하는 JavaScript 양이 줄어듭니다. 이는 특히 초기 페이지 로딩 속도에 결정적인 영향을 미칩니다.
  • 데이터 페칭 워터폴(Waterfall) 방지: 서버에서 필요한 모든 데이터를 병렬로 미리 가져온 후 HTML을 생성하므로, 클라이언트에서 여러 번의 데이터 요청이 순차적으로 발생하는 워터폴 현상을 줄여줍니다.
  • 스트리밍 및 점진적 로딩: Next.js는 서버 컴포넌트의 HTML을 청크(Chunk) 단위로 클라이언트에 스트리밍할 수 있습니다. 이는 페이지의 일부가 먼저 보인 후 데이터 로딩이 완료되는 대로 점진적으로 콘텐츠가 나타나게 하여 사용자 경험을 향상시킵니다.
보안 강화
  • 민감 정보 노출 방지: 데이터베이스 자격 증명, API 키 등 서버 전용 정보가 클라이언트 번들에 포함될 위험이 없습니다.
  • 직접적인 백엔드 접근: Route Handler를 따로 만들 필요 없이 서버 컴포넌트에서 직접 백엔드 로직을 실행할 수 있어 개발 복잡성을 줄이고 안전하게 데이터를 처리할 수 있습니다.
개발 경험 향상
  • 단순화된 데이터 페칭: async/await를 사용하여 useStateuseEffect 없이도 직관적으로 데이터를 가져올 수 있습니다. 이는 getServerSidePropsgetStaticProps보다 유연합니다.
  • 클라이언트-서버 간 경계 추상화: 개발자는 어떤 컴포넌트가 서버에서 실행될지, 어떤 컴포넌트가 클라이언트에서 실행될지 명시적으로 제어할 수 있습니다.
  • SSR(Server-Side Rendering)의 기본화: 모든 페이지와 레이아웃이 기본적으로 서버에서 렌더링되므로, SSR의 이점(빠른 초기 로딩, SEO)을 별도의 설정 없이 누릴 수 있습니다.

서버 컴포넌트 작동 원리

Server Component는 데이터와 브라우저 번들의 경계를 만든다

서버에서 끝낼 일은 HTML로 남기고 상호작용에 필요한 최소 값과 코드만 경계를 건넌다.

  1. server
    DB · fetch · secret

    민감 로직과 큰 의존성을 서버에서 실행

  2. output
    HTML · RSC payload

    표현 결과와 client 위치를 전송

  3. props
    Serializable data

    plain value만 실행 경계를 통과

  4. client
    Event · state · API

    브라우저 반응이 필요한 작은 leaf

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

사용자 요청: 사용자가 Next.js 애플리케이션의 URL을 요청합니다.

서버에서 렌더링 시작: Next.js 서버는 해당 URL에 매핑되는 라우트 세그먼트의 서버 컴포넌트들을 실행합니다.

데이터 페칭 및 직렬화: 서버 컴포넌트 내부에서 async/await를 사용하여 데이터 페칭이 이루어집니다.

이 데이터는 JSON으로 직렬화됩니다.

HTML 및 React Server Component Payload (RSC Payload) 생성
  • 서버 컴포넌트들은 UI를 HTML로 렌더링합니다.
  • 동시에, 클라이언트 컴포넌트가 포함된 부분은 HTML 대신 특별한 데이터 형식인 React Server Component Payload (RSC Payload)로 변환됩니다. 이 페이로드는 클라이언트 컴포넌트를 렌더링하는 데 필요한 정보(프롭스, 참조할 클라이언트 컴포넌트 파일 경로 등)를 담고 있습니다.

클라이언트로 전송: 생성된 HTML과 RSC Payload가 클라이언트(브라우저)로 전송됩니다.

클라이언트에서 하이드레이션 및 인터랙션
  • 클라이언트는 서버에서 받은 HTML을 즉시 표시합니다.
  • 이후 RSC Payload를 기반으로 클라이언트 컴포넌트들이 다운로드되고, 이들이 HTML에 하이드레이션(Hydration)되어 상호작용 가능한 애플리케이션이 됩니다.
  • 사용자가 클릭이나 입력과 같은 상호작용을 하면, 클라이언트 컴포넌트 내의 JavaScript가 실행됩니다.
서버 결과와 클라이언트 상호작용은 두 전송 경로로 합쳐진다

HTML은 먼저 화면을 만들고 RSC payload와 client bundle이 필요한 leaf의 이벤트를 연결한다.

  1. 1
    Server render

    DB·파일·내부 API를 읽어 컴포넌트 실행

  2. 2
    HTML

    JavaScript를 기다리지 않고 초기 화면 표시

  3. 3
    RSC Payload

    client 위치와 직렬화 props 참조 전달

  4. 4
    Client bundle

    use client 경계 아래 코드만 다운로드

  5. 5
    Hydration

    클릭·입력·상태 변경 이벤트 연결


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

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

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 번들과는 무관하게 서버에서 처리됩니다.

서버 컴포넌트를 설계할 때는 “서버에서 끝낼 일”과 “브라우저에서 이어갈 일”을 먼저 나누는 것이 좋습니다.

데이터 접근, 민감 정보, 무거운 렌더링은 서버에 남기고, 상태와 이벤트가 필요한 작은 부분만 클라이언트 경계로 분리하면 번들 크기와 보안 면에서 모두 유리합니다.

서버 권한은 plain props를 거쳐 작은 client island에 닿는다

브라우저에는 상호작용에 필요한 데이터만 보내고 비밀과 데이터 접근 권한은 서버에 남긴다.

  1. server
    DB · secret

    외부에 노출되면 안 되는 조회와 정책

  2. render
    Static markup

    상호작용 없는 큰 화면 구조

  3. boundary
    Plain props

    날짜·class·함수를 안전한 값으로 변환

  4. island
    Button · form

    state와 event가 필요한 최소 UI

  5. hydrate
    Stable initial value

    서버·클라이언트 첫 결과를 같게 유지

서버 컴포넌트는 데이터 접근, 보안 경계, 번들 크기 절감을 한곳에서 해결하므로 기본값으로 두고 필요한 상호작용만 클라이언트로 넘기는 방식이 가장 안정적입니다.

기본값은 서버, 상호작용이 필요한 작은 섬만 클라이언트다

데이터 위치·브라우저 기능·번들 비용을 차례로 물으면 컴포넌트 실행 경계가 선명해진다.

  1. DB·secret
    Server Component

    민감 정보와 큰 의존성을 서버에 남김

  2. 초기 표현
    Server Component

    상호작용 없는 콘텐츠를 HTML로 바로 제공

  3. state·event
    Client island

    클릭·입력으로 즉시 변하는 최소 영역만 분리

  4. browser API
    Client island

    window·localStorage가 필요한 leaf에서만 사용

서버 컴포넌트는 Next.js App Router의 성능, 보안, 개발 편의성을 극대화하는 핵심 개념입니다.

이를 통해 개발자는 사용자에게 더 빠르고 효율적인 웹 경험을 제공하는 동시에, 백엔드 로직과 프론트엔드 UI를 통합하여 개발 복잡성을 줄일 수 있습니다.

서버를 기본값으로 두고 상호작용만 경계 밖으로 보낸다

App Router 컴포넌트는 기본적으로 서버에서 실행된다. 브라우저 기능이 필요한 가장 작은 하위 트리에만 'use client' 경계를 둔다.

  1. Bundle

    서버 컴포넌트 자체는 클라이언트 번들에 포함되지 않는다.

  2. Import 방향

    client가 import한 하위 트리는 번들 후보가 되므로 경계를 낮게 둔다.

  3. Waterfall

    서버 await가 직렬로 이어지지 않도록 데이터 준비를 병렬화한다.

마지막으로 서버 컴포넌트가 번들 크기, 데이터 접근, 보안 경계를 어떻게 바꾸는지 정리합니다.

기본값은 서버, 브라우저 이유가 있을 때만 client다

실행 위치를 먼저 정하고 데이터와 상호작용 사이의 전달값을 최소화한다.

  1. DB·secret
    Server Component

    서버 데이터에 직접 접근하고 결과만 렌더

  2. 정적 UI
    Server Component

    벤트 없는 markup은 브라우저 JS 불필요

  3. state·event
    Client leaf

    사용자 입력에 즉시 반응하는 작은 영역

  4. window API
    Client leaf

    브라우저에서만 존재하는 기능