컴포넌트는 4가지와 같은 상황에서 리렌더링이 발생한다.
- 자신이 전달받은 props가 변경될 때
- 자신의 state가 바뀔 때
- 부모 컴포넌트가 리렌더링될 때
- forceUpdate 함수가 실행될 때
컴포넌트의 개수가 많지 않다면 모든 컴포넌트를 리렌더링해도 느려지지 않지만,
약 2000개가 넘어가면 성능이 저하된다.
따라서, 컴포넌트 리렌더링 성능을 최적화해 주는 작업을 진행해야한다.
1. React.memo를 사용하여 컴포넌트 성능 최적화
- React.memo를 쓰기 전과 후 성능 비교
// TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline
} from 'react-icons/md';
import './TodoListItem.scss';
import cn from 'classnames';
const TodoListItem = ({ todo, onRemove, onToggle }) => {
const { id, text, checked } = todo;
return (
<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>
);
};
// 코드 수정
export default React.memo(TodoListItem);
Render duration을 보면 React.memo를 쓰기 전에는 773.5ms 였고, 쓴 후에는 590.2ms로 줄어 든 것을 볼 수 있다.
2. useState의 함수형 업데이트 or useReducer 사용
- useState의 함수형 업데이트를 사용했을 때 성능 비교
// App.js
import { useCallback, useRef, useState } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: `할 일 ${i}`,
checked: false,
})
}
return array;
}
function App() {
const [todos, setTodos] = useState(createBulkTodos)
const nextId = useRef(2501);
// 추가
const onInsert = useCallback(
text => {
const todo = {
id: nextId.current,
text,
checked: false,
}
// 코드 수정
setTodos(todos => todos.concat(todo))
nextId.current += 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;
590.2ms에서 25.5ms로 줄었다.
- useReducer 사용했을 때 성능 비교
// App.js
import { useCallback, useReducer, useRef } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';
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;
}
}
function App() {
// const [todos, setTodos] = useState(createBulkTodos)
const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);
const nextId = useRef(2501);
// 추가
const onInsert = useCallback(
text => {
const todo = {
id: nextId.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;
590.2ms에서 27ms로 줄었다.
결론적으로는 useState의 함수형으로 업데이트 하거나 useReducer을 사용하여 성능을 최적화할 수 있다.
useReducer은 기존 코드를 많이 고쳐야하는 단점이 있지만, 상태를 업데이트하는 로직을 모아 컴포넌트의 밖에 둘 수 있다는 장점이 있다.
개인적으론, useState의 함수형으로 업데이트 하는 것이 편한 것 같다.
3. react-virtualized를 통한 렌더링 최적화
react-virtualized를 사용하면, 리스트 컴포넌트에서 스크롤 되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있다.
yarn을 이용해서 설치부터 해준다.
$ yarn add react-virtualized
- react-virtualized를 사용했을 때 성능 비교
// TodoList.js
import React, { useCallback } from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss'
// 코드 수정
import {List} from 'react-virtualized'
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'}} // List에 기본 적용되는 스타일
/>
);
};
export default React.memo(TodoList);
이 부분만 수정하면 style이 다 깨진다.
TodoList안에 있는 TodoListItem도 같이 스타일을 수정해주어야한다.
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline
} from 'react-icons/md';
import './TodoListItem.scss';
import cn from 'classnames';
// 코드 수정
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);
react-virtualized를 사용 전에는 25.5ms였는데, 사용 후에 15.3ms로 렌더링 속도가 빨라졌다.
Intersection Observer로 무한 스크롤 구현하는 방법을 알고 있었는데, 다음에는 react-virtualized로 무한스크롤을 도전해봐야겠다.
참고
도서 - 리액트를 다루는 기술
'기술 개발 > React' 카테고리의 다른 글
날짜 라이브러리 비교(Day.js, Moment.js, date-fns, Luxon) (0) | 2023.02.10 |
---|---|
Data Fetching Library: React-Query, SWR (0) | 2023.01.31 |
React hooks (useRef) (0) | 2023.01.08 |
React hooks (useCallback) (0) | 2023.01.08 |
React Hooks (useMemo) (0) | 2023.01.06 |