DevToolBox무료
블로그

React Server Components 완전 가이드 2026

15분by DevToolBox

React Server Components (RSC)는 hooks 이후 가장 큰 React 아키텍처 변화입니다.

React Server Components란?

서버에서 렌더링되어 출력을 클라이언트로 보내는 컴포넌트입니다.

RSC 모델에는 세 가지 유형의 컴포넌트가 있습니다.

Server vs Client Components

항목Server ComponentClient Component
App Router 기본값아니오
디렉티브없음"use client"
클라이언트 JS없음 (제로 JS)
useState/useEffect사용 불가사용 가능
이벤트 핸들러사용 불가사용 가능
DB 직접 접근아니오
파일 시스템아니오
서버 환경 변수아니오
비동기 컴포넌트아니오
번들 크기제로증가

결정 트리: Server인가 Client인가?

Should this component be a Server or Client Component?

  Does it need useState, useEffect, or useRef?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it need event handlers (onClick, onChange, onSubmit)?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it use browser-only APIs (window, localStorage, navigator)?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it use third-party libraries that require hooks or browser APIs?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it fetch data or access databases/file system?
  ├── YES → Server Component (default) ← BEST CHOICE
  └── NO → Server Component (default) ← still the default

RSC 패턴

패턴 1: 서버 사이드 데이터 페칭

async/await로 직접 데이터 가져오기.

// app/users/page.tsx — Server Component (default, no directive needed)
import { db } from '@/lib/database';

// This component is async — only possible in Server Components
export default async function UsersPage() {
  // Direct database query — this code NEVER reaches the client
  const users = await db.user.findMany({
    orderBy: { createdAt: 'desc' },
    take: 50,
  });

  return (
    <div>
      <h1>Users ({users.length})</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} — {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

// Benefits:
// - Zero JavaScript sent to client for this component
// - No loading states needed (data is available before render)
// - Database credentials never exposed to the browser
// - No useEffect + fetch + useState dance

패턴 2: Server-Client 조합

Server가 Client를 렌더링 가능.

// components/LikeButton.tsx — Client Component
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => {
      setLiked(!liked);
      setCount(c => liked ? c - 1 : c + 1);
    }}>
      {liked ? 'Unlike' : 'Like'} ({count})
    </button>
  );
}

// app/posts/[id]/page.tsx — Server Component
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';

export default async function PostPage({ params }: { params: { id: string } }) {
  // Server: fetch data
  const post = await db.post.findUnique({ where: { id: params.id } });

  return (
    <article>
      {/* Server-rendered content (zero JS) */}
      <h1>{post?.title}</h1>
      <div>{post?.content}</div>

      {/* Client Component island (only this ships JS) */}
      <LikeButton postId={params.id} />
    </article>
  );
}

// Pattern: Server Component as children of Client Component
// components/Sidebar.tsx
'use client';
export function Sidebar({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return (
    <aside style={{ display: open ? 'block' : 'none' }}>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {children}  {/* Server Component content passed as children */}
    </aside>
  );
}

패턴 3: Server Actions

서버 사이드 함수를 직접 호출.

// app/actions.ts — Server Action
'use server';

import { db } from '@/lib/database';
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 db.post.create({
    data: { title, content, authorId: 'user-1' },
  });

  revalidatePath('/posts');
}

export async function deletePost(postId: string) {
  await db.post.delete({ where: { id: postId } });
  revalidatePath('/posts');
}

// components/CreatePostForm.tsx — Client Component using Server Action
'use client';
import { createPost } from '@/app/actions';
import { useActionState } from 'react';

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" required />
      <textarea name="content" placeholder="Write your post..." required />
      <button type="submit">Create Post</button>
    </form>
  );
}

// Works without API routes!
// The form submits directly to the server action
// Progressive enhancement: works without JavaScript

패턴 4: Suspense 스트리밍

느린 데이터 페칭이 전체 페이지를 차단하지 않음.

// app/dashboard/page.tsx — Streaming with Suspense
import { Suspense } from 'react';

// Slow component — fetches from external API
async function AnalyticsChart() {
  const data = await fetch('https://api.analytics.com/data', {
    next: { revalidate: 60 },
  });
  const analytics = await data.json();
  return <div>Chart: {analytics.visitors} visitors</div>;
}

// Fast component — fetches from local DB
async function RecentPosts() {
  const posts = await db.post.findMany({ take: 5 });
  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* This renders immediately */}
      <Suspense fallback={<div>Loading posts...</div>}>
        <RecentPosts />
      </Suspense>

      {/* This streams in when ready (doesn't block the page) */}
      <Suspense fallback={<div>Loading analytics...</div>}>
        <AnalyticsChart />
      </Suspense>
    </div>
  );
}

// Result: User sees "Dashboard" + posts immediately
// Analytics chart streams in when the API responds
// No client-side loading spinners needed

데이터 페칭 전략

순차적 데이터 페칭

의존성이 있는 경우.

// Sequential: second request depends on first
async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId);           // 1st request
  const posts = await getUserPosts(user.name);  // 2nd (depends on user.name)

  return <div>{user.name}: {posts.length} posts</div>;
}

병렬 데이터 페칭

독립적인 경우 Promise.all 사용.

// Parallel: independent requests — use Promise.all
async function Dashboard() {
  // Start all requests simultaneously
  const [users, posts, analytics] = await Promise.all([
    getUsers(),
    getPosts(),
    getAnalytics(),
  ]);

  return (
    <div>
      <UserList users={users} />
      <PostList posts={posts} />
      <AnalyticsPanel data={analytics} />
    </div>
  );
}

캐싱과 재검증

Next.js는 fetch에 캐싱 제어를 확장합니다.

// Next.js fetch caching strategies

// 1. Static (cached indefinitely until revalidated)
const data = await fetch('https://api.example.com/data');
// Equivalent to: { cache: 'force-cache' }

// 2. Revalidate every 60 seconds (ISR)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 },
});

// 3. Dynamic (no caching — fresh on every request)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// 4. Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

// In a Server Action:
import { revalidateTag } from 'next/cache';
revalidateTag('posts');  // Purge all fetches tagged 'posts'

성능 이점

RSC는 여러 성능 이점을 제공합니다.

RSC Performance Benefits:

1. Zero Client JavaScript for Server Components
   - Only Client Components ship JS to the browser
   - Large libraries used on server (markdown, syntax highlighting) add zero bundle

2. Streaming SSR
   - HTML streams to browser as components resolve
   - First Contentful Paint (FCP) is faster
   - Time to Interactive (TTI) is faster (less JS to hydrate)

3. Automatic Code Splitting
   - Each Client Component is automatically code-split
   - No manual dynamic imports needed

4. Reduced Data Transfer
   - Server Components fetch data on the server (same network)
   - No client-side fetch waterfalls
   - No duplicate data in both HTML and JS payload

5. Smaller Hydration Payload
   - Only Client Components need hydration
   - Server Component output is static — no hydration needed

Typical Results:
   Before RSC: 450KB JS bundle, 2.5s TTI
   After RSC:  180KB JS bundle, 1.1s TTI (60% JS reduction)

일반적인 실수

// Mistake 1: Adding "use client" unnecessarily
// BAD: This doesn't need client-side JavaScript
'use client';  // REMOVE THIS
export function UserCard({ name, email }: { name: string; email: string }) {
  return <div>{name} ({email})</div>;  // No hooks, no events = Server Component
}

// Mistake 2: Importing Server Component in Client Component
// BAD: This won't work
'use client';
import { ServerComponent } from './ServerComponent';  // ERROR
// FIX: Pass as children instead
export function ClientWrapper({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

// Mistake 3: Using "use client" too high in the tree
// BAD: Making a layout a Client Component
'use client';  // This makes EVERYTHING below it client-side
export function Layout({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('dark');
  return <div className={theme}>{children}</div>;
}
// FIX: Extract the interactive part into a small Client Component
// components/ThemeToggle.tsx
'use client';
export function ThemeToggle() {
  const [theme, setTheme] = useState('dark');
  return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
// layout.tsx — stays as Server Component
export function Layout({ children }: { children: React.ReactNode }) {
  return <div><ThemeToggle />{children}</div>;
}

자주 묻는 질문

Next.js 없이 Server Components를 사용할 수 있나요?

기술적으로는 가능하지만, 실용적으로 Next.js가 주요 프레임워크입니다.

Server Components가 SSR을 대체하나요?

아닙니다. 보완합니다.

"use client"는 언제 사용하나요?

hooks, 이벤트 핸들러, 브라우저 API가 필요할 때.

Server Components에서 Context를 사용할 수 있나요?

사용할 수 없습니다.

SEO에 미치는 영향은?

긍정적입니다. 서버에서 HTML 생성.

관련 도구 및 가이드

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

{ }JSON FormatterJSJavaScript Minifier

Related Articles

Next.js App Router: 2026 완벽 마이그레이션 가이드

Next.js App Router 종합 가이드. Server Components, 데이터 페칭, 레이아웃, 스트리밍, Server Actions, Pages Router에서의 단계별 마이그레이션 학습.

React Hooks 완벽 가이드: useState, useEffect, 커스텀 Hooks

실용적인 예제로 React Hooks 마스터. useState, useEffect, useContext, useReducer, useMemo, useCallback, 커스텀 Hooks, React 18+ 동시성 Hooks 학습.

React 성능 최적화: 15가지 팁

React.memo, useMemo 등 15가지 기법으로 React 앱을 최적화하세요.