Zustand Store
💡 Point
- Zustand는 상태(state)와 로직(action)을 한 곳에 모아 관리할 수 있는 가벼운 상태 관리 라이브러리다.
useAppStore
라는 훅을 만들어두면, 컴포넌트 안에서 바로 불러와서 값이나 함수를 쓸 수 있다.useAppStore()
는 훅이므로 무조건 함수처럼 호출해야 한다.useAppStore((state) => state.count)
같이 선택자를 쓰면 필요한 값만 구독해서 불필요한 리렌더링을 줄일 수 있다.- 한 번 만든 스토어는 앱 전체에서 단일 인스턴스(싱글톤)로 동작해, 여러 컴포넌트가 같은 상태를 공유할 수 있다.
1. Store 생성하기
useAppStore
는 단순한 변수처럼 보이지만, 실제로는 React에서 실행 가능한 커스텀 훅이다.
컴포넌트 안에서 호출하면 Zustand가 자동으로 구독을 걸고, 상태가 바뀌면 컴포넌트를 다시 렌더링한다.
tsx
import { create } from "zustand";
interface StoreState {
count: number; // 상태 (state)
increment: () => void; // 상태를 바꾸는 함수 (action)
}
const useAppStore = create<StoreState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export default useAppStore;
create
: Zustand의 핵심 함수, store를 생성한다.set
: Zustand가 제공하는 상태 업데이트 함수.count
: 상태(state) → 현재 값 저장소 (여기선 기본값 0)increment
: action →set
을 이용해 count를 1 증가시키는 함수
🧩 타입 설계 TypeScript
타입스크립트 문법
ts
interface StoreState {
count: number; // 숫자 타입
increment: () => void; // 인자 없음, 반환값 없음
}
StoreState라는 이름의 타입 설계도 생성
count
: 반드시 숫자여야 한다.increment
: 반드시 함수여야 하며, 매개변수와 반환값이 없다.
이렇게 타입을 정의하면, store.count = "abc"
같은 실수를 컴파일 단계에서 막을 수 있다.
자바스크립트라면?
js
const store = {
count: 0,
increment: function () {
store.count += 1;
},
};
JS에서는 구조만 맞으면 문제없이 동작한다. 하지만 TS는 타입 규칙을 엄격히 지켜야 하므로 안정성이 높아진다.
2. 컴포넌트에서 store 사용하기
tsx
import useAppStore from "./store/useAppStore";
function App() {
const { count, increment } = useAppStore();
return (
<div>
<h1>Hello Zustand</h1>
<p>{count}</p>
<button onClick={increment}>increment</button>
</div>
);
}
export default App;
tsx
import useAppStore from "./store/useAppStore";
function App() {
// 1. 각각 꺼내 쓰는 방법 (선택자 사용)
// const count = useAppStore((state) => state.count);
// const increment = useAppStore((state) => state.increment);
// 2. 구조분해 할당으로 한 번에 꺼내기 (스토어 전체에서 꺼냄)
const { count, increment } = useAppStore();
return (
<div>
<h1>Hello Zustand</h1>
<p>{count}</p>
{/* 화살표 함수로 감쌀 필요 없음 */}
{/* <button onClick={() => increment()}>increment</button> */}
{/* 함수 레퍼런스를 직접 전달 (increment가 함수이므로 그대로 전달 가능) */}
<button onClick={increment}>increment</button>
</div>
);
}
export default App;
tsx
import { create } from "zustand";
interface StoreState {
count: number;
increment: () => void;
}
const useAppStore = create<StoreState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export default useAppStore;
onClick={increment}
: 버튼이 클릭되면increment
실행onClick={() => increment()}
: 버튼 클릭 시 익명 함수 실행, 그 안에서increment()
호출
현재 코드에선 두 방식이 동일하지만, 매개변수를 넘겨야 할 때는 화살표 함수가 필요하다.
상태 구독 방식
useAppStore((state) => state.count)
: count만 구독useAppStore((state) => state.increment)
: increment만 구독useAppStore()
: 스토어 전체를 가져와 필요한 값 꺼냄
필요한 값만 선택적으로 구독하면 불필요한 리렌더링을 줄일 수 있다.
실행 흐름 요약
- 앱 시작 → useAppStore 생성 (count=0, increment 준비됨)
- App 컴포넌트가 count 구독
- 버튼 클릭 → increment 실행 → count + 1
- Zustand가 값 변경 감지 → 컴포넌트 리렌더링
- 새로운 count 값이 화면에 반영
숨겨진 유틸
Zustand store는 훅이면서 동시에 스토어 핸들을 가진다.
tsx
useAppStore.getState() // 현재 상태 가져오기
useAppStore.setState(...) // 상태 직접 변경
useAppStore.subscribe(...)// 상태 변경 구독
즉, React 훅처럼 쓰면서도, 리액트 바깥에서도 상태를 제어할 수 있다.