useReducer로 복잡한 상태 로직을 액션 기반으로 관리하는 방법, reducer 패턴의 장점, useState vs useReducer 선택 기준을 실전 예제로 배웁니다.
🤔 언제 useReducer가 필요한가
useState는 단순한 상태 관리에 완벽합니다. 그런데 상태가 복잡해지면 useState만으로 코드가 지저분해집니다. 특히 이런 경우에 useReducer를 고려합니다:
- 여러 state가 서로 연관되어 함께 업데이트됩니다
- 다음 상태가 현재 상태의 여러 필드에 의존합니다
- 상태 업데이트 로직이 여러 핸들러에 분산됩니다
- 가능한 상태 변경 경우의 수가 명확히 정의됩니다
⚙️ useReducer의 작동 원리
useReducer는 Redux의 아이디어를 리액트에 내장한 것입니다. 핵심 개념:
- State: 현재 상태 객체
- Action: 무슨 일이 일어났는지를 설명하는 객체 (type + 선택적 payload)
- Reducer: (state, action) => newState 형태의 순수 함수
- Dispatch: 액션을 발생시키는 함수
// 장바구니 상태 관리 예제
interface CartItem {
id: number
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
isOpen: boolean
}
// 액션 타입 정의 (TypeScript discriminated union)
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: { id: number } }
| { type: 'UPDATE_QUANTITY'; payload: { id: number; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'TOGGLE_CART' }
// 리듀서: 순수 함수 — 항상 새 객체 반환
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find((item) => item.id === action.payload.id)
if (existing) {
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
}
}
return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }] }
}
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter((item) => item.id !== action.payload.id) }
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: Math.max(0, action.payload.quantity) }
: item
).filter((item) => item.quantity > 0),
}
case 'CLEAR_CART':
return { ...state, items: [] }
case 'TOGGLE_CART':
return { ...state, isOpen: !state.isOpen }
default:
return state
}
}
🔌 컴포넌트에서 사용하기
const initialState: CartState = { items: [], isOpen: false }
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState)
const totalPrice = state.items.reduce(
(sum, item) => sum + item.price * item.quantity, 0
)
return (
<div>
<button onClick={() => dispatch({ type: 'TOGGLE_CART' })}>
장바구니 {state.items.length > 0 && `(${state.items.length})`}
</button>
{state.isOpen && (
<div className="border rounded p-4">
{state.items.length === 0 ? (
<p>장바구니가 비었습니다</p>
) : (
<>
{state.items.map((item) => (
<div key={item.id} className="flex items-center justify-between py-2">
<span>{item.name}</span>
<div className="flex items-center gap-2">
<button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity - 1 } })}>-</button>
<span>{item.quantity}</span>
<button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity + 1 } })}>+</button>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>삭제</button>
</div>
</div>
))}
<div className="border-t pt-2 font-bold">합계: {totalPrice.toLocaleString()}원</div>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>전체 비우기</button>
</>
)}
</div>
)}
</div>
)
}
📊 useState vs useReducer — 선택 기준
| 기준 | useState | useReducer |
|---|---|---|
| 상태 복잡도 | 단순한 값(숫자, 문자열, 불리언) | 연관된 여러 필드가 있는 객체 |
| 업데이트 패턴 | 독립적인 단일 값 변경 | 여러 필드가 함께 바뀌는 경우 |
| 가능한 액션 | 명확하지 않음 | 명확히 정의된 액션 목록이 있음 |
| 테스트 | 컴포넌트 단위 | 리듀서 함수 단독 테스트 가능 |
✅ 정리
useReducer: (state, action) => newState 패턴으로 복잡한 상태 관리.dispatch({ type: 'ACTION_TYPE', payload: ... })로 상태 변경을 요청합니다.- 리듀서는 순수 함수여야 합니다 — 항상 새 객체를 반환하세요.
- TypeScript discriminated union으로 액션 타입을 정의하면 타입 안전성이 높아집니다.
관련 주제
- Props Drilling 문제
- createContext
- Provider 패턴
- useContext
- 전역 테마 사용자값
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
