본문으로 건너뛰기
Advertisement

11.2 서버 컴포넌트 타입 — RSC vs Client Component

React Server Components (RSC) 개요

Next.js App Router에서 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다. 클라이언트 컴포넌트는 파일 상단에 "use client" 지시어가 필요합니다.

특성서버 컴포넌트클라이언트 컴포넌트
기본값❌ ("use client" 필요)
async/await
useState/useEffect
브라우저 API
이벤트 핸들러
DB 직접 접근
번들 포함❌ (서버에서만 실행)

async 서버 컴포넌트

// app/users/page.tsx (서버 컴포넌트)
import { db } from '@/lib/db';

interface User {
id: string;
name: string;
email: string;
}

// async 함수로 선언 — 서버에서만 실행
async function UsersPage() {
// DB 직접 접근 가능
const users: User[] = await db.query('SELECT * FROM users');

return (
<div>
<h1>사용자 목록</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}

export default UsersPage;
// 중첩 async 서버 컴포넌트
async function UserCard({ userId }: { userId: string }) {
const user = await fetchUser(userId);

return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

async function Dashboard() {
const userIds = await getActiveUserIds();

return (
<div>
{/* 병렬 렌더링 — Promise.all보다 선언적 */}
{userIds.map(id => (
<UserCard key={id} userId={id} />
))}
</div>
);
}

클라이언트 컴포넌트

// app/components/Counter.tsx
'use client';

import { useState } from 'react';

interface CounterProps {
initialCount?: number;
label?: string;
}

export function Counter({ initialCount = 0, label = '카운터' }: CounterProps) {
const [count, setCount] = useState(initialCount);

return (
<div>
<p>{label}: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setCount(c => c - 1)}>-1</button>
</div>
);
}

RSC와 Client Component 타입 경계

서버 컴포넌트에서 클라이언트 컴포넌트로 Props 전달

// ✅ 직렬화 가능한 값만 전달 가능
async function ServerPage() {
const data = await fetchData(); // { id: string, name: string, count: number }

return (
<ClientComponent
id={data.id} // ✅ string
name={data.name} // ✅ string
count={data.count} // ✅ number
// ❌ 함수, 클래스 인스턴스, Symbol 전달 불가
/>
);
}

// ❌ 잘못된 예 — 함수는 전달 불가
async function ServerPage() {
const handleClick = () => console.log('clicked');

return (
<ClientComponent
onClick={handleClick} // ❌ 함수 직렬화 불가 — 오류 발생
/>
);
}

서버 컴포넌트를 클라이언트 컴포넌트의 children으로 전달

// ✅ 서버 컴포넌트를 children으로 전달 — 가능!
async function Page() {
const serverData = await fetchData();

return (
<ClientWrapper>
{/* 서버 컴포넌트가 children으로 들어감 */}
<ServerComponent data={serverData} />
</ClientWrapper>
);
}

// ClientWrapper.tsx
'use client';

import { ReactNode, useState } from 'react';

function ClientWrapper({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);

return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>토글</button>
{isOpen && children}
</div>
);
}

데이터 페칭 패턴

순차 vs 병렬 페칭

// ❌ 순차 페칭 (느림)
async function SlowPage() {
const user = await getUser(); // 1초
const posts = await getUserPosts(user.id); // 2초
// 총 3초 소요

return <UserPosts user={user} posts={posts} />;
}

// ✅ 병렬 페칭 (빠름)
async function FastPage({ userId }: { userId: string }) {
const [user, posts] = await Promise.all([
getUser(userId),
getUserPosts(userId),
]);
// 총 2초 소요

return <UserPosts user={user} posts={posts} />;
}

Suspense로 스트리밍

import { Suspense } from 'react';

// 각 컴포넌트가 독립적으로 데이터 페칭
async function SlowComponent() {
const data = await fetchSlowData(); // 3초 소요
return <div>{data.content}</div>;
}

async function FastComponent() {
const data = await fetchFastData(); // 0.5초 소요
return <div>{data.content}</div>;
}

// 각각 독립적으로 로딩
function Page() {
return (
<div>
<Suspense fallback={<div>빠른 데이터 로딩 중...</div>}>
<FastComponent />
</Suspense>
<Suspense fallback={<div>느린 데이터 로딩 중...</div>}>
<SlowComponent />
</Suspense>
</div>
);
}

cache와 서버 컴포넌트

import { cache } from 'react';
import { unstable_cache } from 'next/cache';

// cache(): 단일 요청 내에서 중복 호출 제거
const getUser = cache(async (id: string) => {
console.log('DB 조회:', id); // 같은 id로 두 번 호출해도 한 번만 실행
return db.user.findUnique({ where: { id } });
});

// unstable_cache(): 요청 간 캐싱 (Next.js 캐시)
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
['user'],
{ revalidate: 60, tags: ['user'] } // 60초 캐시
);

고수 팁

서버/클라이언트 컴포넌트 구분 패턴

서버 컴포넌트가 적합한 경우:
- DB, API 직접 접근
- 민감한 데이터 처리 (API 키 등)
- 무거운 npm 패키지 사용
- 정적 콘텐츠 렌더링

클라이언트 컴포넌트가 필요한 경우:
- onClick, onChange 등 이벤트 핸들러
- useState, useEffect 등 React 훅
- localStorage, window 등 브라우저 API
- 사용자 상호작용 요소

경계를 "가능한 한 하위"로 내리기

// ❌ 페이지 전체를 클라이언트로 만들지 말기
'use client';
async function BadPage() { /* ... */ }

// ✅ 인터랙티브한 부분만 클라이언트로
async function GoodPage() { // 서버 컴포넌트
const data = await fetchData(); // 서버에서 데이터 페칭
return (
<div>
<StaticContent data={data} /> {/* 서버 컴포넌트 */}
<InteractiveButton /> {/* 클라이언트 컴포넌트 */}
</div>
);
}
Advertisement