Next.js 환경에서 Zustand를 이용하여 스토리지 사용하기(Feat. Hydration error)

이형준·2023년 12월 24일
4

트러블슈팅

목록 보기
4/7


한 번 써보면 헤어나올 수 없는 마성의 React 상태 관리 라이브러리 Zustand!
324kB라는 깃털같은 패키지 사이즈와 간단한 사용법, 압도적 편리함까지..
이렇게 좋아도 되나? 싶은 생각이 들 정도로 애정하는 라이브러리다.
이 녀석을 Next.js 환경에서 사용해보며 마주하고, 해결한 문제를 정리해보려 한다.


persist? 🤔

Zustand 는 상태를 핸들링할 수 있는 다양한 middleware 들을 제공한다. 오늘 이야기해볼 녀석은 이중에서도 바로 persist 라는 미들웨어다.

persist 미들웨어는 웹 프론트엔드 엔지니어라면 익숙할, 클라이언트 측 저장소들인 로컬 / 세션 등의 다양한 스토리지에 상태를 저장할 수 있도록 도와주는 녀석이다. 이를 이용해 페이지 새로고침 or 페이지 재방문 시에 데이터를 그대로 복원하여 유저에 제공할 수 있겠지 ❗


문제 발견 ❌

문제는 Next.js 를 이용한 팀 프로젝트 진행 중 발생했다. 늘 하던 대로 persist 를 이용하여 데이터를 로컬 스토리지에 저장하고, 사용하려 했는데...

  • 간단한 숫자를 저장한 state 를 이용하여 해당 에러를 재현한 모습 (state 초기값 0)

엥? 처음 보는 에러를 만났다. 문제의 요지는 클라이언트 측서버 측의 데이터가 같지 않다는 것.

이러한 경우를 Hydration error 라고 부른다.

HydrationNext.jsSSR 렌더링 과정 중 일부인데, 서버에서 사전 렌더링 된 HTML에 JavaScript를 입혀 상호 작용 가능한 어플리케이션으로 만드는 과정을 의미한다. 이러한 과정 중 클라이언트 / 서버 간 데이터가 일치하지 않는 문제가 발생했다고 보면 될 듯.

문제 발생 상황을 정리해보면,

  1. 클라이언트 측에서 state 를 스토리지에 저장하여 보관하고, 이를 웹 페이지와 상호작용하며 변동을 일으킨다.

  2. 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 를 이용하는 컴포넌트의 첫 렌더링 시 storeundefined 를 반환하는 문제.

이를 피하기 위해, 간단한 undefined 체크가 필요하다. 아래처럼 말야~

const bears = useStore(useBearStore, (state) => state.bears)

return 
<>
   <div>... 기타 JSX 코드들 </div>
   {bears? {bears} : undefined}
</>
profile
저의 미약한 재능이 세상을 바꿀 수 있을 거라 믿습니다.

1개의 댓글

comment-user-thumbnail
2023년 12월 26일

좋은 글 잘 보고 갑니다.
올 한 해도 수고 많으셨습니다!

답글 달기