Zustand 완벽 가이드 | 간단한 상태 관리·React·TypeScript·Middleware·실전 활용
이 글의 핵심
Zustand로 간단한 상태 관리를 구현하는 완벽 가이드입니다. Redux 없이 가볍게, TypeScript 지원, Middleware, Persist까지 실전 예제로 정리했습니다.
실무 경험 공유: Redux에서 Zustand로 전환하면서, 보일러플레이트가 90% 감소하고 번들 크기가 50% 줄어든 경험을 공유합니다.
들어가며: “Redux가 복잡해요”
실무 문제 시나리오
시나리오 1: 보일러플레이트가 너무 많아요
Redux는 복잡합니다. Zustand는 간단합니다.
시나리오 2: 번들 크기가 커요
Redux는 무겁습니다. Zustand는 1KB입니다.
시나리오 3: TypeScript 설정이 어려워요
Redux는 타입 설정이 복잡합니다. Zustand는 자동 추론됩니다.
1. Zustand란?
핵심 특징
Zustand는 간단한 React 상태 관리 라이브러리입니다.
주요 장점:
- 간단한 API: 보일러플레이트 없음
- 작은 크기: 1KB
- TypeScript: 완벽한 지원
- Middleware: Persist, Devtools
- React 외부: Vanilla JS 사용 가능
2. 기본 사용
설치
npm install zustand
Store 생성
// store/useStore.ts
import { create } from 'zustand';
interface Store {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useStore = create<Store>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
컴포넌트에서 사용
// components/Counter.tsx
import { useStore } from '../store/useStore';
export default function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
3. 고급 패턴
Async Actions
interface UserStore {
users: User[];
loading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
}
export const useUserStore = create<UserStore>((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
Computed Values
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
total: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.price, 0);
},
}));
4. Middleware
Persist
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthStore {
user: User | null;
token: string | null;
login: (user: User, token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
user: null,
token: null,
login: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}),
{
name: 'auth-storage',
}
)
);
Devtools
import { devtools } from 'zustand/middleware';
export const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'CounterStore' }
)
);
Immer
import { immer } from 'zustand/middleware/immer';
export const useStore = create<Store>()(
immer((set) => ({
nested: { deep: { value: 0 } },
updateDeep: (value: number) =>
set((state) => {
state.nested.deep.value = value;
}),
}))
);
5. Slice Pattern
// store/slices/userSlice.ts
export const createUserSlice = (set, get) => ({
users: [],
fetchUsers: async () => {
const users = await api.getUsers();
set({ users });
},
});
// store/slices/cartSlice.ts
export const createCartSlice = (set, get) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
});
// store/index.ts
import { create } from 'zustand';
import { createUserSlice } from './slices/userSlice';
import { createCartSlice } from './slices/cartSlice';
export const useStore = create((set, get) => ({
...createUserSlice(set, get),
...createCartSlice(set, get),
}));
6. Selector 최적화
잘못된 예
// 전체 store를 구독 (불필요한 리렌더링)
const store = useStore();
올바른 예
// 필요한 값만 구독
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
Shallow 비교
import { shallow } from 'zustand/shallow';
const { count, increment } = useStore(
(state) => ({ count: state.count, increment: state.increment }),
shallow
);
7. Vanilla JS
import { createStore } from 'zustand/vanilla';
const store = createStore<Store>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 구독
const unsubscribe = store.subscribe((state) => {
console.log('Count:', state.count);
});
// 사용
store.getState().increment();
console.log(store.getState().count); // 1
// 구독 해제
unsubscribe();
정리 및 체크리스트
핵심 요약
- Zustand: 간단한 상태 관리
- 작은 크기: 1KB
- TypeScript: 완벽한 지원
- Middleware: Persist, Devtools, Immer
- Selector: 최적화 가능
- Vanilla JS: React 외부 사용
구현 체크리스트
- Zustand 설치
- Store 생성
- 컴포넌트 연결
- Async Actions 구현
- Middleware 추가
- Selector 최적화
- Slice Pattern 적용
같이 보면 좋은 글
- React Native 완벽 가이드
- Next.js App Router 가이드
- TypeScript 완벽 가이드
이 글에서 다루는 키워드
Zustand, State Management, React, TypeScript, Redux, Frontend, Performance
자주 묻는 질문 (FAQ)
Q. Redux와 비교하면 어떤가요?
A. Zustand가 훨씬 간단하고 가볍습니다. Redux는 더 많은 기능을 제공하지만 복잡합니다.
Q. Context API와 비교하면 어떤가요?
A. Zustand가 성능이 더 좋고 사용이 간편합니다. Context API는 prop drilling을 피하는 용도로 적합합니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, 많은 기업에서 안정적으로 사용하고 있습니다.
Q. SSR을 지원하나요?
A. 네, Next.js와 함께 사용할 수 있습니다.