useReducer는 React Hook으로, 컴포넌트 내부에서 새로운 state를 생성하는 기능을 제공합니다. useState와 유사한 역할을 하지만, 보다 구조화된 방식으로 state를 관리할 수 있도록 설계되었습니다. 사실, useState로 관리할 수 있는 state는 useReducer로도 동일하게 관리할 수 있습니다.
하지만 두 Hook의 주요 차이점은 다음과 같습니다.
useReducer는 state와 그 상태를 변경하는 로직을 컴포넌트 외부로 분리하여 관리할 수 있다는 점에서 useState보다 더 조직적인 코드를 작성할 수 있도록 돕습니다. 이는 특히 상태 변경 로직이 복잡하거나 관리해야 할 상태가 많은 경우에 유용합니다.
아래 코드를 살펴봅니다.
function App() {
const [todos, setTodos] = useState(mockData);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
};
...
}
useState를 사용하여 state를 생성하면, onCreate 함수와 같은 state 관리 로직을 반드시 컴포넌트 내부에 작성해야 합니다.
반면, useReducer를 사용하면 state 생성은 컴포넌트 내부에서 처리하되, state를 관리하는 로직은 useReducer Hook을 통해 컴포넌트 외부로 분리할 수 있습니다. 이를 통해 컴포넌트 코드가 더 간결해지고, 상태 관리 로직이 재사용 가능하며 구조적으로 더욱 체계적인 코드를 작성할 수 있습니다.
TodoList 프로젝트를 개발할 때, ToDo 데이터를 관리하는 state를 App 컴포넌트 내부에서 useState를 사용해 생성했습니다. 그러나 이 방식에서는 todos와 setTodos 함수가 App 컴포넌트 내부에서만 접근 가능해집니다. 따라서 state를 관리하는 코드 역시 App 컴포넌트 내부에만 작성해야 했습니다.
예를 들어, state에 새로운 값을 추가하는 onCreate 함수, 특정 값을 수정하는 onUpdate 함수, 특정 값을 제거하는 onDelete 함수와 같은 todos state 관리 로직도 모두 App 컴포넌트 내부에 작성해야만 했습니다. 이는 이러한 함수들이 내부에서 setTodos 함수나 todos state에 의존하기 때문이며, setTodos와 todos는 App 컴포넌트 외부에서 접근할 수 없기 때문입니다.
이로 인해 App 컴포넌트 내부에 상태 관리 코드가 지나치게 길어지게 되었습니다. 현재 상태로도 충분히 길지만, 만약 state가 더 복잡해지거나 다양한 상태 변경 로직이 추가된다면, App 컴포넌트는 더욱 방대해질 것입니다. 이는 코드의 가독성과 유지보수성을 크게 저하시킬 수 있습니다.
React 컴포넌트의 주요 역할은 UI를 렌더링하는 것입니다. 따라서 state 관리 코드가 과도하게 많아지면, 본래의 역할이 흐려지고 주객이 전도되는 상황이 발생할 수 있습니다. 즉, UI를 렌더링하는 코드보다 state를 관리하는 코드가 훨씬 더 복잡하고 길어지게 되어, 컴포넌트의 본질적인 목적을 잃을 위험이 있습니다.
우선 Exam.jsx 파일을 만들어줍니다.
const Exam = () => {
return (
<div>
<h1>0</h1>
<button>+</button>
</div>
);
};
export default Exam;
아주 간단한 Counter 기능을 만들어봅니다. useReducer 이용하여 새로운 state를 생성해봅니다.
import { useReducer } from "react";
const Exam = () => {
const [count, dispatch] = useReducer();
return (
<div>
<h1>0</h1>
<button>+</button>
</div>
);
};
export default Exam;
useReducer는 useState와 동일하게 새로운 state를 생성하며, 이를 배열의 첫 번째 요소로 반환합니다. 그러나 두 번째 요소로 setState 대신 상태 변화를 요청하는 역할만을 하는 dispatch 함수를 반환한다는 점이 다릅니다.
dispatch : 발송하다. 급송하다.
dispatch 함수는 상태 변화가 필요하다는 사실을 알리거나 이를 요청하는 함수라고 이해할 수 있습니다.
예를 들어, Exam 컴포넌트 내부에서 dispatch 함수를 호출하면 상태 변화가 요청되고, useReducer는 실제로 상태를 업데이트하는 함수를 호출합니다. 이 함수는 사용자가 직접 정의해야 하며, 이를 reducer 함수라고 합니다.
Exam 컴포넌트 외부에 reducer 함수를 작성한 뒤, 이를 useReducer의 인자로 전달합니다. 참고로, reducer는 “변환기”라는 의미를 가지고 있으며, 주어진 상태와 액션을 기반으로 새로운 상태를 반환하는 역할을 합니다.
import { useReducer } from "react";
function reducer() {}
const Exam = () => {
const [count, dispatch] = useReducer(reducer);
return (
<div>
<h1>0</h1>
<button>+</button>
</div>
);
};
export default Exam;
useReducer 함수의 두 번째 인수로는 state의 초기값을 전달할 수 있습니다. 우선 초기값으로 0을 전달하여 state를 설정합니다.
import { useReducer } from "react";
function reducer() {}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h1>{count}</h1>
<button>+</button>
</div>
);
};
export default Exam;
이제 버튼을 클릭하면 state 값이 +1씩 증가하도록 구현해야 합니다. 이를 위해 Exam 컴포넌트 내부에서 버튼 클릭 시 dispatch 함수를 호출하여 상태 변화를 요청하면 됩니다. 먼저 Exam 컴포넌트 안에 onClickPlus라는 함수를 정의한 뒤, 이를 버튼의 이벤트 핸들러로 설정합니다. onClickPlus 함수 내에서 dispatch 함수를 호출해서 상태 변화를 요청합니다.
import { useReducer } from "react";
function reducer() {}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch();
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
</div>
);
};
export default Exam;
dispatch 함수의 인수로는 원하는 상태 변화에 대한 정보를 전달해야 합니다. 이 정보는 일반적으로 객체 형태로 제공되며, 객체의 type 프로퍼티에는 상태를 어떻게 변화시키고 싶은지를 나타내는 값을 설정합니다.
우리는 “+” 버튼을 눌러 state 값을 1씩 증가시켜야 하기 때문에 type 프로퍼티에 "INCREASE"라는 문자열을 할당합니다.
import { useReducer } from "react";
// reducer : 변환기
// ->상태를 실제로
function reducer() {}
const Exam = () => {
// dispatch : 상태 변화가 있어야 한다는 사실을 알리는, 발송하는 함수
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
</div>
);
};
export default Exam;
또한, 얼마나 증가시킬 것인지도 함께 명시해야 합니다. 이를 위해 data 프로퍼티를 사용하여 “1만큼 증가시켜라”라는 정보를 전달합니다.
import { useReducer } from "react";
// reducer : 변환기
// ->상태를 실제로
function reducer() {}
const Exam = () => {
// dispatch : 상태 변화가 있어야 한다는 사실을 알리는, 발송하는 함수
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
data: 1,
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
</div>
);
};
export default Exam;
dispatch 함수에 전달된 요청 정보를 담고 있는 객체를 Action 객체라고 합니다. 위 코드에서 Action 객체는 { type: "INCREASE", data: 1 } 입니다.
Action 객체를 dispatch 함수의 인수로 전달하면, useReducer는 이를 처리하기 위해 상태를 실제로 변경하는 reducer 함수를 호출합니다. 이때, reducer 함수는 두 개의 매개변수를 받습니다. 첫 번째 매개변수로 현재 state 값을, 두 번째 매개변수로는 요청의 내용을 담고 있는 Action 객체를 제공합니다.
import { useReducer } from "react";
function reducer(state, action) {}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
data: 1,
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
</div>
);
};
export default Exam;
reducer 함수에서 state와 action 두 인자를 console.log를 사용해 출력해 봅니다.
import { useReducer } from "react";
function reducer(state, action) {
console.log(state);
console.log(action);
}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
data: 1,
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
</div>
);
};
export default Exam;

reducer 함수는 매개변수로 전달된 현재의 state 값과 Action 객체를 사용하여 실제로 state를 변경하는 역할을 합니다.
상태 변화 함수도 없는데 어떻게 state를 변경할 수 있을까요?
👉🏻 reducer 함수에서 새로운 state의 값을 반환해주기만 하면 됩니다.
reducer 함수에서 반환된 state는 useReducer가 호출하여 실제로 state 값을 업데이트합니다.
reducer 함수 내부에서는 if 조건문을 사용해 Action 객체의 type이 "INCREASE"인지 확인합니다. 만약 type이 "INCREASE"라면, 이는 { type: "INCREASE", data: 1 } 형태로 전달된 Action 객체를 의미하며, “+” 버튼이 클릭된 상황을 나타냅니다. 이 경우, 새로운 state 값으로 state + action.data를 반환합니다.
import { useReducer } from "react";
function reducer(state, action) {
if (action.type === "INCREASE") {
return state + action.data;
}
}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
data: 1,
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
</div>
);
};
export default Exam;
코드를 다시 살펴보면, “+” 버튼을 눌러 dispatch 함수가 호출되면 reducer 함수가 실행됩니다. 이때 reducer 함수의 첫 번째 매개변수인 state에는 초기값인 0이, 두 번째 매개변수인 action에는 { type: "INCREASE", data: 1 } 객체가 전달됩니다.
if 조건문이 참이 되어, 현재의 state 값 0에 action.data의 값 1을 더한 결과를 반환합니다. 반환된 값은 새로운 state 값으로 반영되며, 그 결과 브라우저에서 state가 업데이트된 것을 확인할 수 있습니다.
새로운 상태 변화도 추가적으로 만들어봅니다. "-" 버튼을 생성합니다. onClickMinus 이벤트 핸들러를 추가합니다.
import { useReducer } from "react";
function reducer(state, action) {
if (action.type === "INCREASE") {
return state + action.data;
} else if (action.type === "DECREASE") {
return state - action.data;
}
}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
data: 1,
});
};
const onClickMinus = () => {
dispatch({
type: "DECREASE",
data: 1,
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
<button onClick={onClickMinus}>-</button>
</div>
);
};
export default Exam;
참고로, reducer 함수에서 처리해야 할 Action 타입이 많아질 것으로 예상된다면, 일반적으로 if 조건문 대신 switch 문을 사용하는 것이 더 효율적이고 가독성이 좋습니다.
switch 문에서는 모든 type을 처리할 수 있도록 default 케이스를 추가하는 것이 중요합니다. default 케이스에서는 dispatch의 type이 정의되지 않거나 잘못된 값일 경우, 상태를 변경하지 않고 기존 state를 그대로 반환하도록 설정합니다. 이는 예기치 않은 상태 변화를 방지하는 데 도움이 됩니다.
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "INCREASE":
return state + action.data;
case "DECREASE":
return state + action.data;
default:
return state;
}
}
const Exam = () => {
const [count, dispatch] = useReducer(reducer, 0);
const onClickPlus = () => {
dispatch({
type: "INCREASE",
data: 1,
});
};
const onClickMinus = () => {
dispatch({
type: "DECREASE",
data: 1,
});
};
return (
<div>
<h1>{count}</h1>
<button onClick={onClickPlus}>+</button>
<button onClick={onClickMinus}>-</button>
</div>
);
};
export default Exam;
useReducer를 사용하면 상태 변화를 처리하는 로직을 reducer 함수로 분리할 수 있어, 컴포넌트 내부를 비교적 간결하게 유지할 수 있다는 장점이 있습니다. 이는 특히 상태 관리 로직이 복잡하거나 다양한 상태 변화를 처리해야 하는 경우에 유용합니다.