본문 바로가기
Frontend2026년 4월 21일7분 읽기

React Server Components 5가지 함정과 해결법 — Context·Hydration·캐싱

YS
김영삼
조회 1
React Server Components 5가지 함정과 해결법 — Context·Hydration·캐싱

핵심 요약

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

아직 댓글이 없습니다.
Ctrl+Enter로 등록