Zustand

2025-06-10
Zustand 사용방법 정리 + MMKV 저장소와 연결


Zustand

Zustand는 작고 빠르며 확장 가능한 React 등에서 사용하는 상태 관리 라이브러리이다.

설치 : npm install zustand

기본 사용방법

create를 사용해 만들어진 Store를 통해서 상태를 관리한다.

import { create } from 'zustand';

// Store에 저장할 상태의 타입 선언
type CountState = {
  count: number;
  increase: () => void;
  decrease: () => void;
};

// Store 생성 - 타입을 지정하지 않으면 state는 any타입이 된다.
const useStore = create<CountState>()((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 })),
}));

// Store 사용
function Screen() {
  const count = useStore((state) => state.count);
  const increase = useStore((state) => state.increase);
  const decrease = useStore((state) => state.decrease);

  return (
    <>
      <h1>{count}</h1>
      <button onClick={increase}>INCREASE</button>
      <button onClick={decrease}>DECREASE</button>
    </>
  );
}

useStore

create를 통해 만들어진 useStore는 커스텀 훅으로 useState처럼 작동한다.
상태를 컴포넌트에 넣어 사용하면 자동으로 리랜더링이 적용된다.

create()

create는 Store를 만드는 핵심 함수로 커스텀 훅을 만들어 반환한다.
반환된 훅을 사용하여 useState와 비슷하게 리렌더링을 할 수 있다.

type storeType = {
  count: number;
  increase: () => void;
};

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

create 함수의 타입
function create<T>(stateCreatorFn: StateCreator<T>): UseBoundStore<StoreApi<T>>

  • T : 정의하는 스토어 전체의 상태 타입
  • StateCreator<T> : 스토어를 생성하는 함수
  • UseBoundStore<StoreApi<T>> : 반환 타입, 우리가 사용하는 useStore 훅

더 정확한 타입 ([[커링 패턴]])
create<T>()(stateCreatorFn: StateCreator<T, [], []>): UseBoundStore<StoreApi<T>>

제네릭 타입으로 타입을 명시적으로 전달하고, 상태 생성 함수를 나중에 만들어 전달하게 되어있다.
create 함수가 반환한 상태 생성 함수 (storeCreator)를 곧바로 실행시킨다.

풀어서 설명한다면 흐름이 이런식이다.

  1. create<T>() 호출 → storeCreator 함수 생성
  2. storeCreator(stateCreatorFn) 호출 → userStore 생성
const storeCreator = create<T>();
const userStore = storeCreator(stateCreatorFn);

즉, 아래 한 줄은 위 두 줄과 같다.

const userStore = create<T>()(stateCreatorFn);

create의 사용 방식들

  • create<T>(...) : 미들웨어를 사용하지 않을 때
  • create<T>()(...) : 미들웨어를 사용하며, 타입을 명시적으로 지정할 때
  • create(...) : 미들웨어를 사용하지만 타입 추론에 의존할 때 (combine)

  • set : 상태를 변경하는 함수 (setState)
  • get : 현재 상태를 가져오는 함수 (getState)

아래 두 set 함수는 동일하게 작동한다.

set((state) => ({ count: state.count + 1 }));
set({ count: get().count + 1 });
  • state => ({}) : set() 내부에서만 사용하며 상태가 이전과 달라질 때 사용한다.
  • get() : set() 외부에서도 사용 가능하며 여러 상태를 조합할 때 사용한다.

set()

set 함수는 상태를 업데이트 하는 핵심 함수다.
현재 상태를 받아서, 새 상태로 갱신하거나 부분적으로 병합한다.

set 함수의 타입
set: (partial: Partial<State> | (state: State) => Partial<State>, replace?: boolean) => void

partial은 상태 객체나 상태를 변경하는 함수를 받는다. (useState와 방식이 동일함)

  • 직접 상태 객체 입력 : set({count: 5});
  • 이전 상태를 기반으로 새 객체 반환 : set(state => ({count: state.count + 1}));

replace 옵션은 기존 상태와 병합하지 않고 ‘완전히 새 상태로 교체’ 한다.


+ {}()로 감싸는 이유는 객체를 반환하기 위해서. ()=>{}로 사용하면 단순한 함수가 된다.

const a = () => {
  return {};
};

const b = () => ({});

Middlewares

combine

combine은 상태(state)와 액션(actions)를 분리해 구성할 때 사용하는 함수다.
두 개의 객체(초기 상태 객체, 액션 함수 객체)를 하나로 결합해 stateCreatorFn으로 만들어준다.
액션 함수는 set을 사용해 기존 상태를 업데이트 한다.

combine은 상태와 액션을 결합하며 타입을 자동 추론하기 때문에 제네릭을 통한 명시가 필요 없어진다.

type CountState = { count: number };
type CountActions = { increase: () => void };
type CountStore = CountState & CountActions;

// combine을 사용하지 않음
const useStore1 = create<CountStore>((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}));

// combine을 사용해 상태와 액션을 분리함
const useStore2 = create<CountStore>( // 제네릭 <CountStore> 불필요함
  combine({ count: 0 }, (set) => ({
    increase: () => set((state) => ({ count: state.count + 1 })),
  }))
);

subscribe, subscribeWithSelector

스토어의 상태 변경사항을 감지해 등록된 리스너를 실행시켜주는 리스너 구독 함수이다.

// 리스너 함수 구독
const unsubscribe = useStore.subscribe((state) => {
  console.log('state has changed', state);
});
// 리스너 구독 해제
unsubscribe();

특정 상태값만 감시하고 싶거나 이전 상태와 변경된 상태를 비교할 때는 subscribeWithSelector미들웨어를 사용한다.

create로 스토어를 생성하며 subscribeWithSelector 미들웨어 등록해준다.

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

미들웨어를 사용하면 subscribe 사용방식이 변화한다.

  • selector : 전체 상태에서 특정 값만 선택하는 선택자 함수
    • 선택된 상태값의 변경에만 리스너가 실행된다.
  • listener : 상태 변경시 실행되는 리스너 함수
    • 이전값과 변경값을 인자로 받는다.
// 특정 상태값만 선택해주는 선택자
const selector: (state: CountState) => number = (state) => state.count;
// 이전값과 변경값을 비교해주는 리스너
const listener = (newCount: number, oldCount: number) => {
  console.log(`${newCount} -> ${oldCount}`);
};

// 선택자, 리스너 구독
const unsubscribe = useStore.subscribe(selector, listener);

persist

persist는 스토어의 상태를 브라우저/디바이스의 로컬 저장소(localStorage 등)에 자동으로 저장하고 복원시켜주는 미들웨어이다.
복원(Rehydration) : 로컬 저장소의 데이터를 앱 실행시 자동으로 store에 불러오는 과정

인자로 stateCreatorFnpersistOptions를 받는다.

const useStore = create<CountStore>()(
  persist(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'count-storage', // localStorage 키 이름
    }
  )
);

persistOptions 주요 옵션

  • name : 필수, 저장소의 키 이름
  • storage : 저장소 선택 (기본값 : createJSONStorage(()=>localStorage)) -> MMKV 사용
  • partialize : 저장할 필드(상태)만 선택적으로 지정
  • onRehydrateStorage : 복원 이벤트 핸들러 설정
  • version : 상태 버전 지정 (마이그레이션)
  • migrate : 버전 불일치 시, 저장 상태를 새로운 구조로 변환
  • merge : 복원 시 저장 상태와 초기 상태를 어떻게 병합할지 지정
  • skipHydration : 자동으로 상태를 복원하지 않음

Zustand persist + MMKV

persist 미들웨어를 이용해 Zustand와 MMKV 로컬 스토리지를 연결해 앱 실행시 자동으로 데이터를 불러올 수 있다.

  • MMKV 사용을 위한mmkvStorage 인스턴스를 만든다.
  • Zustand의 persist 미들웨어에 사용할 수 있는zustandStorage 인터페이스 객체를 만든다.
    • createJSONStorage()를 통해 JSON처리와 티입 호환을 맞춘다.
import { MMKV } from 'react-native-mmkv';

const mmkvStorage = new MMKV();

const zustandStorage = createJSONStorage(() => ({
  setItem: (name, value) => {
    return mmkvStorage.set(name, value);
  },
  getItem: (name) => {
    const value = mmkvStorage.getString(name);
    return value ?? null;
  },
  removeItem: (name) => {
    return mmkvStorage.delete(name);
  },
}));

이제 create를 통해 저장할 데이터에 맞는 커스텀 훅을 생성해 사용한다.

type UserStore = {
  userData: UserDataType;
  setUserData: (data: Partial<UserDataType>) => void;
  // Partial : 부분적인 업데이트를 위해서 UserDataType의 부분만 가진 객체를 받는다.
};

export const useUserStore = create<UserStore>()(
  persist(
    (set) => ({
      userData: {
        userID: '',
        nickname: '',
        rank: 0,
        topScore: 0,
      },
      setUserData: (data) =>
        set((state) => ({
          ...state,
          ...data,
        })),
    }),
    {
      name: 'user-store',
      storage: zustandStorage,
      partialize: (state) => ({ ...state.userData }), // userData만 저장, setUserData 저장X
    }
  )
);