Zustand의 Persist라는 middleware를 사용하면 localStorage와 연동하여 자동으로 전역 상태 관리 내용을 localStorage에 저장시키고 관리할 수 있다. (위 링크 참고)
localStorage는 브라우저를 닫아도 그대로 남아있기에 창을 닫아도 상태 값이 남아있어야 하는 경우 유용하게 사용할 수 있다.
필자가 먼저 겪은 문제는 Typescript 환경에서 Zustand의 Persist를 사용하는 경우 type 에러가 나는 것이었다.
이는 위 공식 레포 답변의 workaround를 응용해 해결하였다.
example
store.ts
import create from 'zustand'
import { persist } from 'zustand/middleware'
import { SupplementDetailsType } from '../utils/types'
import { pillListPersist, pillListState } from './storeTypes'
export const useUserPillListStore = create<pillListState>(
// @ts-ignore
(persist as pillListPersist)(
(set) => ({
userTakingPillList: [],
setUserTakingPillList: (data: SupplementDetailsType[]) => {
set((state) => ({...state, userTakingPillList: data}))
}
}),
{
name: 'userTakingPillList'
}
)
)
storeTypes.ts
import { SupplementDetailsType } from '../utils/types'
import { StateCreator } from 'zustand'
import { PersistOptions } from 'zustand/middleware'
export type pillListState = {
userTakingPillList: SupplementDetailsType[]
setUserTakingPillList: (data: SupplementDetailsType[]) => void
}
export type pillListPersist = (
config: StateCreator<pillListState>,
options: PersistOptions<pillListState>
) => StateCreator<pillListState>
이렇게 해놓고 사용하는 컴포넌트에서 아래 코드와 같이 불러오면 된다.
const { userTakingPillList, setUserTakingPillList } = useUserPillListStore()
(+ 모두 불러오면 상관 없는데 둘 중 한 변수만 사용하는 경우 아래와 같이 쓰는 것이 랜더링 측면에서 좋다고 한다.)
const userTakingPillList = useUserPillListStore(state => state.userTakingPillList)
문제는 이 이후 터졌다.
필자는 NextJS를 사용하고 있었는데 Next의 경우 SSR을 하기 때문에 서버에서 랜더링을 할 때 window 객체가 당연히 undefined가 되어 localStorage에 접근이 안되는 것이었다.
(localStorage는 window 객체의 속성이기 때문에)
이러한 이유로 초기 렌더링 페이지에서 zustand-persist의 useStore을 사용하면 Hydration Failed 에러가 발생했다.
이를 해결하기 위해 구글링을 통해 공식 레포 답변들을 참고하였는데 마땅한 해결책이 없었다.
해결책
이러한 이유로 필자는 그냥 따로 localStorage에 접근할 수 있는 커스텀 Hook을 만들기로 했다.
example
import { useEffect, useState } from 'react'
import { SupplementDetailsType } from '../utils/types'
const useGetLocalPillList = (): SupplementDetailsType[] => {
const [userTakingPillList, setUserTakingPillList] = useState<SupplementDetailsType[]>([])
useEffect(() => {
// localStorage에서 'userTakingPillList'라는 key이름으로 데이터를 꺼내봄.
const jsonLocalTakingPillList = localStorage.getItem('userTakingPillList')
// 만약 있다면 (null이 아니라면) if문 안의 내용 실행
if (jsonLocalTakingPillList !== null) {
// localStorage에 넣을 때 Json으로 바꿔줬으므로 다시 되돌리기 위해 파싱
const localTakingPillList = JSON.parse(jsonLocalTakingPillList)
if (localTakingPillList.state.userTakingPillList.length !== 0) {
setUserTakingPillList(localTakingPillList.state.userTakingPillList)
}
}
}, [])
return userTakingPillList
}
export default useGetLocalPillList
위와 같이 커스텀 Hook을 만들고, 오류가 나는 컴포넌트에 Zustand-persist의 훅 대신 위 커스텀 훅으로 교체해주었다.
(+ 23/2/5 추가)
useEffect를 사용하면 굳이 직접 localStorage에 접근하지 않아도 해결할 수 있다.
zustand 이슈를 보니 아직도 이 문제를 가지고 얘기중이던데...
export const useUserIntakePillList = () => {
const intakePillList = useUserIntakeManagementStore(state => state.intakePillList)
const [returnIntakePillList, setIntakePillList] = useState<IntakeManagementType[]>([])
useEffect(() => setIntakePillList(intakePillList), [])
return returnIntakePillList
}
위와 같이 커스텀 훅에서 Store를 불러온 후, 해당 값을 useEffect에서 처리해 반환하는 방식이다.
근본적인 해결은 아니지만 현 상황에서 할 수 있는 최선의 방법 같다.