FE
[React] useReducer란 무엇인가요? 상태 관리의 또 다른 방법!

React에서 상태를 관리할 때 대부분 useState로 시작한다. 간단한 상태라면 이걸로 충분할 수 있다. 예를 들어 숫자 카운트를 증가시키거나 입력 필드를 제어할 때 말이다.
하지만 앱이 커지고 상태가 서로 얽히고설키는 상황이 오면 얘기가 달라진다.
예를 들어 이런 질문이 떠오르게 된다.
버튼 하나를 눌렀을 때 어떤 상태는 바뀌고 어떤 상태는 유지돼야 할까?
여러 조건(if/switch)마다 상태를 바꾸려면 코드가 너무 복잡한데?
이 상태 업데이트... 나중에 유지보수하기 어렵지 않을까?
이럴 때 등장하는 게 바로 useReducer이다!
useReducer란?
useReducer는 상태(state)와 업데이트 로직(dispatch + reducer)를 분리해서 관리할 수 있도록 도와주는 React의 Hook이다.
Redux와 매우 유사한 방식으로 동작하며, 복잡한 상태 전환 로직을 명확하게 분리할 수 있다는 장점이 있다.
// 사용 방법
const [state, dispatch] = useReducer(reducer, initialState);구성 요소 | 설명 |
state | 현재 상태값 |
dispatch | 상태를 업데이트하기 위한 함수 |
reducer | 액션에 따라 상태를 변경하는 로직 |
initialState | 초기 상태값 |
여기서 중요한 건, 상태(state)를 직접 바꾸지 않고, 액션(action)을 통해 간접적으로 바꾼다는 점이다.
useReducer는 언제 쓰면 좋을까?
상태의 전환 로직이 복잡할 때 (예 : if나 switch문이 많을 때)
상태가 여러 개로 나뉘고 서로 의존성이 있을 때
상태 업데이트를 한 곳에 집중시키고 싶을 때
Redux 스타일로 상태를 관리하고 싶을 때
이러한 상황 속에서 useReducer로 구조를 잡게 되면 상태 관리에 도움이 될 것이다.
예제: 할 일(ToDo) 리스트 만들기
예제를 통해서 useReducer를 제대로 이해해보자. 이를 위해 할 일 리스트를 만드는 예제를 가져왔다.
1) 기능 요약
? 추가: input에 값을 입력하고 버튼 클릭 → 리스트에 추가
? 토글: 완료 / 미완료 상태 토글
? 삭제: 항목 삭제2) 코드
import React, { useReducer, useState } from 'react';
// 1. 액션 타입 모음
const ACTIONS = {
ADD: 'add-todo',
TOGGLE: 'toggle-todo',
DELETE: 'delete-todo',
};
// 2. 리듀서 함수
function reducer(todos, action) {
switch (action.type) {
case ACTIONS.ADD:
return [
...todos,
{ id: Date.now(), name: action.payload.name, complete: false },
];
case ACTIONS.TOGGLE:
return todos.map((todo) =>
todo.id === action.payload.id
? { ...todo, complete: !todo.complete }
: todo
);
case ACTIONS.DELETE:
return todos.filter((todo) => todo.id !== action.payload.id);
default:
return todos;
}
}
// 3. 메인 컴포넌트
export default function TodoApp() {
const [todos, dispatch] = useReducer(reducer, []);
const [input, setInput] = useState('');
const handleAdd = () => {
if (input.trim() === '') return;
dispatch({ type: ACTIONS.ADD, payload: { name: input } });
setInput('');
};
return (
<div style={{ padding: 20 }}>
<h2>? ToDo 리스트</h2>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="할 일을 입력하세요"
/>
<button onClick={handleAdd}>추가</button>
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ marginTop: 10 }}>
<span
style={{
textDecoration: todo.complete ? 'line-through' : 'none',
marginRight: 10,
}}
>
{todo.name}
</span>
<button
onClick={() =>
dispatch({ type: ACTIONS.TOGGLE, payload: { id: todo.id } })
}
>
{todo.complete ? '되돌리기' : '완료'}
</button>
<button
onClick={() =>
dispatch({ type: ACTIONS.DELETE, payload: { id: todo.id } })
}
>
삭제
</button>
</li>
))}
</ul>
</div>
);
}3) 코드 설명
(1) reducer 함수
function reducer(state, action) {
switch (action.type) {
...
}
}reducer는 현재 상태와 액션을 받아서 새로운 상태를 반환하는 순수 함수이다.
어떤 액션을 보냈는지에 따라 상태를 바꿔준다.
reducer는 상태를 직접 바꾸지 않고, 기존 상태를 복사해서 새로운 상태를 리턴한다.
(2) dispatch
dispatch({ type: ACTIONS.ADD, payload: { name: input } });dispatch는 상태를 바꾸고 싶다는 요청(액션)을 보내는 함수입니다.
실제로 상태를 바꾸는 건 reducer입니다.
상태를 직접 변경하지 않고, dispatch({ type, payload })를 통해 요청만 보낸다.
type은 어떤 동작인지 명시, payload는 필요한 데이터 전달
요약
개념 | 의미 |
useState | 간단한 상태 관리에 적합 |
useReducer | 복잡한 상태 전환과 구조적인 관리에 적합 |
reducer | 상태를 어떻게 바꿀지를 정의하는 함수 |
dispatch | 상태를 바꿔달라고 요청하는 트리거 함수 |
마무리
상태 관리가 복잡해질수록, 코드가 점점 지저분해지고 버그도 늘어나게 된다.
이럴 때 useReducer는 명확한 흐름과 구조를 만들어주는 훌륭한 도구가 되어줄 것이다.