본문으로 건너뛰기

하이드레이션 오류

발생하는 이유

  • 불일치: 하이드레이션 중 서버 HTML과 클라이언트 렌더링 결과가 다릅니다
  • 일반적인 원인: Intl(로케일/시간대), Date.now(), 무작위 ID, 반응형 전용 로직, 기능 플래그, 사용자 환경설정

전략 1 — 서버와 클라이언트를 일치시킵니다

  • 서버에서 결정론적인 로케일/시간대를 선택하고 클라이언트에서도 동일하게 사용합니다
  • 단일 진실 공급원: 쿠키(권장) 또는 Accept-Language 헤더
  • 서버에서 한 번만 계산하고 초기 상태로 하이드레이션합니다
// src/start.ts
import { createStart, createMiddleware } from '@tanstack/react-start'
import {
getRequestHeader,
getCookie,
setCookie,
} from '@tanstack/react-start/server'

const localeTzMiddleware = createMiddleware().server(async ({ next }) => {
const header = getRequestHeader('accept-language')
const headerLocale = header?.split(',')[0] || 'en-US'
const cookieLocale = getCookie('locale')
const cookieTz = getCookie('tz') // set by client later (see Strategy 2)

const locale = cookieLocale || headerLocale
const timeZone = cookieTz || 'UTC' // deterministic until client sends tz

// Persist locale for subsequent requests (optional)
setCookie('locale', locale, { path: '/', maxAge: 60 * 60 * 24 * 365 })

return next({ context: { locale, timeZone } })
})

export const startInstance = createStart(() => ({
requestMiddleware: [localeTzMiddleware],
}))
// src/routes/index.tsx (example)
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { getCookie } from '@tanstack/react-start/server'

export const getServerNow = createServerFn().handler(async () => {
const locale = getCookie('locale') || 'en-US'
const timeZone = getCookie('tz') || 'UTC'
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone,
}).format(new Date())
})

export const Route = createFileRoute('/')({
loader: () => getServerNow(),
component: () => {
const serverNow = Route.useLoaderData() as string
return <time dateTime={serverNow}>{serverNow}</time>
},
})

전략 2 — 클라이언트가 자신의 환경을 알려주도록 합니다

  • 첫 방문 시 클라이언트 시간대가 포함된 쿠키를 설정합니다. 그때까지 SSR은 UTC을 사용합니다
  • 불일치 위험 없이 이 작업을 수행합니다
import * as React from 'react'
import { ClientOnly } from '@tanstack/react-router'

function SetTimeZoneCookie() {
React.useEffect(() => {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
document.cookie = `tz=${tz}; path=/; max-age=31536000`
}, [])
return null
}

export function AppBoot() {
return (
<ClientOnly fallback={null}>
<SetTimeZoneCookie />
</ClientOnly>
)
}

전략 3 — 클라이언트 전용으로 만듭니다

  • 불안정한 UI를 <ClientOnly>로 감싸 SSR과 불일치를 방지합니다
import { ClientOnly } from '@tanstack/react-router'
;<ClientOnly fallback={<span></span>}>
<RelativeTime ts={someTs} />
</ClientOnly>

전략 4 — 라우트의 SSR을 비활성화하거나 제한합니다

  • 서버에서 컴포넌트를 렌더링하지 않으려면 Selective SSR을 사용합니다
export const Route = createFileRoute('/unstable')({
ssr: 'data-only', // or false
component: () => <ExpensiveViz />,
})

전략 5 — 최후 수단으로 억제

  • 작고 차이가 있음이 알려진 노드에는 React의 suppressHydrationWarning을 사용할 수 있습니다
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>

체크리스트

  • 결정적 입력: 로케일, 시간대, 기능 플래그
  • 클라이언트 컨텍스트에는 쿠키를 우선 사용하고, 불가능하면 Accept-Language을 사용합니다
  • 본질적으로 동적인 UI에는 <ClientOnly>을 사용합니다
  • 서버 HTML을 안정적으로 유지할 수 없을 때는 Selective SSR을 사용합니다
  • 무분별한 억제를 피하고, suppressHydrationWarning은 신중하게 사용합니다

함께 보기: 실행 모델, 코드 실행 패턴, 선택적 SSR, 서버 함수