성능을 분석할 때는 느낌만으로는 충분하지 않다.
정확히 시간이 얼마나 걸렸는지 확인해야 하는데 React DevTools를 사용하여 측정하면 된다.
Profiler 탭을 통해서 성능 분석을 할 수 있다.
Render duration: 리렌더링에 소요된 시간
많은 컴포넌트들이 리렌더링 됐고, 변화를 일으킨 컴포넌트와 관계없는 컴포넌트들도 리렌더링 됐다.
체크리스트를 클릭한 경우에는 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링 된 것이다.
부모 컴포넌트가 리렌더링 되었으니 TodoList 컴포넌트가 리렌더링되고 그 안의 많은 item들이 리렌더링 됐다.
컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 리렌더링 성능을 최적화해줄 수 있다.
TodoListItem.js
const TodoListItem = ({ todo, onRemove, onToggle }) => {
};
export default React.memo(TodoListItem);
기존에 사용하던 코드에서 export 할 때 TodoListItem 컴포넌트를 React.memo로 감싸준다.
이후에는 자신이 전달받은 props(todo, onRemove, onToggle)가 바뀌지 않으면 리렌더링을 하지 않는다.
배열이 업데이트되면 특정 함수도 새롭게 바뀐다.
특정 함수들이 배열 상태를 업데이트하는 과정에서 최신 상태의 배열을 참조하기 때문에 배열이 바뀔 때마다 함수가 새로 만들어진다.
함수가 만들어지는 상황을 방지하는 방법
1. useState의 함수형 업데이트 기능 사용
2. useReducer 사용
const onRemove = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos],
);
const onRemove = useCallback((id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
}, []);
setTodos를 사용할 때 안에 todos =>를 넣어줘서 업데이트 함수로 만들어준다.
성능이 크게 향상된 모습을 볼 수 있다.
App.js
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':
return todos.concat(action.todo);
case 'REMOVE':
return todos.filter((todo) => todo.id !== action.id);
case 'TOGGLE':
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, //Ref를 사용했기 때문에 .current를 사용할 수 있다.
text,
checked: false,
};
dispatch({ type: 'INSERT', todo });
nextId.current += 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;
useReducer를 사용할 때는 두 번째 파라미터에 초기 상태를 넣어주어야 한다.
현재는 두 번째 파라미터에 undefined를 넣고 세 번째 파라미터에 초기 상태를 만들어주는 createBulkTodos를 넣어주었다.
useReducer를 사용하면 기존 코드를 많이 고쳐야 한다는 단점이 있다.
하지만 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있다.
성능상 useState 함수형 업데이트와 비슷하기 때문에 골라서 사용하면 된다.
불변성을 지키기 위해서 기존 데이터를 수정할 때 직접 수정하지 않고, 새로운 배열을 만든 다음에 새로운 객체를 만들어서 필요한 부분을 교체해주는 방식으로 구현했다.
새롱 배열 혹은 객체를 만들기 떄문에 React.memo를 사용했을 때 props 변화의 유무를 알아내서 리렌더링 성능을 최적화해줄 수 있다.
불변성을 지킨다: 기존의 값을 직접 수정하지 않고 새로운 값을 만들어 낸다.
불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.
리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트도 최적화해야 하고, 리스트로 사용되는 컴포넌트 자체도 최적화 해주는 것이 좋다.
리스트와 리스트 아이템 컴포넌트 둘다 최적화하는 것이 중요하다.
React.memo를 사용해서 간단하게 최적화를 진행할 수 있다.
$yarn add react-virtualized
TodoList.js
import React, { useCallback } from 'react';
import { List } from 'react-virtualized';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
const TodoList = ({ todos, onRemove, onToggle }) => {
const rowRenderer = useCallback(
({ index, key, style }) => {
const todo = todos[index];
return (
<TodoListItem
todo={todo}
key={key}
onRemove={onRemove}
onToggle={onToggle}
style={style}
/>
);
},
[onRemove, onToggle, todos],
);
return (
<List
className="TodoList"
width={512}
height={513}
rowCount={todos.length}
rowHeight={57}
rowRenderer={rowRenderer}
list={todos}
style={{ outline: 'none' }}
/>
);
};
export default React.memo(TodoList);
List 컴포넌트를 사용하기 위해 rowRenderer라는 함수를 새로 작성해준다.
이 함수는 List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용한다.
이 함수를 List컴포넌트의 props로 설정해주어야 한다.
이 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아와서 사용한다.
TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo, onRemove, onToggle, style }) => {
const { id, text, checked } = todo;
return (
<div className="TodoListItem-virtualized" style={style}>
<div className="TodoListItem">
<div
className={cn('checkbox', { checked })}
onClick={() => onToggle(id)}
>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
</div>
);
};
export default React.memo(TodoListItem);