하이드레이션 오류
발생하는 이유
- 불일치: 하이드레이션 중 서버 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은 신중하게 사용합니다