<리액트를 다루는 기술>
React - 6장 : 컴포넌트 반복
React - 7장 : 컴포넌트의 LifeCycle
React - 8장 : React Hooks 총정리
React - 11장 : 컴포넌트 성능 최적화(현재)
지난 10장에 이어서 같은 프로젝트를 이용해서 설명하므로, 10장의 프로젝트를 만든 다음에 실습해보자.
11.1 많은 데이터 렌더링해보기
- 실제로 Lag을 경험할 수 있또록 많은 데이터를 렌더링 해보자.
App.js
import {useState, useRef, useCallback} from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import './App.css';
function createBulkTodos(){
const array = [];
for(let i =1; i<=5000; i++){
array.push({
id: i,
text: `할일 ${i}번째`,
checked: false
})
}
return array;
}
function App() {
const [todos, setTodos] = useState(createBulkTodos)
// usetState(createBulkTodos()) 라고 작성하면 리렌더링 될때마다 함수가 호출되지만,
// useState(createBulkTodos) 라고 작성하면 컴포넌트가 처음 렌더링 될때만 함수가 실행된다.
const nextId = useRef(5001);
/// ...
export default App;
- 5000개의 할일을 만들었으므로 개느려졌다.
11.2 크롬 개발자도구를 이용한 성능 모니터링
- React DevTools를 이용하여 측정하면 된다.
- 크롬 확장프로그램 깔고 Profiler 탭을 연다.
- 녹화버튼을 누르고 할 일 아무거나 체크한 다음 화면에 변화가 반영되면 녹화버튼을 한 번 더 눌러 녹화를 끝낸다.
- 우측의 Render duration은 리렌더링에 소요된 시간.
- 변화를 화면에 반영하는 데 423.4ms 가 걸렸다.
- 이번엔 상단의 Ranked chart 아이콘을 눌러본다
- 초록색 아무거나 누르고 내려가보면서 확인해보면 더 자세히 확인이 가능하다.
- 이를 보면 변화를 일으킨 컴포넌트랑 관계없는 컴포넌트들도 리렌더링 된 것을 확인 할 수 있다.
11.3 느려지는 원인 분석
- 컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.
1) 자신이 전달받은 props가 변경될 때
2) 자신의 state가 바뀔 때
3) 부모 컴포넌트가 리렌더링될 때
4) forceUpdate 함수가 실행될 때
- 지금 상황에서는, 할 일 아무거나 체크할 경우 App컴포넌트의 state가 변경되면서 App컴포넌트가 리렌더링된다. 부모 컴포넌트가 리렌더링 되었으니 TodoList 컴포넌트가 리렌더링되고, 그 안의 무수한 컴포넌트들도 리렌더링된다.
- 리렌더링이 불필요할 경우 리렌더링 방지 방법을 알아보자.
11.4 React.memo
- 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정할 수있다.
- 컴포넌트를 만들고나서 감싸 주기만 하면 된다!
TodoListItem.jsx
import React from 'react';
//...
export default React.memo(TodoListItem);
- 근데 이게 끝이 아니다
11.5 onToggle, onRemove 함수가 바뀌지 않게 하기
- 현재 프로젝트에서는 todos 배열이 업데이트되면 onRemove와 onToggle함수도 새롭게 바뀌므로, 최적화가 끝나지 않는다.
- 이 함수들은 배열 상태를 업데이트하는 과정에서 최신 상태의 todos를 참조하기 때문에, todos 배열이 바뀔때마다 함수가 새로 만들어진다.
- 방지하는 방법은 useState의 함수형 업데이트 기능을 쓰거나 useReducer를 사용하면 된다.
11.5.1 useState의 함수형 업데이트
- 기존에 setTodos 함수에서는 새로운 상태를 파라미터로 넣어주었는데, 새로운 상태 대신 상태 업데이트를 어떻게 할지 정의해주는 업데이트 함수를 넣을 수도 있다.
- 이를 함수형 업데이트라고 한다.
예시.jsx
const [number, setNumber] = useState(0);
//prevNumbers는 현재 number값
const onIncrease = useCallback(
()=> setNumber(prevNumber => prevNumber + 1),
[]
);
- setNumber(number+1) 대신 어떻게 업데이트할지 정의해주는 함수를 넣어준다. 그러면 useCallback을 사용할 때 두번째 파라미터로 넣는 배열에 numbers를 넣지 않아도 된다.
- App.js 를 수정해보자
App.js
import {useState, useRef, useCallback} from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import './App.css';
function createBulkTodos(){
const array = [];
for(let i =1; i<=5000; i++){
array.push({
id: i,
text: `할일 ${i}번째`,
checked: false
})
}
return array;
}
function App() {
const [todos, setTodos] = useState(createBulkTodos)
// usetState(createBulkTodos()) 라고 작성하면 리렌더링 될때마다 함수가 호출되지만,
// useState(createBulkTodos) 라고 작성하면 컴포넌트가 처음 렌더링 될때만 함수가 실행된다.
const nextId = useRef(5001);
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((e)=>{
return(
e.id === id ? {...e, checked : !e.checked} : e
)
})
)
},
[]
)
return (
<div className="App">
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
</div>
);
}
export default App;
- onInsert, onRemove, onToggle 에서 setState함수들에 콜백으로 todos => 이거만 추가해주면 되더라
- 다시 React DevTools로 확인해보면,
- Render duration이 15.1ms 로 엄청나게 줄었다.
11.5.2 useReducer 사용하기
- useReduer를 사용해도 onToggle과 onRemove가 계속 새로워지는 문제를 해결할 수 있다.
App.js
import {useState, useRef, useCallback, useReducer} from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import './App.css';
function createBulkTodos(){
const array = [];
for(let i =1; i<=5000; i++){
array.push({
id: i,
text: `할일 ${i}번째`,
checked: false
})
}
return array;
}
function todoReducer(todos, action){
switch(action.type){
case 'INSERT': //{type: 'INTERT', 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: 'TOGGLE', id:1}
return todos.map(todo => todo.id === action.id ? {...todo, checked: !todo.checked} : todo)
default:
return todos;
}
}
function App() {
const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);
// usetState(createBulkTodos()) 라고 작성하면 리렌더링 될때마다 함수가 호출되지만,
// useState(createBulkTodos) 라고 작성하면 컴포넌트가 처음 렌더링 될때만 함수가 실행된다.
const nextId = useRef(5001);
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 (
<div className="App">
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
</div>
);
}
export default App;
- useReducer를 사용할 때는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 한다.
- 지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 상태를 만들어 주는 함수인 createBulkTodos를 넣어주었다.
- 이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출된다.
- useReducer는 코드가 귀찮아지긴 하지만 상태업데이트 로직을 컴포넌트 바깥에 둘 수 있다는 장점이 있다.
11.6 불변성의 중요성
- 불변성을 지킨 예시함수를 잠깐 살펴보면,
const example = ()=>{
const newArr = [...stateSomethinig];
newArr[0] = '새로운요소'
setStateSomething(newArr);
}
- 이와 같이 데이터를 수정할 때 직접 수정하지 않고, 새로운 배열을 만든 다음 필요한 부분을 교체해서 수정하는 방식을 사용한다.
- 업데이트가 필요한 곳에서는 아예 새로운 배열 혹은 새로운 객체를 만들기 때문에, React.memo를 사용했을 때 props가 바뀌었는지 혹은 바뀌지 않았는지를 알아내서 리렌더링 성능을 최적화해줄 수 있다. (불변성이 지켜지지 않으면 감지를 못한다)
- 하지만 Spread Operator는 얕은 복사이므로, 깊이가 깊은 배열/객체는 복사되지 못한다.
- 배열/객체에서 구조가 복잡해진다면 불변성을 유지하면서 업데이트하는 것이 까다롭고 귀찮아진다.
- 이럴땐 immer 라이브러리를 써보자. (12장 내용)
11.7 TodoList 컴포넌트 최적화하기
- 리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트도 최적화해야 하고, 리스트로 사용되는 컴포넌트 자체도 최적화해 주는 것이 좋다.
TodoList.jsx
import React from "react";
//...
export default React.memo(TodoList);
- 지금 프로젝트에서는 상관없지만, 미리 해준 것.
- 책에는 react-visualized를 사용한 렌더링최적화에 대해 이야기하는데, 이는 react17 이상부터 버전 호환성 이슈가 있다.
- react-window를 사용해보자. 리스트 가상화를 통한 렌더링최적화의 기능을 동일하게 가지고 있다.
'Frontend > ReactJS(완)' 카테고리의 다른 글
React - 13장 : 리액트 라우터로 SPA 개발하기 (0) | 2023.02.21 |
---|---|
React - 12장 : Immer 라이브러리를 사용하여 불변성 유지 (1) | 2023.02.20 |
React - 10장 : To-do 앱 실습(컴포넌트 렌더링최적화, Hooks 활용) (0) | 2023.02.17 |
React - 9장 : 컴포넌트의 스타일링 (0) | 2023.02.16 |
React - 8장 : React 자주 쓰이는 Hooks 총 정리 (0) | 2023.02.15 |
댓글