[React] 리액트를 다루는 기술 - 11장 컴포넌트 성능 최적화
10장에서 만들었던 todo-app
에서 데이터가 폭증하면 앱이 느려지겠지? 성능 최적화 전에 실제로 랙(lag
)을 경험할 수 있도록 많은 데이터를 렌더링해보자.
App.js
를 다음과 같이 수정해 데이터 2500개를 생성하는 함수를 통해 todos
를 설정했다.
주의
useState(createBulkTodos())
리렌더링될 때마다 함수 호출
useState(createBulkTodos)
처음 렌더링될 때만 함수 호출
//...
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);
const nextId = useRef(2501);
const onInsert = useCallback(
//...
확실히 느려진 것이 보인다. 근데 원인이 뭘까?
컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.
1. 자신이 전달받은props
가 변경될 때
2. 자신의state
가 바뀔 때
3. 부모 컴포넌트가 리렌더링될 때
4.forceUpdate
함수가 실행될 때
이 상황에서는 항목을 체크할 경우 state
가 변경되면서 App
컴포넌트가 리렌더링되기 때문에 자식 컴포넌트인 TodoList
컴포넌트 또한 리렌더링되고 그 안의 무수한 컴포넌트들도 리렌더링 된다. 체크되지 않는 컴포넌트들까지 리렌더링 되는 것은 불필요하기 때문에 이를 방지하기 위해 최적화가 필요한 것이다.
React.memo를 사용하여 컴포넌트 성능 최적화하기
컴포넌트의 리렌더링을 방지할 때는 shouldComponentUpdate
라는 라이프사이클을 사용하면 된다. 하지만 이를 사용할 수 없는 함수형에서는 어떻게 해야 할까?
그럴 때 쓸 수 있는 것이 바로 React.memo
함수이다. 컴포넌트의 props
가 바뀌지 않았다면 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능의 최적화를 돕는다.
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 }) => {
(...)
};
export default React.memo(TodoListItem);
TodoListItem
컴포넌트를 React.memo()
로 감싸줌으로써 todo
, onRemove
, onToggle
이 바뀌지 않으면 리렌더링을 하지 않도록 만들었다. 하지만 현재 프로젝트에서는 todos
배열이 업데이트되면 onRemove
와 onToggle
함수도 새롭게 바뀌기 때문에 React.memo
로는 부족하다.
1. useState
의 함수형 업데이트
setTodos
를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있는데, 이를 함수형 업데이트라고 한다.
// 사용 예시
const [number, setNumber] = useState(0);
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1),
[],
);
setNumber(number+1)
을 하는 것이 아니라, 위 코드처럼 어떻게 업데이트할지 정의해 주는 업데이트 함수를 넣어 주면 useCallback
안에서 이를 사용할 때 두 번째 파라미터인 배열에 number
를 넣지 않아도 된다. 이와 같이 코드를 바꾸어 주자.
2. useReducer
사용하기
함수형 업데이트 대신 useReducer
를 사용해도 onToggle
과 onRemove
가 계속 새로워지는 문제를 해결할 수 있다.
App.js
import React, { useReducer, useRef, useCallback }from 'react';
...
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);
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(
...
useReducer
를 사용할 때는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 한다. 지금은 두 번째 파라미터에 undefined
를 넣고, 세 번째 파라미터에 초기 상태를 만들어 주는 함수인 createBulkTodos
를 넣어 준다. 이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos
함수가 호출됩니다.
useReducer
를 사용하는 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있다.
react-virtualized를 사용한 렌더링 최적화
초기 데이터를 2,500개 등록했는데, 실제 화면에 나오는 항목은 아홉 개뿐이고 나머지는 스크롤해야만 볼 수 있다. 스크롤하기 전에는 보이지도 않을 컴포넌트들이 미리 렌더링 되는 것은 비효율적이라고 할 수 있다.
react-virtualized
를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있다.
react-virtualized
에서 제공하는 List
컴포넌트를 사용하여 최적화를 진행하는데, 사전에 각 항목의 실제 크기를 px 단위로 알아내야 한다. 이는 개발자 도구에서 확인할 수 있다. (할 일 1은 위쪽 테두리가 없기 때문에 2 아래로 재야 한다)
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' }} // List에 기본 적용되는 outline 스타일 제거
/>
);
};
export default React.memo(TodoList);
List
컴포넌트를 사용하기 위해 rowRenderer
라는 함수를 새로 작성해야 한다. 이 함수는 react-virtualized
의 List
컴포넌트에서 각 TodoItem
을 렌더링할 때 사용하며, 이 함수를 List
컴포넌트의 props
로 설정해 주어야 한다. 이 함수는 파라미터에 index, key, style
값을 객체 타입으로 받아 와서 사용합니다.
List
컴포넌트를 사용할 때는 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 렌더링할 때 사용해야 하는 함수, 그리고 배열을 props
로 넣어 주면 전달받은 props
를 사용하여 자동으로 최적화해 준다.
TodoListItem.js
...
const TodoListItem = ({ todo, onRemove, onToggle, style }) => {
const { id, text, checked } = todo;
return (
<div className="TodoListItem-virtualized" style={style}>
<div className="TodoListItem">
...
</div>
</div>
);
};
...
render
함수에서 기존에 보여 주던 내용을 div
로 한 번 감싸고, 해당 div
에는 TodoListItem-virtualized
라는 className
을 설정하고 props
로 받아 온 style
을 적용시킨다.
TodoListItem.scss
기존의 & + &, &:nth-child(even)를 사용하여 다른 배경 색상을 주는 코드를 지우고, 코드 최상단에 다음 코드를 삽입했다.
.TodoListItem-virtualized {
& + & {
border-top: 1px solid #dee2e6;
}
&:nth-child(even) {
background: #f8f9fa;
}
}
Author And Source
이 문제에 관하여([React] 리액트를 다루는 기술 - 11장 컴포넌트 성능 최적화), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dazzlynn/React-리액트를-다루는-기술-11장-컴포넌트-성능-최적화저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)