
Zustand란?
Zustand는 React 애플리케이션에서 전역 상태(Global State)를 관리하기 위한 경량 상태 관리 라이브러리다.
독일어로 "상태(State)"라는 뜻을 가지고 있으며, Redux보다 훨씬 간단한 API와 적은 보일러플레이트 코드가 특징이다. 현재 React 커뮤니티에서는 가장 인기 있는 상태 관리 라이브러리 중 하나로 자리 잡았으며, 많은 신규 프로젝트에서 Redux 대신 Zustand를 선택하고 있다.

왜 Zustand가 등장했을까?
React는 상태 관리를 위한 기본 기능을 제공한다.
대표적으로:
- useState : 컴포넌트 내부 상태 관리
- useReducer : 복잡한 상태 변경 로직 관리
- Context API : 컴포넌트 간 상태 공유
이러한 기능들은 작은 프로젝트에서는 충분하지만, 애플리케이션 규모가 커지고 여러 컴포넌트에서 같은 상태를 공유해야 하는 상황에서는 한계가 발생한다.
이러한 문제를 해결하기 위해 등장한 것이 Zustand와 같은 전역 상태 관리 라이브러리이다.
1. Props Drilling 문제
React에서는 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달할 때 props를 사용한다.
하지만 컴포넌트 구조가 깊어지면 상태를 사용하지 않는 컴포넌트까지 props를 전달해야 하는 문제가 발생한다.
App
└─ Layout
└─ Header
└─ UserInfo
사용자 정보를 UserInfo에서 사용하려면 중간 컴포넌트들이 모두 props를 받아야 한다.
<Layout user={user} />
<Header user={user} />
<UserInfo user={user} />
처럼 중간 컴포넌트들도 모두 props를 전달해야 한다.
이처럼 상태가 여러 컴포넌트를 거쳐 전달되는 문제를 Props Drilling이라고 한다.
2. Context API의 한계
React는 Props Drilling 문제를 해결하기 위해 Context API를 제공합니다.
Context를 사용하면 중간 컴포넌트를 거치지 않고 데이터를 공유할 수 있습니다.
예를 들어 로그인한 사용자 정보와 다크모드 상태를 Context로 관리한다고 가정해보자.
<UserContext.Provider
value={{
user,
theme,
}}
>
<App />
</UserContext.Provider>
각 컴포넌트에서는 useContext를 사용하여 필요한 값을 가져온다.
const { user } = useContext(UserContext);
Context의 값(value)이 변경되면 해당 Context를 사용하는 컴포넌트들이 모두 다시 렌더링된다.
예를 들어 다크모드만 변경했다고 가정해보자.
setTheme('dark');
실제로 변경된 값은 theme뿐이지만 해당 Context를 사용하는 컴포넌트들은 모두 다시 렌더링된다.
UserProfile ← 다시 렌더링
ThemeButton ← 다시 렌더링
UserProfile은 사용자 정보만 사용하고 있는데도 다시 렌더링되는 것이다.
작은 프로젝트에서는 크게 문제가 되지 않지만, 수십 개 또는 수백 개의 컴포넌트가 Context를 사용하게 되면 불필요한 렌더링이 늘어날 수 있다.
3. Redux의 구조적 복잡성
Redux는 예측 가능한 상태 관리 구조와 강력한 개발 도구를 제공하지만, 상태를 관리하기 위해 이해해야 하는 개념과 설정이 많다.
간단한 상태 하나를 관리하더라도 기본적으로 다음과 같은 흐름을 이해해야 한다.
- Store : 전역 상태를 저장하는 공간
- Action : 상태 변경을 요청하는 객체
- Reducer : Action을 받아 새로운 상태를 만드는 함수
- Provider : React 컴포넌트에서 Store를 사용할 수 있도록 연결
예를 들어 숫자를 증가시키는 상태를 Redux로 관리한다고 가정해보자.
export const increase = () => ({
type: 'INCREASE',
});
다음으로 Action에 따라 상태를 변경하는 Reducer를 작성한다.
const initialState = {
count: 0,
};
export const counterReducer = (
state = initialState,
action
) => {
switch (action.type) {
case 'INCREASE':
return {
count: state.count + 1,
};
default:
return state;
}
};
이후 Reducer를 Store에 등록한다.
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
React 애플리케이션 최상단에서는 Provider를 통해 Store를 연결한다.
<Provider store={store}>
<App />
</Provider>
컴포넌트에서는 useSelector로 상태를 가져오고, useDispatch로 Action을 전달한다.
const count = useSelector(
(state) => state.counter.count
);
const dispatch = useDispatch();
<button
onClick={() => dispatch(increase())}
>
증가
</button>
이처럼 Redux는 상태 변경 과정을 명확하게 분리하고 예측 가능하게 관리할 수 있다는 장점이 있지만, 간단한 상태를 관리하기 위해서도 Store, Action, Reducer, Provider, Dispatch 등 여러 개념을 이해해야 한다.
이러한 구조적 복잡성을 줄이고 더 간단한 방식으로 전역 상태를 관리하기 위해 Zustand와 같은 상태 관리 라이브러리가 등장했다.
Zustand 기본 사용법
설치
npm install zustand
Store 생성
Zustand의 Store는 상태(state)와 상태를 변경하는 함수(action)를 함께 관리한다.
예를 들어 숫자를 증가시키고 감소시키는 Counter Store를 만들어보자.
import { create } from 'zustand';
interface CounterStore {
count: number;
increase: () => void;
decrease: () => void;
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increase: () =>
set((state) => ({
count: state.count + 1,
})),
decrease: () =>
set((state) => ({
count: state.count - 1,
})),
}));
create 함수는 Store를 생성하며, set 함수는 상태를 변경할 때 사용한다.
상태 사용하기
컴포넌트에서는 Store에서 필요한 상태만 선택해서 가져올 수 있습니다.
import { useCounterStore } from './store';
function Counter() {
const count = useCounterStore((state) => state.count);
const increase = useCounterStore((state) => state.increase);
return (
<div>
<p>{count}</p>
<button onClick={increase}>증가</button>
</div>
);
}
비동기 상태 처리
Redux에서는 비동기 작업을 처리하기 위해 Middleware나 Redux Thunk 같은 추가적인 개념이 필요했지만, zustand는 store안에서 일반 함수처럼 비동기 함수를 작성할 수 있다.
* 동기 = 하나 끝날 때까지 기다렸다가 다음 작업 실행
* 비동기 = 기다리는 동안 다른 작업도 수행
import { create } from 'zustand';
import axios from 'axios';
interface UserStore {
users: any[];
fetchUsers: () => Promise<void>;
}
export const useUserStore = create<UserStore>((set) => ({
users: [],
fetchUsers: async () => {
const response = await axios.get('/users');
set({
users: response.data,
});
},
}));
사용
const fetchUsers = useUserStore(
(state) => state.fetchUsers
);
useEffect(() => {
fetchUsers();
}, []);
Persist로 상태 저장하기
Zustand의 상태는 기본적으로 메모리에 저장된다.
따라서 브라우저를 새로고침하면 Store의 상태는 초기화된다.
로그인 정보처럼 새로고침 이후에도 유지해야 하는 상태가 있다면 persist 미들웨어를 사용할 수 있다.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useAuthStore = create(
persist(
(set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}),
{
name: 'auth-storage',
}
)
);
persist를 사용하면 Zustand 상태가 브라우저의 LocalStorage에 자동으로 저장됩니다.
DevTools 사용하기
Zustand는 Redux DevTools와 연동할 수 있다.
애플리케이션 규모가 커지면 상태가 언제 변경되었는지 추적하기 어려워질 수 있다.
이때 DevTools를 사용하면:
- 상태 변경 기록 확인
- 이전 상태와 현재 상태 비교
- 상태 변경 흐름 추적
등을 할 수 있다.
실무에서 자주 사용하는 폴더 구조
src
├── stores
│ ├── auth.store.ts
│ ├── user.store.ts
│ └── theme.store.ts
├── pages
├── components
└── hooks
- auth.store.ts → 로그인, 사용자 인증 상태
- user.store.ts → 사용자 관련 상태
- theme.store.ts → 다크모드 상태
Zustand 사용 시 주의사항
서버 상태는 Zustand로 관리하지 않는다
Zustand는 클라이언트 상태 관리 도구다.
API 요청으로 받아오는 데이터처럼 서버와 동기화가 필요한 상태는 Zustand보다 TanStack Query 같은 서버 상태 관리 도구가 더 적합하다.
Zustand보다 TanStack Query가 적합한 예시
- 사용자 목록
- 게시글 목록
- 상품 목록
- 서버 API 데이터
컴포넌트 내부에서만 사용하는 상태:useState로 관리하는 것이 더 적합하다.
- input 값
- 모달 열림 여부
- 탭 선택 상태
전역 관리가 필요한 상태:
- 로그인 상태
- 테마 설정
- 장바구니
- 사이드바 열림 여부
Zustand vs Redux
| 학습 난이도 | 쉬움 | 높음 |
| 코드량 | 적음 | 많음 |
| Provider | 없음 | 있음 |
| 비동기 처리 | 쉬움 | 추가 설정 필요 |
| TypeScript | 쉬움 | 상대적으로 복잡 |
| 규모가 큰 프로젝트 | 가능 | 매우 강력 |
정리
Zustand는 React 애플리케이션에서 전역 상태를 간단하게 관리하기 위해 만들어진 상태 관리 라이브러리다.
주요 특징:
- Provider 없이 사용 가능
- Redux보다 적은 코드로 상태 관리 가능
- Selector 기반 선택적 구독 지원
- TypeScript 사용 편리
- Persist, DevTools 지원
실무에서는 일반적으로:
- 클라이언트 상태 → Zustand
- 서버 상태 → TanStack Query
처럼 역할을 나누어 사용하는 패턴이 많이 활용한다
'Library' 카테고리의 다른 글
| React Hook Form (0) | 2026.06.20 |
|---|---|
| TypeScript를 쓰는데 왜 Zod까지 사용할까? (0) | 2026.06.18 |
| TanStack Query란? (서버상태 관리도구) (0) | 2026.06.17 |