챌린지반 과제로.. 과제로 나온 Context, RTK 외에 useReducer도 다뤄보자는 의미로 투두리스트 만들기 과제가 생겼다
ㅎㅎㅎ 개인과제를 조금 욕심내서 기능구현 위주로 빨리 해보자고 목표를 잡은게 다행이었던 것 같다..
Reducer
: 여러곳으로 흩어져있는 state 관리 로직을 단일 함수로 통합해 관리하기위해 사용
즉, state를 다루는 방법 중 하나다!
useState에서 useReducer로 변경하기
1. state를 설정하는 것에서 action을 dispatch로 전달하는 것으로 바꾸기
- setState를 사용해야하는 부분에서 setState로 직접 변경하는 것이 아니라 action을 전달해 어떤 형식으로 업데이트 할지 reducer에게 전달
- 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)
);
}
// 변경 후
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
});
}
2. reducer함수 작성하기
- state를 처리할 로직을 담을 함수
- 이 함수는 현재 state값, 전달받은 action 객체 두 인자를 받고 변경된 state값을 반환
function yourReducer(state, action) {
return {action에따라 변환된 state}
}
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);
}
}
3. 컴포넌트에서 reducer 사용하기
- 컴포넌트에서 useReducer hook을 임포트
import { useReducer } from 'react';
- useState를 사용할 부분에서 useReducer로 변경
- useReducer는 reducer 함수, 초기 state값을 인자로 받는다
- useReducer는 state를 담을 값, dispatch 함수를 반환한다
// useState에서
const [tasks, setTasks] = useState(initialTasks);
// useReducer로 변경
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
▼ 완성된 예제
// App.jsx
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 },
];
// tasksReducer.js
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 비교
1) 코드크기
- useReducer는 사용하기위해 작성해야하는 코드 양이 많지만, 여러 곳에서 state를 업데이트하는 경우 useState를 사용했을 때보다 코드의 양이 줄어들 수 있다.
2) 가독성
- 간단한 state 업데이트는 useState가 좋다
- 하지만 복잡한 구조의 state를 다루게되면 컴포넌트 내 코드의 양이 많아져 한눈에 읽기 어려워질 수 있다.
- 따라서 useReducer를 이용해 이벤트 핸들러와 state 업데이트 로직을 분리하면 도움이 될 수 있다
3) 디버깅
- useState는 에러가 발생했을 때 잘못된 곳을 찾기 어려울 수 있음
- useReducer를 사용하면 console.log등을 활용해 어떤 action에서 버그가 발생하는지 확인할 수 있음
- 하지만 useState보다 더 많은 단계를 살펴봐야하는 단점도 있다
reducer 작성시 주의할 점
1) reducer는 순수함수여야 함
- 입력이 같다면 결과가 항상 동일해야 함
- 외부 컴포넌트에 영향을 미치는 작업을 수행해선 안됨
- object, array를 변이없이 업데이트해야한다 (immutable)
2) 각 action은 데이터 안에서 여러 변경이 있어도 하나의 사용자 상호작용을 설명해야 함
- 예를들어 5개의 입력창이 있고, 그를 재설정하는 버튼을 누른 경우 하나의 입력창에 대한 state 변경을 5번 반복하는 것보다 전체 입력창을 초기화하는 동작을 하나 실행하는 것이 좋음
- 의도를 파악하기 쉽고, 디버깅에 도움됨
Reducer + Context
useReucer로 반환받은 state, reducer함수는 하위 함수에서 사용하려면 다시 props로 내려줘야 함
불필요한 props 전달을 방지하기위해 context를 사용할 수 있음
Reducer와 context를 결합하는 방법
1. Context 생성
- reducer를 사용하기위해 두가지 context를 생성해야 함
- 관리할 state (현재 예제에서 task 리스트), 컴포넌트에서 action을 전달할 dispatch 함수
App.js
// App.js
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>Day off in Kyoto</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
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);
}
}
}
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 }
];
TaskContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
AddTask.js
import { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
onAddTask(text);
}}>Add</button>
</>
)
}
TaskList.js
import { useState } from 'react';
export default function TaskList({
tasks,
onChangeTask,
onDeleteTask
}) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
onChange({
...task,
text: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
onChange({
...task,
done: e.target.checked
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>
Delete
</button>
</label>
);
}
2. State와 dispatch 함수를 context에 넣기
useReducer에서 반환된 tasks 및 dispatch를 가져와 돔 트리 전체에 전달할 수 있도록 Provider로 감싸기
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
3. 트리 안에서 context 사용하기
위 단계까지는 task, dispatch 함수를 props로 전달해줘야하지만, context를 이용하면 전달할 필요가 없다!
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
useContext를 이용해 받아주면 된다
state는 여전히 최상위 컴포넌트에서 useReducer로 관리되고 있지만, props로 내려주지 않아도 필요한 컴포넌트에서 context를 읽어와 사용할 수 있다
하나의 파일로 정리하기
TaskContext.js의 경우 두개의 createContext를 이용해 두개의 context를 선언하는 것이 끝이다
그래서 reducer와 함께 하나의 파일로 관리하기도 한다
TaskProvider는 아래 역할을 한다
- Reducer로 state 관리
- 두 context를 하위 컴포넌트에 제공
- children을 prop으로 받음
import { createContext, useReducer } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
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);
}
}
}
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 }
];
추가로 컨텍스트를 사용하기위한 커스텀 훅도 TasksContext.js에서 관리해도 좋다
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
참고자료
https://ko.react.dev/learn/extracting-state-logic-into-a-reducer
https://ko.react.dev/learn/scaling-up-with-reducer-and-context
'챌린지반' 카테고리의 다른 글
[React] useState, useEffect 동작 만들어보기 (0) | 2024.08.21 |
---|---|
[React] You might not need an effect를 읽고 프로젝트 effect 수정하기 (0) | 2024.08.20 |
[React] CRA, Vite 없이 개발환경 구축하기 (0) | 2024.08.19 |
[React] 리액트 훅, 리액트를 함수로 분석하기 (0) | 2024.08.14 |