핵심 요약
React Server Components가 안정화된 지 1년이 넘었지만 도입 첫 분기 실무 팀들이 반복적으로 만나는 함정이 있다. 이 글은 그 중 다섯 가지와 검증된 해결 패턴을 정리한다.
- Context는 RSC에서 작동 안 함
- Hydration mismatch 디버깅이 어려움
- fetch 캐싱이 직관적이지 않음
- third-party 라이브러리가 use client 강제
- 환경 변수 노출 위험
함정 1 — Context Provider 실패
RSC는 컴포넌트 트리에 들어가 있어도 Context를 못 읽는다. Provider는 클라이언트 트리 안에서만 의미.
해결
// ❌ 잘못됨
// app/page.tsx (RSC)
export default function Page() {
const theme = useContext(ThemeContext) // null
}
// ✅ 1) props로 내려줌
import ClientView from './ClientView'
export default async function Page() {
const theme = await getTheme() // 서버에서 직접
return <ClientView theme={theme} />
}
// ✅ 2) cookies/headers + cache로 server-side context
import { cookies } from 'next/headers'
import { cache } from 'react'
export const getTheme = cache(async () => {
return (await cookies()).get('theme')?.value || 'light'
})함정 2 — Hydration Mismatch
서버에서 렌더된 HTML과 클라이언트 첫 렌더가 다르면 발생. 가장 흔한 원인:
- new Date(), Math.random()
- typeof window 체크
- localStorage 읽기
- locale 차이 (날짜 포맷)
해결
'use client'
import { useEffect, useState } from 'react'
// 시간은 클라이언트에서만
function Now() {
const [now, setNow] = useState<Date | null>(null)
useEffect(() => setNow(new Date()), [])
if (!now) return null
return <span>{now.toLocaleTimeString()}</span>
}함정 3 — fetch 캐싱 동작 변경
Next.js 16부터 fetch 기본이 no-store. 이걸 force-cache로 안 바꾸면 매 요청마다 fetch.
// ❌ 매번 fetch
const data = await fetch(url).then(r => r.json())
// ✅ 명시적 캐시
const data = await fetch(url, {
cache: 'force-cache',
next: { revalidate: 3600, tags: ['products'] }
}).then(r => r.json())함정 4 — third-party 라이브러리 use client 강제
일부 라이브러리 (framer-motion·일부 차트 라이브러리)는 use client만 동작. 페이지 전체가 클라이언트로 떨어지지 않도록 분리.
// ❌ 잘못됨 — 페이지 전체가 클라이언트
'use client'
import Chart from 'heavy-chart-lib'
export default function Dashboard() { ... }
// ✅ 클라이언트 부분만 분리
// Chart.tsx
'use client'
import ChartLib from 'heavy-chart-lib'
export default function Chart(props) { return <ChartLib {...props} /> }
// page.tsx (RSC)
import Chart from './Chart'
export default async function Dashboard() {
const data = await fetchData()
return <Chart data={data} />
}함정 5 — 환경 변수 노출
RSC 안에서 process.env.SECRET을 사용하고 그 결과가 클라이언트로 직렬화되면 노출. 서버 전용 모듈로 격리.
// lib/server-only.ts
import 'server-only' // 클라이언트 import 시 빌드 에러
export async function getApiKey() {
return process.env.OPENAI_API_KEY!
}server-only 패키지가 컴파일 시점에 클라이언트 import를 차단. 보안 사고 예방의 가장 강력한 도구.
디버깅 도구
- NEXT_DEBUG=1 next build — dynamic 사유 추적
- React DevTools — Server/Client 컴포넌트 표시
- Suspense Boundary 시각화 — Profiler 탭
- Lighthouse — Hydration 비용 측정
RSC 도입 의사결정
다음 경우는 RSC가 강한 효과:
- 대량 데이터 fetch + 정적 렌더 비중 높음
- SEO 중요 (블로그·이커머스)
- 번들 크기 최소화 필요
다음 경우는 클라이언트 위주가 나음:
- 대시보드·관리 도구 (인터랙션 많음)
- 오프라인 우선 (PWA)
- Auth-heavy 앱
자주 묻는 질문
RSC를 안 쓰는 게 답이 될 때?
대시보드·관리 도구 같이 인터랙티브한 앱은 SPA가 더 단순. RSC 강제 안 함.
Zustand·Jotai는 RSC에서?
모두 클라이언트 한정. RSC에서는 cookies·DB를 그 역할로.
RSC + WebSocket?
WebSocket 자체는 클라이언트에서. 데이터 초기값만 RSC가 props로 전달, 이후 클라이언트가 WebSocket 구독.

댓글 0