React는 여러 개의 컴포넌트로 구성되어 웹 페이지를 만든다.
이렇게 컴포넌트를 나누다 보면, 컴포넌트 간에 상태(변수)를 공유해야 하는 상황이 자주 발생하는데,
이때 보통 props, Context API, 혹은 Redux와 같은 상태 관리 도구를 사용한다.
그중 Redux는 가장 널리 알려진 상태 관리 라이브러리로, 전역 상태를 정의하고 이를 여러 컴포넌트에서 공유할 수 있게 해준다.
하지만 Redux는 초기 설정이 복잡하고, 문법이 다소 어렵다는 단점이 있다.
그래서 최근에는 Redux와 비슷한 기능을 제공하면서도, 더 간단하고 직관적인 API를 가진 상태 관리 도구들이 많이 사용된다.
그 중 하나가 바로 Zustand이다.
이번 글에서는 Redux와 비슷한 기능을 제공하면서도 훨씬 사용하기 쉬운 Zustand에 대해 알아볼 것이다.
Zustand
React에서 사용하는 번들 사이즈가 작고, 설정이 간단한 상태 관리 라이브러리로
Redux처럼 전역 상태를 관리할 수 있지만 훨씬 가볍고 직관적인 API를 제공하며, top-down 방식이다.
* bottom-up 방식으로는 Jotai가 있다.
“곰 우리(Zoo)”라는 의미의 독일어 Zustand에서 유래해서 그런지 도큐먼트 들어가보면 바로 곰이 나온다.
1. 기본 사용법
먼저 설치는 다음과 같이 하면 된다.
npm install zustand
# 또는
yarn add zustand
그 후 스토어를 생성해야 하는데, create() 함수를 통해 스토어(전역 상태)를 정의한다.
import { create } from 'zustand';
interface CounterState {
count: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
// create() 안에서 state와 action을 정의
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 }),
}));
export default useCounterStore;
create()를 호출하면 React Hook인 useCounterStore()가 만들어지고, set() 함수를 통해 상태를 업데이트하면 된다.
이후 스토어를 사용하려면
import React from 'react';
import useCounterStore from './store/useCounterStore';
function App() {
const { count, increase, decrease, reset } = useCounterStore();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
<button onClick={reset}>리셋</button>
</div>
);
}
export default App;
useCounterStore()를 호출하면, Zustand 스토어에서 count, increase 등 상태와 함수를 바로 불러온다.
Redux와 달리 Provider나 connect 등의 설정이 필요하지 않다.
2. 특징
2.1 Selector: 상태 객체를 부분적으로만 가져온다.
function CounterValue() {
const count = useCounterStore((state) => state.count); // 상태 중 count만 선택
return <div>Count: {count}</div>;
}
function CounterButtons() {
const increase = useCounterStore((state) => state.increase);
const decrease = useCounterStore((state) => state.decrease);
return (
<>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
</>
);
}
이렇게 각 컴포넌트가 필요한 값만 구독하도록 분리하면,
버튼을 눌러도 CounterValue만 다시 렌더링하거나 필요한 컴포넌트만 렌더링되어
불필요한 재렌더링을 줄이므로 성능 최적화에 도움이 된다.
2.2 복수 스토어 / Slice 패턴: 여러 개의 스토어를 만드는 방법
규모가 커지면, 여러 개의 전역 상태(스토어)가 필요할 수 있다.
여러 개의 스토어를 만드는 방법은 2가지가 있다. 차례대로 알아보자.
- 스토어 여러 개 생성
// src/store/useAuthStore.ts
import { create } from 'zustand';
interface AuthState {
isLoggedIn: boolean;
login: () => void;
logout: () => void;
}
const useAuthStore = create<AuthState>((set) => ({
isLoggedIn: false,
login: () => set({ isLoggedIn: true }),
logout: () => set({ isLoggedIn: false }),
}));
export default useAuthStore;
// src/store/useCartStore.ts
import { create } from 'zustand';
interface CartState {
items: string[];
addItem: (item: string) => void;
}
const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));
export default useCartStore;
위의 예시 같은 경우, 필요한 전역 상태를 각각 독립된 useXYZStore로 나누었다.
- 한 스토어 내에서 slice(조각) 패턴을 사용
// src/store/useAppStore.ts
import { create } from 'zustand';
// slice 1
const createAuthSlice = (set) => ({
isLoggedIn: false,
login: () => set({ isLoggedIn: true }),
logout: () => set({ isLoggedIn: false }),
});
// slice 2
const createCartSlice = (set) => ({
items: [],
addItem: (item: string) => set((state) => ({ items: [...state.items, item] })),
});
// 단일 스토어로 합치기
const useAppStore = create((set) => ({
...createAuthSlice(set),
...createCartSlice(set),
}));
export default useAppStore;
zustand는 combineSlices 없이도, 하나의 create()에서 여러 slice 함수를 조합할 수 있다.
이 방식으로 useAppStore() 하나만 import해서 전역 state를 모두 사용할 수 있다.
2.3 Middleware (Persist, Devtools 등)
Zustand는 Middleware 형태로 Persist(영구 저장), Redux DevTools 연동 등을 지원한다.
- Persist (LocalStorage 등에 상태 저장)
const useSettingsStore = create<{
darkMode: boolean;
toggleDarkMode: () => void;
}>()(
persist(
(set) => ({
darkMode: false,
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
}),
{
name: 'settings-storage', // localStorage 키 이름
}
)
);
export default useSettingsStore;
이렇게 하면 앱 새로고침이나 페이지 이동 후에도 darkMode 값이 유지된다.
- Devtools
const useCounterStore = create(
devtools((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
);
이렇게 하면 Redux DevTools 브라우저 확장 프로그램에서 Zustand 상태를 모니터링할 수 있다.
3. 메소드
- create(): 스토어 생성
- set(): 상태 업데이트
- get(): 현재 상태 값 조회, 스토어 정의 내에서만 사용 가능
const useCounterStore = create((set, get) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
doubleIfEven: () => {
const count = get().count;
if (count % 2 === 0) set({ count: count * 2 });
},
}));
- useStore(selector): 컴포넌트에서 selector 함수를 통해 부분 상태만 구독할 때 사용
- subscribe(selector, callback): 상태가 변경될 때 특정 콜백 실행
- React 바깥에서 Zustand 스토어를 구독할 때 사용
- unsubscribe()를 호출하여 구독 중지 가능
const count = useCounterStore((state) => state.count); // count만 구독
useCounterStore.subscribe(
(state) => state.count,
(newCount) => {
console.log('카운트 변경됨:', newCount);
}
);
- persist (middleware): 스토어 상태를 LocalStorage 등 브라우저 저장소에 자동 저장 & 복원
- devtools (middleware): Zustand 상태를 Redux DevTools에 연동하여 상태 추적 가능
const useAuthStore = create<{
username: string;
login: (name: string) => void;
logout: () => void;
}>()(
devtools(
persist(
(set, get) => ({
username: '',
login: (name) => set({ username: name }),
logout: () => {
const current = get().username;
set({ username: '' });
},
}),
{ name: 'auth-storage' }
)
)
);
export default useAuthStore;
- combine() (상태 병합 도우미 함수): zustand에서 상태 객체와 액션을 더 명확하게 분리할 때 사용
const useStore = create(
combine(
{ count: 0 }, // 초기 상태
(set) => ({
increase: () => set((state) => ({ count: state.count + 1 })),
})
)
);
- destroy(): 스토어를 초기화하고, 모든 리스너 제거
- 테스트 환경 또는 메모리 해제를 수동으로 제어할 때 사용
useStore.destroy();
'프론트엔드 > React' 카테고리의 다른 글
[React] 상태 관리 라이브러리, 어떤 것을 선택해야 할까? (0) | 2025.03.30 |
---|---|
[React] 서버에서 상태를 가져온다고? React-Query (0) | 2025.03.30 |
[React] 사용자에게 빠른 UI 반응을 제공하려면? Web Worker, Debounce, Throttle (0) | 2025.03.29 |
[React] 초기 로딩 속도가 느리다면? 성능 최적화!! (1) | 2025.03.28 |
[React] DOM?? Virtual DOM?? (1) | 2025.03.25 |