✔ 사실 React.memo를 사용하는 것만으로 컴포넌트 최적화가 끝나지는 않는다. todos 배열이 업데이트되면 onRemove와 onToggle 함수도 새롭게 바뀌기 때문...
✔ onRemove와 onToggle 함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다.
이렇게 함수가 계속 만들어지는 상황을 방지하는 두 가지 방법이 있다.
👉 ① useState의 함수형 업데이트 기능을 사용하는 것
👉 ② useReducer를 사용하는 것
기존에 setTodos 함수를 사용할 때 새로운 상태를 파라미터로 넣어 주었다.
setTodos를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있는데 이를 함수형 업데이트라고 한다.
const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킵니다.
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1),
[],
);
✔ setNumber(number+1)을 하는게 아니라,
✔ 위 코드처럼 어떻게 업데이트할지 정의해 주는 업데이트 함수를 넣어준다.
✔ 그러면 useCallback을 사용할 때, 두 번째 파라미터로 넣는 배열에 number를 넣지 않아도 된다.
import React, { useRef, useState, useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: `할 일 ${i}`,
checked: false,
});
}
return array;
}
const App = () => {
const [todos, setTodos] = useState(createBulkTodos);
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(4);
const onInsert = useCallback(text => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos => todos.concat(todo));
nextId.current += 1; // nextId 1씩 더하기
}, []);
const onRemove = useCallback(id => {
setTodos(todos => todos.filter(todo => todo.id != = id));
}, []);
const onToggle = useCallback(id => {
setTodos(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo,
),
);
}, []);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
);
};
export default App;
useState의 함수형 업데이트를 사용하는 대신, useReducer를 사용해도 onToggle과 onRemove가 계속 새로워지는 문제를 해결할 수 있다.
import React, { useReducer, useRef, useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: `할 일 ${i}`,
checked: false,
});
}
return array;
}
function todoReducer(todos, action) {
switch (action.type) {
case 'INSERT': // 새로 추가
// { type: 'INSERT', todo: { id: 1, text: 'todo', checked: false } }
return todos.concat(action.todo);
case 'REMOVE': // 제거
// { type: 'REMOVE', id: 1 }
return todos.filter(todo => todo.id != = action.id);
case 'TOGGLE': // 토글
// { type: 'REMOVE', id: 1 }
return todos.map(todo =>
todo.id === action.id ? { ...todo, checked: !todo.checked } : todo,
);
default:
return todos;
}
}
const App = () => {
const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(2501);
const onInsert = useCallback(text => {
const todo = {
id: nextId.current,
text,
checked: false,
};
dispatch({ type: 'INSERT', todo });
nextId.current += 1; // nextId 1씩 더하기
}, []);
const onRemove = useCallback(id => {
dispatch({ type: 'REMOVE', id });
}, []);
const onToggle = useCallback(id => {
dispatch({ type: 'TOGGLE', id });
}, []);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
);
};
export default App;
지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 상태를 만들어 주는 함수인 createBulkTodos를 넣었음. 이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출됨.
✔ useReducer를 사용하는 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있음.
✔ 성능상으로는 두 가지 방법이 비슷하기 때문에 어떤 방법을 선택할지는 여러분의 취향에 따라 결정하면 됨.