준영이의 성장일기(FrontEnd)
React 공식문서 도장깨기(10) - useReducer 본문
이번 9월 13일에 정리할 내용은 useReducer이다. 어느덧 챕터로 따지면 10번째 챕터이다. 계속 꾸준하게 주에 3회는 정리하는 시간을 가져보자 ㅎㅎ
<참고 레퍼런스>
https://react-ko.dev/reference/react/useReducer
useReducer – React
The library for web and native user interfaces
react-ko.dev
<useReducer>
개념: useReducer 는 컴포넌트에 reducer를 추가할 수 있는 React hook이다. 사용형태는 다음과 같다.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
state: 초기 state
dispatch: state를 다른 값으로 업데이트하고 리렌더링을 촉발하는 dispatch function
✅ reducer
state가 업데이트되는 방식을 지정하는 reducer 함수이다. 순수 함수여야 하며, state와 액션을 인자로 받아야 하고, 다음 state를 반환한다.
✅ initialArg
초기 state를 의미한다.(ex. {age : 42} ).
✅ init? (Optional 한 요소이고 필수는 아님)
초기 state 계산 방법을 지정하는 초기화 함수로 설정되어 있지 않다면 initialArg에 있는 값이 초기 state이다. 여기에 초기화 함수를 설정할 수 있는데 함수 자체인 초기화 함수를 전달함으로써 state의 재생성을 막을 수 있다.(이전에 useState 내용에서 정리했었음)
➡️ 초기화 함수가 초기 state를 계산하는데 필요한 정보가 userName이므로 initialArg에 설정 해뒀지만 만약 필요 없다면 null로 설정하면 된다.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
<dispatch>
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
useReducer 가 반환하는 dispatch 함수를 사용하면 state를 다른 값으로 업데이트하고 다시 렌더링을 촉발한다. 그리고 dispatch 함수에 유일한 인자로 액션을 넘긴다. 보통 액션으로 type 객체를 넘긴다.
즉, 화면에 표시되는 내용을 업데이트하려면 사용자가 수행한 작업을 나타내는 객체, 액션을 사용한다. dispatch 함수를 사용한 뒤 업데이트 된 상태를 확인할 때 만약 이벤트 핸들러 내부에서 dispatch를 호출했다면 console.log를 해도 업데이트 된 상태로 출력되지 않는다.(이유: state는 스냅샷으로 동작하기 때문이다. state를 업데이트하면 새 state 값으로 다른 렌더링을 요청하지만 이미 실행 중인 이벤트 핸들러의 state JavaScript 변수에는 영향을 미치지 못함)
만약 다음 state를 알고싶다면 수동으로 reducer를 호출한 뒤 결과를 확인한다.
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
<reducer 함수 작성법 - 제일 중요>
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
reducer 함수를 작성하면서 이제 state를 계산하고 반환해야하는 코드를 입력하게 되는데 주로 switch문을 사용하여 액션 타입에 따른 state를 계산 후 반환한다.
state로 배열/객체를 다루는 순간이 있는데 이때 주의할 점은 배열/객체를 직접 수정하면 안된다. 스프레드 연산자를 활용하여 배열/객체의 속성을 복사한 뒤 새로운 배열/객체로 만들어준다.
➡️ ...state를 활용해 모든 state 복사한 뒤 age를 업데이트
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Instead, return a new object
return {
...state,
age: state.age + 1
};
}
밑에 있는 코드는 Todolist에 태스크를 추가/삭제/수정 하는 로직인데 추가 할 때 로직을 보면 마찬가지로 기존 tasks배열을 복사 한 뒤 새로 추가된 task와 합쳐 새로운 배열을 반환한다. (task의 구성요소는 id, text, done이고 initialTasks를 보면 파악할 수 있다.)
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
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);
}
}
}
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 }
];
리액트가 상태가 변경되었는지 감지하는 방식은 참조 비교(shallow comparison)인데. 객체의 참조가 바뀌지 않으면, 리액트는 해당 상태가 변경되지 않았다고 생각한다. 만약 스프레드 연산자를 활용한 복사를 사용하지 않고 기존 객체를 직접 수정한다면 새로운 참조가 생성되지 않기 때문에 리액트는 상태가 변경된 것을 감지하지 못하고, 컴포넌트를 리렌더링 하지 않는다. 즉, 리액트는 불변성을 지켜줘야 하기 때문에 새로운 참조값을 가진 배열이나 객체를 생성해주는 것이다.(리액트의 상태 업데이트는 불변성에 따른다)
다음과 같은 코드가 좋지 못한 코드이다.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object -> 참조 X
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object -> 참조 X
state.name = action.nextName;
return state;
}
// ...
}
}
'프론트엔드 > React.js' 카테고리의 다른 글
useDeferredValue를 활용한 교재 제목 중복검사 API 연동 (1) | 2024.11.30 |
---|---|
React 공식문서 도장깨기(11) - React.lazy() (7) | 2024.09.16 |
React 공식문서 도장깨기(9) - useDeferredValue (2) | 2024.09.11 |
React 공식문서 도장깨기(8) - Suspense 컴포넌트 (3) | 2024.09.09 |
React 공식문서 도장깨기(7) - useContext (0) | 2024.09.09 |