
⏪ 이전 글 (useReducer)
https://velog.io/@wowalswjd/about-React-useReducer
이전 글에서 useReducer에 대해 살펴보았다.
useReducer는 컴포넌트에 reducer 함수를 추가해 상태를 관리하는 React Hook으로,
useState와 기능이 비슷하다.
이 때, useState 로직을 어떻게 useReducer 로직으로 바꿀 수 있는지,
어느 상황에서 어떤 Hook을 쓰면 좋을지 작성해보고자 한다.
🧾 참고 문서 :
https://ko.react.dev/learn/extracting-state-logic-into-a-reducer
컴포넌트가 복잡해지면 컴포넌트의 state가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워짐.
예를 들어, 아래의 TaskApp 컴포넌트는 state에 tasks 배열을 보유하고 있고, 세 가지의 이벤트 핸들러와 set function을 사용하여 task를 추가, 제거 및 수정하고 있음.
복잡성은 줄이고 접근성을 높이기 위해서, 컴포넌트 내부에 있는 state 로직을 컴포넌트 외부의 reducer라고 하는 단일 함수로 옮기는 작업을 3단계에 걸쳐서 진행하고자 함.
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를 변경하는 구체적인 로직을 숨기고,
어떠한 일을 하는 로직인지에 대한 정보를 한 단어로 지정하기
=> 사용자의 의도를 더 명확하게 설명할 수 있음.
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)
);
}
현재 코드에서는 state를 설정함으로써 무엇을 할 것인지를 구체적으로 명시하고 있음
-=> state를 설정하여 React에게 무엇을 할 지에 대한 로직(=setState)를 지정하는 것 대신
사용자가 방금 한 일(=action)을 지정해주어야 함.
위 코드의 setState들을 한 문장으로 정의하면 다음과 같음.
Add를 눌렀을 때 호출되는 handleAddTask(text).Save를 누르면 호출되는 handleChangeTask(task).Delete를 누르면 호출되는 handleDeleteTask(taskId).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
});
}
즉, 이벤트 핸들러를 통해 tasks를 설정하는 대신 task를 추가/변경/삭제하는 action을 전달하는 것임.
dispatch로 넘기게 되는 정보(객체)를 action이라고 함.action은 객체 형태로, 안에 어떤 속성을 추가해도 상관없으나 type 속성은 반드시 포함해야 함 type 속성은 어떤 상황이 발생하는지에 대한 최소한의 정보만 명시해야 함.reducer 함수는 1단계에서 생략했었던 state에 대한 구체적인 로직을 담는 곳.
function yourReducer(state, action) {
// React가 설정하게될 다음 state 값을 반환합니다.
}
현재 state 값과 action 객체다음 state 값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)
);
}
위 TaskApp 컴포넌트에 필요한 reducer 함수를 작성해보면 아래와 같음.
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
} else if (action.type === 'changed') {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter(t => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}
reducer 함수는 state(tasks)를 인자로 받고 있기 때문에, reducer 함수를 컴포넌트 외부에 작성 가능함. if/else문도 사용 가능하지만, 가독성으로 인해 switch문을 사용하는 것이 일반적.{}(중괄호)로 감싸기return으로 끝내기throw문 추가하기import { useReducer } from 'react';
const [tasks, setTasks] = useState(initialTasks);
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
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);
}
}
}
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
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 }
];
state 업데이트 함수와 비슷하게, reducer는 렌더링 중에 실행되기 때문.
(action은 다음 렌더링까지 대기함.)
예를 들어, 사용자가 작성하는 양식에 5개의 필드(name, email 등)가 있다고 가정할 때,
set_field 액션(ex. setName, setEmail 등)보다는reset_form 액션 보내기reducer 함수가 반드시 좋은 것만은 아님.useState < useReduceruseState의 경우 미리 작성해야 하는 코드가 없음.useReducer가 오히려 코드의 양을 줄일 수 있음.useState > useReduceruseState 추천useReducer를 사용하면reducer)을 한꺼번에 모아서 볼 수 있고,action)이 발생했는지를 한 단어로 볼 수 있어 유리함.useState < useReduceruseState는 문제 위치와 원인을 찾기 어려움useReducer를 사용하면 문제 원인 빠르게 파악 가능reducer)과 action에 console.log() 찍어보기useState < useReducerreducer는 컴포넌트에 의존하지 않는 순수 함수이므로useState와 useReducer는 동일한 방식이기 때문에