1. 핵심 개념 먼저 이해하기
| 개념 | 설명 |
| state | 컴포넌트의 상태, 예: 숫자, 문자열, 객체 등 |
| action | 어떤 일이 일어났는지 설명하는 객체 (예: { type: 'UP' }) |
| dispatch | 액션을 reducer에 보내는 함수 |
| reducer | 현재 상태(state)와 액션(action)을 받아서 새로운 상태를 반환하는 함수 |
비교 비유
| 방식 | 비유 |
| useState | 손님이 직접 장부에 기록 |
| useReducer | 손님이 창구에 주문 → 회계 담당자가 장부를 처리 |
2. 실습 단계별 요약
[A] useState로 카운터 만들기
목표: 버튼 3개 (-, 0, +)로 숫자 상태 변경
순서:
- useState로 count 상태 만들기
- 버튼 3개 만들기
- 버튼 클릭 시 count 변경 (setCount 호출)
- <span>{count}</span>으로 현재 값 출력
코드 요약:
const [count, setCount] = useState(0);
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>0</button>
<button onClick={() => setCount(count + 1)}>+</button>
문제점:
상태가 많아지면 setState 함수가 계속 늘어남. 상태 로직이 컴포넌트 안에 흩어짐
[B] useReducer로 리팩토링
목표: 상태 로직을 컴포넌트 밖 reducer로 분리
순서:
- useReducer import
- countReducer(state, action) 함수 작성
- useReducer(countReducer, 초기값) 호출 → [state, dispatch] 반환
- dispatch({ type: 'UP' }) 형식으로 상태 변경 요청
- 버튼 클릭 시 dispatch 호출
코드 요약:
function countReducer(state, action) {
if (action === 'UP') return state + 1;
if (action === 'DOWN') return state - 1;
if (action === 'RESET') return 0;
}
const [count, dispatch] = useReducer(countReducer, 0);
[C] 액션을 객체로 확장 (유저 입력 수만큼 증감)
목표: 입력값만큼 count가 증가/감소하도록 개선
변경사항:
- 액션을 문자열 대신 객체 { type: 'UP', number: 입력값 } 형식으로 전달
- countReducer에서 action.number 사용
reducer 코드:
function countReducer(state, action) {
if (action.type === 'UP') return state + action.number;
if (action.type === 'DOWN') return state - action.number;
if (action.type === 'RESET') return 0;
}
dispatch 호출 예:
dispatch({ type: 'UP', number: 3 });
3. 최종 전체 코드 정리
import React, { useReducer, useState } from "react";
export default function App() {
const [number, setNumber] = useState(1);
function countReducer(oldCount, action) {
if (action.type === "UP") return oldCount + action.number;
if (action.type === "DOWN") return oldCount - action.number;
if (action.type === "RESET") return 0;
}
const [count, dispatch] = useReducer(countReducer, 0);
function up() {
dispatch({ type: "UP", number });
}
function down() {
dispatch({ type: "DOWN", number });
}
function reset() {
dispatch({ type: "RESET" });
}
function changeNumber(e) {
setNumber(Number(e.target.value));
}
return (
<div>
<input type="button" value="-" onClick={down} />
<input type="button" value="0" onClick={reset} />
<input type="button" value="+" onClick={up} />
<input type="number" value={number} onChange={changeNumber} />
<span>{count}</span>
</div>
);
}
4. 이 실습의 핵심 포인트
포인트 설명
| 상태 로직 분리 | reducer 함수로 상태 변경 로직을 컴포넌트 밖으로 분리함 |
| 액션 → 처리 흐름 | 버튼 클릭 → dispatch(action) → reducer에서 처리 → 새로운 state 반환 |
| 가독성 향상 | 상태가 많아질수록 useReducer 구조가 더 깔끔하고 유지보수 쉬움 |
| 추상화 | 상태 변경을 "업무 요청"처럼 처리 가능 (직접 변경이 아닌, 요청 기반 구조) |
5. useReducer의 장단점 요약
항목 장점 단점
| 구조 | 상태 변경 로직이 분리돼서 깔끔함 | 단순한 로직에는 과할 수 있음 |
| 유지보수 | 액션 타입 중심으로 상태 흐름 추적 가능 | 액션 타입, reducer 작성 등 코드가 많아짐 |
| 확장성 | 상태가 많을수록 구조적으로 좋음 | 초보자에겐 처음엔 어려워 보일 수 있음 |
6. 언제 useReducer를 쓰면 좋을까?
- 상태가 객체/배열처럼 복잡할 때
- 상태 변경 조건이 많을 때 (if/switch 많은 경우)
- 컴포넌트 안에 로직이 복잡하게 섞여 있을 때
- 상태 로직을 재사용하거나 테스트하고 싶을 때
'Frontend > React' 카테고리의 다른 글
| React Todo App 만들기 (Vite + useState + 데이터 흐름 이해) (2) | 2025.08.12 |
|---|---|
| Vite와 Create React App(CRA) 비교 (1) | 2025.08.11 |
| React useReducer (2) | 2025.07.27 |
| React State (0) | 2025.07.19 |
| 이벤트 핸들링 (1) | 2025.07.19 |