콘텐츠로 이동

Zustand vs Redux: 실전 비교

Zustand는 가볍고 직관적인 상태 관리 라이브러리입니다. 이 글에서는 Zustand의 기본 사용법과 Redux와의 차이점을 실전 코드와 함께 살펴봅니다.

Zustand는 상태를 유지하는 store를 만드는 데 사용됩니다. 불변 상태 모델을 기반으로 합니다.

import { create } from "zustand";
export const useStore = create(() => ({
count: 0,
text: "hello",
}));
const Component = () => {
const { count, text } = useStore();
return <div>count: {count}</div>;
};

이렇게 사용하면 text 값이 변경되어도 리렌더링이 발생할 수 있습니다.

const Component = () => {
const count = useStore((state) => state.count);
return <div>count: {count}</div>;
};

선택자 함수를 사용하면 count가 변경될 때만 리렌더링됩니다.

선택자 함수는 결과값을 비교해서 렌더링 여부를 판단하므로, 참조 값(객체, 배열)을 반환하도록 지정할 경우 주의해야 합니다.

// 주의: 매번 새 객체를 생성하므로 항상 리렌더링 발생
const Component = () => {
const [{ count }] = useStore((state) => [{ count: state.count }]);
return <div>count: {count}</div>;
};
const selectCount1 = (state) => state.count1;
const Counter1 = () => {
const count1 = useStore(selectCount1);
const inc1 = () => {
useStore.setState((prev) => ({ count1: prev.count1 + 1 }));
};
return (
<div>
count1: {count1} <button onClick={inc1}>+1</button>
</div>
);
};
type StoreState = {
count1: number;
count2: number;
inc1: () => void;
inc2: () => void;
};
const useStore = create<StoreState>((set) => ({
count1: 0,
count2: 0,
inc1: () => set((prev) => ({ count1: prev.count1 + 1 })),
inc2: () => set((prev) => ({ count2: prev.count2 + 1 })),
}));
const selectCount2 = (state) => state.count2;
const selectInc2 = (state) => state.inc2;
const Counter2 = () => {
const count2 = useStore(selectCount2);
const inc2 = useStore(selectInc2);
return (
<div>
count2: {count2} <button onClick={inc2}>+1</button>
</div>
);
};
const selectCount1 = (state) => state.count1;
const selectCount2 = (state) => state.count2;
const TotalCounter = () => {
const count1 = useStore(selectCount1);
const count2 = useStore(selectCount2);
return <div>total: {count1 + count2}</div>;
};

위 경우 count1이 +1 증가하고 count2가 -1 감소하면 결과값은 같지만 리렌더링이 발생합니다.

이를 해결하기 위해 파생된 값에 대한 선택자 함수를 사용합니다:

const selectTotal = (state) => state.count1 + state.count2;
const TotalCounter = () => {
const total = useStore(selectTotal);
return <div>total: {total}</div>;
};

선택자 함수는 결과를 비교하므로, 합산 결과가 같다면 리렌더링이 방지됩니다.

둘 다 단방향 데이터 흐름을 기반으로 합니다.

ZustandRedux
상태 갱신set 함수로 직접 갱신리듀서 기반 (순수 함수)
Store 사용Hook으로 직접 사용useSelector, useDispatch로 간접 사용
항목ZustandRedux
디렉토리 구조추천 구조 없음features 디렉토리 구조 제안, createSlice
Immer개발자 선택기본 내장
단방향 흐름개발자 선택단방향 데이터 흐름 기반
보일러플레이트최소상대적으로 많음
미들웨어선택적devtools, thunk 등 기본 내장
  • 가벼운 전역 상태가 필요할 때
  • 보일러플레이트를 최소화하고 싶을 때
  • 유연한 상태 갱신이 필요할 때
  • 대규모 앱에서 예측 가능한 상태 흐름이 필요할 때
  • 시간 여행 디버깅(DevTools)이 필요할 때
  • 팀 내 일관된 구조가 중요할 때
  • Zustand는 간결하고 유연한 상태 관리 라이브러리
  • 선택자 함수를 통해 필요한 상태만 구독하여 리렌더링 최적화
  • 파생된 값에 대한 선택자를 사용하면 불필요한 리렌더링 방지
  • Redux와 비교 시 보일러플레이트가 적고 학습 곡선이 낮음
  • 프로젝트 규모와 팀 상황에 맞게 선택하는 것이 중요