
최근 Next.js의 /app 디렉토리 구조 채택, loading.tsx의 Suspense 내장화, 그리고 RSC의 도입이 본격화됨에 따라 React는 새로운 렌더링 패러다임으로 전환되고 있습니다. 그 중심에 있는 개념이 바로 React Server Components(RSC) 와 Suspense입니다.
SSR, CSR, ISR, SSG 등 기존 방식의 한계를 넘어서, 클라이언트 번들을 최소화하고 초기 렌더링을 빠르게 제공하는 흐름은 프론트엔드 개발의 핵심 전략이 되고 있다고 합니다.
RSC와 Suspense는 단순한 기능을 넘어 실제 프로젝트에 적용할 가치가 있는 기술이라고 생각해서 이번 주제로 선정하게 되었습니다.
React의 App Router 환경에서는 파일마다 서버 컴포넌트와 클라이언트 컴포넌트가 명확히 분리됩니다.
기본적으로 모든 컴포넌트는 서버 컴포넌트이며, 상단에 'use client' 지시어를 선언한 경우만 클라이언트 컴포넌트로 동작합니다.
이 둘의 차이는 렌더링 위치와 기능적 제약에서 명확히 드러납니다.
| 구분 항목 | Server Component | Client 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로 상호작용을 가능하게 만드는 과정을 의미합니다.
RSC는 CSR과 SSR처럼 렌더링 방식 중 하나입니다. 서버를 컴포넌트화한 것으로, 기존의 서버에서 하던 작업들을 컴포넌트 단위에서 할 수 있기 때문에 서버 사이드 데이터에 접근하거나 UI를 서버에서 그려줄 수 있습니다.
RSC는 단 한번만 렌더링됩니다. 서버에서 단 한번 생성되며 이는 브라우저에 전송되어 바뀌지 않기 때문에 브라우저 API와 React API를 사용하지 못합니다.

Server: renderToReadableStream() → HTML + RSC payload
Client: hydrateRoot() → 렌더링
| 항목 | RSC | 클라이언트 컴포넌트 |
|---|---|---|
| 실행 위치 | 서버 | 브라우저 |
| 번들 포함 | X | O |
| 상태 훅 사용 | X | O |
| DB 직접 접근 | O | X |
| 보안성 | 높음 | 낮음 |
기존 React 앱은 클라이언트 중심으로 작동하기 때문에 서버의 이점을 충분히 활용하지 못합니다.
메타 프레임워크를 통해 SSR을 구현하더라도, 이는 주로 페이지 최초 진입 시에만 의미가 있으며, 이후에는 대부분의 렌더링이 다시 CSR로 이루어집니다.

클라이언트 환경에서 최대한 많은 작업을 수행하기 위해 앱의 구조는 점점 복잡해져만 갔고 여러 문제들이 생겨났습니다.
이러한 문제는 서버에서 할 수 있는 일을 클라이언트가 모두 떠안았기 때문에 발생했습니다.
React 팀은 “React에서 서버를 더 효과적으로 활용할 수 있는 방법은 없을까?”라는 질문에서 출발했습니다.
그리고 그 해답으로, 컴포넌트 모델 자체를 서버로 확장하는 방식, 즉 RSC를 도입하게 됩니다.
hooks로 function component가 자연스럽게 stateful/stateless 변환을 하는 것처럼, 'use strict'와 닮은 'use client' directive로 function component가 자연스럽게 server/client 변환을 할 수 있습니다.



서버는 데이터 베이스, GraphQL, 파일시스템 등 데이터 원본에 직접 접근 할 수 있습니다. 서버는 공용 api 엔드 포인트를 거치지 않고 데이터를 직접 가져올 수 있고, 일반적으로 데이터 소스와 더 가깝게 배치되어 있으므로 브라우저보다 더 빠르게 데이터를 가져올 수 있습니다.
브라우저는 자바스크립트 번들링된 모든 코드를 다운로드 해야하는 것 과 달리, 서버는 모든 의존성을 다운로드 할 필요가 없기 때문에 (미리 다운로드 해놓고 수시로 재사용이 가능하므로) 무거운 코드 모듈을 저렴하게 사용할 수 있습니다.
즉, RSC를 활영하면 서버와 브라우저가 각자 잘 수행하는 작업을 처리할 수 있습니다. 서버 컴포넌트는 데이터를 가져오고 콘텐츠를 렌더링하는데 초점을 맞출 수 있으며 페이지 로딩 속도가 빨라지고 자바스크립트 번들 크기가 작아져서 사용자의 환경이 향상될 수 있습니다.
fallback UI 출력<Suspense fallback={<Loading />}>
<ServerComponent />
</Suspense>
<Suspense> 외부 UI 먼저 출력 가능<main>
<Header /> // Shell
<Suspense fallback={<Skeleton />}>
<Comments /> // Streamed filler
</Suspense>
</main>
/app
└─ page.tsx // 가장 위 RSC 책임
└─ components/
├─ ServerList.tsx // async 각종 fetch 전달
├─ SkeletonList.tsx // Suspense fallback
└─ ClientForm.tsx // 'use client'
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>
);
}
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>
);
}
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가 늦더라도 상호작용은 지연 없이 되는 느낌”
→ 실사용자 경험에 긍정적 효과
loading.tsx와 fallback을 적극 활용React Server Components + Suspense는 단순히 트렌디한 기능이 아니라, 렌더링 성능과 사용성, 개발 편의성을 동시에 충족하는 핵심 기술입니다. 직접 사용해본 결과, 스켈레톤과 Streaming fallback, 코드 분할 구조를 구현했을 때 실질적인 성능 개선이 있었으며, Next.js의 App Router 환경에서는 특히나 RSC 도입이 쉽고 유의미한 성능 향상을 체감할 수 있었습니다.