Zustand 한 번에 정리: 기본 개념 → 실무 패턴
안녕하세요, 솝트 웹 파트 36기 최서희입니다. 👋
4주차에는 Zustand에 대해 정리했습니다. React에서 전역 상태 관리는 더 이상 선택이 아닌 필수!
props drilling이 심해질수록 더 나은 전략이 필요하고, 그 대안 중 하나가 바로 Zustand입니다.
1️⃣ Zustand란?
“Zustand”는 독일어로 ‘상태(state)’. 훅 하나로 전역 상태를 다루는 것이 특징입니다.
✅ 핵심 특징
| 특징 | 설명 |
|---|---|
| 보일러플레이트 없음 | Redux처럼 복잡한 설정 없이 간단하게 구성 |
| 빠름 | 얕은 구독(선택적 구독) 기반으로 불필요 렌더 최소화 |
| 직관적 | 단일 파일 store 정의 → 훅으로 바로 사용 |
2️⃣ 기본 사용 구조
create()로 저장소를 만들고, 컴포넌트에서는 일반 훅처럼 구독해 사용합니다.
예시: 카운터 상태 관리
// src/store/useCounterStore.ts
import { create } from "zustand";
interface CounterState {
count: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// 컴포넌트에서 사용
import { useCounterStore } from "@/store/useCounterStore";
export default function Counter() {
const { count, increase, decrease, reset } = useCounterStore();
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={increase}>+1</button>
<button onClick={decrease}>-1</button>
<button onClick={reset}>리셋</button>
</div>
);
}
3️⃣ 실무에서의 패턴
1) 비동기 API 처리
Zustand 내부에서 async/await를 사용해 간단히 API를 호출하고 상태를 갱신할 수 있습니다.
// src/store/useUserStore.ts
import { create } from "zustand";
import axios from "axios";
interface UserState {
user: string | null;
isLoading: boolean;
error: string | null;
fetchUser: () => Promise<void>;
}
export const useUserStore = create<UserState>((set) => ({
user: null,
isLoading: false,
error: null,
fetchUser: async () => {
set({ isLoading: true });
try {
const res = await axios.get("/api/user");
set({ user: res.data.name, isLoading: false });
} catch (e) {
set({ error: "유저 정보를 가져오지 못했습니다.", isLoading: false });
}
},
}));
2) shallow 비교로 최적화
여러 상태를 동시에 구독할 때 shallow를 쓰면 불필요 렌더를 줄일 수 있습니다.
import { useUserStore } from "@/store/useUserStore";
import { shallow } from "zustand/shallow";
const [user, isLoading] = useUserStore(
(state) => [state.user, state.isLoading],
shallow
);
3) Middleware 활용
persist: localStorage 등 영속 저장devtools: Redux DevTools 연동subscribeWithSelector: 특정 상태 변화 감지
// src/store/useThemeStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface ThemeState {
isDark: boolean;
toggleTheme: () => void;
}
export const useThemeStore = create(
persist<ThemeState>(
(set) => ({
isDark: false,
toggleTheme: () => set((state) => ({ isDark: !state.isDark })),
}),
{ name: "theme-storage" } // localStorage key
)
);
4) React Query와 병행 사용
| 서버 상태 | 데이터 fetch, 캐시, 동기화 → React Query |
|---|---|
| 클라이언트 상태 | UI·폼·탭·모달 등 → Zustand |
// 서버 상태: React Query
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchPosts = () => axios.get("/api/posts").then((res) => res.data);
export const usePosts = () => useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
// 클라이언트 상태: Zustand
import { create } from "zustand";
interface TabState {
selectedTab: string;
setTab: (tab: string) => void;
}
export const useTabStore = create<TabState>((set) => ({
selectedTab: "all",
setTab: (tab) => set({ selectedTab: tab }),
}));
참고
•
• 위
•
shallow는 import { shallow } from "zustand/shallow" (버전에 따라 default export가 아닐 수 있습니다).• 위
useTabStore 예제는 set 파라미터를 명시해 사용하도록 보정했습니다.
4️⃣ 사용 시 주의할 점
| 항목 | 설명 |
|---|---|
| store 분리 | 하나의 store에 모든 상태 몰아넣지 말고 기능별로 분리 |
| 필요한 값만 구독 | selector로 필요한 조각만 구독, shallow로 렌더 최적화 |
| select + shallow | 리렌더 핫스팟에 필수 전략 |
| 사이드 이펙트 분리 | API 호출 등은 service 모듈로 분리해 테스트/재사용성↑ |
5️⃣ Zustand가 실무에 적합한 이유
- Context API보다 구조적이고 예측 가능
- Redux 대비 훨씬 가볍고 보일러플레이트가 적음
- 소규모 → 대규모 앱까지 유연하게 확장
- 모달, 필터, 탭, 테마, 인증 등 UI 상태 관리에 최적
'4주차' 카테고리의 다른 글
| 네이밍 규칙 (1) | 2025.05.13 |
|---|---|
| CommonJS와 ES Modules (0) | 2025.05.13 |
| Tailwind CSS 버전 정리 (0) | 2025.05.13 |
| Tanstack Query (0) | 2025.05.13 |
| Lazy loading과 Suspense (0) | 2025.05.13 |