React Server Components + Suspense

두밥비·2025년 6월 13일

article

목록 보기
19/23

▪️주제 선정 이유

최근 Next.js의 /app 디렉토리 구조 채택, loading.tsx의 Suspense 내장화, 그리고 RSC의 도입이 본격화됨에 따라 React는 새로운 렌더링 패러다임으로 전환되고 있습니다. 그 중심에 있는 개념이 바로 React Server Components(RSC)Suspense입니다.

SSR, CSR, ISR, SSG 등 기존 방식의 한계를 넘어서, 클라이언트 번들을 최소화하고 초기 렌더링을 빠르게 제공하는 흐름은 프론트엔드 개발의 핵심 전략이 되고 있다고 합니다.

RSC와 Suspense는 단순한 기능을 넘어 실제 프로젝트에 적용할 가치가 있는 기술이라고 생각해서 이번 주제로 선정하게 되었습니다.


▪️이 글에서 다룰 핵심 내용

  • RSC의 정의 및 동작 원리
  • Suspense를 활용한 Streaming Rendering 구조
  • Next.js 환경에서의 실습 코드 및 구조 분석
  • 실전에서 RSC 도입 시 마주치는 이슈와 대응 전략

▪️Server Components vs Client Components 정리

React의 App Router 환경에서는 파일마다 서버 컴포넌트클라이언트 컴포넌트가 명확히 분리됩니다.

기본적으로 모든 컴포넌트는 서버 컴포넌트이며, 상단에 'use client' 지시어를 선언한 경우만 클라이언트 컴포넌트로 동작합니다.

이 둘의 차이는 렌더링 위치와 기능적 제약에서 명확히 드러납니다.


Server Components vs Client Components

구분 항목Server ComponentClient Component
렌더링 위치서버에서 렌더링브라우저에서 렌더링
상태 관리 Hook사용 불가 (useState, useEffect 등 금지)사용 가능
이벤트 처리불가 (버튼 클릭 등 인터랙션 불가)가능
DB, API 직접 접근가능 (서버 내 코드이므로)불가 (fetch 등 간접 호출만 가능)
보안안전 (API Key 등 비노출)보안 위험 존재 (코드 번들에 포함)
JS 번들 크기포함되지 않음 (번들 크기 감소 효과)포함됨 (브라우저에 전달되어 번들 크기 증가)
성능 최적화 관점초기 렌더링 속도에 유리인터랙션 중심 UI에 필요
React 특징 사용 제한useContext, useRef 등 일부 hook도 제한됨모든 React 기능 사용 가능

사용 예시

서버 컴포넌트 (기본 동작)

export default async function ProductList() {
  const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
  const data = await res.json();
  return <div>{data.map((p) => <p>{p.name}</p>)}</div>;
}

클라이언트 컴포넌트 (상호작용 필요 시)

'use client';

import { useState } from 'react';

export default function Form() {
  const [input, setInput] = useState('');
  return (
    <input value={input} onChange={(e) => setInput(e.target.value)} />
  );
}

요약

  • 서버 컴포넌트는 "정적인 데이터를 빠르게 보여주는 데 적합"
  • 클라이언트 컴포넌트는 "사용자 상호작용을 필요로 할 때 필수"
  • 두 컴포넌트를 조합하여, 렌더링 성능과 사용자 경험을 모두 확보하는 것이 핵심입니다.

서버사이드 렌더링이 아니다?

RSC는 정확히 말해서 서버사이드 렌더링이 아닙니다.

물론 둘다 명칭에서 '서버'가 포함되어 있어서 혼란의 여지가 있지만…. RSC를 사용하면 SSR을 사용할 필요가 없고, 반대의 경우도 마찬가지입니다. SSR은 응답 받은 트리를 raw html로 렌더링하기 위한 환경을 시뮬레이션 합니다. 즉, 서버와 클라이언트 컴포넌트를 별도로 구별하지 않고 동일한 방식으로 렌더링합니다.

물론 SSR와 RSC를 함께 사용하여 서버 컴포넌트를 서버 컴포넌트를 서버쪽에서 렌더링을 하고, 브라우저에서는 적절하게 하이드레이션을 거치게 할 수 있습니다.

하이드레이션(Hydration) - React와 같은 클라이언트 프레임워크에서 SSR(서버 사이드 렌더링) 이후 클라이언트 측에서 JS로 상호작용을 가능하게 만드는 과정을 의미합니다.


▪️React Server Components란?

RSC는 CSR과 SSR처럼 렌더링 방식 중 하나입니다. 서버를 컴포넌트화한 것으로, 기존의 서버에서 하던 작업들을 컴포넌트 단위에서 할 수 있기 때문에 서버 사이드 데이터에 접근하거나 UI를 서버에서 그려줄 수 있습니다.

RSC는 단 한번만 렌더링됩니다. 서버에서 단 한번 생성되며 이는 브라우저에 전송되어 바뀌지 않기 때문에 브라우저 API와 React API를 사용하지 못합니다.

  • 서버에서만 실행되는 React 컴포넌트
  • 브라우저에 JS 번들로 포함되지 않음
  • HTML 결과만 전달되어, 클라이언트 부담 최소화

동작 흐름

Server: renderToReadableStream() → HTML + RSC payload

Client: hydrateRoot() → 렌더링

효과

  • 초기 로딩 속도 향상 (JS bundle 감소)
  • SEO 향상
  • 보안 강화 (API Key 등 서버 내 처리 가능)

특징 비교하기

항목RSC클라이언트 컴포넌트
실행 위치서버브라우저
번들 포함XO
상태 훅 사용XO
DB 직접 접근OX
보안성높음낮음

왜 RSC가 만들어졌을까❓ - 클라이언트 중심 앱

기존 React 앱은 클라이언트 중심으로 작동하기 때문에 서버의 이점을 충분히 활용하지 못합니다.

메타 프레임워크를 통해 SSR을 구현하더라도, 이는 주로 페이지 최초 진입 시에만 의미가 있으며, 이후에는 대부분의 렌더링이 다시 CSR로 이루어집니다.

클라이언트 환경에서 최대한 많은 작업을 수행하기 위해 앱의 구조는 점점 복잡해져만 갔고 여러 문제들이 생겨났습니다.


❗ 클라이언트 중심 React 구조에서 발생한 주요 문제들

  1. 번들 사이즈 증가
    • 모든 로직을 브라우저에서 처리해야 하므로, JS 번들이 비대해짐
    • 초기 로딩 시간 증가 및 사용자의 네트워크 환경에 따라 UX 저하
  2. 보안 이슈
    • 클라이언트에서 API 호출 로직을 직접 다루다 보니, 토큰이나 API Key 노출 위험 존재
    • 민감한 비즈니스 로직도 브라우저에 노출됨
  3. SEO 한계
    • CSR 환경에서는 DOM이 JS 실행 후 구성되므로, 크롤러가 제대로 읽지 못함
    • SSR로 이를 보완해도 최초 진입 시에만 서버 렌더링, 이후에는 CSR
  4. 데이터 중복 fetch
    • 클라이언트에서 페이지마다 fetch를 수행하므로, 서버 렌더링에서도 같은 데이터를 다시 가져와야 하는 이중 fetch 발생
  5. 앱 아키텍처의 복잡화
    • 상태 관리, 캐싱, Suspense, 코드 스플리팅 등을 모두 클라이언트에서 처리해야 하므로, 앱 구조가 복잡해지고 유지 보수성 저하
  6. 성능 병목
    • 무거운 연산(필터링, 정렬, 계산 등)을 브라우저가 감당해야 함
    • 저사양 기기, 모바일 환경에서는 UX 크게 악화

이러한 문제는 서버에서 할 수 있는 일을 클라이언트가 모두 떠안았기 때문에 발생했습니다.

React 팀은 “React에서 서버를 더 효과적으로 활용할 수 있는 방법은 없을까?”라는 질문에서 출발했습니다.

그리고 그 해답으로, 컴포넌트 모델 자체를 서버로 확장하는 방식, 즉 RSC를 도입하게 됩니다.

  1. 서버에서 실행되기 때문에 서버의 이점을 충분히 사용 가능
  2. 서버와 클라이언트가 Component를 통해 마찰 없이 매끄럽게 결합
  3. Component 모델은 그대로지만 서버에서 실행된다는 개념만 추가

hooks로 function component가 자연스럽게 stateful/stateless 변환을 하는 것처럼, 'use strict'와 닮은 'use client' directive로 function component가 자연스럽게 server/client 변환을 할 수 있습니다.


▪️어떤 구조적 차이를 만들었을까?

  • 기존 React 구조에서는 서버가 단순히 HTML을 렌더링하는 수준에 그쳤고, 모든 인터랙션과 데이터 로직은 클라이언트가 감당해야 했습니다. 결과적으로 앱의 복잡도와 번들 사이즈가 크게 증가했습니다.

  • React 팀은 서버에서 더 많은 역할을 할 수 있도록 새로운 트리를 도입했습니다. 이 Server Tree는 데이터와 연산을 처리하고 React Tree로 props를 전달하며, 클라이언트는 그 결과만 받아 렌더링하게 됩니다.

  • 나아가 클라이언트 트리도 분리되어, 상호작용이 필요한 일부 컴포넌트만 클라이언트에서 JS로 번들링됩니다. 이로 인해 초기 로딩 속도는 더욱 빨라지고, 상호작용 성능도 유지됩니다.

▪️브라우저에 대신, 서버에서 렌더링을 해야 하는 이유

서버는 데이터 베이스, GraphQL, 파일시스템 등 데이터 원본에 직접 접근 할 수 있습니다. 서버는 공용 api 엔드 포인트를 거치지 않고 데이터를 직접 가져올 수 있고, 일반적으로 데이터 소스와 더 가깝게 배치되어 있으므로 브라우저보다 더 빠르게 데이터를 가져올 수 있습니다.

브라우저는 자바스크립트 번들링된 모든 코드를 다운로드 해야하는 것 과 달리, 서버는 모든 의존성을 다운로드 할 필요가 없기 때문에 (미리 다운로드 해놓고 수시로 재사용이 가능하므로) 무거운 코드 모듈을 저렴하게 사용할 수 있습니다.

즉, RSC를 활영하면 서버와 브라우저가 각자 잘 수행하는 작업을 처리할 수 있습니다. 서버 컴포넌트는 데이터를 가져오고 콘텐츠를 렌더링하는데 초점을 맞출 수 있으며 페이지 로딩 속도가 빨라지고 자바스크립트 번들 크기가 작아져서 사용자의 환경이 향상될 수 있습니다.


▪️Suspense와 Streaming Rendering

Suspense란?

  • 비동기 데이터 로딩 시 대기 상태를 처리하는 UI 추상화
  • Fallback 컴포넌트를 지정하여 사용자에게 로딩 상태 제공
  • Suspense 기본 구조
    • 비동기 로딩 중 fallback UI 출력
    • 서버 컴포넌트/클라이언트 컴포넌트 양쪽에서 사용 가능
<Suspense fallback={<Loading />}>
  <ServerComponent />
</Suspense>

Streaming Rendering이란?

  • 서버에서 HTML을 조각 단위로 스트리밍
  • LCP 개선, 빠른 TTFB 구현
  • Streaming + Shell-Filler
    • ServerComponent가 늦게 도착해도 <Suspense> 외부 UI 먼저 출력 가능
    • 실시간 스트리밍처럼 유저가 빠르게 부분 UI 경험 가능
<main>
  <Header /> // Shell
  <Suspense fallback={<Skeleton />}>
    <Comments /> // Streamed filler
  </Suspense>
</main>

▪️Next.js 실습 구조: App Router + RSC + Suspense

폴더 구조

/app
 └─ page.tsx              // 가장 위 RSC 책임
 └─ components/
     ├─ ServerList.tsx    // async 각종 fetch 전달
     ├─ SkeletonList.tsx  // Suspense fallback
     └─ ClientForm.tsx    // 'use client'

📂 ServerList.tsx (RSC)

import Image from "next/image";
import { Product } from "@/src/types/product";

export default async function ServerList() {
  const res = await fetch('https://dummyjson.com/products?limit=100', {
    cache: 'no-store',
  });
  const data = await res.json();

  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
      {data.products.map((item: Product) => (
        <div key={item.id} className="bg-white rounded-sm shadow-md p-5 border hover:shadow-lg transition">
          <Image
            width={100} height={40}
            src={item.thumbnail}
            alt={item.title}
            className="w-full h-40 object-cover rounded-sm mb-3 border border-gray-200"
          />
          <h2 className="text-lg font-semibold text-[#333]">{item.title}</h2>
          <p className="text-sm text-gray-600 line-clamp-2">{item.description}</p>
          <div className="mt-2 flex justify-between text-sm text-gray-500">
            <span>💰 ${item.price}</span>
            <span>{item.rating}</span>
          </div>
        </div>
      ))}
    </div>
  );
}

SkeletonList.tsx

export default function SkeletonList() {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
      {Array(9).fill(0).map((_, i) => (
        <div key={i} className="animate-pulse bg-gray-100 h-60 rounded shadow-inner" />
      ))}
    </div>
  );
}

page.tsx

import { Suspense } from 'react';
import ServerList from './components/ServerList';
import SkeletonList from './components/SkeletonList';
import ClientForm from './components/ClientForm';

export default function HomePage() {
  return (
    <main className="min-h-screen bg-gray-50 flex flex-col items-center p-10">
      <div className="w-full max-w-3xl space-y-6">
        <h1 className="text-3xl font-bold text-center">제품 리스트</h1>
        <Suspense fallback={<SkeletonList />}>
          <ServerList />
        </Suspense>
        <ClientForm />
      </div>
    </main>
  );
}

▪️성능 측정 결과 비교

Lighthouse: 78점 Lighthouse: 94점

“사용자 입장에서 빠르게 콘텐츠를 보고, JS가 늦더라도 상호작용은 지연 없이 되는 느낌”

→ 실사용자 경험에 긍정적 효과


활용 전략

  • 기존 프로젝트를 점진적으로 RSC + Suspense 구조로 리팩토링 가능
  • 마이크로 UX 개선을 위해 loading.tsx와 fallback을 적극 활용
  • RSC + Suspense는 React 성능 최적화의 새로운 기본이 될 수 있음

▪️결론

  • 페이지 초기 로딩 개선
  • JS bundle 크기 감소 (그리고 hydration error 없음)
  • 코드 분할 및 유지보수 향상
  • 서버/클라이언트 컴포넌트 분리로 설계가 명확해짐

React Server Components + Suspense는 단순히 트렌디한 기능이 아니라, 렌더링 성능과 사용성, 개발 편의성을 동시에 충족하는 핵심 기술입니다. 직접 사용해본 결과, 스켈레톤과 Streaming fallback, 코드 분할 구조를 구현했을 때 실질적인 성능 개선이 있었으며, Next.js의 App Router 환경에서는 특히나 RSC 도입이 쉽고 유의미한 성능 향상을 체감할 수 있었습니다.


▪️참고자료

profile
개발새발

0개의 댓글