Zustand

장유진·2025년 3월 5일
post-thumbnail

2~3년 전에 Zustand 한 방에 정리라는 글을 작성했었는데, 그 때와는 달라진 점이 많아 다시 정리해보고자 한다.

공식문서: https://zustand.docs.pmnd.rs

1. 설치하기

npm install zustand

yarn add zustand

pnpm add zustand

2. 기본 사용법

먼저 store를 생성하고 상태와 액션을 정의한다.

import { create } from 'zustand';

interface CounterState {
  count: number;
  increase: () => void;
  decrease: () => void;
}

const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 }))
}));

생성한 store를 React 컴포넌트에서 가져와서 사용한다.

import React from 'react';
import { useCounterStore } from './store';

const Counter = () => {
  const { count, increase, decrease } = useCounterStore();

  return (
    <div>
      <h1>카운트: {count}</h1>
      <button onClick={increase}>증가</button>
      <button onClick={decrease}>감소</button>
    </div>
  );
};

export default Counter;

3. create, createStore 그리고 createWithEqualityFn

zustand에서는 create 또는 createStore 또는 createWithEqualityFn을 사용해서 store를 생성할 수 있다. 이 셋의 차이를 알아보자.

3-1. create

https://zustand.docs.pmnd.rs/apis/create

기본 store 생성 함수로, React 컴포넌트에서 직접 사용할 수 있는 훅을 생성한다.

const useCounterStore = create((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}));

const count = useCounterStore((state) => state.count);

3-2. createStore

https://zustand.docs.pmnd.rs/apis/create-store

zustand 4.0 이후로 도입된 함수로, 독립적인 store 객체를 반환하여 React 바깥에서도 사용할 수 있도록 한다. zustand를 웹 워커나 서버 환경에서 활용할 수 있게 된다.

const counterStore = createStore((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}));

// createStore는 useStore와 함께 사용 가능
import { useStore } from 'zustand';
const count = useStore(counterStore, (state) => state.count);

3-3. createWithEqualityFn

기본적으로 zustand는 상태가 변경되면 이전 상태와 현재 상태를 Object.js(얕은 비교)로 비교하고 달라진 점이 있다면 상태가 변경된 것으로 감지하고 리렌더링이 발생하게 된다. 하지만 객체 내부의 깊은 변경은 감지되지 않기 때문에 상태 내부에 중복된 객체가 있다면 createWithEqualityFn을 사용하여 커스텀한 비교 함수를 적용하는 것이 좋다.

import { createWithEqualityFn } from 'zustand/traditional';

const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
// deepEqual 대신 lodash의 isEqual 함수를 사용해도 됨! 뭐든 deep-equal이 가능한 함수면 상관없다.

const useStore = createWithEqualityFn((set) => ({
  user: { name: 'Alice', age: 25 },
  updateAge: (age) => set((state) => ({ user: { ...state.user, age } })),
}), deepEqual);

4. Selector 사용

zustand에서는 store를 사용할 때 selector를 사용하는 것을 권장한다. selector를 사용하면 원하는 상태나 액션만을 가져올 수 있다.

// selector 사용 X
const { count } = useCounterStore();
const count = useStore(counterStore);

// selector 사용 O
const count = useCounterStore((state) => state.count);
const count = useStore(counterStore, (state) => state.count);

selector를 사용하지 않으면 useCounterStore가 전체 상태 객체를 가져오고 그 중에서 count를 구조분해할당하는 방식이기 때문에 count 외의 다른 상태 값이 변경되어도 리렌더링이 발생한다.

반면에 selector를 사용하면 count 값만 가져오게 도므로 불필요한 리렌더링을 줄일 수 있다.

5. Middleware

zustand에서는 다양한 미들웨어를 지원한다.

5-1. combine

https://zustand.docs.pmnd.rs/middlewares/combine

combine을 사용하면 TypeScript를 사용할 때 상태의 타입을 직접 작성하지 않고 추론하도록 할 수 있다.

// combine 사용 X
type State = {
  count: number;
}

type Action = {
  increase: () => void;
}

const useCounterStore = create<State & Action>( set => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}))

// combine 사용 O
const useCounterStore = create(
  combine(
    { count: 0 }, // initialState
    set => { // action
      increase: () => set((state) => ({ count: state.count + 1 }))
    }
  )
)

5-2. devtools

https://zustand.docs.pmnd.rs/middlewares/devtools

Redux Devtools Extension을 사용하게 해주어 상태를 시각적으로 확인할 수 있다.

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools((set) => ({ 
    count: 0, 
    increase: () => set((state) => ({ count: state.count + 1 })) 
  }))
);

5-3. immer

https://zustand.docs.pmnd.rs/middlewares/immer

immer 미들웨어를 사용하면 상태를 업데이트할 때 자동으로 불변성을 유지하도록 관리해준다.

배열 업데이트

// immer 사용 X
const useStore = create(
  (set) => ({
  	items: [],
    addItem: (item) => set((state) => ({ arr: [...state.arr, item]})),
  })
);

// immer 사용 O
const useStore = create(
  immer((set) => ({
  	items: [],
    addItem: (item) => set((state) => { state.arr.push(item); }),
  }))
);

객체 업데이트

// immer 사용 X
const useStore = create(
  (set) => ({
  	person: { name: 'John', age: 30 },
    setName: (str) => set((state) => ({ person: {...state.person, name: str} })),
  })
);

// immer 사용 O
const useStore = create(
  immer((set) => ({
  	person: { name: 'John', age: 30 },
    setName: (str) => set((state) => { state.person.name = str; }),
  }))
);

5-4. persist

https://zustand.docs.pmnd.rs/middlewares/persist

persist 미들웨어는 상태를 로컬 스토리지나 세션 스토리지에 저장하여 새로고침 후에도 유지할 수 있게 한다.

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const usePersistedStore = create(
  persist(
    (set) => ({ 
      count: 0, 
      increase: () => set((state) => ({ count: state.count + 1 })) }),
    { name: 'counter-storage' }
  )
);

5-5. subscribeWithSelector

https://zustand.docs.pmnd.rs/middlewares/subscribe-with-selector

특정 상태의 변경을 감지하도록 구독하고 리스너를 등록할 수 있다.

const useCounterStore = create(
  subscribeWithSelector(
    (set) => ({
    count: 0,
    increase: () => set((state) => ({ count: state.count + 1   })),
})));

store에서 subscribe 함수를 실행할 수 있고, selector와 listener를 인자로 받는다. subscribe가 반환하는 함수를 실행하면 구독을 중단할 수 있다.

// 구독 시작
const unsubscribe = useCounterStore.subscribe(state => state.count, (state) => console.log(state.count));

// 구독 중단
unsubscribe();

6. shallow, useShallow

shallow와 useShallow는 이름은 비슷하지만 아주 다른 역할을 한다.

6-1. shallow

https://zustand.docs.pmnd.rs/apis/shallow

shallow 함수는 두 인자를 받고, 두 값이 같은 지 다른 지 얕은 비교를 한 결과를 리턴한다.
따라서 중첩된 객체에는 사용할 수 없고, primitive 값이나 중첩이 없는 객체에만 사용할 수 있다.

shallow(1, 1) // true
shallow({ name: 'A'}, { name: 'A'}) // true

const person1 = {
  name: {
  	firstname: 'A',
    lastname: 'B'
  }
}
const person2 = {
  name: {
  	firstname: 'A',
    lastname: 'B'
  }
}
shallow(person1, person2) // false

6-2. useShallow

https://zustand.docs.pmnd.rs/hooks/use-shallow

useShallow는 selector로 선택된 값들에 대해 얕은 비교를 진행하고 결과가 같으면 리렌더링이 발생하지 않도록 해주는 React 훅이다. 따라서 상태의 개수가 많아서 4번의 selector를 일일히 사용해주기 힘들 때 대신 사용해줄 수 있다.

// shallow 사용 X
const type = useStore(state => state.type);
const name = useStore(state => state.name);
const count = useStore(state => state.count);

// shallow 사용 O
const { type, name, count} = useStore(
	useShallow(state => ({
    	type: state.type,
      	name: state.name,
      	count: state.count
    }));
);
profile
프론트엔드 개발자

0개의 댓글