한 번 써보면 헤어나올 수 없는 마성의 React 상태 관리 라이브러리 Zustand!
324kB라는 깃털같은 패키지 사이즈와 간단한 사용법, 압도적 편리함까지..
이렇게 좋아도 되나? 싶은 생각이 들 정도로 애정하는 라이브러리다.
이 녀석을 Next.js 환경에서 사용해보며 마주하고, 해결한 문제를 정리해보려 한다.
Zustand
는 상태를 핸들링할 수 있는 다양한 middleware
들을 제공한다. 오늘 이야기해볼 녀석은 이중에서도 바로 persist
라는 미들웨어다.
persist
미들웨어는 웹 프론트엔드 엔지니어라면 익숙할, 클라이언트 측 저장소들인 로컬 / 세션 등의 다양한 스토리지에 상태를 저장할 수 있도록 도와주는 녀석이다. 이를 이용해 페이지 새로고침 or 페이지 재방문 시에 데이터를 그대로 복원하여 유저에 제공할 수 있겠지 ❗
문제는 Next.js
를 이용한 팀 프로젝트 진행 중 발생했다. 늘 하던 대로 persist
를 이용하여 데이터를 로컬 스토리지에 저장하고, 사용하려 했는데...
state
를 이용하여 해당 에러를 재현한 모습 (state
초기값 0)엥? 처음 보는 에러를 만났다. 문제의 요지는 클라이언트 측과 서버 측의 데이터가 같지 않다는 것.
이러한 경우를 Hydration error
라고 부른다.
Hydration
은 Next.js
의 SSR
렌더링 과정 중 일부인데, 서버에서 사전 렌더링 된 HTML에 JavaScript를 입혀 상호 작용 가능한 어플리케이션으로 만드는 과정을 의미한다. 이러한 과정 중 클라이언트 / 서버 간 데이터가 일치하지 않는 문제가 발생했다고 보면 될 듯.
문제 발생 상황을 정리해보면,
클라이언트 측에서 state
를 스토리지에 저장하여 보관하고, 이를 웹 페이지와 상호작용하며 변동을 일으킨다.
state
에 변화가 일어난 후, 유저가 페이지 새로고침 / 재접속 시에 Hydration error
가 발생한다.
그렇다면 왜 이런 문제가 발생하는 걸까? Next.js
의 공식 문서 서칭을 통해 답을 찾을 수 있었다.
에러 메세지에서도 볼 수 있듯, 서버 측에서는 스토리지에 저장된 state
가 변동되었는 지 알 방도가 없다. 따라서 서버는 state
의 초기값인 0으로 HTML
을 사전 렌더링하여 클라이언트 측에 전송하는 것.
이 때, 클라이언트 측에서는 물음표를 띄우게 된다. "어? 내가 기억하는 state
는 1이라, 화면에 1을 렌더링 해야 하는데.. Next.js 서버에서는 0을 그려서 나한테 보내줬네?" 헷갈려!
이런 경우가 위의 에러 상황이라고 할 수 있는거지 😀
친절하게도, 나의 사랑 Zustand 는 이러한 문제를 해결할 수 있는 공식적인 가이드라인을 Docs에서 알려주고 있었다.
방법은 바로 커스텀 훅을 통한 데이터 동기화였다. 백문이 불여일견. 일단 한번 코드를 보자.
// useStore.ts
import { useState, useEffect } from 'react'
const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F,
) => {
const result = store(callback) as F
const [data, setData] = useState<F>()
useEffect(() => {
setData(result)
}, [result])
return data
}
export default useStore
useStore
커스텀 훅// yourComponent.tsx
import useStore from './useStore'
import { useBearStore } from './stores/useBearStore'
const bears = useStore(useBearStore, (state) => state.bears)
Hydration error
를 방지하며 store
을 사용하는 코드핵심 포인트는 커스텀 훅의 useEffect
부분이다.
서버 사이드에서 store
상태를 받아온 후, 로컬 store
에 저장된 데이터로 이를 업데이트해준다.
이렇게 커스텀 훅을 이용하여 중간에 store
데이터를 동기화해주는 작업을 추가함으로써, Hydration error
를 피할 수 있는 것.
더 자세한 설명은, 해당 방법을 찾아 낸 원작자의 블로그 글에서 찾아볼 수 있다.
다만, 이 방법을 이용 시 추가적인 문제가 발생한다.
바로 커스텀 훅을 이용하여 store
를 이용하는 컴포넌트의 첫 렌더링 시 store
가 undefined
를 반환하는 문제.
이를 피하기 위해, 간단한 undefined
체크가 필요하다. 아래처럼 말야~
const bears = useStore(useBearStore, (state) => state.bears)
return
<>
<div>... 기타 JSX 코드들 </div>
{bears? {bears} : undefined}
</>
좋은 글 잘 보고 갑니다.
올 한 해도 수고 많으셨습니다!