CS/프로그래밍

Zustand란 무엇인가?

고래강이 2024. 3. 23. 13:52
개요
1.  Zustand의 특징
2. 다른 라이브러리와 비교
3. 사용 방법

 

 

✅ Zustand


npm trends

우리는 Client state를 관리하기 위해서 상태 관리 라이브러리를 사용해야하는데 현재 update가 활발하게 이루어지고 있으며 사용량도 꾸준히 증가하고 있는 Zustand에 대해서 알아보겠습니다. (Jotai를 개발한 회사에서 만들었다고 한다.)

 

📦 Zustand의 개발 이유

React에서 일어나는 3가지 문제점에 대한 해결책을 제시하기 위해 제작되었다고 한다.
  • Zombie children: 하위 컴포넌트인 "A"에서 실행하던 어떠한 일이 끝나기 전 상위 컴포넌트가 unmount되었을 시에 A가 작업을 끝낸 후 화면세엇 사라져도 메모리를 지속적으로 잡아먹는 문제
  • React concurrnecy: 우선순위를 나누어서 랜더링 관리를 하는 방식으로 랜더링 순서를 예측하기 힘들기에 외부 라이브러리들과 호환성에서 문제가 생길 수 있다는 문제
  • Context Loss: 너무 깊숙하게 얽혀있는 구조에서는 context에 접근이 제대로 되지 않을 수 있다는 문제

 

 

📦 Zustand의 장점

상태라는 뜻의 독일어로 작고 빠르며, 확장 가능한 베어본 상태 관리 솔루션이라고 공식 문서는 이를 소개한다. Zustand는 Redux와 비슷하게 flux 패턴을 사용하지만 redux에 비해 매우 단순하고 직관적이인 사용법 적은 보일러 플레이트, 작은 패키지 사이즈의 장점을 가지고 있다.
  • Redux와 동일하게 Redux devtools를 사용할 수 있다.
  • Provider가 필요 없으며 action이 없이도 상태 변경이 가능하다.
  • Store를 생성하고 selector를 통해 상태를 호출한다는 점은 Redux와 동일하다.

 

 


 

 

✅ 다른 라이브러리와 비교 (Comparison)


 

📦 Redux와 비교

불변 상태 모델을 기반으로 한다는 개념은 매우 유사하나 Redux에 비해서 간결하며, Redux와는 달리 Zustand는 Provider로 래핑하지 않아도 된다.

 

📦 Valtio와 비교

불변 상태 모델을 기반으로 하는 Zustand와 달리 Valtio는 가변 상태 모델을 기반으로 한다.
selector를 통해서 랜더링 최적화를 하는 Zustand와 달리 Valtio는 Propertiy aceess를 통해서 랜더링 최적화를 한다.

 

📦 Jotai와 비교 🔗

single store기반의 Zustand와 달리 Jotai는 atom이라는 작은 단위를 기반으로 해서 복잡한 상태 관리 로직을 구성할 수 있다.
외부에서 상태를 수정하거나 접근할 때 Zustand는 외부 스토어이기에 더욱 유용하다.
  • 비동기 작업에서 상태 관리
  • React 컴포넌트 밖에서 상태 사용
  • 비React 환경 간 상태를 공유해야할 때
  • 전역 상태 관리

 

📦 Recoil와 비교

Jotai와의 차이점과 비슷하다
atom의 종속성을 통해 최적화를 하는 Recoil에 비해 Zustand는 selector를 통해서 랜더링 최적화를 한다.

 

 


 

 

✅ 사용 방법


 

📦 기본 사용법

  • create 함수를 이용해서 store를 생성한다.
import { create } from "zustand";

interface CountState {
  counts: number;
  increaseCount: () => void;
}

const useCountStore = create<CountState>(set => ({
  counts: 0,
  increaseCount: () => set(state => ({ counts: state.counts + 1 })),
}));
  • 사용할 컴포넌트에서 selector함수를 전달하여 훅을 사용한다.
function Counter() {
  const count = useCountStore(state => state.count);

  return <div>{count}</div>;
}

function IncreaseButton() {
  const increaseCount = useCountStore(state => state.increaseCount);

  return <button onClick={increaseCount}>증가</button>;
}

 

 

 

 

📦 상태 변화 (updating state)

Flat updates

새로운 상태로 제공된 set함수를 호출하여 저장소의 기존 상태와 얕게 병합한다.

🎉 예시 코드)

더보기
import { create } from 'zustand'

type State = {
  firstName: string
}

type Action = {
  updateFirstName: (firstName: State['firstName']) => void
}

// 스토어를 만드는데 액션과 state를 포함하는 것임
const usePersonStore = create<State & Action>((set) => ({
  firstName: '',
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
}))

function App() {
  // 스토어에서 필요한 것을 가져다 쓰면 됨 state든 set함수든
  const firstName = usePersonStore((state) => state.firstName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)

  return (
    <main>
      <label>
        First name
        <input
          onChange={(e) => updateFirstName(e.currentTarget.value)}
          value={firstName}
        />
      </label>
      ...
    </main>
  )
}

 

Deeply nested object

type State = {
  deep: {
    nested: {
      obj: { count: number }
    }
  }
}

중첩된 깊은 상태값에 대해서 접근을 할 때에는 프로세스가 불변하도록 해야하므로 추가적인 조치가 필요하다.

  • Noraml approach

각 레벨의 상태를 복사해서 접근하는 방식으로 ...(spead 연산자)를 함께 사용하고 새 상태 값과 수동으로 병합하여 수행한다. 이 방식은 매우 길어 다른 대안을 찾아 볼 것임

  • with Immer, optics-ts, Ramda

불변성에 대해서 보장하기 위해서 immer, optics-ts, Ramda와 같은 라이브러리와 함께 사용하는 방식이 있으며 이러한 방식이 코드량도 적고 직관적이기에 선택적으로 사용하면 될 것 같다.

immerInc: () => set(produce((state: State) => {++state.deep.nested.obj.count;})),
opticsInc: () => set(O.modify(O.optic<State>().path("deep.nested.obj.count"))((c) => c + 1)),
ramdaInc: () => set(R.over(R.lensPath(["deep", "nested", "obj", "count"]), (c) => c + 1)),

 

전형적인 상태 변화 예시

import { create } from 'zustand'

const useCountStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

 

스토어 액션 없이 구현하기

스토어를 구현하면서 스토어 내부에 액션이 꼭 있어야 하는 것은 아니며, 이는 분리해서 사용할 수 있다

 

🎉 예시)

더보기
export const useBoundStore = create((set) => ({
  count: 0,
  text: 'hello',
  inc: () => set((state) => ({ count: state.count + 1 })),
  setText: (text) => set({ text }),
}))

export const useBoundStore = create(() => ({
  count: 0,
  text: 'hello',
}))

export const inc = () =>
  useBoundStore.setState((state) => ({ count: state.count + 1 }))

export const setText = (text) => useBoundStore.setState({ text })

 

미들웨어를 통한 URL 해시 활용

Zustand의 middleware기능을 통해서 URL값을 action안에서 조작이 가능하다.

 

🎉 예시)

더보기
import { create } from 'zustand'
import { persist, StateStorage, createJSONStorage } from 'zustand/middleware'

const hashStorage: StateStorage = {
  getItem: (key): string => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    const storedValue = searchParams.get(key) ?? ''
    return JSON.parse(storedValue)
  },
  setItem: (key, newValue): void => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    searchParams.set(key, JSON.stringify(newValue))
    location.hash = searchParams.toString()
  },
  removeItem: (key): void => {
    const searchParams = new URLSearchParams(location.hash.slice(1))
    searchParams.delete(key)
    location.hash = searchParams.toString()
  },
}

export const useBoundStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage', // unique name
      storage: createJSONStorage(() => hashStorage),
    },
  ),
)

 

 

 

 

 

 

📌 Reference

'CS > 프로그래밍' 카테고리의 다른 글

DX  (2) 2024.04.03
useLayoutEffect의 활용  (2) 2024.04.02
MSA(마이크로서비스 아키텍처)  (0) 2024.03.23
디자인 패턴 - 팩토리 패턴 / 전략 패턴 / 옵저버 패턴  (1) 2024.03.22
SOLID 원칙  (2) 2024.03.16