[이제와서 시작하는 React 마스터하기 #13] 상태 관리 라이브러리 - 전역 상태 관리하기
[이제와서 시작하는 React 마스터하기 #13] 상태 관리 라이브러리 - 전역 상태 관리하기
100일 챌린지 Day 13 - 앱이 커지면 상태 관리가 복잡해져요! 전역 상태 관리 라이브러리로 깔끔하게 정리해봐요. 📦
배울 내용
이번 시간에는 전역 상태를 관리하는 라이브러리들을 배워봐요.
- Props Drilling 문제 이해하기
- Zustand로 간단하게 상태 관리하기
- Redux 기본 개념 이해하기
- 언제 어떤 도구를 사용할지 결정하기
시작하기 전에
이번 내용을 더 잘 이해하려면 아래 내용을 먼저 보고 오세요:
일상 비유로 이해하기
상태 관리를 일상으로 비유하면 이렇습니다:
🏢 사무실 메모 공유하기
Props로 전달하기 = 메모를 손으로 전달
- 사원 → 대리 → 과장 → 차장 → 부장 (5단계!)
- 중간 사람들은 메모 내용에 관심 없는데 계속 전달해야 해요
- 비효율적이에요
Context API = 사내 게시판
- 누구나 게시판에 메모를 붙일 수 있어요
- 필요한 사람만 확인해요
- 간단하지만, 메모가 많아지면 복잡해져요
Zustand/Redux = 중앙 문서 관리 시스템
- 모든 문서를 한 곳에 체계적으로 보관해요
- 누구든 필요할 때 가져다 쓸 수 있어요
- 규칙이 있어서 정리되어 있어요
1️⃣ 문제 상황: Props Drilling
Props Drilling이 뭔가요?
Props를 여러 단계에 걸쳐 전달하는 것을 말해요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 문제 상황
function App() {
const [user, setUser] = useState({ name: '김철수', age: 25 });
return (
<div>
<Header user={user} />
</div>
);
}
function Header({ user }) {
// Header는 user를 사용하지 않지만 전달만 해요
return (
<header>
<Navigation user={user} />
</header>
);
}
function Navigation({ user }) {
// Navigation도 user를 사용하지 않지만 전달만 해요
return (
<nav>
<UserMenu user={user} />
</nav>
);
}
function UserMenu({ user }) {
// 여기서야 실제로 user를 사용해요!
return <div>안녕하세요, {user.name}님!</div>;
}
문제점:
- App → Header → Navigation → UserMenu (3단계 전달!)
- Header와 Navigation은 user를 사용하지 않아요
- user가 바뀌면 중간 컴포넌트들도 다시 렌더링돼요
- 코드 수정이 어려워요
Context API로 해결하기
Day 7에서 배운 Context API를 사용할 수 있어요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { createContext, useContext, useState } from 'react';
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: '김철수', age: 25 });
return (
<UserContext.Provider value={user}>
<Header />
</UserContext.Provider>
);
}
function Header() {
// Props를 받지 않아도 돼요!
return (
<header>
<Navigation />
</header>
);
}
function Navigation() {
return (
<nav>
<UserMenu />
</nav>
);
}
function UserMenu() {
const user = useContext(UserContext); // 직접 가져와요!
return <div>안녕하세요, {user.name}님!</div>;
}
장점: Props를 여러 단계 전달하지 않아도 돼요!
단점:
- Context가 많아지면 Provider가 너무 많아져요
- Context 값이 바뀌면 모든 구독자가 리렌더링돼요
- 디버깅이 어려워요
2️⃣ Zustand - 가장 간단한 상태 관리
Zustand란?
가장 간단하고 사용하기 쉬운 상태 관리 라이브러리예요!
설치하기
1
npm install zustand
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// stores/useUserStore.js
import { create } from 'zustand';
const useUserStore = create((set) => ({
// 상태
user: null,
// 액션 (상태를 변경하는 함수)
setUser: (user) => set({ user }),
clearUser: () => set({ user: null })
}));
export default useUserStore;
컴포넌트에서 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// App.js
import useUserStore from './stores/useUserStore';
function App() {
const setUser = useUserStore((state) => state.setUser);
const handleLogin = () => {
setUser({ name: '김철수', age: 25 });
};
return (
<div>
<button onClick={handleLogin}>로그인</button>
<Header />
</div>
);
}
function Header() {
return (
<header>
<UserMenu />
</header>
);
}
function UserMenu() {
const user = useUserStore((state) => state.user);
const clearUser = useUserStore((state) => state.clearUser);
if (!user) {
return <div>로그인해주세요</div>;
}
return (
<div>
<p>안녕하세요, {user.name}님!</p>
<button onClick={clearUser}>로그아웃</button>
</div>
);
}
완전 간단하죠? Props를 전혀 전달하지 않았어요!
실습: 쇼핑 카트 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// stores/useCartStore.js
import { create } from 'zustand';
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
increaseQuantity: (productId) => set((state) => ({
items: state.items.map(item =>
item.id === productId
? { ...item, quantity: item.quantity + 1 }
: item
)
})),
decreaseQuantity: (productId) => set((state) => ({
items: state.items.map(item =>
item.id === productId && item.quantity > 1
? { ...item, quantity: item.quantity - 1 }
: item
)
})),
// 계산된 값
getTotalPrice: () => {
const items = get().items;
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
},
getTotalItems: () => {
const items = get().items;
return items.reduce((total, item) => total + item.quantity, 0);
},
clearCart: () => set({ items: [] })
}));
export default useCartStore;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// components/ProductList.js
import useCartStore from '../stores/useCartStore';
function ProductList() {
const addItem = useCartStore((state) => state.addItem);
const products = [
{ id: 1, name: '노트북', price: 1000000 },
{ id: 2, name: '마우스', price: 50000 },
{ id: 3, name: '키보드', price: 100000 }
];
return (
<div>
<h2>상품 목록</h2>
{products.map(product => (
<div key={product.id} style=>
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}원</p>
<button onClick={() => addItem(product)}>
장바구니 추가
</button>
</div>
))}
</div>
);
}
export default ProductList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// components/Cart.js
import useCartStore from '../stores/useCartStore';
function Cart() {
const items = useCartStore((state) => state.items);
const removeItem = useCartStore((state) => state.removeItem);
const increaseQuantity = useCartStore((state) => state.increaseQuantity);
const decreaseQuantity = useCartStore((state) => state.decreaseQuantity);
const getTotalPrice = useCartStore((state) => state.getTotalPrice);
const clearCart = useCartStore((state) => state.clearCart);
const totalPrice = getTotalPrice();
return (
<div style=>
<h2>장바구니</h2>
{items.length === 0 ? (
<p>장바구니가 비어있습니다</p>
) : (
<>
{items.map(item => (
<div key={item.id} style=>
<div>
<h4>{item.name}</h4>
<p>{item.price.toLocaleString()}원</p>
</div>
<div style=>
<button onClick={() => decreaseQuantity(item.id)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => increaseQuantity(item.id)}>+</button>
<button onClick={() => removeItem(item.id)}>삭제</button>
</div>
</div>
))}
<div style=>
<h3>총 금액: {totalPrice.toLocaleString()}원</h3>
<button
onClick={clearCart}
style=
>
장바구니 비우기
</button>
</div>
</>
)}
</div>
);
}
export default Cart;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// components/CartBadge.js
import useCartStore from '../stores/useCartStore';
function CartBadge() {
const getTotalItems = useCartStore((state) => state.getTotalItems);
const totalItems = getTotalItems();
return (
<div style=>
🛒 장바구니: {totalItems}개
</div>
);
}
export default CartBadge;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// App.js
import ProductList from './components/ProductList';
import Cart from './components/Cart';
import CartBadge from './components/CartBadge';
function App() {
return (
<div style=>
<h1>쇼핑몰</h1>
<CartBadge />
<div style=>
<ProductList />
<Cart />
</div>
</div>
);
}
export default App;
포인트:
- ProductList, Cart, CartBadge 모두 같은 store를 사용해요
- Props를 전혀 전달하지 않았어요
- 한 곳에서 상품을 추가하면 모든 곳에 반영돼요!
3️⃣ Redux - 예측 가능한 상태 관리
Redux란?
가장 유명한 상태 관리 라이브러리예요. 규칙이 엄격해서 코드가 예측 가능해요.
특징:
- 하나의 큰 Store (중앙 집중식)
- Action으로 상태 변경 요청
- Reducer가 실제로 상태를 변경
- 시간 여행 디버깅 가능 (상태 변화 추적)
설치하기
1
npm install @reduxjs/toolkit react-redux
기본 사용법 (Redux Toolkit 사용)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
1
2
3
4
5
6
7
8
9
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
1
2
3
4
5
6
7
8
9
// main.jsx or index.js
import { Provider } from 'react-redux';
import { store } from './store';
root.render(
<Provider store={store}>
<App />
</Provider>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// components/Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from '../store/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div style=>
<h1>카운터: {count}</h1>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(10))}>+10</button>
</div>
);
}
export default Counter;
Zustand와 비교
같은 기능을 Zustand로 구현하면:
1
2
3
4
5
6
7
8
9
10
11
// stores/useCounterStore.js
import { create } from 'zustand';
const useCounterStore = create((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
incrementByAmount: (amount) => set((state) => ({ value: state.value + amount }))
}));
export default useCounterStore;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// components/Counter.js
import useCounterStore from '../stores/useCounterStore';
function Counter() {
const { value, increment, decrement, incrementByAmount } = useCounterStore();
return (
<div style=>
<h1>카운터: {value}</h1>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<button onClick={() => incrementByAmount(10)}>+10</button>
</div>
);
}
차이점:
- Zustand가 훨씬 간단해요!
- Redux는 Provider, dispatch, action 필요
- Zustand는 그냥 hook처럼 사용
4️⃣ 비교: useState vs Context vs Zustand vs Redux
언제 무엇을 사용할까요?
| 도구 | 사용 시기 | 장점 | 단점 |
|---|---|---|---|
| useState | 한 컴포넌트 안에서만 사용 | 간단함 | 공유 불가 |
| Props | 부모-자식 1단계 | 명확함 | 깊은 전달 어려움 |
| Context API | 2-3개 전역 상태 | 내장 기능, 간단 | 많아지면 복잡 |
| Zustand | 여러 전역 상태 관리 | 매우 간단, 성능 좋음 | 큰 팀에선 규칙 부족 |
| Redux | 큰 프로젝트, 팀 협업 | 예측 가능, 디버깅 좋음 | 보일러플레이트 많음 |
실제 사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ✅ useState: 한 컴포넌트 안에서만
function SearchBox() {
const [query, setQuery] = useState('');
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
// ✅ Props: 부모-자식 간단히 전달
function Parent() {
const [name, setName] = useState('김철수');
return <Child name={name} />;
}
// ✅ Context API: 테마, 언어 설정 등 2-3개
const ThemeContext = createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
// ✅ Zustand: 사용자, 장바구니, 알림 등 여러 상태
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}));
// ✅ Redux: 대규모 프로젝트, 복잡한 상태 로직
// 은행 앱, 관리자 대시보드, 복잡한 SaaS 등
5️⃣ 추천: 어떤 걸 선택할까요?
초보자라면
Zustand를 추천해요!
- 배우기 쉬워요
- 코드가 간단해요
- 성능이 좋아요
- 대부분의 프로젝트에 충분해요
이미 Redux를 쓰는 프로젝트라면
Redux를 계속 사용하세요
- 팀이 익숙한 도구를 쓰는 게 좋아요
- Redux도 충분히 좋은 도구예요
간단한 프로젝트라면
Context API만으로도 충분해요
- 추가 라이브러리 필요 없어요
- 상태가 2-3개면 Context로도 OK
흔한 실수와 해결 방법
실수 1: 모든 상태를 전역으로 만들기
1
2
3
4
5
6
7
// ❌ 이건 전역일 필요 없어요
const useFormStore = create((set) => ({
name: '',
email: '',
setName: (name) => set({ name }),
setEmail: (email) => set({ email })
}));
해결:
1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 폼 상태는 컴포넌트 안에서 useState 사용
function SignupForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</form>
);
}
전역 상태로 만들어야 하는 것:
- 여러 페이지에서 사용하는 사용자 정보
- 장바구니
- 알림
- 테마 설정
로컬 상태로 충분한 것:
- 폼 입력 값
- 모달 열림/닫힘
- 탭 선택 상태
실수 2: Store를 너무 많이 만들기
1
2
3
4
5
6
7
// ❌ Store가 너무 많아요
const useUserStore = create(...);
const useThemeStore = create(...);
const useLanguageStore = create(...);
const useNotificationStore = create(...);
const useSettingsStore = create(...);
// ... 20개의 store
해결:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 관련된 것끼리 묶기
const useAppStore = create((set) => ({
user: null,
theme: 'light',
language: 'ko',
notifications: [],
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
addNotification: (notification) => set((state) => ({
notifications: [...state.notifications, notification]
}))
}));
실수 3: 상태를 직접 수정하기
1
2
3
// ❌ Zustand에서 상태를 직접 수정
const items = useCartStore((state) => state.items);
items.push(newItem); // 안 돼요!
해결:
1
2
3
// ✅ 액션 함수 사용
const addItem = useCartStore((state) => state.addItem);
addItem(newItem);
정리
오늘 배운 내용
- Props Drilling 문제를 이해했어요
- Zustand로 간단하게 상태 관리를 할 수 있어요
- Redux의 기본 개념을 이해했어요
- 언제 어떤 도구를 사용할지 알아요
핵심 정리
상태 관리 선택 기준:
- 작은 프로젝트: Context API
- 중간 프로젝트: Zustand (추천!)
- 큰 프로젝트: Redux
Zustand 패턴:
1
2
3
4
5
6
7
8
9
10
const useStore = create((set, get) => ({
// 상태
value: 0,
// 액션
setValue: (value) => set({ value }),
// 계산된 값
getDoubleValue: () => get().value * 2
}));
다음 단계
- Zustand middleware 사용하기 (persist, devtools)
- Redux DevTools로 디버깅하기
- 비동기 작업 처리하기
숙제
필수 과제
- 카운터 앱 (Zustand 사용)
- 증가/감소 버튼
- 리셋 버튼
- 여러 컴포넌트에서 count 표시
- Todo 앱 (Zustand 사용)
- 할 일 추가/삭제
- 완료 토글
- 전체/완료/미완료 필터
선택 과제
- 테마 전환 앱
- Light/Dark 모드
- 여러 페이지에서 테마 적용
- localStorage에 저장
- 간단한 쇼핑몰
- 상품 목록
- 장바구니 추가/삭제
- 총 금액 계산
“상태 관리는 도구가 아니라 전략이에요! 🎯”
프로젝트 크기에 맞는 도구를 선택하세요. 작은 프로젝트에 Redux는 과해요!
React 완벽 가이드 시리즈
- React란 무엇인가? 시작하기 전 알아야 할 모든 것
- useState와 useEffect - 상태와 생명주기 기초
- 컴포넌트 이해하기 - 레고 블록으로 만드는 웹
- Props - 컴포넌트끼리 대화하기
- useEffect - 생명주기 이해하기
- 조건부/리스트 렌더링 - 똑똑하게 보여주기
- Context API - Props 지옥 탈출하기
- Custom Hooks - 나만의 Hook 만들기
- React Router - 페이지 이동하기
- 폼 처리 - 사용자 입력 받기
- 성능 최적화 - 똑똑하게 렌더링하기
- 에러 처리와 로딩 화면 - 사용자 경험 챙기기
- 상태 관리 라이브러리 - 전역 상태 관리하기 ← 현재 글
- 서버 상태 관리 - React Query/TanStack Query 활용
- React와 TypeScript - 타입 안전한 React 개발
- Next.js 입문 - SSR/SSG와 풀스택 React
- React 테스팅 전략 - Jest, React Testing Library, E2E
- 고급 패턴과 베스트 프랙티스 - HOC, Render Props, Compound Components
- 실전 프로젝트 - Todo 앱에서 쇼핑몰까지
- React 배포와 DevOps - 빌드 최적화와 CI/CD
관련 자료
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
