DYO 공부하는 블로그
[Zustand] Zustand 기본 사용법, v5 변경점 본문

상태 관리 라이브러리중에 가볍고, 사용하기 쉬워 트렌드가 된 Zustand에 대해서 알아보자. 기본적인 사용법부터 24년 10월에 업데이트된 Zustand v5의 변경점까지 다뤄보자.
예시와 함께 보는 Zustand 사용법
📥 설치
# NPM
npm install zustand
# 다른 패키지 매니저도 상관없다.
설치는 이게 끝이다. zustand의 많은 장점 중에는 보일러플레이트가 작다는 장점이 있다.
🔔 Store 선언
상태를 공유하는 저장소를 Zustand에서는 Store라고 정의한다. 접두어를 커스텀 훅처럼 use를 접두어를 붙여서 선언해줘야 한다. 기존 상태와 얕게 병합(shallow merge)되기 때문에 깊은 병합을 하기 위해서는 immer또는 전개식을 이용하여 깊은 복사를 처리해줘야 한다.
Store 전체 구조
import { create } from 'zustand'
// 타입 선언
type FeedState = {
selectedFeedId: string | null
setSelectedFeed: (id: string | null) => void
}
// create<[객체 타입]>()([콜백])으로 선언
export const useFeedStore = create<FeedState>((set) => ({
selectedFeedId: null,
setSelectedFeed: (id) => set({ selectedFeedId: id }),
}))
JS에서는 create([callback]) 으로 사용하면 되지만 TS에서는 create<[객체 타입]>()([콜백])으로 실행시킨 뒤에 사용해야 한다. TS의 일종의 버그에 의해서 이렇게 사용해야 한다. 또한 객체를 return하는 구조를 가지고 있어야 하기 때문에 () => ({[store 객체]}) 와 같은 구조로 객체를 return하는 구조로 이루어져 있다는 것을 확인해야겠다.
store 선언에서의 set과 get
// set : 값을 변경, get : store의 모든 값을 가져옴
create<FeedState>((set, get) => ({
selectedFeedId: null,
setSelectedFeed: (id) => set({ selectedFeedId: id }),
}))
create의 Callback에서 set과 get을 이용해 메서드를 작성할 수 있다.
set에서 store 내부의 값 사용하기
setSelectedFeed: (id) => set(({selectedFeedId}) => {
let newId = ""
if(selectedFeedId) newId += selectedFeedId + 'tail'
return {selectedFeedId: newId} }),
// 기존 id값 + 'tail'로 설정됨
콜백을 사용하면 기존 store 내부의 값을 파라미터로 참조해 사용할 수 있으며, return하는 값이 set()함수의 처리를 받게 된다.
바깥에 함수 선언하기
import { create } from 'zustand'
// 타입 선언
type FeedState = {
selectedFeedId: string | null
setSelectedFeed: (id: string | null) => void
}
// create<[객체 타입]>()([콜백])으로 선언
export const useFeedStore = create<FeedState>((set) => (
const setSelectedFeed = (id) => set(({selectedFeedId}) => {
let newId = ""
if(selectedFeedId) newId += selectedFeedId + 'tail'
return {selectedFeedId: newId}}),
return {
selectedFeedId: null,
setSelectedFeed,
}))
로직이 길어질 경우 이런 식으로 함수를 분리해 좀 더 명확하게 로직을 파악하도록 할 수 있다.
📥 Store 가져오기
Store를 가져오는 방법에 따라서 해당 store를 참조하는 컴포넌트들의 렌더링 트리거가 변경되므로, 상황에 따라서 필요한 방법을 선택해야 한다. 기본적으로 Zustand 공식 문서에서 추천하는 방법은 store의 모든 값은 selector. 즉, 모든 store의 값은 분리된 상태로 독립적으로 참조하는 것이 기본으로 하여 불필요한 리렌더링을 방지하는 것이다.
store 전체 가져오기
// store 선언
const useCountStore = create<{
count: number;
step: number;
increase: () => void;
decrease: () => void;
}>()((set) => ({
count:0,
step:1,
increase:()=>set((s) => ({count: s.count + s.step})),
decrease:()=>set((s) => ({count: s.count - s.step}))
}));
// store의 모든 메서드와 값을 가져온다.
const store = useFeedStore();
// 구조분해할당으로 받아올 수도 있다.
const {count, step, increase, decrease} = useCountStore();
console.log(store.count) // 스토어의 count값을 참조
가장 쉬운 형태인 전체 store를 가져오는 구조이다. store 내부의 메서드를 호출해 state가 변경될 경우 store를 참조하는 모든 컴포넌트가 리렌더된다.
store의 값을 seletor로 쪼개기 - 각각 호출해 분리
const count = useCountStore((s) => s.count)
const increase = useCountStore((s) => s.increase)
const decrease = useCountStore((s) => s.decrease)
위와 같은 형태로 각각 호출한다면 각자의 호출 동작에 대해서 독립적으로 동작하게 된다. useStore를 호출해 콜백 내부에 참조할 store의 state나 action(메서드)을 각각 독립된 형태로 참조할 수 있다.
store의 값을 seletor로 쪼개기 - useShallow를 이용해 배열로 참조
const [count, increase, decrease] = useCountStore(
useShallow((s) => [s.count, s.increase, s.decrease]
)
훨씬 더 간략한 형태로 state가 분리된 상태로 참조할 수 있다.
😊 실제로 사용해보기
실제로 사용할 때는 create로 선언한 store들을 다른 파일들로 분리하여 사용하게 된다. 예를 들면 각각의 컴포넌트 디렉터리에 @store.ts 와 같은 파일을 생성하거나, store 디렉터리를 src 디렉터리에 생성하여 사용할 수도 있다.
- 원하는 위치에 @store.ts 생성하고 useCountStore 작성하기
// @store.ts
const useCountStore = create<{
count: number;
step: number;
increase: () => void;
decrease: () => void;
}>()((set) => ({
count:0,
step:1,
increase:()=>set((s) => ({count: s.count + s.step})),
decrease:()=>set((s) => ({count: s.count - s.step}))
}));
- 필요한 파일에서 store 불러오기
// Counter.tsx
import { useShallow } from "zustand/shallow";
import { useCountStore } from "./@store";
function Counter() {
const [count, increase, decrease] = useCountStore(
useShallow((s) => [s.count, s.increase, s.decrease])
);
return (
<div>
<div>현재 카운터 {count}</div>
<button type="button" onClick={increase}>
+
</button>
<button type="button" onClick={decrease}>
-
</button>
</div>
);
}
export default Counter;
이렇게 Zustand를 이용한 간단한 카운터 컴포넌트가 완성되었다.
유의할 점
유의할 점이자 Zustand를 이용하는 장점이라고 할 수 있는데, 상태를 공유하는 store를 이제 Props로 내려줄 필요 없이 만약 해당 state나 action이 필요한 상황이라면 하위 컴포넌트에서도 Store를 호출해 사용하면 된다.
v5 변경점
상세한 변경사항은 Zustand 공식 문서의 How to Migrate to v5 from v4를 확인하면 된다. v5에서는 기능 추가는 거의 없고 기능 재조정이 대부분이다.
https://zustand.docs.pmnd.rs/migrations/migrating-to-v5
How to Migrate to v5 from v4 - Zustand
zustand.docs.pmnd.rs
리액트 18 버전 이전 지원 중단
React 18+에서만 지원하도록 변경되었다. 이전 버전은 v4를 사용해야 한다.
equalityFn가 기본 훅에서 제거
create/useStore에 전달하던 equalityFn은 v5에서 빠졌고, 필요하면 zustand/traditional의 createWithEqualityFn, useStoreWithEqualityFn을 불러와야 한다.
Persist 미들웨어 동작 변경(초기 저장 관련)
v5에서는 초기 상태가 자동 저장되지 않을 수 있음(특히 비동기 스토리지). 필요 시 첫 마운트에서 한 번 setState로 시드를 넣는 식으로 초기 쓰기를 트리거해야 한다는 가이드가 있음.
환경/엔트리 포인트 정리
React 18 이상, TypeScript 4.5 이상이 최소 요건.
UMD/SystemJS, ES5 지원 제거, 패키지 엔트리 포인트 재정리.
타입 & 세부 개선
setState의 replace 플래그 사용 시 타입이 더 엄격해짐 등 여러 타입 관련 개선.
배열형으로 한번에 상태를 불러올 때 변경
useShallow 함수가 추가되어 v4+ 버전과 다르게 배열형으로 한번에 불러올 때 useShallow를 사용하여야 한다.
- 변경된 코드
const [count, increase, decrease] = useCountStore(
useShallow((s) => [s.count, s.increase, s.decrease]
)
- v4코드
const [count, increase, decrease] = useCountStore((s) => [s.count, s.increase, s.decrease])
'React' 카테고리의 다른 글
| 프론트의 로깅은 무엇일까? 로깅을 알아보자 (0) | 2025.09.11 |
|---|---|
| [Zustand] Root 페이지의 오버레이 통제와 subscribeWithSelector (0) | 2025.08.25 |
| Presentational-Container 패턴과 Hook (1) | 2025.08.10 |
| [React] useRef 훅 정리 (0) | 2025.07.29 |
| [React] React 유용한 스니펫들 정리 (0) | 2025.07.08 |