Ch 04. 하이드레이션 이해와 mismatch 해결
서버에서 완성된 HTML이 브라우저에 도착하면 Vue는 그 HTML 위에 반응성을 "붙이는" 작업을 합니다. 이 과정을 하이드레이션(Hydration)이라고 합니다. DOM을 새로 만들지 않고 기존 HTML 구조를 재활용하기 때문에 빠르게 동작합니다.
문제는 서버가 만든 HTML과 클라이언트가 그려야 할 HTML이 다를 때 발생합니다. Vue는 이 불일치를 감지하면 브라우저 콘솔에 경고를 남기고, 경우에 따라 화면이 깜빡이거나 레이아웃이 틀어지기도 합니다.
mismatch가 발생하는 주요 원인
첫 번째, 비결정적 값을 렌더링에 사용할 때.
<!-- 잘못된 예 -->
<template>
<p>방문 ID: {{ Math.random() }}</p>
<p>접속 시각: {{ new Date().toLocaleTimeString() }}</p>
</template>
서버가 실행한 Math.random()과 클라이언트가 실행한 Math.random()은 값이 다릅니다. Vue가 두 값을 비교하면 mismatch가 납니다.
두 번째, 브라우저 전용 코드가 <script setup> 최상위에서 실행될 때.
window나 localStorage에 접근하는 코드가 onMounted 밖에 있으면 서버에서도 실행을 시도합니다. 서버에는 해당 객체가 없으므로 오류가 발생하거나 undefined가 반환됩니다.
세 번째, 서버와 클라이언트의 데이터 불일치.
서버에서 가져온 데이터와 클라이언트 첫 렌더링에 사용된 데이터가 다른 경우입니다. useFetch나 useAsyncData를 사용하면 서버 응답이 클라이언트로 전달되므로 이 문제를 예방할 수 있습니다.
해결 방법
ClientOnly로 문제 구간을 격리합니다.
비결정적 값이나 브라우저 API를 사용하는 부분을 <ClientOnly>로 감싸면 서버 렌더링 대상에서 제외됩니다.
<template>
<ClientOnly>
<p>현재 시각: {{ currentTime }}</p>
</ClientOnly>
</template>
useId()로 고유 ID를 생성합니다.
폼 요소의 id나 aria- 속성에 임의 값을 쓰면 mismatch가 납니다. Nuxt 3.10 이상에서는 useId()를 사용하면 서버와 클라이언트가 동일한 ID를 생성합니다.
const inputId = useId() // 서버와 클라이언트에서 동일한 값 반환
useCookie로 일관된 초기값을 사용합니다.
사용자 설정(예: 다크 모드, 언어)을 쿠키에 저장하고 useCookie로 읽으면 서버와 클라이언트 모두 같은 값으로 초기 렌더링을 합니다.
const theme = useCookie('theme', { default: () => 'light' })
콘솔 경고 해석하기
브라우저 개발자 도구 콘솔에서 다음과 같은 경고를 만나면 mismatch입니다.
[Vue warn]: Hydration text content mismatch on <p>
- Server rendered: "0.73829..."
- Client rendered: "0.41029..."
Server rendered와 Client rendered 값을 비교하면 어떤 데이터가 문제인지 바로 파악할 수 있습니다. 해당 컴포넌트에서 비결정적 값이나 브라우저 전용 코드를 찾아 수정하면 됩니다.