일정관리 애플리케이션을 만들어 봤다. 현재까지는 이 애플리케이션을 사용할 때 불편함이 없다. 하지만 추가되어 있는 데이터가 무수히 많아 지면, 애플리케이션이 느려지는 것을 체감할 수 있다. 그래서 이번에는 컴포넌트 성능 최적화를 해보자!
우선 실제로 랙(lag)을 경험할 수 있도록 많은 데이터를 렌더링해보자! 물론 데이터를 하나하나 직접 입력하지 않고 코드를 사용하여 쉽게 추가해보자.
// App.js
import { useCallback, useRef, useState } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
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);
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(2501);
const onInsert = useCallback(
text => {
const todo = {
id: nextId.current,
text,
checked:false,
};
setTodos(todos.concat(todo));
nextId.current += 1; // nextId 1씩 더하기
},
[todos],
);
const onRemove = useCallback(
id => {
setTodos(todos.filter(todo => todo.id !== id));
},
[todos],
);
const onToggle = useCallback(
id => {
setTodos(
todos.map(todo =>
todo.id === id ? {...todo, checked: !todo.checked} : todo,
),
);
},
[todos],
)
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
</TodoTemplate>
);
};
export default App;
데이터를 하나하나 직접 입력할 수는 없으므로 createBulkTodos라는 함수를 만들어서 데이터 2,500개를 자동으로 생성했다. 여기서 주의할 점은 기본값에 함수를 넣어 주었다는 것이다!! 여기서 useState(createBulkTodo())라고 작성하면 리렌더링될 때마다 createBulkTodos 함수가 호출되지만, useState(createBulkTodos)처럼 파라미터를 함수 형태로 넣어 주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos 함수가 실행될 것이다.
항목 중 하나를 체크하면 이전보다 훨씬 느려진 것을 볼 수 있다.
성능을 분석해야 할 때는 느려졌다는 느낌만으로 충분하지 않다. 정확히 몇 초가 걸리는지 확인해야 하는데, 이는 React DevTools를 사용하여 측정하면 된다. 리액트 v17 전에는 브라우저에 내장된 성능 측정 도구의 User Timing API를 사용했지만, v17 부터는 리액트 전용 개발자 도구인 React DevTools를 사용해야 성능 분석을 자세하게 할 수 있다.
우선 크롬 웹 스토어에 가서 React Developer Tools를 설치한다. 개발자 도구에 들어가면 Profiler라는 탭을 열어보면 좌측 상단에 파란색 녹화 버튼이 보인다.
녹화 버튼을 누르고 '할 일 1' 항목을 체크한 다음, 화면에 변화사 반영되면 녹화 버튼을 한번 더 누르면 아래와 같이 성능 분석 결과가 나타난다.
우측의 Render duration은 리렌더링에 소요된 시간을 의미한다. 변화를 화면에 반영하는 데 402ms가 걸렸다는 의미다(1ms는 0.001초입니다). 참고로 소요 시간은 컴퓨터 환경에 따라 다르게 나타날 수 있다.
Profiler 탭의 상단에 있는 Lanked 차트 아이콘을 눌러보자!
이 화면에서는 리렌더링된 컴포넌트를 오래 걸린 순으로 정렬하여 나열해준다. 스크롤을 해보면 정말 많은 컴포넌트가 리렌더링된 것을 확인할 수 있다. 초록색 박스들이 너무 작아서 텍스트 내용아 잘려서 보이지 않는다. 클릭을 하면 크기가 늘어나 내용을 확인할 수 있다.
이를 보면 이번에 변화를 일으킨 컴포넌트랑 관계없는 컴포넌트들도 리렌더링된 것을 확인할 수 있다. 하나의 항목만 업데이트하는 데 렌더링 소요시간이 402ms가 걸리고 있다. 이는 결코 좋지 못한 성능이다. 이제 이를 최적화하는 방법을 알아보자!!!
컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.
지금 상황을 분석해 보면, '할 일 1'항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링된다. 부모 컴포넌트가 리렌더링되었으니 TodoList 컴포넌트가 리렌더링되고 그 안의 무수한 컴포넌트도 리렌더링된다.
'할 일 1' 항목은 리렌더링되어야 하는 것이 맞지만, '할 일 2'부터 '할 일 2500'까지는 리렌더링을 안 해도 되는 상황인데 모두 리렌더링되고 있으므로 이렇게 느린 것이다. 컴포넌트의 개수가 많지 않다면 모든 컴포넌트 리렌더링해도 느려지지 않는데, 지금처럼 약 2000개가 넘어가면 성능이 저하가 된다.
이럴 때는 컴포넌트 리렌더링 성능을 최적화해 주는 작업을 해 주어야 한다. 즉, 리렌더링이 불필요할 떄는 리렌더링을 방지해 주어야 한다.
컴포넌트의 리렌더링을 방지할 때는 shouldComponentUpdate라는 라이프사이클을 사용하면 된다. 그런데 함수 컴포넌트에서는 라이프사이클 메서드를 사용할 수 없습니다. 그 대신 React.memo라는 함수를 사용한다. 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수 컴포넌트의 리렌더링 성능을 최적화해 줄 수 있다.
React.memo의 사용법은 매우 간단하다. 컴포넌트를 만들고 나서 감싸 주기만 하면 된다.
// 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 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않는다.
React.memo를 사용하는 것만드로 컴포넌트 최적화가 끝나지는 않는다. 현재 프로젝트에서는 todos 배열이 업데이트되면 onRemove와 onToggle 함수도 새롭게 바뀌기 때문이다.
onRemove와 onToggle 함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다. 이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 두 가지이다.
기존에 setTodos 함수를 사용할 때는 새로운 상태를 파라미터로 넣어 주었다. setTodo를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있다. 이를 함수형 업데이트라고 부른다.
예시를 한번 보자!
const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킵니다.
cosnt onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber +1),
[],
);
setNumber(number+1)을 하는 것이 아니라, 위 코드처럼 어떻게 업데이트할지 정의해 주는 업데이트 함수를 넣어 준다. 그러면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 number를 넣지 않아도 된다.
그럼 이제 onToggle, onRemove 함수에서 useState의 함수형 업데이트를 사용해 보자. 이 과정에서 onInsert 함수도 함께 수정해보자.
// App.js
import { useCallback, useRef, useState } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
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);
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(4);
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,
),
);
},[]
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert}/>
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
</TodoTemplate>
);
};
export default App;
setTodos를 사용할 때 그 안에 todos =>만 앞에 넣어 주면 된다.
렌더링 소요시간이 402ms에서 28.6ms으로 줄어들었다. 엄청 신기하다 .. 👍 클릭을 했을 때 동작들도 훨씬 빨라진 것을 체감할 수 있었다. 아래쪽을 보면 회색 빗금이 그어져 있는 박스들이 있다. 이는 React.memo를 통해 리렌더링되지 않는 컴토넌트를 나타낸다.
Lanked를 눌러보면 리렌더링된 컴포넌트의 수가 몇 개 없는 것을 확인할 수 있다.
useState의 함수형 업데이트를 사용하는 대신, useReducer를 사용해도 onToggle과 onRemove가 계속 새로워지는 문제를 해결할 수 있다.
// App.js
import { useCallback, useReducer, useRef } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
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 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);
// 고윳값으로 사용될 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;
useReducer를 사용할 때는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 한다. 지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 상태를 만들어 주는 함수인 createBulkTodos를 넣어 주었는데, 이렇게 하면 컴포넌트기 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출된다.
useReducer를 사용하는 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있다. 성능상으로는 두 가지 방법이 비슷하기 때문에 아무 방식을 사용하면 된다.
실제로 해보면 useReducer가 더 빠른 렌더링 속도를 보여주었다. 28.6ms에서 22ms 더 줄였다. 그래도 코드를 너무 많이 수정한다는 점에서 보면 useState의 함수형 업데이트가 개인적으로는 나은 방법인 거 같다.
리액트 컴포넌트에서 상태를 업데이트할 때 불변성을 지키는 것은 매우 중요하다. 앞에서 useState를 사용해 만든 todos 배열과 setTodos 함수를 사용하는 onToggle 함수를 확인해보자!
const onToggle = useCallback(id => {
setTodos(todos =>
todos.map(todo =>
todo.id === id? {...todo, checked: !todo.checked} : todo,
),
);
}, []);
기존 데이터를 수정할 때 직접 수정하지 않고, 새로운 배열을 만든 다음에 새로운 객체를 만들어서 필요한 부분을 교체해주는 방식으로 구현했다. 업데이트가 필요한 곳에서는 아예 새로운 배열 혹은 새로운 객체를 만들기 때문에, React.memo를 사용했을 때 props가 바뀌었는지 혹은 바뀌지 않았는지를 알아내서 리렌더링 성능을 최적화해 줄 수 있다.
이렇게 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 '불변성을 지킨다'고 한다.
아래 예시 코드를 보고 불변성을 어떻게 지키고 있는지 생각해 보자~
const array = [1, 2, 3, 4, 5];
const nextArrayBad = array; // 배열을 복사하는 것이 아니라 똑같은 배열을 가리킵니다.
netxArrayBad[0] = 100;
console.log(array === nextArrayBad); // 완전히 같은 배열이기 때문에 true
const nextArrayGood = [...array]; // 배열 내부의 값을 모두 복사합니다.
nextArrayGood[0] = 100;
console.log(array === nexArrayGood); // 다른 배열이기 때문에 false
const object = {
foo: 'bar',
value: 1
};
const nextObjectBad = object; // 객체가 복사되지 않고, 똑같은 객체를 가리킵니다.
nextObjectBad.value = nextObjectBad.value+1;
console.log(object === nextObjectBad); // 같은 객체이기 때문에 true
const nextObjectGood ={
...object, // 기존에 있던 내용을 모두 복사해서 넣습니다.
value: objcect.value + 1 // 새로운 값을 덮어 씁니다.
}
console.log(object === nextObjectGood); // 다른 객체이기 때문에 false
불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다. 그러면 React.memo에서 서로 비교하여 최적화하는 것이 불가능하겠죠??
추가로 전개 연산자(...문법)를 사용하여 객체나 배열 내부의 값을 복사할 때는 얕은 복사(shallow copy)를 하게 된다. 즉, 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사가 된다. 따라서 내부의 값이 객체 혹은 배열이라면 내부의 값 또한 따로 복사해 주어야 한다.
아래 코드를 읽어 보면 쉽게 이해될 것이다.
const todos = [{id: 1, checked: true},{id: 2, checked: true}];
const nextTodos = [...todos];
nextTodos[0].checked = false;
consoel.log(todos[0] === nextTodos[0]); // 아직까지는 똑같은 객체를 가리키고 있기 때문에 true
nextTodos[0] = {
...nextTodos[0],
checked: false
};
console.log(todos[0] === nextTodos[0]); // 새로운 객체를 할당해 주었기에 false
만약 객체 안에 있는 객체라면 불변성을 지키면서 새 값을 할당해야 하므로 다음과 같이 해 주어야 한다.
const nextComplexObject = {
...complexObject,
objectInside:{
...complexObject.objectInside,
enabled: false
}
};
console.log(complexObject === nexComplexObjcet); // false
console.log(complexObject === nexComplexObject.objectInside); // false
배열 혹은 객체의 구조가 정말 복잡해진다면 이렇게 불변성을 유지하면서 업데이트하는 것도 까다로워진다. 이렇게 복잡한 상황일 경우 immer라는 라이브러리의 도움을 받으면 정말 편하게 작업할 수 있다. 이건 나중에 배워서 작성해 보겠다.
리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트도 최적화해야 하고, 리스트로 사용되는 컴포넌트 자체도 최적화해 주는 것이 좋다.
// TodoList.js
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);
위 최적화 코드는 현재 프로젝트 성능에 전혀 영향을 주지 않는다. 왜냐하면, TodoList 컴포넌트의 부모 컴포넌트인 App 컴포넌트가 리렌더링되는 유일한 이유가 todos 배열이 업데이트 될 때이기 때문이죠. 즉, 지금 TodoList 컴포넌트는 불필요한 리렌더링이 발생하지 않는다. 하지만 App 컴포넌트에 다른 state가 추가되어 해당 값들이 업데이트될 때는 TodoList 컴포넌트가 불필요한 리렌더링을 할 수도 있겠죠. 그렇기 때문에 지금 React.Memo를 사용해서 미리 최적하해 준 것이다.
리스트 관련 컴포넌트를 작성할 때는 리스트 아이템과 리스트, 이 두 가지 컴포넌트를 최적화해 주는 것을 잊지 마세요. 그러나 내부 데이터가 100개를 넘지 않거나 업데이트가 자주 발생하지 않는다면, 이런 최적화 작업을 반드시 해 줄 필요는 없다.
지금까지 리액트 컴포넌트 리렌더링 성능을 최적화 하는 방법을 알아보았다. 리렌더링 성능을 최적화할 때는 필요할 때만 리렌더링하도록 설정해 주었지요? 이번에는 또 다른 렌더링 성능 최적화 방법을 알아보자. 일정 관리 애플리케이션에 초기 데이터가 2,500개 등록되어 있는데, 실제 화면에 나오는 항목은 아홉 개뿐입니다. 나머지는 스크롤해야만 볼 수 있죠.
현재 컴포넌트가 맨 처음 렌더링될 때 2,500개 컴포넌트 중 2,491개 컴포넌트는 스크롤하기 전에는 보이지 않음에도 불구하고 렌더링이 이루어진다. 꽤 비효율적이죠? 그리고 나중에 todos배열에 변동이 생길 때도 TodoList 컴포넌트 내부의 map 함수에서 배열의 처음부터 끝까지 컴포넌트로 변환해 주는데, 이 중에서 2,491개는 보이지 않으므로 시스템 자원 낭비이다.
이번 절에서 배울 react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있습니다. 그리고 만약 스크롤되면 해당 스크롤 위치에서 보여 주어야 할 컴포넌트를 자연스럽게 렌더링시키죠. 이 라이브러리를 사용하면 낭비되는 자원을 아주 쉽게 아낄 수 있다.
우선 터미널을 켜서 설치해 보자!!
$ npm install react-virtualized --save
react-virtualized에서 제공하는 List 컴포넌트를 사용하여 TodoList 컴포넌트의 성능을 최적화할 것입니다.
최적화를 수행하려면 사전에 먼저 해야 하는 작업이 있는데, 바로 각 항목의 실제 크기를 px 단위로 알아내는 것입니다. 이 값은 우리가 작성한 CSS를 확인해서 직접 계산해도 되지만, 이보다 훨씬 더 편리하게 알아낼 수 있습니다. 크롬 개발자 도구의 좌측 상단에 있는 아이콘을 눌러서 크기를 알고 싶은 항목에 커서를 대 보세요.
각 항목의 크기는 가로497px, 세로 57px이다. 크기를 알아낼 때 두 번째 항목을 확인해야 하는데, 그 이유는 두 번째 항목부터 테두리가 포함되어 있기 때문이다.(첫 번째 항목은 테두리가 없기 때문에 56px가 된다.)
// 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 컴포넌트를 사용하기 위해 roewRenderer라는 함수를 새로 작성해 주었다. 이 함수는 react-virtualized의 List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용하며, 이 함수를 List 컴포넌트의 props로 설정해 주어야 한다. 이 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아 와서 사용한다.
List 컴포넌트 사용할 때는 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 렌더링 할 때 사용해야 하는 함수, 그리고 배열을 props로 넣어 주어야 한다. 그러면 이 컴포넌트가 전달받은 props를 사용하여 자동으로 최적화해 준다.
TodoList를 저장하고 나면 스타일이 깨져서 난리가 난다 ㅋㅋ TodoListItem 컴포넌트를 다음과 같이 수정하면 해결된다.
// 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);
render 함수에서 기존에 보여주던 내용을 div로 한번 감싸고, 해당 div에는 TodoListItem-virtualized라는 className을 설정하고, pops로 받아 온 style을 적용시켜 주었다.
여기서 TodoListItem-virtualized라는 클래스를 만든 것은 컴포넌트 사이사이에 테두리를 제대로 쳐 주고, 홀수 번째/짝수 번째 항목에 다른 배경 색상을 성정하기 위함이다.
그 다음에는 TodoListItem의 스타일 파일에서 최하단에 있던 & + &를 사용하여 .TodoListItem사이사이에 테두리를 설정했던 코드와 &:nth-child(even)을 사용하여 다른 배경 색상을 주는 코드를 지우고, 코드 최상단에 다음 코드를 삽입하자.
// TodoListItem.scss
.TodoListItem-virtualized {
& + & {
border-top: 1px solid #dee2e6;
}
&:nth-child(even) {
background: #f8f9fa;
}
}
(...)
React.memo를 통해 28.6ms까지 줄였는데, 이번에는 6.9ms까지 줄어들었다!!!!
리액트 컴포넌트의 렌더링은 기본적으로 빠르기 때문에 컴포넌트를 개발할 때 최적화 작업에 대해 너무 큰 스트레스를 받거나 모든 컴포넌트에 일일이 React.memo를 작성할 필요는 없다. 단, 리스트와 관련된 컴포넌트를 만들 때 보여 줄 항목이 100개 이상이고 업데이트가 자주 발생한다면, 이 장에서 학습한 방식을 사용하여 꼭 최적화 하자!!!
리액트를 다루는 기술 - 김민준