Zustand persist가 SSR hydration mismatch를 터뜨리는 이유
Zustand persist가 Next.js SSR에서 hydration mismatch 경고를 만드는 원인과 마운트 플래그, skipHydration 해결법을 정리했어요.
새로고침할 때마다 콘솔에 hydration mismatch 경고가 쌓인 적 없나요? 다크모드 토글을 Zustand persist로 저장했을 뿐인데요. 혼자 운영하는 서비스에서 이 경고를 처음 만나면 어디서부터 봐야 할지 막막해요.
원인은 localStorage가 있는 위치예요
persist 미들웨어는 상태를 localStorage에 저장하고, 스토어가 만들어질 때 그 값을 다시 읽어요. 문제는 localStorage가 브라우저에만 있다는 점이에요.
서버는 저장된 값을 모른 채 초기값으로 HTML을 만들고, 브라우저는 저장된 값으로 첫 화면을 그려요. React는 이 둘이 같다고 가정하고 hydration을 진행하는데, 결과가 다르니 경고가 터져요.
직접 재현해 보니 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를 붙이기 전에 이 값을 서버도 아는지부터 확인해 보세요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.