리액트 최적화(2) 리스트 컴포넌트
React.memo 란?
예제
const MyComponent = React.memo(({ name }) => {
return <div>Hello, {name}!</div>;
});
❗React.memo 과정
💡 위의 MyComponent 컴포넌트에서는 name이라는 props를 사용.
💡 React.memo는 이전에 렌더링된 결과를 name 값이 변경되었는지를 비교.
💡 변경이면 재랜더링 / 그대로면 재사용.
리스트 컴포넌트 최적화
❗필수개념
💡 리스트 컴포넌트를 사용한다면 리스트 내부에서 사용하는 컴포넌트도 최적화 해야한다.(권장)
💡 리스트는 반복적으로 생성되기 때문이다. (갱신이 계속 일어난다.)
ScheduleBoard.js
import React, { useRef, useState, useCallback } from 'react';
import TodoTemplate from './TodoTemplate';
import TodoInsert from './TodoInsert';
import TodoList from './TodoList';
function createBulkTodos() {
const array = [];
for(let i=1; i<2500; i++) {
array.push( { id: i, text: `할 일 ${i}`, checked: false } );
}
return array;
}
const ScheduleBoard = () => {
const [todos, setTodos] = useState(createBulkTodos);
const nextId = useRef(todos.length);
// 삽입
// 갱신 되지지않지만 접근 방법을 자식컴포넌트에서 미리 useMemo 해놨다. 그래서 오버헤드는 없다.
const onInsert = useCallback( (text) => {
const temp = {
id: nextId.current += 1
, text: text
, checked: false
};
setTodos(todos.concat(temp));
}, []);
// 삭제
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 (
<div>
<h1>ScheduleBoard Page</h1>
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
</div>
);
};
export default ScheduleBoard;
❗이전 포스팅 처럼 최적화가 된 상태이다.
💡 여기서 리스트를 렌더링하는를 집중해서 보자.
💡 등록된 props 는 [todos], [onRemove], [onTogle] 이다.❗현재 리렌더링 조건
💡 부모 컴포넌트의 리렌더링
💡 todos 상태 변경
💡 onInsert, onRemove, onToggle 함수의 변경 시점에 리렌더링
TodoList.js
-
AS-IS
import TodoListItem from './TodoListItem'; import '../TodoList.scss'; const TodoList = ({ todos, onRemove, onToggle }) => { return ( // 리스트 상세 컴포넌트 <div className="TodoList"> {todos.map(todo => ( <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle} /> ))} </div> ); }; export default TodoList;
-
TO-BE
import React from 'react'; import TodoListItem from './TodoListItem'; import '../TodoList.scss'; const TodoList = ({ todos, onRemove, onToggle }) => { return ( // 리스트 상세 컴포넌트 <div className="TodoList"> {todos.map(todo => ( <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle} /> ))} </div> ); }; export default React.memo(TodoList); // React.Memo 사용 // todos, onRemove, onToggle 프롭스가 변경되지 않는 한 재렌더링을 방지함.
❗React.memo 기능으로 방지되는 항목
리액트 렌더링 조건과 TodoList 비교- 💡 자신이 전달받은 props가 변경될 때
- todos, onRemove, onToggle 프롭스를 전달받고 있다.
- TodoList 컴포넌트가 React.memo 기능으로 인해 위 프롭스가 변경되지 않는한 리랜더링 되지 않는다.
- 💡 자신의 state가 바뀔 때
- TodoList 컴포넌트는 상태(state)를 사용하고 있지 않으므로, 해당 상황은 발생하지 않는다.
- 💡 부모 컴포넌트가 리렌더링될 때
- 부모 컴포넌트가 리렌더링될 때마다 ScheduleBoard 컴포넌트의 내용이 재생성될 수 있다.
- 즉, 이 말은 TodoList 자식 컴포넌트도 재렌더링될 가능성이 있다.
-
참고로 현재는 부모컴포넌트가 재랜더링 수행하는 조건은 최적화를 적용 했기에 별로 없다. 아래 내용이 존재 할 수 있다. // 부모컴포넌트의 랜더링 가능성 1. 초기 마운트시점 랜더링 2. 각 함수 변경시 랜더링 (정확히는 todos 변경시 랜더링 되어짐) 3. 부모컴포넌트 리랜더링 시점에 랜더링
- 💡 forcUpdate 함수가 실행될 때
- forceUpdate 함수가 실행되면 어떤 컴포넌트던 강제로 리렌더링된다.
- 💡 자신이 전달받은 props가 변경될 때
최적화 완료(정리)
❗리스트 컴포넌트 최적화 (필수개념)
💡 (1). 부모 ScheduleBoard 컴포넌트의 onInsert, onRemove, onToggle 함수들은 모두 todos 상태가 변경될 때만 새로 생성된다.
- 비 의존성 useCallback 훅을 사용하여 함수를 최적화하고, 함수 내부에서 참조하는 외부 변수(여기서는 todos)가 변경되었을 때만 새로운 함수를 생성하도록 설정했다.
- 즉, todos 가 변경되지 않으면 함수는 랜더링되지 않는 멋진 상태로 만들어놨다.
💡 (2). 자식 TodoList 컴포넌트는 React.memo 으로 인해 프롭스가 변경되지 않는 한 재렌더링을 방지한다.
- 그런데 1번에서 무분별한 함수생성 최적화를 미리 해둔 todos 가 현재 TodoList의 프롭스로 사용되고 있다.
💡 (3). 결론적으로 ScheduleBoard 컴포넌트에서 onInsert, onRemove, onToggle 함수가 호출되어도, 전달받은 todos가 변경되지 않는 한 TodoList 컴포넌트는 재렌더링을 안한다.
- 변경되지 않은 녀석은 함수가 갱신되지 않을 것이고,
- 함수가 갱신되지 않아서 자식컴포넌트에 전달된 프롭스도 그대로라서 갱신되지 않는다.
react-virtualized 최적화
❗React.memo 과정
💡 2500개의 할 일 리스트가 있지만 사용자 눈에 보이는 갯수는 9개 정도밖에 없다.
💡 그럼 나머지 2,491개의 컴포넌트는 스크롤하기 전까지 렌더링하지 않고 크기만 던져두게끔 하는 방법이다. 💡 변경이면 재랜더링 / 그대로면 재사용.
STEP1. react-virtualized 설치
yarn add react-virtualized
STEP2. 리스트로 뿌려질 각 항목의 크기 알아내기
return (
<List
className="TodoList"
width={512} // 전체 크기
height={513} // 전체 높이
rowCount={todos.length} // 항목 개수
rowHeight={57} // 항목 높이
rowRenderer={rowRenderer} // 항목을 렌더링할 때 쓰는 함수
list={todos} // 배열
style= // List에 기본 적용되는 outline 스타일 제거
/>
);
❗react-virtualized API의 CSS 정보
💡 List 컴포넌트를 이런식으로 호출한다.
💡 해당 리스트의 css 정보를 props 으로 적용 해야한다. 💡 그러면 이 컴포넌트가 전달 받은 props를 사용하여 자동으로 최적화 해준다.
STEP3. TodoList.js
import React, {useCallback} from "react";
import {List} from 'react-virtualized';
import TodoListItem from "./TodoListItem";
import '../scss/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= // List에 기본 적용되는 outline 스타일 제거
/>
);
}
export default React.memo(TodoList);
❗React.memo 과정
💡 상단에서 임포트하면 List 컴포넌트를 불러와 사용할 수 있게 된다.
💡 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 [렌더링할 때 사용하는 함수], 그리고 [배열을 props]로 던져주면
💡 List 컴포넌트가 전달받은 props를 사용해 자동으로 최적화 해준다.그리고 각 리스트를 나타내는 TodoListItem.js를 수정해주어야 한다.
STEP4. 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);
💡 style 프롭스가 추가되었다. 누락하지 말자!
STEP4. TodoListItem.scss
//추가
.TodoListItem-virtualized {
& + & {
border-top: 1px solid #dee2e6;
}
&:nth-child(even) {
background: #f8f9fa;
}
}
❗React.memo 과정
💡 상단에서 임포트하면 List 컴포넌트를 불러와 사용할 수 있게 된다.
💡 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 [렌더링할 때 사용하는 함수], 그리고 [배열을 props]로 던져주면
💡 List 컴포넌트가 전달받은 props를 사용해 자동으로 최적화 해준다.
정리
최종 결과
💡v1 속도가 상대적으로 매우 느리다는것을 알 수 있다.
💡v2 와 v3 는 동일한 수준의 최적화를 보인다.
💡v4 는 보이는 부분만 렌더링해서 엄청나게 속도가 향상되었다.
Leave a comment