본문으로 건너뛰기 Zustand Complete Guide | Minimal React State

Zustand Complete Guide | Minimal React State

Zustand Complete Guide | Minimal React State

이 글의 핵심

Zustand is a small, hook-based state library for React. You get global stores without providers, excellent TypeScript ergonomics, and optional middleware for persistence and Redux DevTools — usually with far less code than Redux Toolkit for the same features.

What This Guide Covers

Zustand keeps global state boring in a good way: one create() call, functions that call set, and React components that subscribe through hooks. This guide walks from a counter to async flows, middleware, and production-minded tips.


1. Install & Basic Store

npm install zustand
// store/counter.ts
import { create } from 'zustand';

type CounterState = {
  count: number;
  inc: () => void;
  dec: () => void;
  reset: () => void;
};

export const useCounter = create<CounterState>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
  dec: () => set((s) => ({ count: s.count - 1 })),
  reset: () => set({ count: 0 }),
}));
function Counter() {
  const count = useCounter((s) => s.count);
  const inc = useCounter((s) => s.inc);
  return (
    <button type="button" onClick={inc}>
      {count}
    </button>
  );
}

2. Selectors & get for Derived State

type CartState = {
  items: { id: string; price: number }[];
  add: (item: { id: string; price: number }) => void;
};

export const useCart = create<CartState>((set, get) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
}));

// Selector in component — only re-renders when total changes logic requires care:
export function useCartTotal() {
  return useCart((s) => s.items.reduce((n, i) => n + i.price, 0));
}

For expensive derived values, consider storing them in the slice or memoizing at the call site.


3. Async Actions

type UserState = {
  users: { id: string; name: string }[];
  loading: boolean;
  error: string | null;
  load: () => Promise<void>;
};

export const useUserStore = create<UserState>((set) => ({
  users: [],
  loading: false,
  error: null,
  load: async () => {
    set({ loading: true, error: null });
    try {
      const res = await fetch('/api/users');
      if (!res.ok) throw new Error(String(res.status));
      const users = await res.json();
      set({ users, loading: false });
    } catch (e) {
      set({
        loading: false,
        error: e instanceof Error ? e.message : 'Failed to load',
      });
    }
  },
}));

4. Middleware: persist & devtools

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

type UiState = { theme: 'light' | 'dark'; toggle: () => void };

export const useUi = create<UiState>()(
  persist(
    (set) => ({
      theme: 'light',
      toggle: () =>
        set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
    }),
    {
      name: 'ui-storage',
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({ theme: state.theme }),
    },
  ),
);

For Redux DevTools during development:

import { devtools } from 'zustand/middleware';

export const useDebugStore = create<CounterState>()(
  devtools(
    (set) => ({
      count: 0,
      inc: () => set((s) => ({ count: s.count + 1 })),
      dec: () => set((s) => ({ count: s.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    { name: 'CounterStore' },
  ),
);

5. Slices Pattern (Large Stores)

Split logic into slice factories and merge:

import { create } from 'zustand';

const createBearSlice = (set: any) => ({
  bears: 0,
  addBear: () => set((s: { bears: number }) => ({ bears: s.bears + 1 })),
});

const createFishSlice = (set: any) => ({
  fish: 0,
  addFish: () => set((s: { fish: number }) => ({ fish: s.fish + 1 })),
});

type Store = ReturnType<typeof createBearSlice> &
  ReturnType<typeof createFishSlice>;

export const useBound = create<Store>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}));

(Adjust typings with proper StateCreator types in production code.)


6. Vanilla Store (No React)

import { createStore } from 'zustand/vanilla';

const store = createStore<CounterState>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
  dec: () => set((s) => ({ count: s.count - 1 })),
  reset: () => set({ count: 0 }),
}));

const unsub = store.subscribe(() => console.log(store.getState()));
store.getState().inc();
unsub();

7. Testing

  • Reset store state in beforeEach by exposing a reset action or replacing the module in tests.
  • Prefer testing components that consume the store; unit-test pure selectors separately.
beforeEach(() => {
  useCounter.setState({ count: 0 });
});

8. Best Practices

Do

  • Keep actions on the store — avoid mutating shared objects outside set.
  • Use shallow selectors; split stores if unrelated domains grow large.
  • Co-locate store files next to features when using modular architecture.

Avoid

  • Storing non-serializable values (e.g., class instances) if you use persist without transforms.
  • Subscribing to the entire store in leaf components (useStore() with no selector).

Summary & Checklist

  • Zustand = minimal global state with hooks and optional middleware.
  • Selectors prevent extra renders; get enables derived logic inside actions.
  • persist / devtools integrate with browser storage and DX tooling.
  • Vanilla API supports non-React consumers.

Checklist

  • Create typed store with create<T>
  • Add selectors to every consumer component
  • Move async IO into store actions with explicit loading / error
  • Add persist only for safe, non-secret data
  • Document store boundaries in larger apps (slices or multiple stores)

More career guides (Korean on pkglog.com)

If you also read Korean, these pair well with interview prep: resume & interview guide, tech interview prep, practical job hunting.

Related posts:


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Learn Zustand for React: tiny API, TypeScript inference, selectors, async actions, middleware (persist, devtools), and p… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Zustand, React, State Management, TypeScript, Redux, Frontend 등으로 검색하시면 이 글이 도움이 됩니다.