React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지

섹션 15. 전역 상태 관리 - Zustand

favor 2026. 6. 12. 16:48

Zustand란?

Zustand : 전역 상태 관리 라이브러리 

ㄴ Redux Toolkit, Context API보다 적은 코드와 직관적인 방식으로 상태 관리 가능

 

공식 홈페이지

https://zustand-demo.pmnd.rs/

 

Zustand

 

zustand-demo.pmnd.rs

 

Zustand 사용하기

  1. 패키지 설치
    • npm install zustand 명령어를 사용하여 다운로드
  2. 폴더 구조 설계
    • src/store/ 하위에 ts 파일로 생성 
  3. store 파일 생성 및 기본 뼈대 정의
    • store : 애플리케이션의 전역 상태(State)와 이를 변경하는 비즈니스 로직(Action)을 한데 모아 관리하는 보관소
    • store에서 사용할 데이터와 함수의 타입 설계도를 interface로 정의
    • Zustand의 create 훅을 사용하여 컴포넌트들이 구독할 전역 store 생성 
    • export const use***Store = create((set, get) => ({}));
      • set : 스토어 내부의 데이터를 안전하게 변경하기 위해 사용하는 상태 변경 콜백 함수
      • get : 특정 액션(함수) 내부에서 현재 스토어에 담긴 최신 상태값들을 동기적으로 참조하기 위해 사용하는 현재 상태 조회 함수

 

// src/store/countStore.ts

import { create } from "zustand";

type CountStore = {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

// use***Store
export const useCountStore = create<CountStore>((set) => ({
  count: 0,
  decrement: () => set((state) => ({ count: state.count - 1 })),
  increment: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 })
}));

 

// src/components/CountOutside.tsx

import { useCountStore } from "../store/countStore";

export default function CountOutside() {
  const count = useCountStore((state) => state.count);

  return (
    <>
      <h1>CountOutside: {count}</h1>
    </>
  );
}
// src/components/CountDisplay.tsx

import { useCountStore } from "../store/countStore";

export default function CountDisplay() {
  const count = useCountStore((state) => state.count);

  return (
    <>
      <h1>Count: {count}</h1>
    </>
  );
}
// src/components/CountButton.tsx

import { useCountStore } from "../store/countStore";

export default function CountButton() {
  const increment = useCountStore((state) => state.increment);
  const decrement = useCountStore((state) => state.decrement);
  const reset = useCountStore((state) => state.reset);

  return (
    <>
      <button onClick={decrement}>감소</button>
      <button onClick={reset}>리셋</button>
      <button onClick={increment}>증가</button>
    </>
  );
}

 

Zustand 매개변수

Zustand는 외부에서 매개변수를 전달받거나 비동기 함수를 사용할 때에도 매우 쉽게 처리 가능

 

매개변수 처리

// src/components/CountButton.tsx

import { useCountStore } from "../store/countStore";

export default function CountButton() {
  const increment = useCountStore((state) => state.increment);
  const decrement = useCountStore((state) => state.decrement);
  const reset = useCountStore((state) => state.reset);

  return (
    <>
      <button onClick={decrement}>감소</button>
      <button onClick={reset}>리셋</button>
      <button onClick={() => increment(5)}>증가</button>
    </>
  );
}

^ increment(5) --> 5라는 매개변수 전달

// src/store/countStore.ts

import { create } from "zustand";

type CountStore = {
  count: number;
  increment: (amount: number) => void;
  decrement: () => void;
  reset: () => void;
}

export const useCountStore = create<CountStore>((set) => ({
  count: 0,
  increment: (amount: number) => set((state) => ({ count: state.count + amount })), // 전달받은 값만큼 증가 
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));

^ amount: number

 

비동기 함수 처리 

// src/store/countStore.ts

import { create } from "zustand";

type CountStore = {
  count: number;
  increment: (amount: number) => void;
  decrement: () => void;
  reset: () => void;
};

export const useCountStore = create<CountStore>((set) => ({
  count: 0,
  increment: async (amount: number) => { // async 
    await new Promise((resolve) => setTimeout(resolve, 1000)); // await new Promise
    set((state) => ({ count: state.count + amount }));
  },

  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

^ async/await를 활용해 1초의 지연 후 상태를 갱신하는 비동기 함수 구현

 

Zustand persist

Zustand의 상태 데이터는 브라우저를 새로고침하면 모두 초기화됨 

 

상태를 새로고침해도 유지되게 하기 위해서는 local storage, session storage에 상태를 동기화시켜야 함 

ㄴ 다른 전역 상태 관리 라이브러리는 그러한 스토리지 작업을 직접 해야 해야 했지만, Zustand는 작업을 대신 처리해주는 persist 미들웨어 기능을 보유

 

미들웨어(middleware) : 특정 라이브러리의 핵심 동작을 확장하거나 가로채서 추가적인 기능을 수행할 수 있도록 지원하는 소프트웨어 구조 

ㄴ 어떤 하나의 기능을 지칭하는 것은 X

 

Persist 미들웨어 구현 문법 및 사용법

  1. create 함수의 콜백 함수를 persist() 미들웨어 함수로 감싸줌
  2. persist()의 두 번째 매개변수로 객체 지정
    • storage: createJSONStorage(() => sessionStorage) : 생략 시 기본값은 localStorage
  3. 해당 객체에 name 속성을 정하여 스토리지에 저장될 고유 Key를 선언

 

// src/store/countStore.ts

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

type CountStore = {
  count: number;
  increment: (amount: number) => void;
  decrement: () => void;
  reset: () => void;
};

export const useCountStore = create<CountStore>()(
  persist(
    (set) => ({
      count: 0,
      increment: (amount: number) => {
        set((state) => ({ count: state.count + amount }));
      },
      decrement: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: "count-storage",
      storage: createJSONStorage(() => sessionStorage) // 세션 스토리지에 저장되도록 설정 
    },
  ),
);

 

Zustand subscribeWithSelector

subscribeWithSelector 미들웨어 : 특정 상태가 변경될 때를 감지해서 특정 로직을 수행할 수 있는 구독 기능을 사용할 수 있게 해주는 미들웨어

 

subscribeWithSelector 미들웨어 구현 문법 및 사용법

  1. 스토어를 만들 때 subscribeWithSelector 미들웨어 함수로 감싸줌
  2. 구독할 변수, 변수값에 변화가 생기면 실행할 함수 설정 

 

// src/store/countStore.ts

import { create } from "zustand";
import {
  createJSONStorage,
  persist,
  subscribeWithSelector,
} from "zustand/middleware";

type CountStore = {
  count: number;
  increment: (amount: number) => void;
  decrement: () => void;
  reset: () => void;
};

export const useCountStore = create<CountStore>()(
  subscribeWithSelector(
    // subscribeWithSelector 미들웨어 적용
    persist(
      (set) => ({
        count: 0,
        increment: (amount: number) => {
          set((state) => ({ count: state.count + amount }));
        },
        decrement: () => set((state) => ({ count: state.count - 1 })),
        reset: () => set({ count: 0 }),
      }),
      {
        name: "count-storage",
        storage: createJSONStorage(() => sessionStorage), 
      },
    ),
  ),
);
// src/components/Count.tsx

import { useEffect } from "react";
import CountGroup from "./CountGroup";
import { useCountStore } from "../store/countStore";

export default function Count() {
  useEffect(() => {
    const unsubscibe = useCountStore.subscribe(
      (state) => state.count, // count 상태를 구독하고 있다가, 
      (newCount) => { // 상태에 변화가 생기면 해당 콜백 함수를 실행 
        console.log("newCount" + newCount);
      },
    );
    return () => {
      unsubscibe();
    };
  }, []);
  return (
    <>
      <CountGroup />
    </>
  );
}

 

Zustand immer

immer 미들웨어 : 사용자가 불변성을 고려하지 않아도 자동으로 불변성을 처리해주는 미들웨어

 

  1. 패키지 설치
    • npm install immer 명령어를 사용하여 다운로드 
  2. 사용하기
    • subscribeWithSelector 미들웨어 전체를 감싸는 것 X
    • set 함수가 콜백 함수로 전달되는 함수의 영역을 감싸는 것 O

 

// src/count/countStore.ts

import { create } from "zustand";
import {
  createJSONStorage,
  persist,
  subscribeWithSelector,
} from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

type CountStore = {
  count: number;
  increment: (amount: number) => void;
  decrement: () => void;
  reset: () => void;
};

export const useCountStore = create<CountStore>()(
  subscribeWithSelector(
    persist(
      immer((set) => ({ // immer 미들웨어 적용
        count: 0,
        increment: (amount: number) => {
          set((state) => {
            state.count += amount;
          });
        },
        decrement: () =>
          set((state) => {
            state.count -= 1;
          }),
        reset: () =>
          set((state) => {
            state.count = 0;
          }),
      })),
      {
        name: "count-storage",
        storage: createJSONStorage(() => sessionStorage),
      },
    ),
  ),
);

 

Zustand devtools

Zustand는 가벼운 상태 관리 라이브러리 --> 자체 GUI 디버깅 도구를 내장하고 있지 X

ㄴ 대신 브라우저 확장 프로그램인 Redux Devtools와 완벽하게 연동되는 미들웨어를 제공

 

서비스 배포(Production) 환경에서 상태 변화 흐름이 무방비하게 노출되면 보안 취약점이 될 수 있고, 성능 저하를 유발하기도 함

∴ 개발(Development) 환경에서만 DevTools가 켜지도록 조건부 제어하는 것이 필수적

 

DevTools 자동화 설정 문법

  • devtools 미들웨어의 두 번째 매개변수 객체에 설정 주입
  • name : 디버거 창 내에서 각 스토어를 식별하기 위한 고유 이름표 지정
  • enabled : 환경 변수를 활용하여 현재 빌드 모드가 development일 때만 동적으로 활성화 처리 
    • { enabled: false } --> 연동되지 않도록 설정
    • ㄴ 개발모드에선 true, 그렇지 않을 때에는 false라고 지정하면 됨
    • { enabled: import.meta.env.MODE === "development" }
    • ㄴ 자동화

 

// src/store/countStore.ts

import { create } from "zustand";
import {
  createJSONStorage,
  devtools,
  persist,
  subscribeWithSelector,
} from "zustand/middleware";
import { immer } from "zustand/middleware/immer";

type CountStore = {
  count: number;
  increment: (amount: number) => void;
  decrement: () => void;
  reset: () => void;
};

export const useCountStore = create<CountStore>()(
  devtools( // devtools 미들웨어 적용
    subscribeWithSelector(
      persist(
        immer((set) => ({
          count: 0,
          increment: (amount: number) => {
            set((state) => {
              state.count += amount;
            });
          },
          decrement: () =>
            set((state) => {
              state.count -= 1;
            }),
          reset: () =>
            set((state) => {
              state.count = 0;
            }),
        })),
        {
          name: "count-storage",
          storage: createJSONStorage(() => sessionStorage),
        },
      ),
    ),
  ),
);