Zustand 상태 설계 패턴
Zustand는 자유로운 상태 관리 도구이지만, 어떤 구조로 설계하냐에 따라 유지 보수성과 확장성이 크게 달라진다.
💡 Zustand 상태 설계의 핵심
- 스토어 구조화: 상태, 액션, 계산된 값 구분
- 선택적 구독: selector로 필요한 상태만 선택
- 안전한 업데이트: set/get로 불변성 유지
- 복잡한 구조 관리: 중첩 객체·배열도 불변성 지키기
스토어 설계 구조
Zustand 스토어는 크게 상태(State), 액션(Action), 계산된 값(Computed Value) 으로 나눌 수 있다.
jsx
const useAppStore = create((set, get) => ({
// 상태
count: 0,
// 액션
increment: () => set((state) => ({ count: state.count + 1 })),
// 계산된 값
doubleCount: () => get().count * 2,
}));
set()
: 상태 변경get()
: 현재 상태 조회
역헐울 구분해 두면 가독성과 유지보수성이 좋아진다.
상태 선택 (Selector)
Zustand는 필요한 값만 선택적 구독할 수 있다. 이 방식은 불필요한 리렌더링을 줄여 성능 최적화에 도움이 된다.
jsx
const name = useUserStore((state) => state.user.name);
const isLoggedIn = useUserStore((state) => state.user.isLoggedIn);
- name만 바뀌면 isLoggedIn을 사용하는 컴포넌트는 리렌더링되지 않는다.
(state) => state.user.name;
같은 함수를 selector 함수라고 부른다.
💡 최적화 Tip
- 꼭 필요한 값만 선택
- selector 함수는 컴포넌트 바깥에서 선언해 재사용
- 객체 비교 최적화를 위해 useShallow 훅 활용
상태 업데이트 (set, get)
Zustand는 불변성을 지키며 상태를 변경해야 한다.
jsx
setCount: (value) => set({ count: value });
increment: () => set((state) => ({ count: state.count + 1 }));
doubleCount: () => get().count * 2;
불변성이 중요한 이유
💡 불변성이란?
한 번 생성된 값은 변경되지 않는다는 성질을 의미한다.
상태는 직접 변경하면 변경 사실을 추적하기 어렵고, 예기치 않은 부작용이 발생할 수 있다.
- React는
===
얕은 비교로 상태 변경 여부를 판단한다. - 객체/배열을 직접 수정하면 변경을 감지하지 못해 리렌더링이 안될 수 있다.
- 따라서 상태를 바꿀 때는 매번 새로운 객체/배열을 생성해야 한다.
복잡한 상태 구조 다루기
중첩 객체 업데이트
jsx
updatePersonalInfo: (newInfo) =>
set((state) => ({
profile: {
...state.profile,
personal: {
...state.profile.personal,
...newInfo,
},
},
}));
- 기존의 profile, personal을 펼친 뒤, 새로운 값 newInfo만 덮어쓴다.
- 나머지 값은 유지되면서 원하는 값만 안전하게 업데이트 된다.
- 복잡한 중첩 상태는 Immer 라이브러리 고려
배열 업데이트
jsx
addTask: (task) =>
set((state) => ({
tasks: [...state.tasks, task],
}));
- 기본 배열을 복사 (...state.tasks) 후 새 값을 추가해야 React가 변경을 감지하고 리렌더링이 발생한다.