React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
웹 개발 환경은 빠르게 변화하고 있으며, 사용자 경험과 성능에 대한 기대치는 끊임없이 높아지고 있습니다. 이러한 요구사항에 발맞춰 React 생태계는 혁신적인 변화를 시도하고 있으며, 그 중심에 바로 React Server Components (RSC)가 있습니다. RSC는 프론트엔드 개발의 패러다임을 전환하고, 서버와 클라이언트 간의 경계를 허물어 더 효율적이고 강력한 웹 애플리케이션을 구축할 수 있는 새로운 가능성을 제시합니다. 이 글에서는 React Server Components의 개념부터 등장 배경, 동작 원리, 그리고 Next.js App Router에서의 실전 활용법까지 심층적으로 탐구하며, 여러분의 Next.js 및 React 개발 역량을 한 단계 끌어올리는 데 도움을 드리고자 합니다.
React Server Components (RSC)란 무엇인가?
React Server Components (RSC)는 이름 그대로 서버에서 렌더링되고 실행되는 React 컴포넌트입니다. 기존의 React 컴포넌트는 대부분 브라우저(클라이언트)에서 JavaScript를 통해 렌더링되고 상호작용하는 클라이언트 컴포넌트였습니다. 하지만 RSC는 이러한 전통적인 방식을 넘어, 서버의 강력한 컴퓨팅 자원을 활용하여 초기 HTML을 생성하고, 필요한 경우에만 클라이언트 측 JavaScript를 전송하여 애플리케이션의 성능을 최적화하는 것을 목표로 합니다.
RSC의 핵심 아이디어는 모든 컴포넌트가 클라이언트에서 실행될 필요는 없다는 것입니다. 예를 들어, 데이터베이스에서 데이터를 가져와 단순히 화면에 표시하는 컴포넌트는 사용자와의 직접적인 상호작용이 필요 없으므로 서버에서 처리하는 것이 훨씬 효율적일 수 있습니다. 이를 통해 클라이언트로 전송되는 JavaScript 번들 크기를 줄이고, 초기 페이지 로딩 속도를 크게 개선할 수 있습니다.
RSC의 등장 배경 및 주요 이점
RSC가 등장하게 된 배경에는 기존 클라이언트 사이드 렌더링(CSR) 방식의 한계점이 자리 잡고 있습니다. CSR은 모든 JavaScript 번들을 클라이언트로 다운로드한 후 애플리케이션을 실행하기 때문에, 초기 로딩 시간이 길어지고 특히 저사양 기기나 네트워크 환경이 좋지 않은 사용자에게는 불편함을 초래할 수 있었습니다.
RSC는 이러한 문제들을 해결하기 위해 다음과 같은 주요 이점들을 제공합니다.
- 번들 사이즈 감소: 서버 컴포넌트는 클라이언트로 전송되는 JavaScript 번들에 포함되지 않습니다. 이는 특히 큰 서드파티 라이브러리나 복잡한 로직을 서버에서 처리할 때 빛을 발합니다. 클라이언트는 오직 상호작용이 필요한 컴포넌트의 JavaScript 코드만 다운로드하면 됩니다.
- 초기 로딩 속도 개선: 서버에서 미리 데이터를 가져와 HTML을 생성하므로, 사용자는 더 빠르게 콘텐츠를 볼 수 있습니다(Time To First Byte, TTFB 및 First Contentful Paint, FCP 개선). 이는 사용자 경험 향상뿐만 아니라 검색 엔진 최적화(SEO)에도 긍정적인 영향을 미칩니다.
- 서버 자원 활용 극대화: 데이터베이스 접근, 파일 시스템 읽기, 민감한 API 키 처리 등 서버에서만 가능한 작업을 서버 컴포넌트 내에서 직접 수행할 수 있습니다. 이는 클라이언트-서버 간의 API 호출 오버헤드를 줄이고, 더 안전한 환경에서 로직을 실행할 수 있게 합니다.
- 개발 경험 향상: 개발자는 서버와 클라이언트 로직을 하나의 React 컴포넌트 트리 안에서 자연스럽게 구성할 수 있습니다. 이는 풀스택 개발 경험을 React 프레임워크 내에서 통합하여 제공하며, 복잡한 데이터 페칭 로직을 단순화할 수 있습니다.
RSC의 동작 원리: 요청-응답 주기
React Server Components는 클라이언트 컴포넌트와는 다른 방식으로 동작하며, 그 핵심에는 React Server Component Payload (RSC Payload)라는 개념이 있습니다.
- 초기 요청: 사용자가 웹 페이지에 접속하면, 브라우저는 서버에 페이지 요청을 보냅니다.
- 서버 렌더링: 서버는 요청을 받아 React Server Components를 렌더링합니다. 이때, 서버 컴포넌트들은 데이터베이스 접근, 파일 시스템 읽기 등 서버 측 작업을 수행하여 필요한 데이터를 가져옵니다.
- RSC Payload 생성: 서버는 렌더링된 서버 컴포넌트의 결과물과 클라이언트 컴포넌트에 대한 참조 정보를 포함하는 특별한 데이터 구조인 RSC Payload를 생성합니다. 이 페이로드는 HTML과는 다르게, React가 클라이언트에서 효율적으로 DOM을 업데이트할 수 있는 형식으로 구성됩니다.
- 클라이언트로 전송: 생성된 RSC Payload는 초기 HTML과 함께 브라우저로 전송됩니다.
- 클라이언트에서 재조정: 브라우저는 HTML을 렌더링하고, React는 RSC Payload를 사용하여 클라이언트 컴포넌트들을 Hydration합니다. Hydration은 서버에서 렌더링된 HTML에 클라이언트 측 JavaScript를 연결하여 상호작용 기능을 부여하는 과정입니다.
- 부분 업데이트: 이후 클라이언트에서 특정 상호작용(예: 버튼 클릭)으로 인해 서버 컴포넌트의 데이터가 변경되어야 할 경우, React는 필요한 부분만 다시 서버에 요청하여 새로운 RSC Payload를 받아와 UI를 효율적으로 업데이트합니다.
이러한 과정을 통해 RSC는 초기 로딩 시점에는 서버에서 대부분의 작업을 처리하고, 이후에는 필요한 부분만 서버와 통신하며 클라이언트의 부담을 최소화합니다.
Next.js App Router와 RSC
Next.js 13 버전부터 도입된 App Router는 React Server Components를 기본적으로 지원하며, Next.js 애플리케이션에서 RSC를 활용하는 가장 강력한 방법입니다. App Router에서는 기본적으로 모든 컴포넌트가 Server Component로 간주됩니다. 이는 app 디렉토리 내의 모든 page.tsx, layout.tsx, 그리고 이들이 import하는 컴포넌트들이 특별한 지시어 없이는 서버에서 실행된다는 의미입니다.
use client 지시어의 역할
만약 특정 컴포넌트가 클라이언트 측에서 상호작용(예: useState, useEffect, 이벤트 핸들러)을 해야 한다면, 파일 상단에 "use client"; 지시어를 명시해야 합니다. 이 지시어는 번들러에게 해당 컴포넌트와 그 자식 컴포넌트들이 클라이언트 번들에 포함되어야 함을 알려줍니다.
// app/components/ClientCounter.tsx
"use client"; // 이 컴포넌트는 클라이언트 컴포넌트입니다.
import { useState } from 'react';
export default function ClientCounter() {
const [count, setCount] = useState(0);
return (
<div className="p-4 border rounded-md shadow-sm bg-blue-50">
<p className="text-lg font-semibold mb-2">현재 카운트: {count}</p>
<button
onClick={() => setCount(count + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
>
증가
</button>
</div>
);
}
이 ClientCounter 컴포넌트는 useState 훅을 사용하기 때문에 반드시 "use client"; 지시어가 필요합니다.
클라이언트 컴포넌트와 서버 컴포넌트 전략
RSC를 효과적으로 사용하기 위해서는 언제 서버 컴포넌트를 사용하고, 언제 클라이언트 컴포넌트를 사용해야 하는지 명확히 이해하는 것이 중요합니다.
서버 컴포넌트 사용 사례
- 데이터 페칭: 데이터베이스, 외부 API, 파일 시스템 등에서 데이터를 가져오는 로직.
- 민감한 정보 처리: API 키, 인증 정보 등 서버에서만 접근해야 하는 데이터를 처리할 때.
- 복잡한 계산: 클라이언트의 부담을 줄이기 위해 서버에서 미리 계산해야 하는 로직.
- 정적인 콘텐츠 렌더링: 사용자 상호작용이 없는 정적인 UI 요소나 마크다운 콘텐츠 렌더링.
클라이언트 컴포넌트 사용 사례
- 상태 관리:
useState,useReducer등 React의 상태 관리 훅을 사용하는 컴포넌트. - 이벤트 핸들링:
onClick,onChange,onSubmit등 사용자 이벤트에 반응하는 컴포넌트. - 브라우저 API 접근:
localStorage,window객체, Geolocation API 등 브라우저 전용 API를 사용하는 컴포넌트. - 써드파티 라이브러리: React Context, 애니메이션 라이브러리, 차트 라이브러리 등 클라이언트 측 JavaScript에 의존하는 라이브러리를 사용하는 컴포넌트.
다음 표는 두 컴포넌트 유형의 주요 특징을 비교합니다.
| 특징 | React Server Components (RSC) | React Client Components (RCC) |
|---|---|---|
| 실행 환경 | 서버 (초기 렌더링 및 데이터 페칭) | 클라이언트 (브라우저) |
| JavaScript 번들 | 클라이언트 번들에 포함되지 않음 | 클라이언트 번들에 포함됨 |
| 데이터 접근 | 서버 자원 (DB, 파일 시스템, 서버 API) 직접 접근 | 클라이언트 API (fetch 등)를 통해 서버 API 호출 |
| 상태/이벤트 | 불가능 (상호작용 없음) | 가능 (useState, useEffect, 이벤트 핸들러) |
| 지시어 | 기본값 (명시 불필요) | "use client"; 명시 필요 |
| 주요 이점 | 번들 사이즈 감소, 초기 로딩 속도, 보안 | 풍부한 상호작용, 동적인 UI 구현 |
실전 코드 예제: Next.js App Router와 RSC 활용
이제 Next.js App Router 환경에서 Server Component와 Client Component를 조합하여 사용하는 실전 예제를 살펴보겠습니다. 우리는 간단한 블로그 게시물 목록을 서버 컴포넌트에서 가져오고, 각 게시물에 좋아요 버튼을 클라이언트 컴포넌트로 추가하는 시나리오를 구현해볼 것입니다.
먼저, 가상의 데이터를 가져오는 함수를 만듭니다. 이 함수는 서버에서만 실행됩니다.
// lib/data.ts
interface Post {
id: number;
title: string;
content: string;
}
export async function getPosts(): Promise<Post[]> {
// 실제 환경에서는 데이터베이스 쿼리나 외부 API 호출이 여기에 들어갑니다.
// 여기서는 2초 지연을 시뮬레이션하여 서버에서 데이터 페칭이 일어남을 보여줍니다.
await new Promise((resolve) => setTimeout(resolve, 2000));
return [
{ id: 1, title: "React Server Components 개요", content: "..." },
{ id: 2, title: "Next.js 13 App Router 심층 분석", content: "..." },
{ id: 3, title: "프론트엔드 성능 최적화 전략", content: "..." },
];
}
다음으로, 좋아요 기능을 담당할 클라이언트 컴포넌트를 생성합니다.
// app/components/LikeButton.tsx
"use client";
import { useState } from 'react';
interface LikeButtonProps {
postId: number;
}
export default function LikeButton({ postId }: LikeButtonProps) {
const [likes, setLikes] = useState(0); // 실제로는 서버에서 좋아요 수를 가져올 수 있습니다.
const handleLike = () => {
setLikes(prev => prev + 1);
// 실제 환경에서는 서버에 좋아요 수를 업데이트하는 API 호출이 여기에 들어갑니다.
console.log(`Post ${postId}에 좋아요! 현재 ${likes + 1}개`);
};
return (
<button
onClick={handleLike}
className="mt-2 px-3 py-1 bg-pink-500 text-white rounded-md hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:ring-opacity-50 text-sm"
>
❤️ 좋아요 ({likes})
</button>
);
}
마지막으로, 이들을 조합하여 페이지를 렌더링하는 서버 컴포넌트인 page.tsx를 작성합니다.
// app/page.tsx
import { getPosts } from '@/lib/data'; // 서버에서만 실행되는 함수
import LikeButton from './components/LikeButton'; // 클라이언트 컴포넌트
export default async function HomePage() {
const posts = await getPosts(); // 서버 컴포넌트에서 직접 데이터 페칭
return (
<main className="container mx-auto p-8">
<h1 className="text-4xl font-bold mb-8 text-center text-gray-800">블로그 게시물</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<article key={post.id} className="bg-white p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300">
<h2 className="text-2xl font-semibold mb-3 text-gray-900">{post.title}</h2>
<p className="text-gray-700 leading-relaxed mb-4">{post.content}</p>
<LikeButton postId={post.id} /> {/* 클라이언트 컴포넌트 사용 */}
</article>
))}
</div>
<p className="mt-12 text-center text-gray-600 text-sm">
이 페이지는 React Server Components와 Client Components가 함께 렌더링됩니다.
</p>
</main>
);
}
이 예제에서 HomePage는 서버 컴포넌트이므로 async 키워드를 사용하여 await getPosts()를 직접 호출할 수 있습니다. getPosts 함수는 lib/data.ts에 정의되어 있으며, 서버에서 데이터를 가져오는 역할을 합니다. 가져온 데이터는 LikeButton이라는 클라이언트 컴포넌트에 postId prop으로 전달됩니다. LikeButton은 useState 훅을 사용하여 좋아요 수를 관리하고, 사용자 클릭에 반응하는 상호작용을 담당합니다.
이처럼 Next.js App Router는 개발자가 명시적으로 use client 지시어를 사용하지 않는 한 모든 컴포넌트를 서버 컴포넌트로 처리하여, 서버와 클라이언트의 역할을 효율적으로 분담할 수 있도록 돕습니다.
RSC 사용 시 고려사항 및 주의점
React Server Components는 강력한 도구이지만, 몇 가지 고려사항과 주의점을 인지하고 사용해야 합니다.
-
use client의 전파:use client지시어는 해당 파일뿐만 아니라 그 파일에서 import하는 모든 자식 컴포넌트에도 영향을 미칩니다. 즉, 클라이언트 컴포넌트 내에서 import되는 모든 컴포넌트는 클라이언트 컴포넌트가 됩니다. 따라서, 번들 사이즈를 최소화하기 위해 클라이언트 컴포넌트가 서버 컴포넌트를 import하는 구조를 피하고, 서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 받는 "Server Component to Client Component boundary" 패턴을 사용하는 것이 좋습니다. - 클라이언트 컴포넌트의 props: 서버 컴포넌트에서 클라이언트 컴포넌트로 props를 전달할 때는 직렬화 가능한(serializable) 데이터만 전달해야 합니다. 함수, Date 객체, 클래스 인스턴스 등은 직접 전달할 수 없습니다.
- 데이터 무효화 (Data Invalidation): 서버 컴포넌트에서 페칭한 데이터는 기본적으로 캐싱됩니다. 데이터 변경 시 UI를 업데이트하려면 Next.js의
revalidatePath또는revalidateTag와 같은 데이터 캐시 무효화 전략을 이해하고 적용해야 합니다. - 에러 핸들링: 서버 컴포넌트에서 발생하는 에러는 클라이언트 컴포넌트와는 다른 방식으로 처리될 수 있습니다. Next.js의
error.tsx파일을 활용하여 서버 컴포넌트 렌더링 중 발생하는 에러를 효과적으로 처리하는 방법을 익혀야 합니다. - 디버깅 복잡성: 서버와 클라이언트 모두에서 코드가 실행될 수 있기 때문에 디버깅 과정이 다소 복잡해질 수 있습니다. 각 컴포넌트가 어느 환경에서 실행되는지 명확히 이해하는 것이 중요합니다.
마무리
React Server Components는 React의 미래를 이끌어갈 핵심 기술이며, Next.js App Router를 통해 이미 실전에 적용되고 있습니다. RSC는 클라이언트 번들 사이즈를 줄이고, 초기 로딩 성능을 향상시키며, 서버 자원을 효율적으로 활용하여 더욱 강력하고 사용자 친화적인 웹 애플리케이션을 구축할 수 있는 길을 열어줍니다.
물론 새로운 패러다임인 만큼 학습 곡선이 존재하지만, 클라이언트 컴포넌트와 서버 컴포넌트의 명확한 역할 분담 전략을 이해하고 적절히 활용한다면, 여러분의 Next.js 및 React 프로젝트는 한층 더 높은 수준의 성능과 개발 효율성을 달성할 수 있을 것입니다. 이 가이드가 React Server Components의 깊은 이해와 실전 적용에 도움이 되었기를 바랍니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.
Turborepo Monorepo: React, Next.js 프로젝트를 위한 효율적인 구축 가이드
Turborepo를 활용한 Monorepo 구축 방법을 상세히 다루며, React, Next.js 기반의 프론트엔드 프로젝트 관리를 위한 TypeScript, JavaScript, CSS 설정 및 최적화 전략을 소개합니다.