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
beforeEachby exposing aresetaction 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
persistwithout 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;
getenables derived logic inside actions. persist/devtoolsintegrate 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
persistonly 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와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- React 18 Deep Dive | Concurrent Features· Suspense
- Next.js 15 Complete Guide | Turbopack· React 19
- TypeScript 5 Complete Guide | Decorators· satisfies
이 글에서 다루는 키워드 (관련 검색어)
Zustand, React, State Management, TypeScript, Redux, Frontend 등으로 검색하시면 이 글이 도움이 됩니다.