Frontend/Next.js
SSR과 Server Component(RSC) 이해하기
JTB
2025. 11. 3. 18:29
Next.js와 React 18+ 환경에서 SSR(Server-Side Rendering) 과 Server Component(RSC) 의 차이를 정확히 이해하는 것은 성능 최적화와 클라이언트 번들 최소화 측면에서 매우 중요하다. 많은 개발자가 혼동하는 영역이므로, 핵심 개념과 예제 코드를 중심으로 깔끔하게 정리한다.
1. SSR (Server-Side Rendering)
1. 정의
SSR은 클라이언트의 요청 시점마다 서버에서 HTML을 생성해 전달하는 방식이다. Next.js의 App Router 환경에서는 getServerSideProps 대신 fetch의 cache: 'no-store' 옵션을 사용하여 SSR을 구현한다.
2. 작동 방식
- 클라이언트가 페이지(URL)를 요청한다.
- Next.js 서버는 해당 요청 시점에 React 컴포넌트를 실행하고, HTML을 서버에서 생성한다.
- 생성된 HTML이 클라이언트로 전송되어 초기 화면이 즉시 표시된다.
- 이어서 JS 번들이 로드되고, React가 서버에서 렌더된 HTML과 클라이언트 렌더 트리를 비교하여 Hydration(하이드레이션) 을 수행한다 — 즉, 이벤트 핸들러와 상태를 연결해 인터랙션을 활성화한다.
3. 특징
- 페이지 단위로 서버 렌더링을 수행한다.
- 서버에서 데이터를 호출하여 HTML에 반영할 수 있다.
- HTML과 JS 번들이 모두 클라이언트로 전달된다.
- SEO 최적화와 초기 로딩 속도 개선에 유리하다.
4. 예제 코드 (App Router 방식)
cache: 'no-store'를 사용하면 Next.js는 이 fetch를 요청마다 새로 실행하여, 기존 getServerSideProps와 동일한 SSR 효과를 낸다.
// app/page.tsx
export default async function Page() {
// ✅ SSR: 요청 시마다 서버에서 새로 실행
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // SSR 방식
});
const posts = await res.json();
return (
<main>
<h1>서버 사이드 렌더링 (SSR)</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}
5. ISR(Incremental Static Regeneration) 버전
ISR은 “매 요청마다”가 아니라 “일정 시간마다” 서버에서 새로 렌더링한다.
export default async function Page() {
// ✅ ISR: 일정 주기마다 서버에서 페이지를 재생성
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 60초마다 새로 생성
});
const posts = await res.json();
return (
<main>
<h1>ISR (부분적 SSR)</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}
2. Server Component (RSC, ‘use server’)
1. 정의
Server Component는 React 18+에서 도입된 서버 전용 컴포넌트로, 클라이언트로 JS 번들을 거의 보내지 않고 서버에서만 렌더링되는 구조를 가진다. 인터랙션 없는 컴포넌트에 최적화되어 있다.
2. 작동 방식
- 서버에서 React Server Component를 실행한다.
- HTML과 직렬화된 Props만 클라이언트로 전달한다.
- 클라이언트는 이 HTML을 그대로 렌더링하며, 필요한 경우 'use client' 컴포넌트와 결합해 인터랙션을 추가한다. — 즉, 인터랙션이 필요하면 Server Component 안에 'use client' 컴포넌트를 섞어서 사용하면 된다.
3. 특징
- 컴포넌트 단위로 서버 렌더링을 수행한다.
- 서버에서 DB나 API를 직접 호출할 수 있다.
- 클라이언트 JS 번들을 최소화해 성능을 극대화한다.
- 클라이언트와 서버 간 경계가 명확하여 데이터 접근이 안전하다.
4. 예제 코드 (Next.js App Router)
Server Component는 클라이언트에서 조작이 필요 없는 컴포넌트를 서버에서만 렌더링하여 클라이언트 번들을 최소화한다.
// app/components/ServerMessage.tsx
'use server';
async function getServerData() {
const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
return res.json();
}
export default async function ServerMessage() {
const data = await getServerData();
return (
<div>
<h2>Server Component 예제</h2>
<p>서버에서 받아온 데이터: {data.message}</p>
</div>
);
}
// app/page.tsx
import ServerMessage from './components/ServerMessage';
export default function Page() {
return (
<main>
<h1>Next.js Server Component</h1>
<ServerMessage /> {/* 클라이언트 JS 최소화 */}
</main>
);
}
3. SSR vs Server Component 비교 정리
| 구분 | SSR | Server Component (RSC) |
| 렌더링 단위 | 페이지 단위 | 컴포넌트 단위 |
| 렌더링 시점 | 요청마다 (cache: ‘no-store’) | 빌드 또는 요청 시 서버 실행 |
| 데이터 호출 | 서버 가능 | 서버 가능 |
| 클라이언트 JS | HTML + JS 번들 전달 | HTML + Props만 전달 (JS 최소) |
| 목적 | SEO, 초기 로딩 속도 개선 | 성능 최적화, JS 번들 최소화 |
| 상호작용 | Hydration으로 추가 가능 | 'use client' 컴포넌트와 조합 |
| 대표 코드 | fetch(..., { cache: 'no-store' }) | 'use server' 함수 또는 기본 Server Component |
4. 핵심 요약
- SSR은 요청마다 서버에서 HTML을 새로 생성하며, 클라이언트로 JS 번들을 함께 전달한다. → SEO와 초기 로딩 속도를 개선하는 데 적합하다.
- Server Component는 컴포넌트 단위로 서버에서 렌더링하며, 클라이언트로 HTML과 Props만 전달해 번들 크기를 극적으로 줄인다. → 서버 데이터 접근이 많은 대규모 애플리케이션의 성능을 최적화한다.
- App Router 환경에서 SSR을 구현하려면 fetch(..., { cache: 'no-store' }) 또는 next: { revalidate: 0 } 옵션을 사용한다.