lifecarelog
프론트엔드

Zustand persist가 SSR hydration mismatch를 터뜨리는 이유

Zustand persist가 Next.js SSR에서 hydration mismatch 경고를 만드는 원인과 마운트 플래그, skipHydration 해결법을 정리했어요.

4분 읽기

새로고침할 때마다 콘솔에 hydration mismatch 경고가 쌓인 적 없나요? 다크모드 토글을 Zustand persist로 저장했을 뿐인데요. 혼자 운영하는 서비스에서 이 경고를 처음 만나면 어디서부터 봐야 할지 막막해요.

원인은 localStorage가 있는 위치예요

persist 미들웨어는 상태를 localStorage에 저장하고, 스토어가 만들어질 때 그 값을 다시 읽어요. 문제는 localStorage가 브라우저에만 있다는 점이에요.

서버는 저장된 값을 모른 채 초기값으로 HTML을 만들고, 브라우저는 저장된 값으로 첫 화면을 그려요. React는 이 둘이 같다고 가정하고 hydration을 진행하는데, 결과가 다르니 경고가 터져요.

서버가 초기값으로 HTML 생성브라우저 전송localStorage 값으로 첫 렌더서버 HTML과 불일치경고

직접 재현해 보니 10분이 안 걸렸어요. 테마를 다크로 저장하고 새로고침하면 서버 HTML은 라이트, 클라이언트 첫 렌더는 다크라서 바로 경고가 떠요.

해결은 2가지 방법이면 충분해요

가장 단순한 건 마운트 플래그예요. 마운트 전에는 서버와 같은 초기값을 그리고, useEffect가 돈 뒤에 저장된 값을 보여주는 거예요.

function useHydrated() {
  const [hydrated, setHydrated] = useState(false);
  useEffect(function markHydrated() {
    setHydrated(true);
  }, []);
  return hydrated;
}

코드 여섯 줄이면 끝나요. 적용해 봤더니 초기값이 잠깐 보이는 깜빡임이 1회 생기지만 경고는 사라졌어요. 깜빡임이 거슬리면 그 자리에 스켈레톤을 그려주면 돼요.

다른 하나는 persist 옵션에 skipHydration을 1줄 추가하고, useEffect 안에서 rehydrate()를 직접 부르는 방법이에요. 복원 시점을 코드로 제어할 수 있어서 스토어가 여러 개일 때 편했어요.

반대로 suppressHydrationWarning으로 경고만 감추는 건 피하는 게 좋아요. 돌려 보니 경고는 사라져도 서버 HTML과 다른 화면이 그려지는 문제 자체는 그대로 남아 있었어요.

로그인 없는 도구일수록 자주 만나요

플랜씨(plan-c) 금융계산기처럼 로그인 없이 쓰는 도구는 사용자 설정을 서버 대신 브라우저에 저장할 일이 많아요. 라이프케어로그 화면을 만들면서도 persist를 붙이는 곳마다 이 패턴을 기본으로 깔아요.

테스트해 보면 규칙은 하나로 정리돼요. 서버가 모르는 값은 첫 렌더에 그리지 않기. persist를 붙이기 전에 이 값을 서버도 아는지부터 확인해 보세요.

#Zustand#Next.js#hydration

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.