Front-End/Next.JS

[NextJS / Typescript] Zustand Persist middleware localStorage 접근이 안되는 문제 (+ Zustand Persist Typescript 환경 type 에러)

koh1018 2022. 8. 4. 23:43
반응형
 

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.

github.com

Zustand의 Persist라는 middleware를 사용하면 localStorage와 연동하여 자동으로 전역 상태 관리 내용을 localStorage에 저장시키고 관리할 수 있다. (위 링크 참고)

 

localStorage는 브라우저를 닫아도 그대로 남아있기에 창을 닫아도 상태 값이 남아있어야 하는 경우 유용하게 사용할 수 있다.

 

필자가 먼저 겪은 문제는 Typescript 환경에서 Zustand의 Persist를 사용하는 경우 type 에러가 나는 것이었다.

 

TypeScript type conflict with persist middleware · Issue #650 · pmndrs/zustand

I am unable to get the persist middleware working with TypeScript if I define state properties with multiple types or with the boolean type. zustand version: 3.6.4 TypeScript version: 4.1.3 Example...

github.com

이는 위 공식 레포 답변의 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 에러가 발생했다.

 

이를 해결하기 위해 구글링을 통해 공식 레포 답변들을 참고하였는데 마땅한 해결책이 없었다.

 

Zustand, Next.js, localStorage · Issue #268 · pmndrs/zustand

Excuse me, in fact this is not an issue, this is a beginner's question. I'm using Zustand with Next.js, and trying to set an initial value from localStorage in const useStore. E.g. if exist...

github.com

 

 

Persist middleware in server rendered environments · Issue #245 · pmndrs/zustand

Hi there! While working with the new persist middleware I figured using it in a server rendered environment (in my case, next.js) it would throw as the default localStorage api expectedly isn't...

github.com

 

 

해결책

이러한 이유로 필자는 그냥 따로 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에서 처리해 반환하는 방식이다.

 

 

근본적인 해결은 아니지만 현 상황에서 할 수 있는 최선의 방법 같다.

반응형