`useReducer` 훅은 컴포넌트 최상단에서 리듀서로 상태를 관리하게 해준다.
리듀서란?
Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a
reducer.
많은 상태 업데이트를 갖는 컴포넌트가 여러 이벤트 핸들러에 걸쳐있으면 성능저하가 일어나는데, 이러한 여러 상태 업데이트를 하나의 함수로 통합하여 사용할 수 있으며, 이 함수를 리듀서라고 부른다.
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.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},
];
이 코드에서 이벤트 핸들러(`handleAddTask`, `handleChangeTask`, `handleDeleteTask`)는 상태 업데이트를 위해 `setTasks` 함수를 호출하고 있다. `TaskList` 컴포넌트가 증가할수록 컴포넌트 전체에 확산되어 있는 로직도 증가하게 된다.
이러한 복잡도를 줄이고 로직을 접근하기 쉽게 한 곳에 보관하기 위해 컴포넌트 바깥에 있는 하나의 함수에 상태 로직을 옮겨 사용할 수 있다.
리듀서의 기본구조
useReducer의 기본 구조는 이렇다.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
먼저 `reducer`는 상태가 업데이트 되는 방식을 지정한 함수이다. 상태와 동작을 파라미터로 받아 다음 상태를 반환해야 한다.
`initialArg`는 초기 상태값이다. 어떠한 타입도 허용한다.
`init`은 옵셔널한 값인데, 초기값을 리턴하는 이니셜라이저 함수이다.
리팩토링 전략
앞서 살펴본 코드는 하나의 `state`에 대해 `setState` 하는 과정이 각각 분리되어 있어 관리하기 어렵다.
따라서 리팩토링을 하고자 하는데, 크게 세단계의 스탭으로 나누어 리팩토링할 수 있다.
1. 각 함수 내부의 로직을 dispatch로 묶는다.
2. dispatch 함수마다 setState 하기 위한 타입을 지정하고 매개변수를 전달한다.
3. 리듀서 함수를 정의한다.
리팩토링
1. 이벤트 핸들러(`handleAddTask`, `handleChangeTask`, `handleDeleteTask`) 함수 고치기
각 함수의 역할에 대해 간단히 정리해보면
`handleAddTask`는 새로운 태스크를 생성하는 함수로 이전 배열을 복사하고 새로운 내용을 추가한다.
입력한 텍스트를 매개변수로 전달받는다.
`handleChangeTask`는 `tasks` 배열을 순회하면서 아이디를 검사하고 아이디 존재여부에 따라 리턴한 값을 저장한다.
새로운 task를 매개변수로 받는다.
`handleDeleteTask`는 지정한 아이디를 찾아 다른 아이디인 것들만 모아서 값을 저장한다.
task의 아이디를 매개변수로 받는다.
각 함수의 역할에 대해 정리하였고, 함수의 내부에서 `dispatch`를 호출하게 한다.
function handleAddTask(text) {
dispatch();
}
function handleChangeTask(task) {
dispatch();
}
function handleDeleteTask(taskId) {
dispatch();
}
2. `dispatch` 함수에 인자 전달하기
이제 각 함수의 내부에 있는 `dispatch`에 인자를 전달할건데, 여기서는 함수의 역할과 작업에 필요한 것들을 전달하기로 한다.
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,
});
}
3. 리듀서 함수 정의하기
이제 리듀서 함수를 정의하는데, 리듀서 함수는 `tasks`라는 배열과 이벤트 핸들러에서 호출한 `dispatch` 함수에 넘긴 인자들을 `action`으로 받게 해줘야 한다.
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}`;
}
}
}
action으로 넘겨준 type에는 사전에 함수별로 역할을 지정해주었고, 어떤 역할인지에 따라 분기하여 작업을 수행하게 한다.
이렇게 리팩토링하게 되면 하나의 함수에서 하나의 state에 대한 다양한 상태관리를 수행할 수 있다.
'기술스택 > React.js' 카테고리의 다른 글
[React 19] useMemo (0) | 2025.03.30 |
---|---|
[React 19] useEffect (1) | 2025.03.26 |
[React 19] useContext (0) | 2025.03.25 |
[React 19] useCallback (0) | 2025.03.23 |
[React 19] useActionState (1) | 2025.03.22 |