이 튜토리얼에서는 React 서버 컴포넌트(React Server Components, RSC)의 동작 원리를 살펴보고, 그 개념을 바탕으로 앱에 간단한 조회수 카운터를 직접 만들어 보겠습니다. 바로 구현 단계로 넘어가고 싶은 분은 프로젝트 설정 섹션부터 읽으셔도 좋습니다.
React 서버 컴포넌트 이해하기
RSC가 어떻게 동작하는지 제대로 이해하려면 먼저 클라이언트 사이드 렌더링(CSR)과 서버 사이드 렌더링(SSR)을 간단히 짚고 넘어갈 필요가 있습니다.
클라이언트 사이드 렌더링(CSR)
CSR에서는 대부분의 렌더링 작업이 클라이언트, 즉 브라우저에서 수행됩니다.
- 사용자가 사이트를 요청합니다.
- 서버가 HTML 파일과 CSS·JS 파일 링크를 전송합니다.
- 클라이언트가 JS 리소스를 다운로드합니다.
- 클라이언트가 내용 없는 페이지를 우선 렌더링합니다.
- 클라이언트가 서버의 API에서 데이터를 가져옵니다.
- 클라이언트가 콘텐츠와 함께 페이지를 다시 렌더링합니다.

서버 사이드 렌더링(SSR)
SSR에서는 요청이 들어올 때마다 서버가 완전한 HTML 콘텐츠를 생성해 클라이언트에 전달합니다.
- 사용자가 사이트를 요청합니다.
- 서버가 실제 HTML 파일을 렌더링하지만, 아직 인터랙티브하지 않습니다.
- 클라이언트가 JS 리소스를 다운로드합니다.
- 클라이언트에서
hydrateRoot()함수를 사용해 이벤트 리스너, 상태 등 React 컴포넌트의 인터랙티브 요소를 서버가 생성한 기존 DOM 요소에 연결합니다. 이 과정을 하이드레이션(hydration)이라고 부릅니다. - 클라이언트가 서버의 API에서 데이터를 가져옵니다.
- 클라이언트가 콘텐츠와 함께 페이지를 다시 렌더링합니다.

SSR의 장점
- 더 나은 SEO: 검색 엔진 크롤러가 완전히 렌더링된 HTML을 읽을 수 있어 SEO 친화적입니다.
- 빠른 초기 로딩: 서버가 완성된 HTML을 전송하기 때문에 사용자가 페이지 콘텐츠를 빠르게 볼 수 있습니다.
SSR의 단점
- 느린 페이지 이동: 새로운 페이지를 요청할 때마다 서버와의 왕복 통신과 전체 HTML 리로드가 필요합니다.
- 증가하는 서버 부하: 렌더링 작업 대부분을 서버가 담당하므로, 여러 요청을 처리하려면 더 많은 리소스가 필요할 수 있습니다.
React 서버 컴포넌트(RSC)
RSC는 컴포넌트를 서버에서 온전히 렌더링할 수 있게 해주는 React의 기능입니다.
- 서버가 데이터를 가져옵니다.
- 서버가 콘텐츠와 함께 앱을 렌더링합니다.
- 클라이언트가 JS 리소스를 다운로드합니다.
- 클라이언트에서 하이드레이션이 일어납니다.
참고: 서버 컴포넌트는 서버에서만 렌더링되며, JS 번들에 포함되지 않고 하이드레이션도 되지 않습니다. 따라서 위의 3번과 4번 단계는 클라이언트 컴포넌트에만 해당됩니다.

RSC의 장점
- 효율적인 데이터 처리: 데이터 페칭이 서버, 즉 데이터베이스에 가까운 곳에서 수행됩니다. 클라이언트-서버 간 불필요한 왕복이 줄어 성능이 향상됩니다.
- 클라이언트 JavaScript 크기 감소: 서버 컴포넌트는 클라이언트에 JavaScript를 전혀 전송하지 않으므로 전체 번들 크기가 줄어들어 성능이 개선됩니다.
- 더 빠른 초기 페이지 로딩: 서버에서 미리 렌더링된 컴포넌트 덕분에 클라이언트가 완성된 HTML을 더 빨리 받아 FCP(First Contentful Paint) 시간이 단축됩니다.
- 향상된 SEO: 콘텐츠가 서버 측에서 완전히 렌더링되므로 검색 엔진이 콘텐츠를 크롤링하고 색인하기 쉽습니다.
RSC의 한계
- 상태와 라이프사이클: 서버 컴포넌트는 서버에서 렌더링되기 때문에
useState,useEffect같은 브라우저 API나 훅을 사용할 수 없습니다. - 클라이언트 측 인터랙티비티 부재: 서버 컴포넌트는 동적 업데이트나 인터랙션을 위한 용도가 아닙니다. 인터랙티브한 부분에는 클라이언트 컴포넌트를 사용해야 합니다.
React 서버 컴포넌트 활용하기
Next.js에서의 React 서버 컴포넌트
Next.js는 기본적으로 서버 컴포넌트를 사용하며, 세 가지 방식으로 렌더링됩니다.
- 정적 렌더링(기본값): 라우트가 빌드 시점에, 또는 데이터 재검증(revalidation) 후 백그라운드에서 렌더링됩니다.
- 동적 렌더링: 라우트가 각 사용자의 요청 시점에 렌더링됩니다.
- 스트리밍: UI가 서버에서 점진적으로 렌더링되어 전달됩니다.
React 서버 컴포넌트는 언제 사용해야 할까?
다음과 같은 경우 RSC를 사용하는 것이 좋습니다:
- 자주 변경되지 않는 정적이고 비인터랙티브한 콘텐츠를 다룰 때
- 클라이언트로 전송되는 JavaScript를 줄여 성능을 개선하고 싶을 때
- 애플리케이션에서 SEO가 중요할 때
- 무거운 데이터 페칭이나 연산 작업을 서버로 오프로드하고 싶을 때
반면 다음 경우에는 RSC를 피하는 것이 좋습니다:
- 컴포넌트에 클라이언트 측 인터랙티비티, 상태, 훅이 필요할 때
- 브라우저 API 접근이나 실시간 업데이트가 필요할 때
- 무거운 클라이언트 로직이나 사용자별 맞춤 콘텐츠가 있는 복잡한 페이지를 만들 때
흔히 겪는 함정들
클라이언트 컴포넌트가 클라이언트에서만 렌더링되는 것은 아닙니다
클라이언트 컴포넌트는 클라이언트와 서버 양쪽에서 모두 렌더링됩니다. 'Client Component'라는 이름은 서버 컴포넌트와 구분하기 위한 명명일 뿐입니다.
'use server' 지시자는 서버 액션(Server Actions)을 위한 것입니다
Next.js는 기본적으로 서버 컴포넌트를 사용하므로 별도로 지정할 필요가 없습니다. 클라이언트 컴포넌트를 사용하려면 'use client' 지시자를 추가해 서버 모듈과 클라이언트 모듈의 경계를 선언합니다. 반면 'use server'는 서버 액션을 위해 쓰이는 완전히 다른 지시자로, 이 글의 범위를 벗어나므로 자세한 설명은 생략합니다.
프로젝트 설정
Vercel에서 제공하는 블로그 템플릿을 사용하겠습니다:
pnpm create next-app --example https://github.com/vercel/examples/tree/main/solutions/blog blog
예제를 로컬에서 실행해 어떤 모습인지 확인해 볼 수 있습니다:
cd blog
pnpm dev
이제 @upstash/redis를 설치합니다:
pnpm add @upstash/redis
환경 설정
- Upstash Console → Redis로 이동해 새 데이터베이스를 생성합니다:

- 아래로 스크롤해 REST API 섹션으로 이동한 뒤,
.env탭을 열고 환경 변수를 복사합니다:

.env파일을 생성하고 환경 변수를 붙여 넣습니다:
UPSTASH_REDIS_REST_URL=<YOUR_URL>
UPSTASH_REDIS_REST_TOKEN=<YOUR_TOKEN>
Views 컴포넌트 구성하기
/app/components/views.tsx 파일을 생성합니다:
import { headers } from 'next/headers'
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv();
async function view(slug: string, ip: string) {
// IP 주소를 해싱하여 익명화합니다
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(ip));
const hash = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
// 중복 조회수를 방지합니다
const newView = await redis.set(`deduplicate:${hash}:${slug}`, true, {
nx: true, // 키가 존재하지 않을 때만 설정
ex: 24 * 60 * 60, // 24시간 후 키 만료
});
if (newView) {
await redis.incr(`pageviews:${slug}`); // 조회수 증가
}
}
export default async function Views({ slug }: { slug: string }) {
// 사용자의 IP 주소를 가져옵니다
const header = headers()
const ip = (header.get('x-forwarded-for') ?? '127.0.0.1').split(',')[0]
// 조회수를 증가시킵니다
await view(slug, ip)
// 조회수를 가져옵니다
const views = await redis.get<number>(`pageviews:${slug}`) || 0
return (
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{views} views
</p>
)
}
Views 컴포넌트 가져와서 표시하기
/app/blog/[slug]/page.tsx 파일을 수정합니다:
...
import Views from 'app/components/views'
...
<div className="flex justify-between items-center mt-2 mb-8 text-sm">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{formatDate(post.metadata.publishedAt)}
</p>
<Views slug={post.slug} />
</div>
...
https://localhost:3000/blog/vim 에 접속하면 조회수 카운터가 실제로 동작하는 것을 확인할 수 있습니다:

클라이언트 컴포넌트 방식과 비교
클라이언트 컴포넌트를 활용한 구현이 궁금하다면, 저희 블로그 글 "Adding a View Counter to your Next.js Blog"를 참고해 보세요.
앞서 소개한 여러 장점 외에도 React 서버 컴포넌트를 사용하면 다음과 같은 이점이 있습니다:
- 별도의 API를 만들 필요가 없어집니다.
- 컴포넌트와 그 로직을 타입 체킹의 장점과 함께 하나의 통합된 형태로 관리할 수 있습니다.
배포하기
다음 명령어 하나로 사이트를 Vercel에 배포할 수 있습니다:
vercel
마무리
서버 컴포넌트와 클라이언트 컴포넌트의 장점을 적절히 조합하면, 애플리케이션에서 성능과 인터랙티비티 사이의 균형점을 찾을 수 있습니다. 이 가이드가 React 서버 컴포넌트를 언제, 어떻게 효과적으로 활용할지 판단하는 데 도움이 되기를 바랍니다.