Zustand — 가볍고 직관적인 상태관리
Zustand는 보일러플레이트 없이 전역 상태를 관리할 수 있는 경량 라이브러리입니다.
Redux의 복잡함, Context API의 성능 한계가 고민이라면 Zustand가 좋은 대안입니다. 설정이 거의 없고, 코드량도 적으며, 자동으로 리렌더링을 최적화합니다.
설치와 기본 사용
npm install zustand
import { create } from 'zustand';
// 스토어 정의 — 상태와 액션을 한 곳에
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// 사용 — Hook처럼 사용
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
<View>
<Text>{count}</Text>
<Pressable onPress={increment}><Text>+1</Text></Pressable>
</View>
);
}
Redux와 비교하면 action type, reducer, dispatch가 전부 필요 없습니다.
set함수 하나로 상태 업데이트가 끝납니다.
실전: 인증 스토어
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
}
interface AuthStore {
user: User | null;
token: string | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
setUser: (user: User) => void;
}
const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true });
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const { user, token } = await response.json();
set({ user, token, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: () => set({ user: null, token: null }),
setUser: (user) => set({ user }),
}));
컴포넌트에서 사용
function LoginScreen() {
const { login, isLoading } = useAuthStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
try {
await login(email, password);
// 로그인 성공 — 네비게이션은 자동 전환
} catch {
Alert.alert('오류', '로그인에 실패했습니다');
}
};
return (
<View>
<TextInput value={email} onChangeText={setEmail} />
<TextInput value={password} onChangeText={setPassword} secureTextEntry />
<Pressable onPress={handleLogin} disabled={isLoading}>
<Text>{isLoading ? '로그인 중...' : '로그인'}</Text>
</Pressable>
</View>
);
}
function Header() {
// 필요한 상태만 구독 — user만 변경될 때 리렌더링
const userName = useAuthStore((state) => state.user?.name);
return <Text>{userName ?? '로그인하세요'}</Text>;
}
Selector로 리렌더링 최적화
// 나쁜 예: 전체 스토어 구독 → 어떤 상태든 변하면 리렌더링
function Bad() {
const store = useAuthStore(); // 전체 구독
return <Text>{store.user?.name}</Text>;
}
// 좋은 예: 필요한 것만 구독
function Good() {
const name = useAuthStore((state) => state.user?.name);
return <Text>{name}</Text>;
}
// 여러 값을 한번에 선택 (shallow 비교 필요)
import { useShallow } from 'zustand/react/shallow';
function MultiSelect() {
const { user, isLoading } = useAuthStore(
useShallow((state) => ({
user: state.user,
isLoading: state.isLoading,
}))
);
return <Text>{isLoading ? '로딩중' : user?.name}</Text>;
}
Persist 미들웨어 — 상태 영속화
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface SettingsStore {
isDarkMode: boolean;
language: string;
toggleDarkMode: () => void;
setLanguage: (lang: string) => void;
}
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
isDarkMode: false,
language: 'ko',
toggleDarkMode: () => set((s) => ({ isDarkMode: !s.isDarkMode })),
setLanguage: (language) => set({ language }),
}),
{
name: 'settings-storage',
// React Native에서는 AsyncStorage 사용
storage: createJSONStorage(() => AsyncStorage),
}
)
);
스토어 분리 패턴
// stores/useAuthStore.ts — 인증
const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
login: async () => { /* ... */ },
logout: () => set({ user: null, token: null }),
}));
// stores/useCartStore.ts — 장바구니
const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((s) => ({
items: [...s.items, item],
})),
removeItem: (id) => set((s) => ({
items: s.items.filter((i) => i.id !== id),
})),
// get()으로 현재 상태 읽기
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));
// stores/useNotificationStore.ts — 알림
const useNotificationStore = create<NotificationStore>((set) => ({
unreadCount: 0,
incrementUnread: () => set((s) => ({ unreadCount: s.unreadCount + 1 })),
clearUnread: () => set({ unreadCount: 0 }),
}));
스토어 외부에서 상태 접근
// 컴포넌트 밖에서도 상태를 읽거나 변경 가능
// API 인터셉터, 네비게이션 가드 등에 유용
// 상태 읽기
const token = useAuthStore.getState().token;
// 상태 변경
useAuthStore.setState({ user: null });
// 구독
const unsubscribe = useAuthStore.subscribe((state) => {
console.log('상태 변경:', state);
});
Zustand vs Redux vs Context 비교
| 기준 | Zustand | Redux Toolkit | Context API |
|---|---|---|---|
| 보일러플레이트 | 최소 | 중간 | 낮음 |
| 번들 크기 | ~1KB | ~11KB | 0 (내장) |
| DevTools | 지원 | 지원 | 제한적 |
| 미들웨어 | persist, devtools 등 | 풍부 | 없음 |
| 리렌더링 최적화 | 자동 (selector) | 자동 | 수동 |
| 비동기 처리 | 직접 async/await | createAsyncThunk | 직접 |
정리
- Zustand는 최소한의 코드로 전역 상태를 관리 할 수 있습니다
- Selector 를 사용해 필요한 상태만 구독하면 불필요한 리렌더링을 방지합니다
- persist 미들웨어 + AsyncStorage로 앱 재시작 후에도 상태를 유지할 수 있습니다
- 스토어를 ** 도메인별로 분리 **하면 관리가 편해집니다
- 컴포넌트 밖에서도
getState(),setState()로 상태에 접근할 수 있습니다
댓글 로딩 중...