코딩공부/상태관리 Zustand

Zustand의 개념 및 기능 정리

표자 2025. 5. 13. 10:27

Zustand: 간단하고 강력한 상태 관리 라이브러리 🐻

Zustand는 React에서 사용하기 쉬운 상태 관리 라이브러리입니다. Redux의 복잡성 없이 단순하면서도 강력한 기능을 제공합니다.

1. create 함수 (콜백 set, get) 🛠️

Zustand의 핵심은 create 함수로, 상태와 그 상태를 변경하는 액션을 정의합니다.

import { create } from 'zustand';

interface BearState {
  bears: number;
  increasePopulation: () => void;
}

const useBearStore = create<BearState>((set, get) => ({
  bears: 0,
  increasePopulation: () => set({ bears: get().bears + 1 }),
}));

// 컴포넌트에서 사용
const bears = useBearStore((state) => state.bears);
const increase = useBearStore((state) => state.increasePopulation);

🛠 set 상태 업데이트 함수

set은 Zustand 스토어의 상태를 변경하는 함수입니다. 이를 통해 특정 상태 값을 업데이트 가능!

  • set 함수는 새로운 상태 객체를 전달하여 해당 값을 변경합니다.
  • 또한, 이전 상태를 참조하는 콜백 함수를 사용할 수도 있습니다.
set({ bears: 5 }); // 'bears' 값을 5로 설정
set((state) => ({ bears: state.bears + 1 })); // 기존 값에서 1 증가

🔍 get 현재 상태 조회 함수

get은 현재 스토어의 상태를 가져오는 함수로, 현재 상태를 조회하는 용도로 사용됩니다.

  • get()을 호출하면 현재 상태 객체를 반환합니다.
  • 이를 통해 새로운 값을 설정하기 전에 기존 값을 참고할 수 있습니다.
const currentBears = get().bears; // 현재 'bears' 값을 가져옴
set({ bears: currentBears + 1 }); // 기존 값에서 1 증가

🏗️ set과 get을 함께 활용하기

두 함수를 함께 사용하면 기존 상태를 참고하면서 새로운 값을 설정할 수 있습니다.

increasePopulation: () => set({ bears: get().bears + 1 })

위 코드에서는 get()을 사용해 현재 bears 값을 가져오고, set()을 통해 해당 값을 1 증가시킵니다.

2. 상태와 액션의 개념 📊

  • 상태(State): 애플리케이션의 데이터
  • 액션(Action): 상태를 변경하는 함수
interface DeviceState {
  devices: Device[];
  loading: boolean;
  fetchDevices: () => Promise<void>;
  addDevice: (device: Device) => void;
}

const useDeviceStore = create<DeviceState>((set) => ({
  devices: [],
  loading: false,

  fetchDevices: async () => {
    set({ loading: true });
    const response = await fetch('/api/devices');
    const devices = await response.json();
    set({ devices, loading: false });
  },

  addDevice: (device) => set((state) => ({
    devices: [...state.devices, device]
  })),
}));

1️⃣ 상태(State)란?

상태는 애플리케이션이 현재 보유하고 있는 데이터라고 생각하면 됩니다. 예를 들어 쇼핑몰 앱이라면

  • 현재 장바구니에 담긴 상품 목록
  • 로그인한 사용자의 정보
  • 로딩 상태 (true 또는 false)

상태는 앱의 UI를 결정하는 중요한 요소로, 변경될 때마다 UI가 업데이트됩니다.

💡 예제 속 DeviceState에서 상태는 devices와 loading이며,

  • devices: 현재 저장된 디바이스 목록
  • loading: 데이터 요청이 진행 중인지 여부

2️⃣ 액션(Action)이란?

액션은 상태를 변경하는 함수입니다. 즉, **현재 상태를 원하는 값으로 업데이트하는 동작(함수)**을 담당.

예제 코드에서 fetchDevices와 addDevice가 액션입니다.

  • fetchDevices: API 요청을 보내고, 가져온 데이터로 devices를 업데이트
  • addDevice: 새로운 device를 기존 목록에 추가

💡 액션을 사용하면 상태를 직접 변경하지 않고 안전하게 업데이트할 수 있습니다!

3. lodash를 이용한 상태 삭제 🗑️

lodash를 사용하여 상태의 일부를 쉽게 제거할 수 있습니다.

import { create } from 'zustand';
import { omit } from 'lodash';

interface AlertState {
  alerts: Record<string, Alert>;
  removeAlert: (id: string) => void;
}

const useAlertStore = create<AlertState>((set) => ({
  alerts: {},
  removeAlert: (id) => set((state) => ({
    alerts: omit(state.alerts, id)
  })),
}));

🚀 lodash란?

Lodash는 JavaScript에서 자주 사용되는 유틸리티 라이브러리로, 배열, 객체 등을 쉽게 다룰 수 있도록 도와줍니다.

특히, 객체에서 특정 키를 제거하는 omit 함수는 상태 관리에 유용하게 활용됩니다.

🏗️ 예제 코드 분석

import { omit } from 'lodash';
alerts: omit(state.alerts, id)

이 코드는 lodash에서 omit 함수를 가져옵니다.

omit은 객체에서 특정 속성을 제외한 새로운 객체를 반환하는 함수입니다.

  • state.alerts는 현재 alerts 객체
  • id에 해당하는 키를 제거한 새로운 객체를 반환
  • 기존 객체를 직접 수정하지 않고, 불변성(immutability)을 유지하면서 상태를 업데이트

🔍 객체에서 특정 항목 제거하기

const obj = { a: 1, b: 2, c: 3 };
const newObj = omit(obj, 'b'); // 'b' 속성을 제거

console.log(newObj); // { a: 1, c: 3 }

✅ omit(obj, 'b')를 호출하면 b 속성이 제거된 새 객체가 반환됩니다.

🎯 왜 omit을 사용할까?

  • 객체의 불변성을 유지하면서 특정 키를 제거
  • 새 객체를 생성하여 React의 상태 변경 감지를 원활하게 함
  • 직접 상태를 수정하지 않음 → 예측 가능한 상태 관리 가능

4. 미들웨어 🔄

미들웨어를 통해 스토어의 동작을 확장할 수 있습니다. 로깅, 영속성 등 다양한 기능을 추가할 수 있습니다.

import { create } from 'zustand';
**import { devtools } from 'zustand/middleware';**

const useServerStore = create<ServerState>()(
  devtools(
    (set) => ({
      servers: [],
      addServer: (server) => set((state) => ({
        servers: [...state.servers, server]
      })),
    }),
    { name: 'Server Store' }
  )
);

🚀 미들웨어란?

미들웨어는 스토어의 동작을 확장할 수 있도록 돕는 기능입니다.

즉, 상태 관리 로직에 추가적인 기능을 삽입할 수 있습니다

✔️ 미들웨어를 사용하면 다음과 같은 기능을 추가로 사용 할 수 있음!

  • 로깅(logging) → 상태 변경을 추적하고 디버깅
  • 영속성(persistence) → 상태를 로컬 스토리지 등에 저장하여 유지
  • 개발 도구 지원(devtools) → 개발 환경에서 상태를 쉽게 관리

🛠 예제 코드 분석

const useServerStore = create<ServerState>()(
  devtools(
    (set) => ({
      servers: [],
      addServer: (server) => set((state) => ({
        servers: [...state.servers, server]
      })),
    }),
    { name: 'Server Store' }
  )
);

📌 주요 부분을 하나씩 살펴보면: 1️⃣ 스토어 생성 → create<ServerState>() 2️⃣ 미들웨어 적용 → devtools(...) 3️⃣ 상태 정의

  • servers: 서버 목록을 관리하는 상태
  • addServer: 서버 추가 액션 4️⃣ 개발 도구 설정 → { name: 'Server Store' }
  • 개발 도구에서 이 스토어를 'Server Store'라는 이름으로 관리

🔍 미들웨어 없이 상태 관리하면 어떻게 될까?

미들웨어를 사용하지 않으면 Zustand 스토어는 기본적인 상태 관리 기능만 제공합니다.

하지만 미들웨어를 적용하면 상태 변경을 기록하고, 유지하고, 디버깅이 가능해집니다.

정리

🔹 미들웨어 → 상태 관리 기능을 확장

🔹 devtools 미들웨어 → 개발 도구에서 상태를 추적 가능

🔹 스토어 확장 가능 → 로깅, 영속성 기능 추가 가능

5. combine 🔗

여러 상태 슬라이스를 하나의 스토어로 결합할 수 있습니다.

// combine 사용
import { create } from 'zustand';
import { combine } from 'zustand/middleware';

// 서버와 랙의 타입 정의
interface Server {
  id: string;
  name: string;
}

interface Rack {
  id: string;
  capacity: number;
}

// 상태 타입 정의
interface DcimState {
  servers: Server[];
  racks: Rack[];
  addServer: (server: Server) => void;
  addRack: (rack: Rack) => void;
}

// 타입스크립트를 적용한 Zustand 스토어
const useDcimStore = create<DcimState>()(
  combine(
    { servers: [] as Server[], racks: [] as Rack[] },
    (set) => ({
      addServer: (server: Server) => set((state) => ({
        servers: [...state.servers, server],
      })),
      addRack: (rack: Rack) => set((state) => ({
        racks: [...state.racks, rack],
      })),
    })
  )
);
// combine 미사용
import { create } from 'zustand';

interface ServerState {
  servers: string[]; // 서버 목록
  addServer: (server: string) => void;
}

interface RackState {
  racks: string[]; // 랙 목록
  addRack: (rack: string) => void;
}

// 서버 상태
const useServerStore = create<ServerState>((set) => ({
  servers: [],
  addServer: (server) => set((state) => ({
    servers: [...state.servers, server]
  })),
}));

// 랙 상태
const useRackStore = create<RackState>((set) => ({
  racks: [],
  addRack: (rack) => set((state) => ({
    racks: [...state.racks, rack]
  })),
}));

🚀 combine이란?

Zustand에서 combine을 사용하면 여러 개의 상태 슬라이스(state slice)를 하나의 스토어로 통합할 수 있습니다. 즉, 서로 다른 상태를 관리하는 여러 부분을 하나의 Zustand 스토어에 결합하는 기능을 제공합니다.

🛠️ 예제 코드 분석

1️⃣ 초기 상태 정의 → { servers: [], racks: [] }

  • servers: 서버 목록을 관리하는 배열
  • racks: 랙(rack) 목록을 관리하는 배열

2️⃣ 상태 업데이트 액션 정의

  • addServer: 새로운 서버를 추가하는 함수
  • addRack: 새로운 랙을 추가하는 함수

🔍 combine을 사용하는 이유

  • 여러 개의 상태를 분리하여 관리하면서도 하나의 스토어로 결합할 수 있음
  • 각각의 상태 조각(slices)이 독립적이면서도 하나의 Zustand 스토어에서 함께 사용 가능
  • 코드 가독성을 높이고, 상태 구조를 깔끔하게 유지

정리

✔️ combine()을 사용하면 여러 상태를 하나의 스토어로 결합 가능

✔️ 상태를 개별적으로 관리하면서도 하나의 스토어에서 접근할 수 있음

✔️ 코드를 모듈화하고 유지보수를 용이하게 만듦

6. 중첩된 객체 변경 (immer) 🌳

immer를 사용하면 중첩된 객체를 변경하기 쉬워집니다.

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface DataCenter {
  id: string;
  facilities: {
    [id: string]: {
      name: string;
      status: string;
    }
  }
}

interface DCState {
  datacenters: {
    [id: string]: DataCenter
  };
  updateFacilityStatus: (dcId: string, facilityId: string, status: string) => void;
}

const useDCStore = create<DCState>()(
  immer((set) => ({
    datacenters: {},
    updateFacilityStatus: (dcId, facilityId, status) => set((state) => {
      // immer를 사용하면 불변성 걱정 없이 직접 수정 가능
      state.datacenters[dcId].facilities[facilityId].status = status;
    }),
  }))
);

🚀 immer란?

immer는 JavaScript에서 불변성(immutability)을 유지하면서 객체를 쉽게 업데이트할 수 있도록 도와주는 라이브러리입니다. Zustand에서 immer 미들웨어를 사용하면 상태를 불변성을 유지한 채 직접 수정하는 방식으로 변경 가능합니다. 기존 객체를 직접 수정하는 것처럼 보이지만, 내부적으로 새로운 객체를 생성하여 불변성을 유지합니다.

immer 없이 중첩된 객체 업데이트하기

const useDCStore = create<DCState>((set) => ({
  datacenters: {},

  updateFacilityStatus: (dcId, facilityId, status) =>
    set((state) => ({
      datacenters: {
        ...state.datacenters, // 기존 데이터센터를 복사
        [dcId]: {
          ...state.datacenters[dcId], // 기존 데이터센터의 데이터 복사
          facilities: {
            ...state.datacenters[dcId].facilities, // 기존 시설 목록 복사
            [facilityId]: {
              ...state.datacenters[dcId].facilities[facilityId], // 기존 시설 데이터 복사
              status, // 새로운 상태 적용
            },
          },
        },
      },
    })),
}));

❌ 문제점

  • ...state.datacenters → 기존 데이터 복사
  • ...state.datacenters[dcId] → 데이터센터 복사
  • ...state.datacenters[dcId].facilities → 시설 목록 복사
  • ...state.datacenters[dcId].facilities[facilityId] → 개별 시설 복사

📌 중첩된 객체를 변경하기 위해 매번 새로운 객체를 생성해야 하므로 코드가 복잡해지고 실수할 가능성이 높아집니다.

🔍 immer를 활용한 상태 업데이트

const useDCStore = create<DCState>()(
  immer((set) => ({
    datacenters: {},
    updateFacilityStatus: (dcId, facilityId, status) => set((state) => {
      // immer를 사용하면 불변성 걱정 없이 직접 수정 가능
      state.datacenters[dcId].facilities[facilityId].status = status;
    }),
  }))
);

📌 여기서 immer를 적용했기 때문에 state.datacenters[dcId].facilities[facilityId].status = status;처럼 마치 객체를 직접 수정하는 것처럼 작성할 수 있지만, 내부적으로 불변성을 유지하면서 새로운 객체를 생성합니다.

💡 immer 없이 상태를 업데이트하려면 불변성을 유지하기 위해 spread 연산자(...) 또는 map() 같은 방법을 사용해야 합니다. 하지만 immer를 사용하면 더 직관적인 방식으로 상태를 변경할 수 있어 코드가 간결해집니다.

정리

✔️ immer 사용 시 상태를 직접 수정하는 것처럼 작성 가능

✔️ 내부적으로 불변성을 유지하며 새로운 객체를 생성하여 업데이트

✔️ 중첩된 객체를 수정할 때 매우 유용함

7. 상태 구독 👂

컴포넌트 외부에서도 상태 변화를 구독하고 반응할 수 있습니다.

import { create } from 'zustand';

interface AlertState {
  alerts: string[];
  addAlert: (msg: string) => void;
}

const useAlertStore = create<AlertState>((set) => ({
  alerts: [],
  addAlert: (msg) => set((state) => ({ alerts: [...state.alerts, msg] })),
}));

// 구독 설정
const unsubscribe = useAlertStore.subscribe(
  (state) => state.alerts,
  (alerts, prevAlerts) => {
    if (alerts.length > prevAlerts.length) {
      console.log('새 알림 발생:', alerts[alerts.length - 1]);
    }
  }
);

// 필요 시 구독 해제
// unsubscribe();

🚀 구독(subscription)이란?

구독 기능을 활용하면 컴포넌트 외부에서도 상태 변화를 감지하고 반응할 수 있습니다.

보통 React 컴포넌트 내에서 useStore를 호출하여 상태를 가져오지만,

구독 기능을 사용하면 컴포넌트 바깥에서도 상태 변경을 추적할 수 있습니다.

✔️ 특정 상태가 변경될 때 콜백 함수가 실행되므로, 외부 로직에서 이를 활용할 수 있어요!

🏗 코드 분석

const unsubscribe = useAlertStore.subscribe(
  (state) => state.alerts, // 구독할 상태 지정
  (alerts, prevAlerts) => { // 상태 변화 감지 후 실행할 함수
    if (alerts.length > prevAlerts.length) {
      console.log('새 알림 발생:', alerts[alerts.length - 1]);
    }
  }
);

1️⃣ 구독 설정 → useAlertStore.subscribe(...)를 사용하여 상태 변화를 감지

2️⃣ 첫 번째 인자 → state.alerts: alerts 상태를 구독

3️⃣ 두 번째 인자 → alerts와 prevAlerts를 비교해 상태 변경 감지

4️⃣ 새 알림이 추가되었는지 확인 → alerts.length > prevAlerts.length라면 새 알림이 발생

📌 즉, 이전 alerts 배열과 현재 alerts 배열을 비교해서 새로운 알림이 추가되었을 때만 실행됩니다!

🔄 구독 해제

// 필요 시 구독 해제
unsubscribe();

✅ unsubscribe()를 호출하면 구독을 중지할 수 있습니다.

✅ 컴포넌트가 언마운트될 때 메모리 누수를 방지하기 위해 구독을 해제하는 것이 좋습니다!

🎯 상태 구독이 필요한 경우

✔️ 외부 이벤트 리스너에서 상태 변화 감지 (예: 서버 응답, WebSocket 메시지)

✔️ 컴포넌트 바깥에서 상태 변경을 추적하여 특정 로직 실행

✔️ 데이터를 React 컴포넌트가 아닌 곳에서 사용해야 할 때

8. 스토리지 사용 (Persist) 💾

상태를 로컬 스토리지나 세션 스토리지에 저장하여 페이지 새로고침 후에도 유지할 수 있습니다.

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

interface UserState {
  user: User | null;
  setUser: (user: User | null) => void;
}

const useUserStore = create<UserState>()(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
    }),
    {
      name: 'user-storage', // 스토리지 키 이름
      storage: localStorage, // 사용할 스토리지
    }
  )
);

🚀 persist란?

Zustand의 persist 미들웨어를 사용하면 상태를 로컬 스토리지 또는 세션 스토리지에 저장할 수 있습니다. 즉, 페이지를 새로고침해도 상태가 유지되므로 로그인 정보 같은 중요한 데이터를 저장할 때 유용함.

✔️ persist를 사용하면 상태를 저장하고 복원하는 과정을 자동화할 수 있습니다!

🏗 코드 분석

import { persist } from 'zustand/middleware';

✅ persist 미들웨어를 가져와 상태 저장 기능을 추가합니다.

interface UserState {
  user: User | null;
  setUser: (user: User | null) => void;
}

✅ UserState 인터페이스를 정의하여 1️⃣ user: 현재 로그인한 사용자 상태 (null 또는 User 객체)

2️⃣ setUser: 사용자 정보를 설정하는 함수

🔄 persist 적용

const useUserStore = create<UserState>()(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
    }),
    {
      name: 'user-storage', // 로컬 스토리지에 저장될 이름
      storage: localStorage, // 사용할 스토리지(자동으로 JSON 변환됨)
      partialize: (state) => ({ user: state.user }), // 특정 상태만 저장

  )
);

📌 주요 부분 설명: ✔️ persist 내부에서 Zustand 스토어를 정의 (user 상태와 setUser 액션 포함)

✔️ name: 'user-storage' → 로컬 스토리지에서 저장될 키 이름

✔️ storage: localStorage → 데이터를 localStorage에 저장 (세션 유지하려면 sessionStorage 사용 가능)

✔️ setUser 호출 시 자동으로 상태가 스토리지에 저장되고 페이지 새로고침 후에도 복원

🔍 persist 사용 시 주의할 점

스토리지 데이터는 문자열로 저장되므로 JSON 변환이 자동으로 이루어짐

보안이 중요한 데이터는 로컬 스토리지에 저장하지 않는 것이 좋음 (예: 비밀번호, 민감한 사용자 정보)

페이지 로드 시 자동으로 저장된 데이터를 불러오기 때문에 초기 상태 설정 필요

🎯 정리

✔️ persist 사용 시 Zustand 상태를 자동으로 로컬 또는 세션 스토리지에 저장

✔️ 페이지 새로고침 후에도 유지되므로 로그인 정보 같은 데이터 저장에 유용

✔️ 스토리지 보안 및 데이터 형식에 주의해야 함

9. 개발자 도구 (Devtools) 🔍

개발 중 상태 변화를 추적하고 디버깅하기 위한 도구입니다.

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface MonitorState {
  metrics: Metric[];
  addMetric: (metric: Metric) => void;
}

const useMonitorStore = create<MonitorState>()(
  devtools(
    (set) => ({
      metrics: [],
      addMetric: (metric) => set(
        (state) => ({ metrics: [...state.metrics, metric] }),
        false,
        'addMetric' // 액션 이름 (개발자 도구에 표시됨)
      ),
    }),
    { name: 'Monitor Store' } // 스토어 이름
  )
);

🚀 devtools란?

devtools는 Zustand에서 제공하는 개발자 도구 미들웨어로, 상태 변화를 추적하고 디버깅할 수 있도록 도와줍니다. 이를 통해 개발 중에 스토어의 변경 사항을 확인하고 디버깅을 쉽게 할 수 있습니다.

🏗 코드 분석

import { devtools } from 'zustand/middleware';

✅ devtools 미들웨어를 가져와 개발자 도구 기능을 추가합니다.

interface MonitorState {
  metrics: Metric[];
  addMetric: (metric: Metric) => void;
}

✅ MonitorState 인터페이스를 정의하여 1️⃣ metrics: 측정 지표(metric) 목록을 관리

2️⃣ addMetric: 새로운 측정 지표를 추가하는 함수


🔍 개발자 도구 적용

const useMonitorStore = create<MonitorState>()(
  devtools(
    (set) => ({
      metrics: [],
      addMetric: (metric) => set(
        (state) => ({ metrics: [...state.metrics, metric] }),
        false,
        'addMetric' // 액션 이름 (개발자 도구에서 표시됨)
      ),
    }),
    { name: 'Monitor Store' } // 개발자 도구에서 보이는 스토어 이름 설정
  )
);

📌 주요 부분 설명: ✔️ devtools로 Zustand 스토어를 감싸서 Redux DevTools 확장과 연동

✔️ set() 함수의 세 번째 인자로 액션 이름을 설정 ('addMetric')

✔️ { name: 'Monitor Store' } → 개발자 도구에서 보이는 스토어 이름을 지정

🔄 개발자 도구에서 확인할 수 있는 정보

현재 상태 → 스토어에 저장된 값이 실시간으로 반영됨

액션 실행 내역 → 어떤 액션('addMetric' 등)이 실행되었는지 기록됨

시간 여행 디버깅 → 상태 변화를 순차적으로 확인 가능

초기 상태와 변경된 상태 비교

📌 Redux DevTools 확장 프로그램을 설치하면 브라우저에서 직접 상태를 추적할 수 있어요!

🎯 정리

✔️ Zustand의 devtools 미들웨어로 상태를 추적 및 디버깅 가능

✔️ Redux DevTools와 연동하여 액션 실행 내역을 확인 가능

✔️ 스토어 이름과 액션 이름을 설정하면 더 쉽게 관리 가능

 

참고 사이트

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

https://lambda-log.tistory.com/9

728x90