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와 연동하여 액션 실행 내역을 확인 가능
✔️ 스토어 이름과 액션 이름을 설정하면 더 쉽게 관리 가능
참고 사이트
'코딩공부 > 상태관리 Zustand' 카테고리의 다른 글
상태관리 라이브러리 Zustand의 기본 개념과 기능 (0) | 2025.05.12 |
---|