Zustand

김소희·2025년 11월 7일

Zustand란?

Zustand는 React에서 상태 관리를 단순하고 가볍게 만들기 위한 라이브러리이다.
Zustand는 store 딱 1개 만들고 set 사용 이라는 단순한 구조로 상태를 관리할 수 있다.

기존에 전역 변수를 관리하기 위해 Redux를 쓰면:

  • boilerplate 코드 많고
  • action / reducer / middleware / store 분리가 복잡했다.

그러면 이제 Redux를 완전히 사용하지 않아도 되는가? 그건 아니다.
Redux는 여전히 중·대규모 팀 환경에서 많이 쓰인다.

왜냐면 Redux는 강한 규칙과 흐름이 정형화되어 있어서
팀원이 10명, 20명으로 늘어나도 코드 스타일이 크게 흔들리지 않기 때문이다.
게다가 디버깅 편의성, time-travel, Redux DevTools 같은 도구 지원도 매우 강력하다.
action 기반으로 상태 변화 흐름을 명확하게 추적할 수 있다는 점도 여전히 장점으로 남아 있다.

언제 Zustand 쓰는가?

  • FE 단일 팀 개발
  • React 상태 복잡하지만 Redux까지는 과함
  • store 코드 짧고 유지보수 쉽게 하고싶을 때

전역 상태 관리를 할 수 있는 라이브러리는 Zustand외에도 많이 있다
Recoil, Jotai, Redux Toolkit 등등 존재하지만,
현재 기준으로 Zustand가 가장 단순하고 쓰기 편하다고들 한다.


Zustand의 장점

항목설명
코드가 짧아짐Redux 대비 압도적으로 간결
글로벌 상태 쉽게 공유props drilling 해결
immer optional불변성 유지 편함
selector 사용 쉬움렌더링 최적화 유리
React Context 없이 가능Provider 필요 없음

기본 폼

import { create } from "zustand";

export const useCounterStore = create((set) => ({
  count: 0,
  increase: () => set((state)=>({ count: state.count + 1 })),
  decrease: () => set((state)=>({ count: state.count - 1 })),
}));

그리고 컴포넌트에서

const { count, increase } = useCounterStore();

이걸로 전역 상태 끝.


Zustand의 핵심 개념

(1) state + actions 를 한곳에서 관리

Zustand는 상태와 로직을 분리하지 않는다.

→ 즉 “store가 domain-service” 역할까지 수행

(2) selector (부분만 구독)

const count = useCounterStore((state)=>state.count);

이러면 count가 바뀔 때만 렌더링됨
→ 글로벌 상태여도 불필요한 re-render 적음

(3) persist 미들웨어

전역 상태를 localStorage에 자동 저장 가능

import { create } from "zustand";
import { persist } from "zustand/middleware";

export const usePostStore = create(
  persist(
    (set) => ({
      posts: [],
      add:(text) => set((state)=>({ posts:[...state.posts,{id:Date.now(), text}] })),
    }),
    { name:"post-storage" }
  )
);

immer와 함께 쓰기

React에서 state는 원본을 직접 바꾸면 안 된다.
원본을 수정하지 않고, 새로운 객체를 만들어서 교체하는 방식을 요구한다.
immer는 귀찮은 불변성 처리를 대신해준다.

import { create } from "zustand";
import { produce } from "immer";

export const useTodo = create((set)=>({
  todos:[],
  toggle:(id)=>set(produce(state=>{
    const t = state.todos.find(t=>t.id===id)
    t.isDone = !t.isDone
  }))
}))

위 코드는 실제로 이렇게 한 것과 같다.

toggle:(id)=>set((state)=>({
  todos: state.todos.map(t =>
    t.id===id ? {...t, isDone: !t.isDone} : t
  )
}))

즉, immer는 직접 값을 바꾸는 것처럼 보이는 코드를 사용하더라도
실제로는 불변성을 유지한 새로운 state 객체를 생성해준다.


Async (fetch)

Redux 쓰던 시절엔 비동기 작업을 하려면
dispatch → action → reducer → thunk → middleware 이런 흐름이 필요했다.

API 호출하는 것도 thunk 라는 별도의 미들웨어를 설치해서 다뤘다.

Zustand는 그런 절차가 없이 store 안에서 함수 하나 만들어서
그 안에서 await 쓰면 그게 끝이다.

export const useUser = create((set)=>({
  user:null,
  fetchUser: async ()=> {
    const res = await fetch("api/user");
    const data = await res.json();
    set({ user:data });
  }
}));

Zustand vs Redux 결론 비교

기준ZustandRedux
코드량적음많음
러닝커브쉬움높음
Provider 필요없음있음
TS쉬움verbose
대규모 팀충분규칙 필요시 여전히 선호

요약: Redux의 본질적 강점이 규칙으로 인한 안정성이라면
Zustand는 개발자가 똑똑하다고 가정하고 자유를 주는 것이다.


결론

Zustand는 현대 React 전역상태의 default 선택지가 되었다.
store가 action과 state를 1개 파일에 모으고 single-source-of-truth 이 된다
그리고 selector 기반이라 렌더링도 효율적이다.

실제 프로젝트에서 어떤 라이브러리를 쓸지는 팀 상황과 목표에 따라 달라질 수 있겠지만,
개발 환경이 가볍고, 복잡한 설정 없이 빠르게 전역 상태를 도입하고 싶다면
Zustand는 충분히 좋은 시작점이 될 수 있다고 생각한다.

profile
개발자 소희의 노트

0개의 댓글