React Server Components

김강민·2025년 9월 26일

개발

목록 보기
28/32

React 18에서 처음 공개된 서버 컴포넌트(React Server Components, RSC)는 이제 Next.js App Router의 기본 컴포넌트 모델이 되었다.서버 컴포넌트는 React 애플리케이션을 만드는 방식을 근본적으로 바꾸고, 성능과 사용자 경험을 크게 향상시킬 수 있는 강력한 패러다임이다.

🥊 서버 컴포넌트(RSC) vs 서버 사이드 렌더링(SSR)

가장 먼저 명확히 해야할 점은 서버 컴포넌트 (RSC)는 서버사이드 렌더링(SSR)과는 완전히 다르다는 것이다. 둘다 ‘서버’라는 단어가 공통적으로 들어가지만, 목적과 동작 방식은 서로 다르다.

  • SSR: 서버와 클라이언트 컴포넌트를 구분하지 않고, 모든 컴포넌트를 서버에서 순수한 HTML 문자열로 만들어 브라우저에 보낸다. 주된 목적은 빠른 첫 화면 로딩 (FCP)과 검색 엔진 최적화(SEO)이다.
  • RSC: 서버 컴포넌트만 서버에서 실행해서, HTML이 아닌 RSC 페이로드 (Payload)라는 특별한 데이터 포맷으로 만든다. 주된 목적은 JavaScript 번들 사이즈 감소와 효율적인 데이터 로딩이다.

두 기술은 서로 다른 목적을 가지며, Next.js 에서는 이 둘을 함께 사용하여 최고의 성능을 이끌어낼 수 있다.

🍀 서버 컴포넌트를 사용하는 이유 (주요 특징 및 장점)

  1. 클라이언트 JavaScript 번들 사이즈 감소

    서버 컴포넌트는 코드의 브라우저로 전송되지 않기 때문에 클라이언트가 다운로드 해야할 JavaScript의 양이 줄어든다.

    예를 들어, 날짜 포맷팅 라이브러리 date-fns 처럼 무거운 라이브러리를 서버 컴포넌트에서만 사용하면, 해당 라이브러리 코드는 클라이언트 번들에 포함되지 않아 초기 페이지 로딩 속도가 비약적으로 빨라진다.

  2. 서버 자원에 직접 접근

    데이터베이스, 내부 API, 파일 시스템 같은 서브 측 자원에 직접 접근할 수 있으며, API 키 같은 민감한 데이터를 안전하게 보관할 수 있다.

  3. async/await 를 사용한 직접적인 데이터 페칭

    서버 컴포넌트는 컴포넌트 자체를 async 함수로 선언할 수 있다. 이로 인해 useEffectuseState 같은 훅(hook) 없이도, 컴포넌트 최상단에서 바로 데이터를 가져오는 것이 가능하다.

    // app/posts/page.tsx (서버 컴포넌트)
    async function getPosts() {
      const res = await fetch('https://.../posts');
      return res.json();
    }
    
    // 컴포넌트 자체가 async 함수
    export default async function PostsPage() {
      const posts = await getPosts();
    
      return (
        <ul>
          {posts.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
    }
  4. 자동 캐싱 및 렌더링 최적화

    서버 컴포넌트의 렌더링 결과는 Next.js에 의해 자동으로 캐싱된다. 동일한 요청에 대해서는 다시 렌더링할 필요 없이 캐시된 결과를 사용하기 때문에 응답 속도가 매우 빠르다.

🍀 서버 컴포넌트는 '어떻게' 동작할까?

How React server components work: an in-depth guide

서버 컴포넌트 내부 동작의 핵심은 RSC 페이로드라는 특별한 데이터 포맷에 있다.

1단계: 서버 렌더링 (RSC 페이로드 생성)

사용자가 페이지를 요청하면, 서버에서 React가 렌더링을 시작한다.

  • 서버 컴포넌트를 만나면: 데이터베이스 조회 같은 서버 작업을 수행하고, 그 결과를 HTML이 아닌 RSC 페이로드에 기록한다.
  • 클라이언트 컴포넌트를 만나면: 렌더링하지 않고, 대신 "이 자리에는 /src/components/Counter.tsx 파일의 default export를 렌더링해줘" 와 같은 '모듈 참조(module reference)' 객체를 페이로드에 기록한다.

2단계: 데이터 스트리밍

서버에서 생성된 이 RSC 페이로드는 완성될 때까지 기다리지 않고, 생성되는 즉시 스트림(Stream) 형태로 브라우저에 전송됩니다. 덕분에 사용자는 전체 페이지가 로드되기 전에도, 먼저 준비된 UI를 볼 수 있다.

3단계: 클라이언트 렌더링 (조립 및 하이드레이션)

브라우저의 React는 스트리밍되는 RSC 페이로드를 받아서 화면에 그리기 시작한다.

  • 서버 컴포넌트의 결과물은 그대로 화면에 표시한다.
  • 클라이언트 컴포넌트의 '모듈 참조'를 만나면, 해당 컴포넌트의 JavaScript 코드를 다운로드해서 그 자리에 렌더링하고, 상호작용이 가능하도록 이벤트를 연결(하이드레이션)한다.

🤔 언제 서버 컴포넌트를 쓰고, 언제 클라이언트 컴포넌트를 써야할까?

Next.js 공식문서 - Server Components

Next.js 공식 문서에서는 해당 이슈에 대한 확실한 해결책을 제공한다. 기본적으로 Next.js의 모든 컴포넌트는 서버 컴포넌트로 시작하고, 꼭 필요한 경우에만 "use client";를 추가하여 클라이언트 컴포넌트로 전환하는 것을 권장한다.

기능별 컴포넌트 선택

기능서버 컴포넌트클라이언트 컴포넌트
데이터 페칭✅ (권장)
백엔드 자원 직접 접근✅ (권장)
민감 정보 보관 (토큰, API 키)✅ (권장)
무거운 의존성 사용✅ (권장)
사용자 이벤트 처리 (onClick, onChange)✅ (필수)
상태 관리 (useState, useReducer)✅ (필수)
라이프사이클 훅 (useEffect)✅ (필수)
브라우저 전용 API (window, localStorage)✅ (필수)
Context API (상태 관리)✅ (필수)
커스텀 훅 (훅 사용)✅ (필수)

🍀 서버와 클라이언트의 공존: 제약 사항과 패턴

서버 컴포넌트는 서버에서만 렌더링되므로, useState, useEffect 같은 훅이나 onClick 같은 이벤트 리스너를 사용할 수 없다. 이러한 인터랙티브한 기능이 필요할 때, 우리는 파일 최상단에 "use client"; 지시어를 추가하여 클라이언트 컴포넌트를 사용한다.

여기서 중요한 규칙은, 클라이언트 컴포넌트는 서버 컴포넌트를 직접 import할 수 없다는 것이다. 서버 컴포넌트의 코드는 브라우저로 전송되지 않기 때문이다.

하지만 컴포지션(Composition), 즉 children prop을 통해 서버 컴포넌트를 전달받아 렌더링하는 것은 가능하다.

// OuterServerComponent.tsx (서버 컴포넌트)
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';

export default function OuterServerComponent() {
  // ClientComponent에 ServerComponent를 자식으로 전달
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

이러한 패턴을 통해 우리는 서버와 클라이언트의 장점을 모두 취하는 유연한 컴포넌트 트리를 구성할 수 있다.

✏️ 결론

서버 컴포넌트는 단순히 서버에서 렌더링하는 기술을 넘어, '일은 서버에서 하고, 결과물에 대한 설계도(RSC 페이로드)만 클라이언트에 보내주는' 새로운 패러다임이다. Next.js와 같은 메타 프레임워크는 이 모든 복잡한 과정을 자동화해주므로, 개발자는 각 컴포넌트의 역할에만 집중하여 더 빠르고, 안전하며, 강력한 웹 애플리케이션을 만들 수 있다.

profile
인생은 프레임워크처럼, 공부는 라이브러리처럼

0개의 댓글