여러 개의 state 업데이트가 여러 이벤트 핸들러에 분산되어 있는 컴포넌트
는 과부하가 걸릴 수 있다.
이러한 경우,reducer
라고 하는 단일 함수를 통해
컴포넌트 외부의 모든 state 업데이트 로직을 통합할 수 있다.아래 예시가
여러 개의 state 업데이트가 여러 이벤트 핸들러에 분산되어 있는 컴포넌트
이다.
tasks
라는 상태를 여러 핸들러 함수,
handleAddTask
,handleChangeTask
,handleDeleteTask
로 업데이트 하는 코드다.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}, ];
reducer를 사용한 state 관리는 state를 직접 설정하는 것과 약간 다르다.
state를 설정하여 React에게무엇을 할 지
를 지시하는 대신,
이벤트 핸들러에서action
을 전달하여사용자가 방금 한 일
을 지정합니다.
state 업데이트 로직은 다른 곳에 있다!즉, 이벤트 핸들러를 통해
tasks를 설정
하는 대신,
task를 추가/변경/삭제
하는 action을 전달하는 것이다.
이러한 방식이 사용자의 의도를 더 명확하게 설명한다.
dispatch
함수에 넣어준 객체를action
이라고 한다.
이 객체는 일반적인 JavaScript 객체이다.
여기에 무엇을 넣을지는 개발자가 결정하지만,
일반적으로 무슨 일이 일어났는지 에 대한 최소한의 정보(type 속성)를 포함해야 한다.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, }); }
reducer 함수에 state 업데이트 로직을 둘 수 있다.
두 개의 매개변수를 가지는데, 하나는현재 state
이고 하나는action 객체
이다.
그리고 이 함수가변경될 state를 반환
한다.
if-else 문을 사용해도 괜찮으나 react 공식문서에서는 가독성 상 Switch 문을 권장하고 있다.
그리고
case 블럭을 모두 중괄호 { }로 감싸는 걸 권장한다.
이렇게 하면 다양한 case들 안에서 선언된 변수들이 서로 충돌하지 않는다.블록 레벨 스코프
또한,
하나의 case는 보통 return으로 끝나야한다.
만약 return을 잊는다면 이 코드는 다음 case에 빠지게 될 것이고,
이는 실수로 이어질 수 있다.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); } } }
마지막으로, 컴포넌트에 tasksReducer를 연결해야 한다.
React에서 useReducer Hook을 import해야한다.import { useReducer } from 'react';
그런 다음 useState 대신
useReducer
로 바꾼다.// const [tasks, setTasks] = useState(initialTasks); const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer Hook은
reducer 함수
와초기 state
두 개의 인자를 받는다.
그리고
state값
과dispatch 함수 (사용자의 action을 reducer에 “전달”해주는 함수)
를 반환한다.
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.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},
];
export default 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);
}
}
}
일반적으로 useState를 사용하면 미리 작성해야 하는 코드가 줄어든다.
useReducer를 사용하면 reducer 함수 와 action을 전달하는 부분 모두 작성해야 한다.하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우,
useReducer를 사용하면 코드를 줄이는 데 도움이 될 수 있다.
useState로 간단한 state를 업데이트 하는 경우 가독성이 좋다.
그렇지만 state의 구조가 더욱 복잡해지면,
컴포넌트의 코드의 양이 부풀어 오르고 한눈에 읽기 어려워질 수 있다.이 경우
useReducer
를 사용하면 업데이트 로직이 어떻게 동작 하는지와
이벤트 핸들러를 통해 무엇이 일어났는지 를 깔끔하게 분리할 수 있습니다.
useState에 버그가 있는 경우, state가 어디서 잘못 설정되었는지,
그리고 왜 그런지 알기 어려울 수 있다.
useReducer를 사용하면, reducer에 콘솔 로그를 추가하여
모든 state 업데이트와 왜 (어떤 action으로 인해) 버그가 발생했는지 확인할 수 있다.
각 action이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 알 수 있다.
하지만 useState를 사용할 때보다 더 많은 코드를 살펴봐야 한다.
reducer는 컴포넌트에 의존하지 않는 순수한 함수이다.
즉, 별도로 분리해서 내보내거나 테스트할 수 있다.
일반적으로 보다 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만,
복잡한 state 업데이트 로직의 경우,
reducer가 특정 초기 state와 action에 대해
특정 state를 반환한다고 단언하는 것이 유용할 수 있다.
어떤 사람은 reducer를 좋아하고 어떤 사람은 싫어한다.
괜찮다. 취향의 문제다.
useState 와 useReducer는 언제든지 앞뒤로 변환할 수 있으며, 서로 동등하다!
Reducer
란 State를 여러 방식으로 업데이트 하는공간
이다.
Dispatch
란 State 업데이트를제어하는 수단 그자체(동사)
이다.
Action
이란 Dispatch의 내용, 즉요구의 내용
이다.이때 Action은 자유 형식의 일반적인 객체이다.
그리고, 어떤 내용인지를 정의하는 type 속성은 반드시 있어야 한다.
즉,
Reducer
에는 상태를 여러 방식으로 업데이트하기 위한 로직들을 가지고 있으며
Dispatch
로 Reducer에 있는 로직을 실행할 수 있다.단, Reducer는 여러 로직을 가지고 있기 때문에 하나의 로직을 실행하기 위해서
Action 객체
로 어떤 로직을 수행할 것인지 Dispatch에게 알려주어야 한다.
useReducer
란상태
와상태를 제어하기 위한 수단
을 만들어주는(반환
) React 훅이다.
이를 만들어 내기 위해서 는상태를 업데이트하는 수단을 만들어 받아들일 수 있는 공간
과
상태의 초기값
을 지정(파라미터 지정
)해줘야한다.이때 공간이 하는 역할의 코드는 파일 분리를 해서 관리할 수 있다.
이러한 흐름으로 다음과 같은 사용법이 만들어진다.const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);