useReducer 훅 소개
useReducer의 state·action·dispatch 흐름으로 여러 상태 전이를 한곳에 모으고 Context와 결합할 때의 책임 경계를 익힙니다.
리액트에서 여러 상태 업데이트가 여러 이벤트 핸들러에 흩어져 읽기 어려울 때, useReducer를 사용하면 상태 전이 규칙을 reducer 함수 한곳으로 모을 수 있습니다.
useReducer가 언제나 useState보다 나은 것은 아닙니다. 단순한 토글이나 입력값은 useState가 더 짧고 직접적이며, reducer는 “무슨 일이 있었는가”와 “그 사건 뒤 상태가 어떻게 바뀌는가”를 분리할 가치가 있을 때 유용합니다.
Context와 결합할 수도 있지만 Context 자체가 전역 상태 저장소가 되는 것은 아닙니다. state는 useReducer를 호출한 컴포넌트가 소유하고, Context는 선택한 Provider 하위 트리에 state와 dispatch를 전달합니다.
useReducer 상태 전이 계약 — 상태 추적
count 0 핸들러의 dispatch 뒤 지역 snapshot 0과 새 reducer state 1/다음 UI 1이 다르다. 기존 객체 불변과 updater 타이밍을 같은 역할로 보여 줄 가치가 있다.
REACT STATE · OWNERSHIP & TRANSITION
dispatch는 다음 렌더를 요청하고 현재 snapshot은 유지합니다
핸들러는 사건을 action으로 보냅니다. 순수 reducer가 현재 state와 action으로 다음 state를 계산하며, 컴포넌트가 그 state를 소유합니다.
실제 카운터 · action으로 설명하는 전이
count: 0
incremented → 다음 렌더 1
amountAdded, amount: 5 → 6
reset → 0dispatch 직후 실행 중인 핸들러에서 읽는 state는 이전 snapshot입니다. 알 수 없는 action은 본문의 reducer가 오류로 드러냅니다.
Action 계약
action은 보통 type과 계산에 필요한 최소 정보만 담아, 값 설정 명령보다 발생한 사건을 설명합니다.
한 상호작용이 여러 필드를 바꾸더라도 하나의 의미 있는 action으로 표현할 수 있습니다.
Snapshot과 bailout
dispatch 직후 같은 핸들러에서 읽는 state는 이전 snapshot입니다.
반환값이 현재 state와 Object.is로 같으면 React는 해당 컴포넌트와 자식의 렌더를 건너뜁니다.
-
UI 사건
클릭·입력·응답 도착처럼 상태가 바뀌어야 하는 사건이 발생합니다. 핸들러는 다음 state를 직접 저장하는 대신 action을 만듭니다.
-
dispatch(action)안정적인 identity를 가진
dispatch가 action을 React에 전달하고 다음 렌더의 상태 갱신을 요청합니다. 반환값은 없습니다. -
순수 reducer
reducer(state, action)가 같은 입력에 같은 다음 state를 반환합니다. 객체와 배열을 직접 변형하지 않고, 실제 변경이 있을 때 새 값을 만듭니다. -
다음 렌더
React는 reducer 반환값을 다음 state로 저장하고 그 snapshot으로 UI를 렌더합니다. 현재 핸들러의 지역 변수는 중간에 바뀌지 않습니다.
불변 갱신
객체는 spread, 배열은 map·filter 같은 방식으로 새 구조를 반환합니다. 의미 있는 변화가 없을 때는 기존 state를 그대로 반환할 수 있습니다.
부수효과 경계
API 요청·타이머·저장소 접근은 reducer 밖에 둡니다. 상호작용 때문에 생긴 작업은 이벤트 핸들러에서, 렌더 결과와 외부 시스템의 동기화는 Effect에서 처리합니다.
구현 전 체크리스트
- Owner와 scope: 어느 컴포넌트가 state를 소유하고 누가 읽어야 하는가?
- State shape와 init: 함께 변하는 필드와 불변 조건은 무엇이며 초기화 비용은 큰가?
- Action vocabulary: 각 상호작용과 최소 payload가 상태 변화의 이유를 드러내는가?
- Reducer 계약: 모든 분기가 순수하고 불변 갱신을 하며 알 수 없는 action을 분명히 처리하는가?
- 공급 경계: props로 충분한가, 아니면 제한된 Provider 하위 트리에 state와 dispatch를 공급해야 하는가?
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | action은 보통 type과 계산에 필요한 최소 정보만 담아, 값 설정 명령보다 발생한 사건을 설명합니다. |
| 원도표 설명 | 반환값이 현재 state와 Object.is로 같으면 React는 해당 컴포넌트와 자식의 렌더를 건너뜁니다. |
| 원도표 설명 | 안정적인 identity를 가진 dispatch가 action을 React에 전달하고 다음 렌더의 상태 갱신을 요청합니다. 반환값은 없습니다. |
| 원도표 조건·주석 | dispatch(action) 안정적인 identity를 가진 dispatch가 action을 React에 전달하고 다음 렌더의 상태 갱신을 요청합니다. 반환값은 없습니다. |
| 원도표 설명 | reducer(state, action)가 같은 입력에 같은 다음 state를 반환합니다. 객체와 배열을 직접 변형하지 않고, 실제 변경이 있을 때 새 값을 만듭니다. |
| 원도표 조건·주석 | 순수 reducer reducer(state, action)가 같은 입력에 같은 다음 state를 반환합니다. 객체와 배열을 직접 변형하지 않고, 실제 변경이 있을 때 새 값을 만듭니다. |
| 원도표 설명 | 객체는 spread, 배열은 map·filter 같은 방식으로 새 구조를 반환합니다. 의미 있는 변화가 없을 때는 기존 state를 그대로 반환할 수 있습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 현재 handler | handler.count: 0 / action: incremented | count 0 snapshot에서 incremented를 dispatch한다 | dispatch는 현재 지역 변수를 즉시 바꾸지 않는다 |
| reducer 결과 | old: count0 / next: count1 | 기존 객체를 유지하며 count 1의 새 state를 만든다 | 순수 계산이 전이 결과를 정한다 |
| 다음 렌더 | UI: 1 / 기존 snapshot: 0 | UI는 count 1을 읽고 기존 handler의 관찰은 0이다 | 현재 실행의 snapshot과 다음 렌더의 입력을 분리한다 |
useReducer 상태 전이 계약 — 상태 추적
count 0 핸들러의 dispatch 뒤 지역 snapshot 0과 새 reducer state 1/다음 UI 1이 다르다. 기존 객체 불변과 updater 타이밍을 같은 역할로 보여 줄 가치가 있다.
REACT STATE · OWNERSHIP & TRANSITION
dispatch는 다음 렌더를 요청하고 현재 snapshot은 유지합니다
핸들러는 사건을 action으로 보냅니다. 순수 reducer가 현재 state와 action으로 다음 state를 계산하며, 컴포넌트가 그 state를 소유합니다.
실제 카운터 · action으로 설명하는 전이
count: 0
incremented → 다음 렌더 1
amountAdded, amount: 5 → 6
reset → 0dispatch 직후 실행 중인 핸들러에서 읽는 state는 이전 snapshot입니다. 알 수 없는 action은 본문의 reducer가 오류로 드러냅니다.
Action 계약
action은 보통 type과 계산에 필요한 최소 정보만 담아, 값 설정 명령보다 발생한 사건을 설명합니다.
한 상호작용이 여러 필드를 바꾸더라도 하나의 의미 있는 action으로 표현할 수 있습니다.
Snapshot과 bailout
dispatch 직후 같은 핸들러에서 읽는 state는 이전 snapshot입니다.
반환값이 현재 state와 Object.is로 같으면 React는 해당 컴포넌트와 자식의 렌더를 건너뜁니다.
-
UI 사건
클릭·입력·응답 도착처럼 상태가 바뀌어야 하는 사건이 발생합니다. 핸들러는 다음 state를 직접 저장하는 대신 action을 만듭니다.
-
dispatch(action)안정적인 identity를 가진
dispatch가 action을 React에 전달하고 다음 렌더의 상태 갱신을 요청합니다. 반환값은 없습니다. -
순수 reducer
reducer(state, action)가 같은 입력에 같은 다음 state를 반환합니다. 객체와 배열을 직접 변형하지 않고, 실제 변경이 있을 때 새 값을 만듭니다. -
다음 렌더
React는 reducer 반환값을 다음 state로 저장하고 그 snapshot으로 UI를 렌더합니다. 현재 핸들러의 지역 변수는 중간에 바뀌지 않습니다.
불변 갱신
객체는 spread, 배열은 map·filter 같은 방식으로 새 구조를 반환합니다. 의미 있는 변화가 없을 때는 기존 state를 그대로 반환할 수 있습니다.
부수효과 경계
API 요청·타이머·저장소 접근은 reducer 밖에 둡니다. 상호작용 때문에 생긴 작업은 이벤트 핸들러에서, 렌더 결과와 외부 시스템의 동기화는 Effect에서 처리합니다.
구현 전 체크리스트
- Owner와 scope: 어느 컴포넌트가 state를 소유하고 누가 읽어야 하는가?
- State shape와 init: 함께 변하는 필드와 불변 조건은 무엇이며 초기화 비용은 큰가?
- Action vocabulary: 각 상호작용과 최소 payload가 상태 변화의 이유를 드러내는가?
- Reducer 계약: 모든 분기가 순수하고 불변 갱신을 하며 알 수 없는 action을 분명히 처리하는가?
- 공급 경계: props로 충분한가, 아니면 제한된 Provider 하위 트리에 state와 dispatch를 공급해야 하는가?
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | action은 보통 type과 계산에 필요한 최소 정보만 담아, 값 설정 명령보다 발생한 사건을 설명합니다. |
| 원도표 설명 | 반환값이 현재 state와 Object.is로 같으면 React는 해당 컴포넌트와 자식의 렌더를 건너뜁니다. |
| 원도표 설명 | 안정적인 identity를 가진 dispatch가 action을 React에 전달하고 다음 렌더의 상태 갱신을 요청합니다. 반환값은 없습니다. |
| 원도표 조건·주석 | dispatch(action) 안정적인 identity를 가진 dispatch가 action을 React에 전달하고 다음 렌더의 상태 갱신을 요청합니다. 반환값은 없습니다. |
| 원도표 설명 | reducer(state, action)가 같은 입력에 같은 다음 state를 반환합니다. 객체와 배열을 직접 변형하지 않고, 실제 변경이 있을 때 새 값을 만듭니다. |
| 원도표 조건·주석 | 순수 reducer reducer(state, action)가 같은 입력에 같은 다음 state를 반환합니다. 객체와 배열을 직접 변형하지 않고, 실제 변경이 있을 때 새 값을 만듭니다. |
| 원도표 설명 | 객체는 spread, 배열은 map·filter 같은 방식으로 새 구조를 반환합니다. 의미 있는 변화가 없을 때는 기존 state를 그대로 반환할 수 있습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 현재 handler | handler.count: 0 / action: incremented | count 0 snapshot에서 incremented를 dispatch한다 | dispatch는 현재 지역 변수를 즉시 바꾸지 않는다 |
| reducer 결과 | old: count0 / next: count1 | 기존 객체를 유지하며 count 1의 새 state를 만든다 | 순수 계산이 전이 결과를 정한다 |
| 다음 렌더 | UI: 1 / 기존 snapshot: 0 | UI는 count 1을 읽고 기존 handler의 관찰은 0이다 | 현재 실행의 snapshot과 다음 렌더의 입력을 분리한다 |
useReducer의 기본 계약
useReducer는 reducer와 초기 인자를 받고, 필요할 때 세 번째 인자로 초기화 함수를 받습니다.
const [state, dispatch] = useReducer(reducer, initialArg, init);reducer(state, action)은 현재 state와 action을 받아 다음 state를 반환하는 순수 함수입니다.initialArg는 초기 state를 계산할 때 쓰는 값입니다.init이 없으면 이 값 자체가 초기 state가 됩니다.init(initialArg)는 선택 사항입니다. 초기 계산 비용이 클 때 초기 state를 지연 생성합니다.state는 현재 렌더의 snapshot입니다.dispatch(action)은 다음 렌더에 반영할 상태 갱신을 요청하며 반환값은 없습니다.
action은 어떤 타입이어도 되지만, 보통 사건을 식별하는 type과 reducer 계산에 필요한 최소 정보를 가진 객체를 사용합니다. 한 상호작용이 여러 필드를 함께 바꾼다면 여러 “값 설정” action보다 하나의 의미 있는 사건으로 표현하는 편이 전이 이유를 잘 드러냅니다.
reducer가 지켜야 할 규칙
reducer는 렌더 중에 실행되므로 순수해야 합니다.
- 같은 state와 action에는 같은 다음 state를 반환합니다.
- state의 객체나 배열을 직접 변경하지 않습니다.
- 실제 변화가 있을 때 새 객체나 배열을 반환합니다.
- 의미 있는 변화가 없다면 기존 state를 그대로 반환할 수 있습니다.
- API 요청, 타이머, 저장소 접근 같은 부수효과를 실행하지 않습니다.
- 알 수 없는 action은 현재 state로 조용히 숨기기보다 오류로 드러내는 방식을 선택할 수 있습니다.
React는 reducer 반환값과 현재 state를 Object.is로 비교합니다. 둘이 같으면 해당 컴포넌트와 자식의 렌더를 건너뜁니다. React가 컴포넌트 함수를 먼저 호출한 뒤 결과를 버릴 수는 있으므로, 이 최적화에 기대어 렌더 중 부수효과를 작성해서는 안 됩니다.
개발 환경의 Strict Mode에서는 우발적인 불순성을 찾기 위해 reducer와 initializer를 두 번 호출할 수 있습니다. 한 번의 결과만 사용되므로 순수한 함수라면 동작이 달라지지 않습니다.
카운터로 보는 action과 상태 전이
import { useReducer } from 'react';
const initialState = { count: 0 };
function counterReducer(state, action) {
switch (action.type) {
case 'incremented':
return { ...state, count: state.count + 1 };
case 'decremented':
return { ...state, count: state.count - 1 };
case 'reset':
return initialState;
case 'amountAdded':
return { ...state, count: state.count + action.amount };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function CounterWithReducer() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<section>
<h2>Reducer 카운터</h2>
<p aria-live="polite">현재 카운트: {state.count}</p>
<button onClick={() => dispatch({ type: 'incremented' })}>
증가
</button>
<button onClick={() => dispatch({ type: 'decremented' })}>
감소
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
초기화
</button>
<button
onClick={() => dispatch({ type: 'amountAdded', amount: 5 })}
>
5 추가
</button>
</section>
);
}버튼은 다음 count를 직접 정하지 않고 발생한 사건을 action으로 보냅니다. reducer는 현재 state와 action만으로 다음 state를 계산하고, React는 그 반환값으로 다음 렌더를 수행합니다.
dispatch를 호출해도 실행 중인 이벤트 핸들러의 state 변수는 즉시 바뀌지 않습니다.
function handleIncrement() {
dispatch({ type: 'incremented' });
console.log(state.count); // 이 렌더가 받은 이전 snapshot
}dispatch의 identity는 안정적입니다. 따라서 이 함수를 props로 전달할 때 함수 identity 자체가 매번 바뀌지는 않습니다. 다만 부모가 렌더되면 자식도 실행될 수 있으므로, “dispatch를 전달하면 불필요한 렌더가 없다”라고 단정할 수는 없습니다. memoization과 bailout은 별도의 렌더 경계 문제입니다.
부수효과도 reducer 밖에 둡니다. 사용자 상호작용 때문에 생긴 요청은 이벤트 핸들러에서 처리하고, 렌더 결과를 외부 시스템과 동기화해야 할 때는 Effect를 사용합니다.
useState와 useReducer 선택
useState의 함수형 updater도 이전 state를 바탕으로 다음 값을 계산할 수 있으므로, “이전 값에 의존한다”는 사실 하나만으로 reducer가 필요한 것은 아닙니다.
다음과 같은 신호를 함께 봅니다.
- 단순하고 독립적인 값의 짧은 갱신은
useState가 더 읽기 쉽습니다. - 여러 이벤트 핸들러에 비슷한 갱신 코드와 분기가 반복되면 reducer가 규칙을 모으는 데 도움이 됩니다.
- 하나의 사건이 관련 필드 여러 개를 함께 바꾸거나 불변 조건을 지켜야 하면 reducer의 action 경계가 유용합니다.
- 순수 전이 함수를 UI와 분리해 테스트하거나 action별로 변화 이유를 추적할 필요가 있으면 reducer가 적합할 수 있습니다.
- reducer는 action과 분기 코드를 더 작성해야 하므로 단순한 상태까지 모두 옮길 필요는 없습니다.
한 컴포넌트 안에서 useState와 useReducer를 함께 사용해도 됩니다. 선택 기준은 state 개수나 객체 여부가 아니라 전이 규칙의 복잡도와 설명 가능성입니다.
REACT STATE · OWNERSHIP & TRANSITION
전이 규칙과 공유 범위를 별개의 문제로 결정합니다
단순한 테마 토글은 useState로 직접 설명할 수 있습니다. 여러 사건과 불변 조건은 useReducer로 모으고, 필요할 때만 Context로 전달 범위를 넓힙니다.
실제 CounterProvider · 세 역할을 구분
CounterProvider → state 소유
counterReducer → 다음 state 계산
CounterStateContext → state 전달
CounterDispatchContext
→ dispatch 전달custom Hook은 별도 상태 소유자가 아닙니다. dispatch가 안정적이어도 부모 렌더와 bailout은 별도의 경로입니다.
| 판단 신호 | useState |
useReducer |
|---|---|---|
| 전이 규칙 | 단순 토글·입력처럼 setter와 updater만으로 의도가 바로 읽힙니다. | 여러 이벤트 핸들러에 분기와 상태 갱신 규칙이 흩어져 있습니다. |
| 관련 값 | 값이 독립적이고 함께 지켜야 할 불변 조건이 적습니다. | 하나의 사건이 관련 필드 여러 개를 함께 바꾸거나 전이 조건이 얽힙니다. |
| 설명 방식 | “다음 값을 무엇으로 둘지”를 가까운 코드에서 설명하는 편이 짧습니다. | “무슨 일이 있었고 어떻게 바뀌는지”를 action과 reducer로 분리하면 선명합니다. |
| 검증 비용 | 컴포넌트 동작을 읽는 것만으로 변경 규칙을 쉽게 추적할 수 있습니다. | 순수 reducer를 따로 테스트하고 action 로그로 변화 이유를 추적할 가치가 큽니다. |
| 코드 비용 | 초기 보일러플레이트가 적습니다. | action과 reducer 코드가 늘어나는 대신 복잡한 갱신 규칙을 한곳에 모읍니다. |
전이 규칙
useState: 단순 토글·입력처럼 setter와 updater만으로 의도가 바로 읽힙니다.
useReducer: 여러 이벤트 핸들러에 분기와 상태 갱신 규칙이 흩어져 있습니다.
관련 값
useState: 값이 독립적이고 함께 지켜야 할 불변 조건이 적습니다.
useReducer: 하나의 사건이 관련 필드 여러 개를 함께 바꾸거나 전이 조건이 얽힙니다.
설명 방식
useState: “다음 값을 무엇으로 둘지”를 가까운 코드에서 설명하는 편이 짧습니다.
useReducer: “무슨 일이 있었고 어떻게 바뀌는지”를 action과 reducer로 분리하면 선명합니다.
검증 비용
useState: 컴포넌트 동작을 읽는 것만으로 변경 규칙을 쉽게 추적할 수 있습니다.
useReducer: 순수 reducer를 따로 테스트하고 action 로그로 변화 이유를 추적할 가치가 큽니다.
코드 비용
useState: 초기 보일러플레이트가 적습니다.
useReducer: action과 reducer 코드가 늘어나는 대신 복잡한 갱신 규칙을 한곳에 모읍니다.
Hook을 호출한 컴포넌트
useState나 useReducer를 직접 또는 custom Hook을 통해 호출한 컴포넌트가 state를 소유합니다. custom Hook은 별도 owner가 아닙니다.
다음 state 계산
현재 state와 action을 받아 순수하고 불변인 전이 규칙만 수행합니다. API·타이머·저장소 접근과 공급 범위는 책임 밖입니다.
값 공급과 범위
React 19에서는 Context 객체를 Provider로 렌더하고 value를 줍니다. 소비자는 가장 가까운 일치 Provider의 값을 읽습니다.
Context는 전역 상태나 owner가 아니라 선택한 하위 트리의 전달 채널입니다.
구현 전 체크리스트
- Owner와 scope: 어느 컴포넌트가 state를 소유하고 누가 읽어야 하는가?
- State shape와 init: 함께 변하는 필드와 불변 조건은 무엇이며 초기화 비용은 큰가?
- Action vocabulary: 각 상호작용과 최소 payload가 상태 변화의 이유를 드러내는가?
- Reducer 계약: 모든 분기가 순수하고 불변 갱신을 하며 알 수 없는 action을 분명히 처리하는가?
- 공급 경계: props로 충분한가, 아니면 제한된 Provider 하위 트리에 state와 dispatch를 공급해야 하는가?
dispatch의 identity는 안정적이지만, 이를 props로 전달하는 것만으로 자식 렌더가 사라지는 것은 아닙니다. 부모 렌더와 memoization bailout은 별도로 판단합니다.
reducer와 Context의 책임 분리
reducer는 상태를 저장하거나 공급 범위를 정하지 않습니다. 현재 state와 action을 받아 다음 state를 계산하는 규칙입니다.
state는 useReducer를 직접 또는 custom Hook을 통해 호출한 컴포넌트가 소유합니다. custom Hook은 상태 로직을 캡슐화하지만 별도의 owner가 되지는 않습니다. 여러 하위 컴포넌트가 state와 dispatch를 읽어야 하고 props 전달이 반복된다면, 소유 컴포넌트가 Context Provider 역할까지 맡을 수 있습니다.
아래 예시는 앞 카운터의 reducer와 초기 state를 별도 모듈로 옮겨 export했다고 가정합니다.
import { createContext, useReducer } from 'react';
import { counterReducer, initialState } from '../state/counterReducer.js';
export const CounterStateContext = createContext(null);
export const CounterDispatchContext = createContext(null);
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<CounterStateContext value={state}>
<CounterDispatchContext value={dispatch}>
{children}
</CounterDispatchContext>
</CounterStateContext>
);
}이 프로젝트의 React 19에서는 Context 객체를 직접 Provider로 렌더하고 value를 전달합니다. 소비자는 트리에서 가장 가까운 일치하는 Provider의 값을 읽습니다.
Context는 어디까지 전달할지를 정하는 채널이고 reducer는 어떻게 바꿀지를 정하는 계산 규칙입니다. 이 예시에서는 useReducer를 호출한 CounterProvider가 state의 owner이며, Context가 자동으로 앱 전체의 전역 상태가 되는 것은 아닙니다. 소비 범위가 작거나 props가 구조를 더 분명하게 드러내면 Context 없이 전달하는 방식도 충분히 좋습니다.
각 action 뒤의 snapshot을 계산하기
위 CounterWithReducer.jsx를 기존 앱의 App에서 import해 렌더하면 첫 문구는 현재 카운트: 0입니다. 증가, 5 추가, 감소 순서로 각각 클릭하고 렌더가 반영된 문구를 읽으면 1 → 6 → 5입니다. 초기화는 별도의 새 사건이며 initialState의 0으로 돌아갑니다. 원 reducer의 감소에는 하한 검사가 없으므로 0에서 감소하면 -1입니다.
handleIncrement의 로그는 dispatch 전 렌더가 받은 snapshot을 읽습니다. 예를 들어 0이 보인 렌더에서 호출한 로그는 0이고, 그 후 화면이 1을 표시할 수 있습니다. 서로 다른 시점의 값을 읽은 것이므로 reducer가 실패했다는 뜻은 아닙니다. React의 useReducer 계약에서도 다음 렌더의 갱신과 실행 중인 변수의 값을 구분합니다.
원 규칙과 새 요구를 분리해 풀기
초기 상태부터 amountAdded에 amount: 3, decremented, reset, decremented를 순서대로 적용합니다. 각 단계의 count와 원 초기 객체의 count를 적으세요. 이어서 “count가 0보다 작아지지 않아야 한다”는 새 요구가 생기면 어느 분기에 어떤 조건을 더할지 제안합니다. 새 요구가 이미 원 코드에 있다고 가정하지 않습니다.
전이 계산과 입력 state 보존의 풀이
원 규칙의 결과는 3 → 2 → 0 → -1입니다. 증가·감소·추가는 spread로 새 객체를 반환하므로 이전 객체의 count를 바꾸지 않습니다. initialState.count는 계속 0이고 reset은 그 초기 객체를 반환합니다.
하한 요구에는 decremented 분기에서 현재 count가 0 이하인지 먼저 검사해 그대로 반환하거나, 허용된 다음 count를 계산하는 규칙을 둘 수 있습니다. 버튼 비활성화는 UI 안내이고 순수 전이 규칙 자체의 하한과는 별개입니다. 다른 호출자가 같은 action을 보낼 수 있다면 reducer에도 조건이 있어야 합니다. 이때 어떤 입력을 허용하는지부터 정하고, 알 수 없는 action의 오류 정책과 혼동하지 않습니다.
"useReducer 훅 소개"는 여기까지입니다.
상태 변화의 이유를 action으로 표현하고, 순수 reducer가 불변 방식으로 다음 state를 계산하며, React가 이를 다음 렌더의 snapshot으로 반영하는 흐름을 살펴봤습니다.
다음 절에서는 소유 컴포넌트가 관리하는 state와 dispatch를 Context로 공급하는 구조를 더 자세히 다룹니다.