본문으로 건너뛰기
개발 머꼬
개발 노트Nuxt.js
hohyeon.dev20

localStorage를 읽었더니 하이드레이션 오류가 난 이유

  • #Engineering Note
  • #Nuxt.js
  • #SEO

문제 발생

localStorage에 저장한 테마를 화면에 반영했더니 콘솔에 하이드레이션 오류가 떴고, 화면이 잠깐 깜빡였습니다.

<script setup>
const theme = ref(localStorage.getItem("theme") ?? "light");   // 서버에는 localStorage가 없다
</script>
[Vue warn]: Hydration node mismatch
500  localStorage is not defined

원인 분석

서버에는 localStoragewindow도 없습니다. 그래서 서버 렌더링 단계에서 그대로 터지거나, 값을 얻지 못해 서버가 만든 HTML과 브라우저가 만든 결과가 달라집니다. 후자가 하이드레이션 불일치입니다.

Nuxt의 <ClientOnly>는 정확히 이 두 가지를 위해 있습니다. 공식 문서가 드는 이유가 그대로입니다 — 서버 렌더 HTML과 클라이언트 렌더 결과가 어긋나는 하이드레이션 불일치, 그리고 브라우저에만 있는 localStorage·window·DOM 조작 같은 API입니다.

동작도 명확합니다. 문서에 따르면 기본 슬롯의 내용은 서버 빌드에서 트리 셰이킹으로 제거됩니다. 서버는 그 자리를 아예 그리지 않습니다.

해결 방안

  1. 브라우저에서만 의미 있는 부분을 감쌉니다. 페이지 전체가 아니라 그 조각만입니다.
<template>
  <ClientOnly>
    <ThemeToggle />
  </ClientOnly>
</template>
  1. 자리를 비워두지 않습니다. 서버가 아무것도 그리지 않으면 하이드레이션 순간 레이아웃이 밀립니다. fallback 슬롯이나 fallback/fallback-tag prop으로 같은 크기의 자리를 잡아둡니다.
<ClientOnly fallback-tag="div" fallback="댓글을 불러오는 중입니다.">
  <Comments />
</ClientOnly>
  1. 값 하나 때문이라면 감쌀 필요가 없습니다. 마운트 이후에 읽으면 됩니다 — 서버 렌더는 기본값으로 나가고, 브라우저에서 실제 값으로 바뀝니다.
<script setup>
const theme = ref("light");
onMounted(() => {
  theme.value = localStorage.getItem("theme") ?? "light";
});
</script>
  1. 분기가 필요하면 import.meta.client / import.meta.server를 씁니다. 번들러가 이 조건을 정적으로 제거할 수 있는 형태입니다.
if (import.meta.client) {
  window.addEventListener("resize", onResize);
}
  1. <ClientOnly>를 습관적으로 쓰지 않습니다. 감싼 만큼 그 내용은 검색엔진의 초기 HTML에서 사라지고, 문서가 지적하듯 그 안 컴포넌트의 CSS가 초기 HTML에 인라인되지 않을 수 있습니다. 본문·목록처럼 읽혀야 하는 콘텐츠에는 쓰지 않습니다.

  2. Date.now()Math.random()도 같은 부류입니다. 서버와 클라이언트에서 값이 달라지므로 하이드레이션 불일치를 만듭니다. 렌더 결과에 직접 넣지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.