React Server Components
RSC
서버에서 렌더링되는 React 컴포넌트. 번들 크기 감소, 직접 DB 접근.
RSC
서버에서 렌더링되는 React 컴포넌트. 번들 크기 감소, 직접 DB 접근.
React Server Components(RSC)는 React 팀이 개발한 새로운 컴포넌트 패러다임으로, 컴포넌트를 서버에서만 실행하고 렌더링 결과만 클라이언트로 전송합니다. 기존 SSR(Server-Side Rendering)과 달리 RSC는 JavaScript 번들에 포함되지 않아 번들 크기가 획기적으로 줄어듭니다. 무거운 라이브러리(moment.js, lodash 등)를 서버 컴포넌트에서만 사용하면 클라이언트로 전송되지 않습니다.
RSC의 핵심 특징은 async/await를 컴포넌트에서 직접 사용할 수 있다는 점입니다. 서버 컴포넌트는 데이터베이스, 파일 시스템, 내부 API에 직접 접근할 수 있으며 useEffect나 React Query 없이 데이터를 가져옵니다. 이는 Waterfall 요청 문제를 해결하고, 서버와 데이터베이스 간 낮은 레이턴시를 활용할 수 있게 합니다.
Server Components와 Client Components는 명확히 구분됩니다. 클라이언트 컴포넌트는 파일 상단에 'use client' 지시어를 추가해야 하며, useState, useEffect, 이벤트 핸들러, 브라우저 API 사용이 필요할 때 사용합니다. 서버 컴포넌트는 기본값이며, 클라이언트 컴포넌트를 자식으로 포함할 수 있지만 그 반대는 불가능합니다(props로 전달은 가능).
Next.js 13+ App Router가 RSC의 대표적 구현체입니다. page.tsx, layout.tsx, loading.tsx, error.tsx가 기본적으로 서버 컴포넌트로 동작하며, Streaming SSR과 결합하여 점진적 렌더링이 가능합니다. React 19에서는 Server Actions도 도입되어 form 제출과 mutation을 서버에서 직접 처리할 수 있습니다.
// app/posts/page.tsx - 서버 컴포넌트 (기본값)
// 'use client' 없으면 서버 컴포넌트
import { prisma } from '@/lib/prisma';
import { formatDate } from 'date-fns'; // 이 라이브러리는 번들에 포함 안 됨!
// async 컴포넌트 - 서버에서만 가능
export default async function PostsPage() {
// DB 직접 접근 (API 불필요)
const posts = await prisma.post.findMany({
include: { author: true },
orderBy: { createdAt: 'desc' },
take: 10,
});
// 무거운 데이터 처리도 서버에서
const processedPosts = posts.map(post => ({
...post,
excerpt: post.content.slice(0, 150),
formattedDate: formatDate(post.createdAt, 'yyyy년 M월 d일'),
}));
return (
<main className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">최신 게시글</h1>
<div className="grid gap-4">
{processedPosts.map(post => (
<article key={post.id} className="p-6 border rounded-lg">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 mt-2">{post.excerpt}...</p>
<div className="mt-4 text-sm text-gray-500">
<span>{post.author.name}</span>
<span className="mx-2">•</span>
<span>{post.formattedDate}</span>
</div>
</article>
))}
</div>
</main>
);
}
// 메타데이터도 서버에서 생성
export async function generateMetadata() {
const totalPosts = await prisma.post.count();
return {
title: `게시글 목록 (${totalPosts}개)`,
description: '최신 게시글을 확인하세요',
};
}
// app/posts/actions.ts - Server Actions
'use server';
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await prisma.post.create({
data: { title, content, authorId: 1 }
});
// 캐시 무효화 - 목록 페이지 갱신
revalidatePath('/posts');
}
export async function deletePost(id: number) {
await prisma.post.delete({ where: { id } });
revalidatePath('/posts');
}
// -------------------------------------------------
// components/CreatePostForm.tsx - 클라이언트 컴포넌트
'use client';
import { useFormStatus } from 'react-dom';
import { createPost } from '@/app/posts/actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
>
{pending ? '저장 중...' : '게시글 작성'}
</button>
);
}
export function CreatePostForm() {
return (
<form action={createPost} className="space-y-4">
<input
name="title"
placeholder="제목"
required
className="w-full border p-2 rounded"
/>
<textarea
name="content"
placeholder="내용"
required
rows={5}
className="w-full border p-2 rounded"
/>
<SubmitButton />
</form>
);
}
// -------------------------------------------------
// app/posts/new/page.tsx - 서버 컴포넌트에서 클라이언트 컴포넌트 사용
import { CreatePostForm } from '@/components/CreatePostForm';
export default function NewPostPage() {
return (
<main className="max-w-2xl mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">새 게시글 작성</h1>
{/* 클라이언트 컴포넌트 삽입 */}
<CreatePostForm />
</main>
);
}