본문 바로가기
IT & 개발

React Server Components 완전 정복 - 기존 방식과 코드 비교로 배우는 RSC 실전 가이드

by 냉국이 2026. 3. 13.
728x90

왜 지금 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 기존 방식 성능 비교

지표기존 CSRRSC (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를 쓰고 있다면, 지금 바로 도입할 가치가 있다.

300x250

댓글