클라이언트 전역 상태 라이브러리 zustand를 사용하다가 에러가 발생했다.
supabase Authentication session API를 사용해서 조건부 렌더링을 하던 도중 해당 에러를 마주하게 됐다.
const useAuthStore = create<AuthState>()(
persist(
set => ({
session: null,
setSession: session => set({ session }),
}),
{ name: 'session-status' },
),
);
스토리지의 저장하기 위해 persist
를 사용했고 처음엔 로직 상 문제는 없었보였지만 에러에서 착하게도 던져준 링크와 zustand 공식 문서에 나와있는 내용을 확인해보니 프레임워크 상 Next.js는 SSR을 사용하니까 서버에서 렌더링된 요소와 클라이언트에서 렌더링된 구성 요소를 비교하게 되는데 구성 요소를 변경하기 위해 브라우저의 데이터를 사용하고 있어서 두 렌더링이 다르게 되니까 Next에서 경고가 표시됐었다.
zustand에서 말하는 에러 메세지는 총 3개이고, 우리 팀원 분도 2번 에러가 발생했었다.
Text content does not match server-rendered HTML
Hydration failed because the initial UI does not match what was rendered on the server
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering
에러를 해결하기 위해서 Next에서 제시한 방법은 useEffect를 클라이언트에서만 사용하는 방법과
사전 렌더링 비활성화
를 하는 동적 import 방법을 제시했다.
근데 사전 렌더링 비활성화를 하는 방법을 사용하면 서버 사이드와 클라이언트 사이드에 상태가 다를 수도 있을 거 같아서 사용하지 않았고 에러 발생 지점 컴포넌트에서 useEffect를 사용하여 로딩을 걸어줬다.
const StickBar = () => {
const { logout } = useAuth();
const { session } = useAuthStore();
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setIsLoaded(true);
}, []);
return (
<>
{isLoaded && (
<div className={styles.wrapper}>
// ...
<div className={styles.tabArea}>
{session === null ? (
<>
<Link href="/auth/signup">회원가입</Link>
<Link href="/auth/login">로그인</Link>
</>
) : (
<>
<Link href="/" onClick={() => logout()}>
로그아웃
</Link>
</>
)}
</div>
</div>
)}
</>
);
};
export default StickBar;
zustand 에서도 요소를 비교하고 변경하기 전에 잠시 기다리라는 커스텀 훅을 만들라고 한다. 그런데 특정 한 컴포넌트에서만 그러니 별도로 만들지는 않았다.
Next.js 공식 문서 참고: https://nextjs.org/docs/messages/react-hydration-error
zustand: 공식 문서 참고: https://docs.pmnd.rs/zustand/integrations/persisting-store-data#hydration-and-asynchronous-storages