Frontend
Redux보다 가볍고 Context보다 단순하게 — Zustand로 전역 상태를 관리
· 4분 읽기
안녕하세요, 이번 포스팅에서는 React 전역 상태 관리의 핵심 라이브러리인 Zustand를 정리해봤어요. React로 프로젝트를 하다 보면 한 번쯤 이런 고민을 하게 됩니다.
“props를 몇 번이나 내려보내고 있지?” “Context가 점점 늘어나고 있는데 괜찮을까?” “Redux는 너무 무겁고 설정이 복잡한데...”
저도 같은 고민 끝에 Zustand를 선택했습니다. 결제 페이지처럼 여러 단계의 상태를 관리해야 할 때, Zustand는 깔끔하고 단순한 해법을 제시해줬습니다.
1. Zustand를 선택한 이유
당시 진행 중이던 프로젝트는 인터파크 티켓 결제 리디자인이었습니다. 결제는 다음과 같은 단계로 구성되어 있었죠.
- 예매자 정보 입력
- 배송 방법 선택
- 결제 수단 선택
문제는 단계가 바뀔 때마다 이전 단계의 데이터를 유지해야 했다는 점입니다. Context API는 Provider가 중첩되고, Redux는 보일러플레이트가 너무 많았습니다. 그때 알게 된 것이 바로 Zustand입니다. 한 줄의 create()로 전역 상태를 만들 수 있는 가볍고 직관적인 라이브러리였어요.
2. Zustand의 기본 구조
Zustand의 핵심은 create() 함수입니다. store를 만들고, 훅처럼 구독해서 사용하는 방식이죠.
// 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>
);
}✅ Redux의 dispatch, action, reducer 없이 set() 한 줄로 상태를 갱신할 수 있습니다.
3. Zustand로 실무 패턴 다루기
3.1 비동기 API 관리
Zustand는 store 내부에서 async/await를 바로 사용할 수 있습니다. 로딩 상태, 에러 상태까지 함께 관리할 수 있죠.
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 {
set({ error: "유저 정보를 가져오지 못했습니다.", isLoading: false });
}
},
}));3.2 렌더링 최적화 — shallow 비교
Zustand는 shallow 함수를 이용해 불필요한 리렌더링을 막을 수 있습니다. 상태가 커질수록 이 한 줄이 체감됩니다.
import { shallow } from "zustand/shallow";
import { useUserStore } from "@/store/useUserStore";
const [user, isLoading] = useUserStore(
(state) => [state.user, state.isLoading],
shallow
);⚡ shallow는 “얕은 비교”를 통해 필요한 상태만 감지합니다. 변경된 데이터만 렌더링되므로 성능이 크게 향상됩니다.
3.3 LocalStorage에 상태 저장 — persist
사용자의 테마나 설정을 저장하려면 persist 미들웨어를 쓰면 됩니다.
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" }
)
);💾 페이지를 새로고침해도 상태가 유지됩니다.
3.4 React Query와 병행하기
Zustand와 React Query는 완벽한 짝입니다. React Query는 서버 상태를, Zustand는 클라이언트 상태를 담당하도록 나누면 깔끔합니다.
// React Query - 서버 상태
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
export const usePosts = () =>
useQuery({
queryKey: ["posts"],
queryFn: () => axios.get("/api/posts").then((res) => res.data),
});
// Zustand - 클라이언트 상태
import { create } from "zustand";
export const useTabStore = create((set) => ({
selectedTab: "all",
setTab: (tab) => set({ selectedTab: tab }),
}));4. 결제 페이지에서 Zustand 활용하기
SOPT 36기 프로젝트에서는 Zustand를 사용해 결제 단계별 상태를 전역으로 관리했습니다.
import { create } from "zustand";
export const usePaymentStore = create((set) => ({
ticketCount: 0,
totalPrice: 0,
deliveryMethod: "",
userInfo: null,
paymentMethod: "",
setTicketCount: (count) => set({ ticketCount: count }),
setTotalPrice: (price) => set({ totalPrice: price }),
setDeliveryMethod: (method) => set({ deliveryMethod: method }),
setUserInfo: (info) => set({ userInfo: info }),
setPaymentMethod: (method) => set({ paymentMethod: method }),
resetPayment: () =>
set({
ticketCount: 0,
totalPrice: 0,
deliveryMethod: "",
userInfo: null,
paymentMethod: "",
}),
}));- Step1에서 입력한 예매자 정보
- Step2에서 선택한 배송 방법
- Step3에서 설정한 결제 수단
모두 하나의 store에서 관리합니다. 결제 완료 후에는 resetPayment()로 초기화하면 끝입니다.
✅ 덕분에 단계별 데이터 흐름이 단순하고 명확해졌습니다.
5. Zustand 사용할 때 주의할 점
항목 설명
<table style="border-collapse: collapse; width: 100%;" border="1" data-ke-align="alignLeft"><tbody><tr><td>1. Store 분리</td><td>기능별로 나누어 관리</td></tr><tr><td>2. 필요한 값만 구독</td><td>selector로 최소 단위로 접근</td></tr><tr><td>3. shallow 적극 활용</td><td>리렌더링 최소화</td></tr><tr><td>4. 비즈니스 로직 분리</td><td>API 호출은 service 모듈로 이동</td></tr></tbody></table>
Zustand는 간단하지만, 모든 상태를 한곳에 몰아넣으면 Redux보다 복잡해질 수도 있습니다. 작게 쪼개고, 필요한 것만 구독하는 습관이 중요합니다.
6. Zustand가 실무에서 사랑받는 이유
- Provider 불필요 → 구조가 단순하다.
- Redux보다 코드량이 3분의 1 수준이다.
- React Query와 찰떡궁합이다.
- 소규모부터 대규모 앱까지 유연하게 확장된다.
// Context API
<AuthProvider>
<ThemeProvider>
<CartProvider>
<App />
</CartProvider>
</ThemeProvider>
</AuthProvider>
// Zustand
const { user } = useAuthStore();
const { theme } = useThemeStore();댓글
GitHub 계정으로 댓글을 남길 수 있어요.