Zustand

Paul Mo·2023년 1월 15일
0

지금 다니고 있는 회사는 상태관리를 위해서 내가 전에 사용해 봤던 Redux나 Redux Tool Kit, Recoil이 아닌 Zustand와 RxJS를 사용하고 있다. 그래서 이번에는 그중에 Zustand에 대해 글을 작성해보려고 한다.

Zustand란?

Zustand 또한 상태관리 라이브러리이다. 공식 Github 문서에서 이렇게 설명한다.

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.

심플화된 플럭스 원리를 사용하는 작고 빠르고 확장 가능한 상태 관리 솔루션이고 Hook을 기반으로 하는 편리한 API라고 한다. 그리고 Redux와 Context를 보완한 사용하기 쉬운 상태 관리 라이브러리라고도 하는데 이유는 다음과 같다.

  • Redux 대신 Zustand를 사용하는 이유
    • 간단하고 의견이 없음
    • 훅을 주로 사용
    • Context Provider로 wrap 하지 않아도 됨
    • 렌더링 없이 상태 변화를 컴포넌트에 알릴 수 있음
  • Context 대신 Zustand를 사용하는 이유
    • 적은 보일러플레이트 코드
    • 상태 값이 변경될 때만 렌더링
    • 중앙화된 액션 베이스 상태 관리

사용법

공식문서에서 설명하는 사용법은 정말 간단하다.

  1. 스토어 생성
import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))
  1. 컴포넌트 바인딩
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

create함수로 스토어를 생성하고 해당 스토어를 바로 컴포넌트 안에서 useBearStore((state) => state.bears)로 불러와서 상태의 값을 가져와서 보여준다. 상태를 변경하기 위해서는 함수 안에서 스토어를 생성할 때 명시한 increasePopulation 함수를 useBearStore((state) => state.increasePopulation)으로 호출해서 사용하면 끝이다.

개인적으로 Recoil 만큼 사용하기 편리하고 간단한 것 같았다. 객관적으로 봐도 공식문서에서 당당하게 말했던 것처럼 정말 심플한 상태관리 라이브러리다.

코어코드

Zustand가 무엇인지 그리고 어떻게 사용하는지 간단하게 알아보았다. 공식문서에만 있는 정보를 습득하는 것에 만족하지 않고 Zustand가 어떻게 실제로 구현되는 것인지 코어코드를 한번 살펴보았다.

const createStoreImpl: CreateStoreImpl = (createState) => {
  type TState = ReturnType<typeof createState>;
  type Listener = (state: TState, prevState: TState) => void;

  // 스토어의 상태는 클로저로 관리
  let state: TState;

  // 상태 변경을 구독할 리스너를 Set으로 관리
  // Set으로 관리할 경우 자동으로 중복 값을 제거
  const listeners: Set<Listener> = new Set();

  // 함수를 전달받을 경우 현재 상태를 인자로 넘겨주는 식으로 '다음 상태' 를 정의
  // 함수가 아니면 받은 인자를 다음 상태로 정의
  const setState: StoreApi<TState>["setState"] = (partial, replace) => {
    const nextState =
      typeof partial === "function"
        ? (partial as (state: TState) => TState)(state)
        : partial;

    // nextState 가 기존 state 와 다른 경우 state 를 갱신
    // 상태를 갱신할 때 간단하고 효과적인 Object.assign 을 사용한다.
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state =
        replace ?? typeof nextState !== "object"
          ? (nextState as TState)
          : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, previousState));
    }
  };

  const getState: StoreApi<TState>["getState"] = () => state;

  // 상태를 구독하는 함수를 등록
  const subscribe: StoreApi<TState>["subscribe"] = (listener) => {
    listeners.add(listener);
    // Unsubscribe
    return () => listeners.delete(listener);
  };

  // 모든 리스너를 제거한다. 하지만 이미 정의된 상태를 초기화하진 않는다.
  const destroy: StoreApi<TState>["destroy"] = () => listeners.clear();

  const api = { setState, getState, subscribe, destroy };

  // 인자로 전달받은 createState 함수를 이용하여 최초 상태를 설정한다.
  state = createState(setState, getState, api);
  return api as any;
};

const createStore = ((createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore;

export default createStore;

스토어는 state 변수로 클로저로 관리하며 set과 get 함수로 해당 상태를 갱신하고 상태값을 가져온다. 리스너를 Set으로 사용하여 중복되는 리스너를 자동으로 걸러내고 subscribe 함수로 상태변경을 구독하면 상태변경을 감지할 수 있다.

Zustand의 코어코드도 이렇게 한번 분석을 해봤는데 생각보다 간단한 코드로 이루어졌고 이 코드들로 상태관리가 가능하게 된다는 것이 놀라웠다. 그리고 코어코드를 이해하고 나서 공식문서에 설명된 코드들을 보니 코어코드의 어느 부분이 동작해서 상태관리가 작동하는 것이 눈앞에 보이는 것 같았다. 이번 경험을 통해서 공식문서의 내용도 물론 중요하지만 그 안의 코어코드를 뜯어보면 구현원리를 알 수 있고 그렇게 되면 사용법 또한 이해하기 쉽다는 교훈을 얻었다. 앞으로도 공식문서만 읽고 지식과 사용방법만 습득하는 것이 아니라 구현원리를 깨닫고 이해할 수 있는 공부를 하도록 노력해 봐야겠다.

profile
프론트 엔드 개발자

0개의 댓글