10장에서 만든 애플리케이션을 최적화 해보자
11.1 많은 데이터 렌더링 하기
function creatBulkTodos() {
const array = [];
for (let i = 0; i <= 2500; i++){
array.push({
id: i,
text: `할 일 ${i}`,
checked: false,
});
}
return array;
}
const App = () => {
const [todos, setTodos] = useState(creatBulkTodos);
//고윳값으로 사용될 id
//ref를 사용하여 변수 담기
const nextId = useRef(2501);
const onInsert = useCallback(
text => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos => todos.concat(todo));
nextId.current += 1; // nextId 1씩 더하기
},
[],
);
(...)
위의 코드에서 useState에 creatBulkTodos 함수를 파라미터로 전달했다.
이때 함수에 ()를 붙히고 전달하면 리랜더링될 때만 함수가 호출되지만
지금은, ()를 붙히지 않고 전달하여 처음 랜더링될 때만 함수가 실행된다.
11.2 크롬 개발자 도구를 이용한 리랜더링
React DevTools를 이용해서 Profiler에서 리랜더링 소요시간을 확인 가능하다.
(크롬은 검사에 Profiler에서 확인 가능하다)
11.3 느려지는 원인 분석
컴포넌트가 리랜더링 된느 상황은 아래와 같다
1) 자신이 전달받은 props가 변경될 때
2) 자신의 state가 변결될 때
3) 부모 컴포넌트가 리렌더링될 때
4) forceUpdate 함수가 실행될 때
11.4 React.memo를 사용한 컴포넌트 성능 최적화
class형 커포넌트에서는 shouldComponentUpdate 라이프사이클을 사용하면 되지만,
함수형 컴포넌트는 하이프사이클 메서드가 없기에 React.memo를 사용한다.
컴포넌트 props가 바뀌지 않았다면 리렌더링 없이 설정하여 함수컴포넌트의 리랜더링 최적화가 가능하다.
예시⬇⬇⬇
import React from 'react';
import {
MdOutlineCheckBoxOutlineBlank,
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 /> : <MdOutlineCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={()=>onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
</div>
);
};
//React.memo 함수를 사용해서 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링 하지 않게 했다.
export default React.memo(TodoListItem);
//하지만 todos배열이 업데이트 되면 onRemove와 onToggle 함수도 새롭게 바뀌면 최신 상태의 todos를
//참조하기에 새로운 배열이 만들어진다.
//useState 혹은 usrReducer를 이용해 새로운 배여 생성을 막을 수 있다.(App.js 참고)
11.5 onToggle, onRemove 함수가 바뀌지 않게 하기
현재는 todos가 업데이트 되면 onToggle과 onRemove 함수도 새롭게 바뀐다.
두 함수는 배열 상태 업데이트 과정에 최신 상태의 todos를 참고하기에 todos 배열이
바뀔때 마다 함수가 새로 만들어진다.
이를 방지하는 방법 2가지가 있다.
1) useState 사용
useState를 사용한 함수형 업데이트
state관리 함수 사용시 상태 파라미터를 넣는 대신, 상태 업데이트의 방법을 정해주는 업데이트 함수를 넣어준다.
➡함수형 업데이트
import { useState, useRef, useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
function creatBulkTodos() {
const array = [];
for (let i = 0; i <= 2500; i++){
array.push({
id: i,
text: `할 일 ${i}`,
checked: false,
});
}
return array;
}
const App = () => {
const [todos, setTodos] = useState(creatBulkTodos);
//고윳값으로 사용될 id
//ref를 사용하여 변수 담기
const nextId = useRef(2501);
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,
), // 불병성을 유지하면서 특정 배열 원소를 업데이트 할때 map을 사용해 쉽게 작성이 가능하다.
// todo.id와 파라미터 id 값이 같을 때 규칙대로 새로운 객체를 생성하지만 다를경우 받은 상태 그대로 반환한다.
// 그래서 map을 사용해 만든 배열에서 변화가 필요한 원소만 업데이트 한 후 나머지는 그대로 남는다.
);
},
[],
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
</TodoTemplate>
);
};
export default App;
state관리 함수를 사용하는 곳에 화살표 함수만 넣어주면 된다(위에서는 todos => 이다)
useCallback의 두번째 파라미터는 빈 배열을 전달할 수 있다.
2) useReducer 사용
useReducer를 사용하기
import { useReducer, useRef, useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
function creatBulkTodos() {
const array = [];
for (let i = 0; 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, creatBulkTodos);
//고윳값으로 사용될 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;
useReduce의 두번째 파라미터는 초기상태를 넣어야 하지만,
undefined를 넣고 세번째 파라미터에 초기값을 넣으면 컴포넌트가 맨 처음 랜더링될 때만 함수가 호출된다.
useReducer를 사용하면 기존의 코드를 많이 고쳐야 하는 단점이 있지만,
상태 업데이트 로직을 모아 컴포넌트 외부에 둬서 수정이 편리하다는 장점이 있다.
11.6 불변성의 중요성
리액트 컴포넌트 업데이트시 불변성을 지키는 것이 중요하다.
업데이트가 필요한 곳에서는 아예 새로운 배열 혹은 객체를 만들기 때문에,
React.memo를 사용했을 때 props가 바뀌었는지 안바뀌었는지 알아내서 리렌더링 성능을 최적화할 수 있다.
불변성이 지켜지지 않으면, 객체 내부 값이 바뀌어도 감지가 안된다.
➡➡React.memo에서 서로 비교하여 최적화가 안된다.
펼침연산자는 얕은 복사 이기에 중복 객체면 새값을 할당해주어야 한다.
const todos = [{ id:1, checked: true }, { id:2, checked: true }];
const nextTodos = [...todos];
nextTodos[0].checked = false;
console.log(todos[0] === nextTodos[0]); // 아직까지는 같은 객체를 가르켜 true
nextTodos[0] = {
...nextTodos[0],
checked: false
};
console.log(todos[0] === nextTodos[0]); // 새로운 객체를 할당해 주었기에 false
불변성을 지키면서 새 값을 항당해야 함
const nextObject = {
...Object,
objectInside: {
...Object.objectInside,
checked: false,
}
};
console.log(Object === nextObject); // false
console.log(Object.objectInside === nextObject.objectInside); // false
11.7 TodoList 컴포넌트 최적화
리스트 관련 컴포넌트 최적화시 리스트 내부에서 사용하는 컴포넌트도 최적화 해야하고,
리스트로 사용되는 컴포넌트 자체도 최적화 해주는 것이 좋다.
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 스타일 제거
/>
);
};
//React.memo를 통한 최적화
export default React.memo(TodoList);
//리스트에 관련괸 컴포넌트를 최적화시 리스트 내부에서 사용하는 컴포넌트도 최적화 해준다.
//리스트로 사용되는 컴포넌트 자체도 최적화해 주는 것이 좋다.
//부모 컴포넌트인 App.js에서 todos배열이 업데이트될 때 리렌더링 되기에 지금 당장은 최적화 효과는 없다.
//하지만, App컴포넌트에 다른 state가 추가되어 해당 값들이 업데이트될 때는 불필요한 리렌더링이 있을 수 있기에 미리한것이다.
부모 컴포넌트가 업데이트될 때만 todo배열 업데이크가 되어 당장 효과는 없지만,
다른 state 값이 업데이트 되면 최적화 효과를 얻을 수 있다.
11.8 react-virtualized를 사용한 렌더링 최적화
위의 코드들은 전부 react-virtualized를 이용해 최적화를 마친 상태이다.
할 일들을 보여주는 영역에서 당장 안보이는 항목들을 랜더링 하지 않고 각 항목의 크기를 제고 스크롤 했을때
해당 항목이 보여지게끔 해주는 기술이다.(가상으로 구현한 것으로 생각된다.)
'React' 카테고리의 다른 글
React) 13장 리액트 라우터로 SPA개발하기 (0) | 2022.05.07 |
---|---|
React) 12장 immer 라이브러리 (0) | 2022.05.06 |
React) 10장 TodoList 프로젝트 (0) | 2022.04.17 |
React) 9장 컴포넌트 스타일링 (0) | 2022.04.16 |
React) 8장 Hooks (0) | 2022.04.12 |