joooii

[58일차] Zustand 본문

TIL

[58일차] Zustand

joooii 2025. 11. 24. 22:35

 

Zustand란?

Zustand는 작고 빠르며 확장 가능한 React 프로젝트에서 사용하는 상태 관리(Store) 라이브러리로, 간결한 코드 패턴이 핵심이다.

 

이때 상태 관리(Store)란 애플리케이션의 여러 상태(state)를 중앙에서 관리하는 패턴이다.

이를 통해 컴포넌트 간 데이터를 쉽게 공유하고 데이터 변경을 감지해 자동으로 렌더링할 수 있다.

 

기본적인 상태 전달 방식 (Props)

컴포넌트 간 공유해야 하는 데이터가 있다면 기본적으로 props를 통해 부모 → 자식 컴포넌트 간 데이터 전달이 가능하다.

그러나 이러한 방식은 컴포넌트 중첩이 깊어질 수록 props를 불필요하게 전달하는 컴포넌트가 발생할 수 있으며, 이때 Props Drilling*이 발생하게 된다.

* Props Drilling : props를 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 때 중간 컴포넌트를 거쳐야 하는 과정

https://www.heropy.dev/p/n74Tgc

 

Props Drilling을 방지하기 위해 다양한 상태관리 라이브러리를 사용하는데, 대표적으로 Context API, Recoil, Redux, Zustand, Zotai 등이 존재한다.

 

이 중에서도 Zustand(1) 많은 사람들이 사용하고 있으며, (2) 용량이 매우 가볍고, (3) 매우 직관적이라 학습이 쉽다는 장점으로 인해 인기가 많다.

특히 기본적으로 하나의 create 함수로 상태(state)와 동작(action)을 함께 정의하며, 생성된 Store는 Hooks 기반 API로 접근한다.
React Context나 Provider 없이도 전역 상태를 사용할 수 있고, Store는 클로저(Closure)를 활용해 생성된다.

상태관리 라이브러리 사용량 (인프런 이정환님 한 입 크기로 잘라먹는 실전 프로젝트 -SNS편)
라이브러리별 용량 비교 (인프런 이정환님 한 입 크기로 잘라먹는 실전 프로젝트 -SNS편)

 

설치

npm i zustand

 

기본 사용법 및 구조 소개

  • `/src/store` 폴더 생성 후, 해당 폴더 안에서 상태 파일을 관리한다.
  • return 문 내부 개체가 create() 함수가 생성하는 store로 설정된다.
  • `get` : 현재 스토어를 그대로 반환하는 역할을 한다 (스토어 안에 있는 state를 꺼내올 수 있다)
  • `set` : 인수로 전달한 값으로 현재 스토어를 업데이트 시켜주는 함수 (= `setState`)
    • 함수형 업데이트**도 지원한다
    • 리액트 훅으로 반환할 수도 있다

** 함수형 업데이트: set 메서드를 호출하고, 인수로 callback 함수를 넣어줌으로써 callback 함수가 리턴하는 값으로 스토어를 업데이트 시키는것

 

1. 기본 형태

// store/count.ts
import { create } from "zustand";

type Store = {
  count: number;
  increase: () => void;
  decrease: () => void;
};

export const useCountStore = create<Store>((set) => ({
  count: 0,
  increase: () => {
    set((store) => ({
      count: store.count + 1,
    }));
  },
  decrease: () => {
    set((store) => ({
      count: store.count - 1,
    }));
  },
}));
// 기존
const { increase, decrease } = useCountStore();

 

위처럼 작성해서 값을 호출하게 되면 아래와 같은 단점이 있다.

 

컴포넌트에서 불러온 store 값들 중, 하나라도 업데이트가 되면 해당 컴포넌트를 자동으로 리렌더링한다.

   -> controller 컴포넌트에서 store 객체의 전부를 불러오기 때문이다

   -> 따라서 useHook 인자로 `(store) => store.increase`를 전달해주게 되면, 이에 해당하는 increase 함수만 불러오는 형태로 사용해야 한다. 

// 콜백함수로 전달 (= selector 함수)
const increase = useCountStore((store) => store.increase);
const decrease = useCountStore((store) => store.decrease);

 

 

2. actions 사용 형태

increase와 decrease를 actions를 사용해서 한 번에 작성하는 방법도 있다.

그러나 볼륨이 커지만 유지보수가 조금 힘들다는 단점이 존재하기 때문에 각 프로젝트 상황을 고려해 적절한 방법으로 작성해야 한다.

// store/count.ts
import { create } from "zustand";

type Store = {
  count: number;
  actions: {
    increase: () => void;
    decrease: () => void;
  };
};

export const useCountStore = create<Store>((set) => ({
  count: 0,
  actions: {
    increase: () => {
      set((store) => ({
        count: store.count + 1,
      }));
    },
    decrease: () => {
      set((store) => ({
        count: store.count - 1,
      }));
    },
  },
}));
// controller.tsx
const { increase, decrease } = useCountStore((store) => store.actions);

 

 

 

3. 커스텀 훅 정의

기본적인 selector 방식으로도 필요한 상태만 구독할 수 있지만, 프로젝트 규모가 커지면 여러 컴포넌트에서 동일한 selector를 매번 중복 작성해야 하거나, store 구조가 변경되면 모든 selector를 찾아 수정해야 한다는 단점이 있다.

 

이를 방지하기 위해 상태 단위, 액션 단위로 커스텀 훅을 따로 정의하는 방식이 더 효율적이다.

// store/count.ts

import { create } from "zustand";

type Store = {
  count: number;
  actions: {
    increase: () => void;
    decrease: () => void;
  };
};

export const useCountStore = create<Store>((set) => ({
  count: 0,
  actions: {
    increase: () =>
      set((store) => ({
        count: store.count + 1,
      })),
    decrease: () =>
      set((store) => ({
        count: store.count - 1,
      })),
  },
}));

// ---------- 커스텀 훅 ----------

export const useCount = () => {
  const count = useCountStore((store) => store.count);
  return count;
};
export const useIncreaseCount = () => {
  const increase = useCountStore((store) => store.actions.increase);
  return increase;
};
export const useDecreaseCount = () => {
  const decrease = useCountStore((store) => store.actions.decrease);
  return decrease;
};

 

// controller.tsx
const decrease = useDecreaseCount();

 

 

Zustand 미들웨어

Zustand 미들웨어 (인프런 이정환님 한 입 크기로 잘라먹는 실전 프로젝트 -SNS편)

 

Zustand에는 대표적으로 5가지 미들웨어가 존재한다.


1. combine

  • 타입을 자동으로 추론한다.
  • 단점: 첫 번째 인수로 전달한 state 값만 포함하는 타입으로 추론을 한다 (예: `count: number`만 추론한다).
export const useCountStore = create(
  combine({ count: 0 }, (set, get) => ({
    actions: {
      increase: () => {
        set((store) => ({
          count: store.count + 1,
        }));
      },
      decrease: () => {
        set((store) => ({
          count: store.count - 1,
        }));
      },
    },
  })),
);

 

 

2. immer

  • 불변성 관리
    • 특정 값을 업데이트할 때 직접 접근해서 업데이트하는 것이 아니라, 변경될 값을 포함한 새로운 객체를 만들어서 전달하는 방식으로 업데이트하는 방식이다.
  • `immer ( combine( … ) );`
 immer(
    combine({ count: 0 }, (set, get) => ({
      actions: {
        increase: () => {
          set((state) => {
            state.count += 1;
          });
        },
        decrease: () => {
          set((state) => {
            state.count -= 1;
          });
        },
      },
    })),
  ),

 

 

3. subscribeWithSelector

  • selector 함수를 통해서 store의 특정 값을 구독 → 해당 값이 변경될 때마다 어떠한 기능을 추가로 수행하도록 만들어준다 (= `useEffect`).
  • 사용처: 사용자가 로그아웃해서 세션을 보관하는 어떤 스토어의 값이 바뀌었을 때 로그인 페이지로 리다이렉트한다든지 하는 side effect를 관리할 때 종종 사용한다.
  • `subscribeWithSelector ( immer ( combine( … ) ) )`;
  • `getState()` : 현재 스토어를 불러옴
  • `setState()` : 스토어의 값 업데이트
  • `previousSelectedState` : 선택된 count state가 업데이트 되기 이전의 값까지 함께 제공한다.

현재 값 ❘ 이전 값

useCountStore.subscribe(
  (store) => store.count,
  (count, prevCount) => {   // count의 값이 바뀔 때마다
    console.log(count, prevCount); // 콜백함수가 실행됨 (Listener)
  },
);

 

 

4. persist

  • 현재 스토어 값을 로컬, 세션 스토리지에 보관한다
  • `persist (subscribeWithSelector ( immer ( combine( … ) ) ), { name: ".." });`
  • 스토리지에 저장될 때 actions는 빈 객체로 들어온다.

partialize 적용 전

  • partialize를 통해 어떤 스토어 값들을 저장할 것인지 명시해주면 actions을 표시하지 않게 할 수 있다.
  • `{ name: "countStore", partialize: (store) => ({ count: store.count }) },`

partialize 적용 후

  • 세션 스토리지에 저장하고 싶으면 `storage: createJSONStorage(() => sessionStorage)` 를 추가하면 된다.

 

5. devtools

  • 스토어를 디버깅한다.
  • Chrome 플러그인인 Redux DevTools를 추가하여 디버깅할 수 있다.

https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko

 

Redux DevTools - Chrome 웹 스토어

Redux DevTools for debugging application's state changes.

chromewebstore.google.com

 

 

미들웨어 적용순서

devtools > persist > subscribeWithSelector > immer > combine 순으로 적용하면 된다.

devtools(
    persist(
      subscribeWithSelector(
        immer(
          combine({ count: 0 }, (set, get) => ({
            actions: {
              increase: () => {
                set((state) => {
                  state.count += 1;
                });
              },
              decrease: () => {
                set((state) => {
                  state.count -= 1;
                });
              },
            },
          })),
        ),
      ),
      {
        name: "countStore",
        partialize: (store) => ({ count: store.count }),
        storage: createJSONStorage(() => sessionStorage),
      },
    ),
    {
      name: "countStore",
    },
  ),

 

 


회고

Zustand를 정리하면서 단순한 전역 상태 라이브러리가 아니라, 구독 단위와 리렌더링 구조를 이해하며 최적화가 핵심이라는 걸 알게 됐다.
또한 미들웨어까지 살펴보며 Zustand가 가벼우면서도 확장성 있는 상태관리 도구라는 걸 확실히 이해할 수 있었다.

이번 유레카 프로젝트에서는 zustand를 적극적으로 사용해봐야 할 것 같다.

'TIL' 카테고리의 다른 글

[65일차] 웹 시큐리티  (0) 2025.12.08
[64일차] JWT  (0) 2025.12.02
[53일차] TypeScript와 Vite  (1) 2025.11.17
[49일차] React Hooks  (0) 2025.11.10
[45일차] React 기초  (0) 2025.11.04