useReducer는 "이전 상태와 액션을 받아 새 상태를 돌려주는" 리듀서(Reducer) 함수로 상태를 관리하는 훅이다. 이름처럼 Array.prototype.reduce의 아이디어를 시간축으로 늘린 것이다. 배열 요소 대신 시간순으로 들어오는 액션을 누적한다.
reduce: (acc, current) => newAcc
useReducer: (state, action ) => newState → 새 state면 리렌더type Action = { type: 'increment' } | { type: 'set'; payload: number }
function reducer(state: number, action: Action) {
switch (action.type) {
case 'increment': return state + 1
case 'set': return action.payload
}
}
const [count, dispatch] = useReducer(reducer, 0)
dispatch({ type: 'set', payload: 10 })useState와 고르는 기준
- 단순한 값(토글, 카운터), 서로 독립된 값 →
useState(React 상태 갱신) - 함께 바뀌는 상태 묶음(
isLoading·data·error) → reducer에서 한 번에 바꾼다. 불가능한 조합을 막으려면 상태 자체를 판별 유니언으로 모델링한다 - 변경 로직이 복잡할 때 → 로직이 컴포넌트에 흩어지지 않고 reducer 하나에 모인다
- "무슨 일이 일어났는지"가 중요할 때 →
setStatus('approved')대신dispatch({ type: 'APPROVE', approvedBy })처럼 의도를 남긴다. 명령을 객체로 만드는 커맨드 패턴과 닮았다 - reducer는 순수 함수(Pure Function)라서 컴포넌트 없이 단위 테스트하기 쉽다(자가 테스트 코드)
실행 취소·다시 실행(undo/redo)
상태를 past(지난 상태 목록)·present(지금)·future(되돌린 상태 목록) 셋으로 나누면 reducer 하나로 undo/redo가 된다.
type History<T> = { past: T[]; present: T; future: T[] }
function undoable<T>(state: History<T>, action: { type: 'UNDO' | 'REDO' } | { type: 'SET'; value: T }): History<T> {
const { past, present, future } = state
switch (action.type) {
case 'UNDO':
if (past.length === 0) return state
return { past: past.slice(0, -1), present: past[past.length - 1], future: [present, ...future] }
case 'REDO':
if (future.length === 0) return state
return { past: [...past, present], present: future[0], future: future.slice(1) }
case 'SET': // 새 행동
return { past: [...past, present], present: action.value, future: [] }
}
}- 새 행동을 하면
future를 비운다. 되돌린 뒤 다른 길로 갔으니 예전 redo 경로는 더 이상 유효하지 않다 - 이 방식은 상태 스냅샷을 쌓는다. 커맨드 패턴은 명령 객체마다
undo()를 두고 명령을 쌓는다. 스냅샷은 구현이 단순하지만 상태가 크고 기록이 길면 메모리를 더 쓸 수 있다
전역으로 키운 형태가 Redux다(Redux 이후의 상태 관리 선택).
출처: Redux — Implementing Undo History · Redux — Handling Other Actions