11.1 많은 데이터 렌더링하기
- createBulkTodos 라는 함수를 만들어서 데이터 2,500개를 자동으로 생성
- useStaet(createBulkTodos)처럼 파라미터를 함수 형태로 넣어주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos 함수가 실행됨
App.js
import {useState, useRef, useCallback} 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.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;
11.2 크롬 개발자 도구를 통한 성능 모니터링
- React DevTools를 사용하여 측정
- Render duration은 리렌더링에 소요된 시간을 의미
- 변화를 일으킨 컴포넌트랑 관계없는 컴포넌트들도 리렌더링됨
11.3 느려지는 원인 분석
- 리렌더링 발생 상황
1) 자신이 전달받은 props가 변경될 떄
2) 자신의 state가 바뀔 떄
3) 부모 컴포넌트가 리렌더링될 떄
4) forceUpdate 함수가 실행될 떄
- '할일 1' 항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링됨
- 부모 컴포넌트가 리렌더링되었으니 TodoList 컴포넌트가 리렌더링되고 그 안의 무수한 컴포넌트들도 리렌더링됨
- '할일 2'부터 할일 2500'까지는 리렌더링을 안 해도 되는 상황인데 모두 리렌더링되고 있으므로 느린 것
11.4 React.memo를 사용하여 컴포넌트 성능 최적화
- React.memo 함수를 사용하여 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수 컴포넌트의 리렌더링 성능을 최적화해 줄 수 있음
- TodoListItem 컴포넌트에 React.memo 적용
- todo, onRemove, onToggle이 바뀌지 않으면 리렌더링하지 않음
TodoListItem.js
...
export default React.memo(TodoListItem);
11.5 onToggle, onRemove 함수가 바뀌지 않게 하기
- onRemove, onToggle 함수는 배열 상태를 업데이트 하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어짐
11.5.1 useStaet의 함수형 업데이트
- 기존에 setTodos 함수를 사용할 때는 새로운 상태를 파라미터로 넣어 줌
- setTodos를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수 있음
ex)
const [number, setNumber] = useState(0);
//prevNumbers는 현재 number의 값을 가리킴
const onIncrease = useCallback(
() =>setNumber(prevNumber =>prevNumber + 1),
[],
);
- setNumber(number+1)을 하는 것이 아니라 , 위 코드처럼 어떻게 업데이트할지 정의해주는 업데이트 함수를 넣어 줌
-> useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 number을 넣지 않아도 됨
App.js
- onToggle, onRemove 함수에서 useState의 함수형 업데이트를 사용
...
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,),
);
},
[],
);
...
11.5.2 useReducer 사용하기
- 리듀서는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action)값을 전달받아 새로운 상태를 반환하는 함수
- useReducer를 사용할 떄는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 함
- 지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 상태를 만들어 주는 함수인 createBulkTodos를 넣어줌
=> 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출됨
- useReducer를 사용하면 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있음
App.js
import React, { useReducer, useRef, useCallback } 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;
}
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);
// 고유 값으로 사용 될 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;
11.6 불변성의 중요성
- 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 '불변성을 지킨다'고 함
ex)
const array = [1,2,3,4,5];
const nextArrayBad = array; //배열을 복사하는 것이 아니라 똑같은 배열을 가리킴
nextArrayBad[0] = 100;
console.log(array === nextArrayBad); //완전히 같은 배열이기 때문에 true
const nextArrayGood = [...array]; //배열 내부의 값을 모두 복사
nextArrayGood[0] = 100;
console.log(array === nextArrayGood); //다른 배열이기 때문에 false
const object = {
foo:'bar',
value:1
};
const nextObjectBad = object; //객체가 복사되지 않고, 똑같은 객체를 가리킴
nextObjectBad.value = nextObjectBad.value +1;
console.log(object === nextObjectBad); //같은 객체이기 때문에 true
const nextObjectGood = {
...object, //기존에 있던 내용을 모두 복사해서 넣음
value: object.value +1 //새로운 값을 덮어 씀
};
console.log(object === nextObjectGood); //다른 객체이기 때문에 false
- 불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못함
-> React.memo에서 서로 비교하여 최적화하는 것이 불가능
- 전개 연산자 (... 문법)를 사용하여 객체나 배열 내부의 값을 복사할 때는 얕은 복사(shallow copy)를 하게 됨
-> 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사
-> 내부의 값이 객체 혹은 배열이라면 내부의 값 또한 따로 복사해줘야 함
ex)
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
- 객체 안에 있는 객체라면 불변성을 지키면서 새 값을 할당해야 함
ex)
const nextComplexObject = {
...complexObject,
objectInside: {
...complexObject.objectInside,
enabled:false
}
};
console.log(complexObject === nextComplexObject); //false
console.log(complexObject.objectInside === nextComplexObject.objectInside); //false
11.7 TodoList 컴포넌트 최적화하기
- 리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트도 최적화해야 하고, 리스트로 사용되는 컴포넌트 자체도 최적화해 주는 것이 좋음
11.8 react-virtualized를 사용한 렌더링 최적화
- react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있음
- 만약 스크롤되면 해당 스크롤 위치에서 보여 주어야 할 컴포넌트를 자연스럽게 렌더링 시킴
- 이 라이브러리를 사용하면 낭비되는 자원을 쉽게 아낄 수 있음
11.8.1 최적화 준비
- npm 설치
npm install react-virtualized --save
- react-virtualized에서 제공하는 List 컴포넌트를 사용하여 TodoList 컴포넌트의 성능을 최적화
- 최적화를 수행하려면 각 항목의 실제 크기를 px 단위로 알아내야 함
11.8.2 TodoList 수정
- List 컴포넌트를 사용하기 위해 rowRenderer라는 함수를 새로 작성
- rowRenderer 함수는 react-virtualized의 List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용하며, 이 함수를 List 컴포넌트의 props로 설정해 주어야 함
- roRenderer 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아와서 사용
- List 컴포넌트를 사용할 때는 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 렌더링할 때 사용해야 하는 함수, 그리고 배열을 props로 넣어주어야 함
- 이 컴포넌트가 전달받은 props를 사용하여 자동으로 최적화해 줌
TodoList.js
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);
11.8.3 TodoListItem 수정
TodoListItem.js
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을 설정하고, props로 받아온 style을 적용 시켜 줌
'react' 카테고리의 다른 글
[리액트를 다루는 기술] 13장 리액트 라우터로 SPA 개발하기 (1) | 2023.11.17 |
---|---|
[리액트를 다루는 기술] 12장 immer를 사용하여 더 쉽게 불변성 유지하기 (0) | 2023.11.16 |
인프런 - MERN 스택으로 만드는 지도 서비스 (+TypeScript) (2) (0) | 2023.11.10 |
인프런 - MERN 스택으로 만드는 지도 서비스 (+TypeScript) (1) (3) | 2023.11.10 |
[리액트를 다루는 기술] 10장 일정 관리 웹 애플리케이션 만들기 (0) | 2023.11.08 |