본문 바로가기
이론/Frontend

Zustand로 전역 상태 관리하기

by 유세지 2023. 12. 20.

 

 

 

들어가며

React에서 전역 상태를 관리하는 방법에는 대표적으로 Context API가 있습니다. 단방향의 데이터 흐름을 해치지 않으면서도, Provider를 통해 각 컴포넌트에서 상태를 사용할 수 있게 해주는 기능입니다. React에서 공식적으로 지원하는 기능이기에 공식 문서에서도 이 방법을 추천하고 있습니다.

 

아직 Context API에 대해 잘 모른다면 아래 포스트를 참고해주세요.

 

그러나 Context API를 직접 사용하지 않고 서드 파티 라이브러리를 이용해 전역 상태를 관리하는 방법도 있습니다. 대표적으로 Zustand, Recoil, Jotai 등이 있는데요, 오늘은 Zustand를 사용하는 방법과 주의할 점들을 알아보도록 하겠습니다.

 

 

구성

먼저 사용하는 패키지 매니저를 통해 라이브러리를 설치하고, 필요한 환경을 구성하겠습니다.

 

npm install zustand
# 또는
yarn add zustand

 

 

 

다음으로는 상태를 저장할 store를 만듭니다. 

흔히 전역 상태에 많이 넣어 사용하는 다크모드 플래그로 만들어보겠습니다.

 

import { create } from 'zustand'

const useThemeStore = create((set) => ({
  isDarkMode: false,
  toggleDarkMode: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
}))

export useThemeStore;

 

 

이제 이 store를 컴포넌트에서 사용하기만 하면됩니다.

 

function DarkModeIndicator() {
  const isDarkMode = useThemeStore((state) => state.isDarkMode)

  return <div>{isDarkMode ? 'on' : 'off'}</div>
}

function ToggleDarkModeButton() {
  const toggleDarkMode = useThemeStore((state) => state.toggleDarkMode)

  return <button onClick={toggleDarkMode}>change</button>
}

 

 

 

활용

Persist

다크모드는 이후 방문시에도 쭉 유지되어야 할 사용자 속성입니다.

그러나 React의 상태는 메모리에 저장되어 새로고침을 하면 휘발됩니다.

이는 Zustand도 마찬가지이며, 보통 이런 경우에는 데이터를 로컬 스토리지에 저장합니다.

 

그러나 직접 만들 필요는 없고, 내장된 persist를 사용하면 간편하게 사용할 수 있습니다.

(zustand가 redux의 영향을 많이 받아서인지, persist 또한 redux-persist의 영향을 받은 느낌입니다.)

 

 

import { persist, createJSONStorage } from 'zustand/middleware'

const useThemeStore = create(
  persist(
    (set, get) => ({
      isDarkMode: false,
      toggleDarkMode: () => set(() => ({ isDarkMode: !get().isDarkMode })),
    }),
    {
      name: 'isDarkMode',
      storage: createJSONStorage(() => sessionStorage), // 지정하지 않으면 localStorage가 기본값입니다.
    },
  ),
)

 

 

 

persist를 Next.js에서 사용하는 경우

Next.js에서 브라우저의 데이터를 사용하려면 클라이언트 렌더링 이후에 동작해야합니다.

useEffect를 이용한 커스텀 훅을 만들어 사용할 수 있습니다.

 

// 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;

 

 

사용할 컴포넌트에서 이 커스텀 훅을 불러와서 감싸줍니다.

 

// Component.tsx
import useStore from './useStore'
import { useThemeStore } from './stores/useThemeStore'

const isDarkMode = useStore(useThemeStore, (state) => state.isDarkMode)

 

 

 

주의할 점

상태를 사용하며 주의해야 할 점은 역시 리렌더링 이슈입니다.

일반적으로 상태를 관리할때 흔히 같은 store에 있는 데이터가 변경되면,

해당 데이터를 사용하지 않는 컴포넌트더라도 리렌더링이 발생하는 경우가 더러 존재합니다.

 

Zustand의 경우 공식 문서의 방법대로 상태를 가져오면 이러한 리렌더링이 일어나지 않는데,

커스텀 훅에 데이터를 넣고 사용하던 방식대로 상태를 가져오면 리렌더의 가능성이 있습니다.

 

useThemeStore에 이렇게 두 가지의 상태가 있다고 했을때,

 

import { create } from 'zustand'

const useThemeStore = create((set) => ({
  isDarkMode: false,
  toggleDarkMode: () => set((state) => ({ isDarkMode: !state.isDarkMode })),
  themeColor: 'green',
  changeThemeColor: (color) => set(() => ({ themeColor: color })),
}))

export useThemeStore;

 

 

// Componenet.tsx

// ❌ isDarkMode가 변할경우 리렌더링 발생
const { themeColor } = useThemeStore();

// ✅ isDarkMode가 변하더라도 리렌더링되지 않음
const themeColor = useThemeStore((state) => state.themeColor);

 

 

이는 구조 분해 할당을 사용하여 한 가지의 상태만 가져온 것처럼 보이지만,

실제로는 모든 값을 가져오고 그 중 isDarkMode 하나만 사용할 뿐입니다.

따라서 다른 상태의 변동으로 인해 리렌더링이 발생할 수 있습니다.

 

 

구조 분해 할당 vs state 지정

 

 

 

마치며

Context API나 다른 상태 관리 라이브러리들과 비교했을때 가장 큰 장점 중 하나로,

더 적은 보일러 플레이트로도 상태를 관리할 수 있다는 점이 Zustand의 큰 매력이라고 생각합니다.

 

Flux 패턴을 충실히 따르는, redux와도 결이 비슷한 라이브러리지만

훨씬 간편하게 사용할 수 있어 많은 분들이 적용해나가고 있습니다. (npm trend 기준)

 

빠르게 적용하는데 그치지 않고, Zustand의 내부 구현에 대해 좀 더 자세히 알고 싶다면

아래 글들을 참고해보시는 것도 좋을 것 같습니다.

 

 

윗 글은 Zustand v3 까지의, 밑 글은 v4부터의 구현에 대해 다루고 있는데,

두 글 모두 Zustand를 처음 사용할때 라이브러리에 대해 이해하는데 많은 도움을 주었습니다.

 

읽어주셔서 감사합니다.

반응형

댓글