테마
Zustand 상태관리 컴포넌트 - ShoppingList 구현
Zustand로 간결한 상태관리 스토어를 구현하고 Redux와의 차이를 비교 분석한다
학습 목표
- Zustand의 핵심 API와 스토어 생성 패턴을 익힌다
- Zustand와 Redux Toolkit의 구조적 차이를 이해한다
- Tailwind CSS 환경에서 ShoppingList 컴포넌트를 구현할 수 있다
- 마이크로 프론트엔드에서 상태관리 라이브러리 선택 기준을 세울 수 있다
본문
1. Zustand와 Redux 비교 개요
web 프로젝트는 docs 프로젝트(Redux + ANTD)와 다른 기술 스택을 선택했다. 상태관리에 Zustand를, UI에 Tailwind CSS를 사용한다. 이를 통해 마이크로 프론트엔드에서 각 프로젝트가 독립적인 기술 선택을 할 수 있음을 보여준다.
2. Zustand 스토어 구현
데이터 모델
typescript
// store.ts
import { create } from "zustand";
interface Item {
id: number;
name: string;
price: number;
count: number;
}스토어 생성 - Redux와의 코드량 비교
typescript
// Zustand 스토어 - 단 몇 줄로 완성
interface ShoppingStore {
items: Item[];
deleteItem: (id: number) => void;
}
const useShoppingStore = create<ShoppingStore>((set) => ({
items: [
{ id: 1, name: "MacBook Pro", price: 2500000, count: 1 },
{ id: 2, name: "iPad Mini 6", price: 650000, count: 2 },
{ id: 3, name: "AirPods Pro", price: 350000, count: 3 },
{ id: 4, name: "USB-C Hub", price: 89000, count: 1 },
],
deleteItem: (id: number) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
}));
export default useShoppingStore;Redux Toolkit 동일 기능 대비
typescript
// Redux Toolkit - 상대적으로 긴 설정
const itemSlice = createSlice({
name: "items",
initialState: [...],
reducers: {
deleteItem: (state, action: PayloadAction<number>) => {
return state.filter(item => item.id !== action.payload);
}
}
});
export const { deleteItem } = itemSlice.actions;
export const selectItems = (state: RootState) => state.items;
const store = configureStore({ reducer: { items: itemSlice.reducer } });
// + Provider로 감싸야 함3. 상세 비교 분석
| 비교 항목 | Redux Toolkit | Zustand |
|---|---|---|
| 보일러플레이트 | 많음 (slice, store, provider) | 적음 (create 한 줄) |
| Provider | 필수 | 불필요 |
| 불변성 관리 | Immer 내장 | 수동 (또는 Immer 미들웨어) |
| DevTools | Redux DevTools 내장 | 미들웨어로 지원 |
| 미들웨어 | 풍부한 에코시스템 | persist, devtools 등 |
| TypeScript | 타입 설정 다소 복잡 | 간단한 제네릭 |
| 번들 크기 | ~12KB (gzipped) | ~1KB (gzipped) |
| 학습 곡선 | 높음 | 낮음 |
4. ShoppingList 컴포넌트 구현
tsx
import useShoppingStore from "./store";
function ShoppingList() {
const { items, deleteItem } = useShoppingStore();
const columns = [
{ dataIndex: "id", title: "ID" },
{ dataIndex: "name", title: "상품명" },
{
dataIndex: "price",
title: "가격",
render: (value: number) => `${value.toLocaleString()}원`,
},
{ dataIndex: "count", title: "수량" },
{
dataIndex: "id",
title: "처리",
render: (id: number) => (
<Button buttonType="cancel" onClick={() => deleteItem(id)}>
삭제
</Button>
),
},
];
return (
<div>
<Table columns={columns} datas={items} rowKey="id" />
</div>
);
}5. Zustand의 고급 패턴
셀렉터로 리렌더링 최적화
typescript
// 전체 스토어 구독 - items 외의 변경에도 리렌더링
const store = useShoppingStore();
// 셀렉터 사용 - items 변경 시에만 리렌더링
const items = useShoppingStore((state) => state.items);
// 다중 값 셀렉터
const { items, deleteItem } = useShoppingStore(
(state) => ({ items: state.items, deleteItem: state.deleteItem }),
shallow // 얕은 비교로 불필요한 리렌더링 방지
);미들웨어 적용
typescript
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
const useShoppingStore = create<ShoppingStore>()(
devtools(
persist(
(set) => ({
items: [...],
deleteItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
}),
{ name: "shopping-store" } // localStorage 키
)
)
);6. 마이크로 프론트엔드에서의 상태관리 선택 기준
7. 핵심 포인트: 기술 선택의 독립성
마이크로 프론트엔드의 가장 큰 장점 중 하나는 각 프로젝트가 독립적으로 기술을 선택할 수 있다는 점이다.
- docs 프로젝트: Redux Toolkit + ANTD
- web 프로젝트: Zustand + Tailwind CSS
- legacy 프로젝트: 클래스 컴포넌트 + 순수 CSS
이 세 프로젝트의 컴포넌트가 하나의 페이지에서 동시에 렌더링되더라도, Shadow DOM 격리 덕분에 기술 스택의 차이가 문제가 되지 않는다.
핵심 정리
- Zustand의 간결함:
create()한 번의 호출로 상태와 액션을 모두 정의하고, Provider 없이 훅으로 바로 사용할 수 있다 - Redux 대비 장점: 보일러플레이트가 적고, 번들 크기가 작으며, Provider 없이 동작하므로 Shadow DOM 환경에 더 적합하다
- 선택 기준: 프로젝트 규모, Shadow DOM 사용 여부, 팀 경험에 따라 적절한 라이브러리를 선택한다
- 기술 독립성: 마이크로 프론트엔드에서는 각 프로젝트가 서로 다른 상태관리 라이브러리를 사용해도 문제가 없다