TIL/[TIL]

[TIL]Zustand를 활용한 전역 상태 관리와 Immer 개요

namerong 2026. 4. 24. 11:59

1. 오늘 학습 주제

React에서 전역 상태를 더 간단하게 관리할 수 있는 Zustand를 학습했다.

기존에는 부모에서 자식으로 props를 계속 내려주는 방식으로 상태를 전달했지만, Zustand를 사용하면 필요한 컴포넌트가 store에서 직접 상태를 꺼내 쓸 수 있다.

  • Zustand store의 기본 구조
  • 전역 상태와 액션을 한 곳에서 관리하는 방법
  • props drilling 없이 여러 컴포넌트에서 상태를 공유하는 방법
  • store를 역할별로 나누는 방법
  • Zustand에서 비동기 액션을 처리하는 방법
  • Zustand와 함께 자주 언급되는 Immer의 역할

2. Zustand란

Zustand는 React에서 전역 상태를 간단하게 관리할 수 있도록 도와주는 상태 관리 라이브러리이다.

Redux보다 설정이 훨씬 간단하고, 필요한 상태와 함수만 store에 정의한 뒤 바로 꺼내 쓸 수 있다는 점이 특징이다.

import { create } from "zustand";

정리

  • Zustand는 가볍고 단순한 전역 상태 관리 도구이다.
  • 복잡한 reducer나 action type 없이 상태와 함수를 함께 정의할 수 있다.
  • 필요한 컴포넌트에서 바로 store를 구독해 사용할 수 있다.

3. 기본 Store 생성

기본 예제에서는 create()를 사용해 count, text, 그리고 상태를 바꾸는 액션들을 함께 정의했다.

export const useStore = create((set) => ({
  count: 0,
  text: "",

  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 })),
  setText: (value) => set({ text: value })
}));

정리

  • Zustand store는 create()로 만든다.
  • 상태값과 상태 변경 함수를 한 객체 안에 함께 넣는다.
  • set()을 사용해 상태를 업데이트한다.

4. Zustand에서 상태 읽기

컴포넌트에서는 store 훅을 호출해 필요한 상태와 액션을 꺼내 쓸 수 있다.

const { count, increase, decrease, text, setText } = useStore();

이후 일반 state처럼 화면에 출력하거나 버튼 이벤트에 연결할 수 있다.

<h2>{count}</h2>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
  • store에서 필요한 값만 꺼내 사용할 수 있다.
  • 상태값과 액션을 같은 훅에서 함께 사용할 수 있다.
  • 로컬 state처럼 보이지만 실제로는 전역 상태이다.

5. Props Drilling 문제와 Zustand

기본 예제에서는 Parent -> Child -> GrandChild 구조였지만, 실제 상태는 GrandChild에서 바로 store를 통해 가져왔다.

export default function Parent() {
  return <Child />;
}
export default function Child() {
  return <GrandChild />;
}
const { count, increase, decrease, text, setText } = useStore();
  • 부모에서 자식으로 props를 계속 넘기지 않아도 된다.
  • 필요한 컴포넌트가 직접 전역 상태에 접근할 수 있다.
  • 컴포넌트 트리가 깊어져도 상태 전달이 단순해진다.

6. Zustand와 React state의 차이

React의 useState는 컴포넌트 내부 상태를 관리할 때 적합하다.

반면 Zustand는 여러 컴포넌트가 함께 써야 하는 전역 상태를 관리할 때 유리하다.

  • useState는 컴포넌트 단위 상태 관리에 적합하다.
  • Zustand는 여러 컴포넌트가 공유하는 상태에 적합하다.
  • 공통 상태가 많아질수록 Zustand의 장점이 커진다.

7. Store 분리하기

두 번째 예제에서는 store를 하나로 크게 만들지 않고 역할별로 나누어 관리했다.

  • useUIStore
  • useUserStore
  • useCartStore

이렇게 나누면 상태의 책임이 명확해진다.

  • UI 관련 상태는 UI store
  • 사용자 정보는 user store
  • 장바구니 데이터는 cart store
  • 기능별로 store를 분리하면 유지보수가 쉬워진다.

8. UI Store

UI store에서는 모달 열기/닫기, 사이드바 토글처럼 화면 상태를 관리했다.

export const useUIStore = create((set) => ({
  isModalOpen: false,
  isSidebarOpen: false,

  openModal: () => set({ isModalOpen: true }),
  closeModal: () => set({ isModalOpen: false }),
  toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
}));
  • UI 상태는 서버 데이터와 별개로 관리할 수 있다.
  • 모달, 사이드바, 탭 선택 같은 상태를 전역으로 다루기 좋다.
  • UI 로직이 여러 컴포넌트에 흩어지는 것을 막을 수 있다.

9. User Store

User store에서는 로그인, 로그아웃, 사용자 정보 저장 로직을 관리했다.

export const useUserStore = create((set) => ({
  user: null,
  loading: false,

  login: (userData) => set({ user: userData }),
  logout: () => set({ user: null }),
  setUser: (user) => set({ user }),
}));
  • 로그인한 사용자 정보처럼 앱 전역에서 필요한 데이터 관리에 적합하다.
  • 인증 상태를 여러 페이지와 컴포넌트에서 공유할 수 있다.
  • 액션도 함께 모아두면 상태 변경 흐름이 명확해진다.

10. Cart Store

Cart store에서는 장바구니 항목 추가, 삭제, 비우기 로직을 관리했다.

export const useCartStore = create((set) => ({
  items: [],

  addItem: (item) => set((state) => ({
    items: [...state.items, item],
  })),

  removeItem: (id) => set((state) => ({
    items: state.items.filter((item) => item.id !== id),
  })),

  clearCart: () => set({ items: [] }),
}));
  • 배열 상태를 다룰 때도 Zustand 안에서 직접 관리할 수 있다.
  • 추가는 전개 연산자, 삭제는 filter() 패턴을 사용했다.
  • 상태 업데이트 시 기존 배열을 직접 수정하지 않고 새 배열을 만들어야 한다.

11. Zustand에서 불변성

Zustand를 쓰더라도 React 상태 관리의 기본 원칙인 불변성은 여전히 중요하다.

예제에서도 배열에 push()를 직접 하지 않고, 새 배열을 만들어 교체했다.

items: [...state.items, item]
  • 기존 상태를 직접 수정하면 안 된다.
  • 새로운 객체나 배열을 만들어 상태를 바꾸는 방식이 필요하다.
  • Zustand도 React 생태계 안에서 불변성 원칙을 따르는 것이 중요하다.

12. 비동기 액션 처리

Zustand는 store 안에서 비동기 함수도 바로 정의할 수 있다.

예제에서는 fetchUser라는 비동기 액션을 store 안에 넣어 사용자 정보를 불러왔다.

fetchUser: async () => {
  set({ loading: true });

  try {
    const res = await fetch('<https://jsonplaceholder.typicode.com/users/1>');
    const data = await res.json();

    set({ user: data });
  } catch (error) {
    console.error('유저 정보 조회 실패', error);
  } finally {
    set({ loading: false });
  }
}
  • Zustand store 안에 async 함수를 넣을 수 있다.
  • 비동기 요청 전후로 loading 상태를 관리할 수 있다.
  • 서버 데이터를 store에 직접 저장하는 구조를 만들 수 있다.

13. useEffect와 Zustand 비동기 액션

비동기 페이지 예제에서는 컴포넌트가 마운트되면 fetchUser()를 실행하도록 구성했다.

useEffect(() => {
  fetchUser();
}, [fetchUser]);

그리고 로딩 상태에 따라 화면을 분기했다.

if (loading) {
  return <h1>데이터 로딩중...</h1>;
}
  • API 호출 시점은 useEffect와 함께 제어할 수 있다.
  • 상태 저장은 store가 담당하고, 컴포넌트는 화면 렌더링에 집중한다.
  • 전역 상태와 비동기 로직을 함께 다루는 흐름을 이해할 수 있다.

14. Zustand의 장점

예제를 통해 Zustand의 장점을 체감할 수 있었다.

  • 설정이 간단하다.
  • boilerplate 코드가 적다.
  • props drilling을 줄일 수 있다.
  • store를 기능별로 쉽게 나눌 수 있다.
  • 동기/비동기 액션을 함께 관리할 수 있다.
  • 작은 프로젝트부터 중간 규모 프로젝트까지 가볍게 적용하기 좋다.

15. Immer란

Immer는 불변성을 유지하면서도 마치 객체를 직접 수정하는 것처럼 코드를 작성할 수 있게 도와주는 라이브러리이다.

원래는 아래처럼 새 배열이나 새 객체를 만들어야 한다.

set((state) => ({
  items: [...state.items, item]
}))

Immer를 사용하면 내부적으로는 불변성을 지키면서도 더 직관적으로 쓸 수 있다.

개념적으로는 아래처럼 작성하는 느낌이다.

draft.items.push(item);
  • Immer는 불변성 관리를 쉽게 만들어 준다.
  • 코드가 더 직관적이고 읽기 쉬워진다.
  • 상태 구조가 깊고 복잡할수록 장점이 커진다.
  • Zustand와 함께 쓰면 복잡한 객체 업데이트를 더 편하게 작성할 수 있다.

16. Zustand와 Immer를 함께 쓰는 이유

Zustand만으로도 상태 관리는 가능하지만, 배열이나 중첩 객체가 복잡해질수록 불변성 코드를 직접 쓰는 것이 번거로워질 수 있다.

이럴 때 Immer를 같이 사용하면 상태를 더 자연스럽게 수정할 수 있다.

  • Zustand는 상태 저장 구조를 단순하게 만든다.
  • Immer는 상태 업데이트 코드를 단순하게 만든다.
  • 둘을 함께 쓰면 특히 복잡한 전역 상태를 다룰 때 편해진다.

17. 오늘 배운 핵심 정리

  1. Zustand는 React에서 전역 상태를 간단하게 관리할 수 있는 라이브러리이다.
  2. create()로 store를 만들고, 상태와 액션을 함께 정의할 수 있다.
  3. 필요한 컴포넌트가 store에서 직접 상태를 읽기 때문에 props drilling을 줄일 수 있다.
  4. count, text 같은 기본 상태부터 모달, 유저, 장바구니 같은 실제 앱 상태까지 관리할 수 있다.
  5. store는 기능별로 분리하면 구조가 더 명확해진다.
  6. Zustand에서도 배열과 객체는 불변성을 지키며 업데이트해야 한다.
  7. store 안에 비동기 액션을 직접 정의할 수 있다.
  8. loading, user 같은 상태를 함께 관리하면 비동기 화면 분기가 쉬워진다.
  9. Immer는 불변성을 유지하면서도 마치 직접 수정하는 것처럼 상태 업데이트를 작성하게 도와준다.
  10. Zustand와 Immer를 함께 사용하면 복잡한 상태 업데이트를 더 읽기 쉽게 만들 수 있다.