useState Hook처럼 state를 생성하고 관리해주는 useReducer Hook에 대해서 알아보자.
여러개의 하위 값을 표함하는 복잡한 스테이트를 다워야 할 때 useState보다 간결하고 유지보수를 쉽게 사용할 수 있다.
요구
를 전달하면 내용
을 확인하고 변경해준다.요구
에 해당한다.전달
하는 역할을 한다.요구
에 들어가는 내용
에 해당한다.type
이라고 하는, reducer에서 분기 처리를 할 때 필요한 문자열정보를 포함한다.useState로 관리되고 있는 아래 예제 TaskApp을 useReducer로 변경해보자.
// state로 관리되는 코드
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 },
];
- 컴포넌트가 커질수록 state를 변경하는 로직도 늘어난다.
- 이러한
모든 로직을 한곳에 모으기
위해 외부의reducer라고 하는 단일 함수
로 옯길 수 있다.
- reducer를 사용한 state 관리는 state를 직접 설정하는 것이 아니다.
- 이벤트핸들러(dispatch)에서
action
을 전달하여 reducer에게사용자가 방금 한 일
을 알려준다.- 이벤트 핸들러는
tasks를 설정해
가 아닌task를 추가/변경/삭제 해
라는action
을 전달한다.
// state 설정로직에서 dispatch로 변경된 코드
function handleAddTask(text: string) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task: TaskType) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId: number) {
dispatch({
type: 'deleted',
id: taskId,
});
}
요구
action이다.내용
이 된다.내용
을 확인하고 반영해서 state를 변경한다.현재 state
이고 다른 하나는 action 객체
다.function yourReducer(state, action) {
// return next state for React to set
}
export function tasksReducer(tasks: TaskType[], action: ActionType) : TaskType[] {
switch (action.type) {
case 'added': {
if (action.id === undefined || action.text === undefined) {
// 적절한 처리 로직 (예: 에러 던지기, 기본 값 사용 등)
throw new Error('ID and text must be provided for "added" action.');
}
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 interface TaskType {
id: number;
text: string;
done: boolean;
}
interface ActionType {
type: 'added' | 'changed' | 'deleted';
id?: number;
text?: string;
task?: TaskType;
}
interface TasksReducerType {
tasks: TaskType[];
action: ActionType;
}
import { useReducer } from "react";
// 기존 useState 코드
const [tasks, setTasks] = useState(initialTasks);
// reducer로 변경된 코드
const [tasks, dispatch] = useReducer(1️⃣ tasksReducer, 2️⃣ initialTasks);
// useReducer Hook은 두 개의 인자를 받는다.
// 1️⃣ 생성한 reduecer 함수, 2️⃣ 초기 state 값
[tasks, dispatch]
[tasks, dispatch]
는 [state, setState]
와 같이 dispatch를 통해 tasks를 변경 한다.// reducer로 변경된 TaskApp 코드
import { useReducer } from 'react';
import { tasksReducer, TaskType } from './TasksReducer';
import AddTask from './AddTask/AddTask';
import TaskList from './TaskList/TaskList';
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false },
];
export function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text: string) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task: TaskType) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId: number) {
dispatch({
type: 'deleted',
id: taskId,
});
}
return (
<>
<h1>Day off in Kyoto</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
이벤트핸들러(dispatch)는 action(요구)을 전달하여 무슨 일이 있어났는지만 지정한다.
reducer 함수는 action(요구)의 내용으로 state가 어떻게 변경되는지 결정한다.
코드 크기
: useReducer를 사용하면 reducer 함수 와 action을 전달하는 부분 모두 작성해야 하기에 useState 보다 코드의 길이가 길어진다. 하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer를 사용하면 오히려 코드를 줄이는 데 도움이 될 수 있다.
가독성
: 간단한 state를 업데이트 하는 경우는 useState가 가독성이 좋다. 그렇지만 state의 구조가 더욱 복잡해지면, 컴포넌트의 코드의 양이 부풀어 오르고 한눈에 읽기 어려워질 수 있다. 이 경우 useReducer를 사용하면 업데이트 로직이 어떻게 동작 하는지와 이벤트 핸들러를 통해 무엇이 일어났는지 를 깔끔하게 분리할 수 있다.
디버깅
: useState에 버그가 있는 경우, state가 어디서 잘못 설정되었는지, 그리고 왜 그런지 알기 어려울 수 있다. useReducer를 사용하면, reducer에 콘솔 로그를 추가하여 모든 state 업데이트와 왜 (어떤 action으로 인해) 버그가 발생했는지 확인할 수 있다. 각 action이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 알 수 있다. 하지만 useState를 사용할 때보다 더 많은 코드를 살펴봐야 한다.
테스팅
: reducer는 컴포넌트에 의존하지 않는 순수한 함수다. 즉, 별도로 분리해서 내보내거나 테스트할 수 있다. 일반적으로 보다 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만, 복잡한 state 업데이트 로직의 경우 reducer가 특정 초기 state와 action에 대해 특정 state를 반환한다고 단언하고 테스트 하는 것이 유용할 수 있다.
일부 컴포넌트에서 잘못된 state 업데이트로 인해 버그가 자주 발생하고 더 많은 구조가 필요하다면 reducer를 사용하자.