
⏪ 이전 글 (useState)
https://velog.io/@wowalswjd/about-React-useState
이전 글에서 useState에 대해 살펴보았다.
useState는 컴포넌트에 state 변수를 추가할 수 있는 React Hook으로,
이 Hook을 이용하면 함수형 컴포넌트에서도 쉽게 상태 관리를 할 수 있었으며, UI에 실시간으로 상태 반영이 가능하다.
하지만 useState의 경우 state 로직이 복잡해질 경우 코드가 복잡해진다는 문제가 있었는데,
이는 비슷한 상태 관리 Hook인 useReducer로 대신 사용해서 해결할 수 있다.
🧾 참고 문서 :
https://ko.react.dev/reference/react/useReducer
https://ko.react.dev/learn/extracting-state-logic-into-a-reducer
🔗 이미지 출처 :
https://www.jstopics.com/reactjs/usereducer-with-typescript
reducer를 추가하는 React Hookreducer 함수란? state에 대한 로직을 넣는 곳useState와 비슷하게 함수형 컴포넌트에서 상태 관리할 때 사용reducer 함수 작성function reducer(state, action) {
// ...
}
action은 state 업데이트를 위한 정보라고 생각하면 됨reducer와 연결된 state와 dispatch를 선언const [state, dispatch] = useReducer(reducer, 초기값, 초기화함수?);
dispatch와 type을 작성<button onClick={()=> dispatch({ type: 'TYPE' })}>action</button>
<div>{state}</div>

reducer: state가 어떻게 업데이트 되는지 지정하는 리듀서 함수.state와 action을 인수로 받아야 하고, 다음 state를 반환해야 함.state와 action은 모든 데이터 타입 할당 가능function reducer(state, action) {
// ...
return 변경된state
}initialArg: 초기 state 값. init 인수에 따라 달라짐.init: 초기 state를 반환하는 초기화 함수. init(initialArg)를 호출한 결과가 할당됨.initialArg로 설정됨.2개의 엘리먼트로 구성된 배열을 반환함.
stateinit이 있을 경우 init(initialArg)init이 생략되었을 경우 initialArg로 설정됨.dispatch 함수dispatch는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킴.setState 같은 개념이나, action 정보를 reducer에 넘기는 역할임.useReducer는 Hook이므로 컴포넌트의 최상위 레벨이나 직접 만든 Hook에서만 호출 가능.dispatch 함수를 useEffect의 의존성 배열 안에 추가해도 useEffect가 다시 실행되지 않음.dispatch 함수는 React 내부적으로, 상태 업데이트 함수로만 정의됨.useEffect에서는 의존성 배열이 변경되지 않았다고 판단하여 다시 실행시키지 않음.state를 새로운 값으로 업데이트하고 리렌더링을 일으키는 함수.dispatch 함수 (action 객체 정보 전달)reducer 함수 (state 업데이트 로직 실행)state 반영const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
action: 사용자에 의해 수행된 활동. (ex. add, delete ...)
{
type: 'what_happened', // 필수
id: 1, // 다른 필드는 이곳에 (선택)
}
dispatch는 다음 렌더링을 위해서 state를 업데이트함.dispatch를 호출한 후 바로 state를 읽으면 호출 이전 값을 얻게 됨.set function이 호출된 후에 화면이 업데이트됨. (하나의 이벤트 중에 여러 번 렌더링 되는 것 방지 위함)import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
state를 reducer로 관리하기 위해useReducer를 컴포넌트의 최상단에서 호출function handleClick() {
dispatch({ type: 'incremented_age' });
}
state 값을 업데이트할 때는 dispatch 함수 사용.dispatch 함수 인자로는 action 객체를 넣어주어야 함아래 예제는 카운터 예제
import { useReducer } from 'react';
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
age: state.age + 1
};
}
throw Error('Unknown action.');
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
Increment age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}

dispatch 함수로 incremented_age라는 업데이트 정보(=action)를 보내면reducer 함수에서 action 정보를 받고,incremented_age일 때 실행하는 로직 (age: state.age + 1)을 실행하고 그 값을 return함reducer 함수의 기본 틀은 아래와 같음.
function reducer(state, action) {
// ...
}
각 action type별 처리 로직은 switch 문으로 작성하는게 일반적임.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
action은 어떤 형태로도 작성 가능하나, type 속성을 가진 객체로 사용하는 것이 일반적임.
type은 reducer가 다음 state를 계산하기 위한 최소한의 정보를 포함해야 함.function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Don't mutate an object in state like this:
state.age = state.age + 1;
return state;
}... 사용)function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Instead, return a new object
return {
...state,
age: state.age + 1
};
}아래 예제는 투두 리스트 (배열 state)을 useReducer Hook을 이용해 업데이트하는 예제
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false }
];

React는 초기 state를 저장한 후, 다음 렌더링에서는 이를 무시함.
아래 예제에서 createInitialState(username) 는 원래 의도와는 달리 매 렌더링 때마다 함수가 호출되는 문제가 발생함.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
함수가 큰 배열이나 무거운 연산을 다룰 경우에는 성능상 낭비가 되므로, useReducer의 3번째 인수에 초기화 함수를 전달하는 것이 좋음.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
예제
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!
setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}
🤔 Why?
state는 스냅샷 같이 동작하므로, 리렌더링 기준으로 업데이트되기 때문.

dispatch 함수의 호출은 현재 동작하고 있는 코드의 state를 바로 변경하는 것이 아니라,
새로운 state로 업데이트하게끔 요청을 보낼 뿐이기 때문에
이미 실행중인 이벤트 핸들러의 변수에 영향을 미치지 않음.
😇 해결 방안)
다음 state 값을 알고 싶다면, reducer 함수를 직접 호출해서 다음 값 계산하기
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
Object.is()로 비교한 뒤 다음 state가 이전 state와 같으면 업데이트를 무시하기 때문function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct: creating a new object
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct: creating a new object
return {
...state,
name: action.nextName
};
}
// ...
}
}🤔 Why?
기존 state를 복사해 사용하지 않고 직접 변경하려고 할 때 주로 발생
😇 해결 방안)
새로운 state를 반환할 때 모든 case에서 기존 값을 복사하기
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...
🤔 Why?
case 중 하나에 return이 누락되었거나 action의 타입이 case와 짝지어지지 않을 때 발생
😇 해결 방안)
원인 파악 위해 switch문 밖에서 에러를 throw 하기
(또는 TypeScript를 이용하면 실수를 방지할 수 있음)
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
// 🚩 잘못된 방법: 렌더링 동안 핸들러 요청
return <button onClick={handleClick()}>Click me</button>// ✅ 올바른 방법: 이벤트 핸들러로 전달
return <button onClick={handleClick}>Click me</button>// ✅ 올바른 방법: **인라인 함수**로 전달
return <button onClick={(e) => handleClick(e)}>Click me</button>