왜 지금 React Server Components를 알아야 하는가
2026년 현재 React Server Components(RSC)는 Next.js 15 기준으로 기본값이 됐다. 새 프로젝트를 시작하면 기본으로 Server Components 방식을 사용하게 되며, 클라이언트 컴포넌트는 명시적으로 'use client' 지시어를 선언해야 한다. 이 변화가 낯설고 혼란스럽다면, 이 글에서 Before/After 코드 비교로 명확하게 정리해 드린다.
RSC 등장 배경: 기존 방식의 문제
기존 React(CSR, Client-Side Rendering)의 가장 큰 문제는 번들 크기와 초기 로딩 속도였다. 서버에서 HTML 껍데기만 보내고, 클라이언트에서 JS를 다운로드한 뒤 렌더링하는 방식은 First Contentful Paint(FCP)가 느려지는 원인이었다.
SSR(Server-Side Rendering)이 나왔지만 완전한 해결책이 아니었다. 서버에서 HTML을 만들어 보내도, Hydration(자바스크립트를 HTML에 결합하는 과정) 과정에서 같은 컴포넌트가 서버와 클라이언트 모두에서 실행됐다. 즉, 번들 크기 문제는 여전히 남아있었다.
Before/After 코드 비교: 데이터 페칭
기존 방식 (useEffect + useState)
// pages/products.tsx (기존 CSR 방식)
'use client'; // 전체 파일이 클라이언트 컴포넌트
import { useState, useEffect } from 'react';
export default function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 클라이언트에서 매번 API 호출
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
문제점: fetch 라이브러리가 번들에 포함되고, 매 렌더링마다 API 호출이 발생한다. 초기 화면에 로딩 스피너가 보인다.
RSC 방식 (async Server Component)
// app/products/page.tsx (RSC 방식)
// 'use client' 없음 = Server Component
async function getProducts() {
// DB 직접 접근 또는 내부 API 호출 (클라이언트 노출 없음)
const res = await fetch('https://api.internal/products', {
next: { revalidate: 60 } // 60초마다 캐시 갱신
});
return res.json();
}
export default async function ProductList() {
// await 직접 사용 가능 - useEffect 불필요
const products = await getProducts();
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
개선점: fetch 관련 코드가 번들에서 제외된다. HTML이 서버에서 완성된 상태로 전달된다. 로딩 스피너 없이 즉시 콘텐츠가 보인다.
흔히 겪는 에러와 해결책
에러 1: "useState can only be used in Client Components"
// 잘못된 예시
// app/counter.tsx
import { useState } from 'react'; // RSC에서 hooks 사용 불가
export default function Counter() {
const [count, setCount] = useState(0); // 에러!
return <button onClick={() => setCount(c => c+1)}>{count}</button>;
}
// 해결: 'use client' 추가
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c+1)}>{count}</button>;
}
에러 2: 클라이언트 컴포넌트에 서버 전용 모듈 import
// 잘못된 예시 - 'use client'인데 DB 라이브러리 사용
'use client';
import { db } from '@/lib/db'; // 빌드 에러! 서버 전용 모듈
// 해결: DB 접근은 Server Component에서 하고,
// 데이터만 props로 클라이언트로 전달
RSC vs 기존 방식 성능 비교
| 지표 | 기존 CSR | RSC (Next.js 15) | 개선율 |
|---|---|---|---|
| JS 번들 크기 | 평균 280KB | 평균 95KB | -66% |
| FCP (First Contentful Paint) | 2.1초 | 0.8초 | -62% |
| LCP (Largest Contentful Paint) | 3.5초 | 1.2초 | -66% |
| Lighthouse 성능 점수 | 62점 | 91점 | +47% |
(출처: Vercel 공식 벤치마크, 이커머스 상품 목록 페이지 기준)
언제 Server Component, 언제 Client Component를 써야 하나
| 상황 | 선택 |
|---|---|
| DB, API 데이터 조회 | Server Component |
| 사용자 이벤트 처리 (onClick, onChange) | Client Component |
| useState, useEffect 사용 | Client Component |
| 브라우저 API (localStorage, window) | Client Component |
| 정적 UI 렌더링 | Server Component |
| SEO가 중요한 페이지 | Server Component |
결론
React Server Components는 "클라이언트에서 모든 것을 처리하던 방식"에서 "서버에서 할 수 있는 것은 서버에서 처리하는 방식"으로의 패러다임 전환이다. 처음에는 낯설지만, 데이터 페칭과 렌더링의 책임 분리를 명확히 하면 코드가 더 단순해지고 성능은 극적으로 향상된다. Next.js 15를 쓰고 있다면, 지금 바로 도입할 가치가 있다.
'IT & 개발' 카테고리의 다른 글
| 6개월 후의 나를 위해 코드를 짜다 - 2026년 AI 시대에 더 중요해진 클린코드 5원칙 (0) | 2026.03.16 |
|---|---|
| 주니어 개발자가 사라지는 시대 - AI와 경쟁 말고 협업하는 2026년 콜리어 전략 (0) | 2026.03.16 |
| GitOps with ArgoCD 실전 구축 가이드 - Kubernetes 배포 자동화로 월 장애 건수 80% 줄이기 (0) | 2026.03.13 |
| PostgreSQL 18 핵심 변경 3가지 - 비동기 I/O, UUIDv7, Skip Scan으로 쿼리 응답시간 단축하기 (0) | 2026.03.13 |
| SK하이닉스 HBM4 엔비디아 납품 임박 - AI 반도체 패권 전쟁이 개발자에게 미치는 영향 (0) | 2026.03.13 |
댓글