안녕하세요! delay100입니다. 이번 포스팅에서는 지난 10장에서 만들었던 투두리스트의 컴포넌트들에 대해 성능을 최적화 해봅시다. 이번 학습에서는 10장 프로젝트의 코드를 그대로 가져온 후 시작합니다.
대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요!
책 리액트를 다루는 기술, 개정판의 11장 내용을 다루고 있습니다.
이번 포스팅의 Github 링크
https://github.com/delay-100/study-react/tree/main/ch11/todo-app
1. 성능 저하
우선 성능이 저하되는 경우를 만들어주기 위해 2500개의 투두를 만들어줍시다. App.js를 다음과 같이 수정해줍니다.
- 성능 저하 만들기 - src/App.js
import { useState, useRef, useCallback } from 'react';
import TodoInsert from './components/TodoInstert';
import TodoTemplate from './components/TodoTemplate';
import TodoList from './components/TodoList';
const App = () => {
// createBulkTodos 함수 - 데이터 2500개를 자동으로 생성해주는 함수
function createBulkTodos() {
const array = [];
for (let i = 1; i <= 2500; i++) {
array.push({
id: i,
text: `할 일 ${i}`,
checked: false,
});
}
return array;
}
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(2501); // id값은 useRef를 사용하여 관리 => id 값은 렌더링되는 정보가 아니고, 새로운 항목을 만들 때 참조되는 값일 뿐이기 때문
// onInsert 함수 - App 컴포넌트에서 todos 배열에 새 객체를 추가하는 함수
const onInsert = useCallback(
// 컴포넌트의 성능을 아낄 수 있도록 useCallback으로 감싸줌(props로 전달해야 할 함수를 만들 때는 useCallback을 사용해 함수를 감싸줌)
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current += 1; // nextId 1씩 더하기
},
[todos],
);
// onRemove 함수 - App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수
const onRemove = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos],
);
const onToggle = useCallback(
(id) => {
setTodos(
todos.map(
(todo) =>
// 배열 내장 함수 map을 사용해 특정 id를 가지고 있는 객체의 checked 값을 반전시켜 줌
// 특정 배열 원소를 업데이트해야 할 때 이렇게 map을 사용하면 짧은 코드로 쉽게 작성 가능
todo.id === id ? { ...todo, checked: !todo.checked } : todo, // todo.id와 현재 파라미터로 사용된 id 값이 같을 때는 새로운 객체를 생성하지만, id 값이 다를 때는 처음 받아왔던 상태 그대로를 반환함
),
);
},
[todos],
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
{/* todos, onRemove를 TodoList의 props로 전달*/}
</TodoTemplate>
);
};
export default App;
실행 결과를 보면 상태가 매우 느리게 바뀌는 것을 볼 수 있습니다.
2. 성능 개선
컴포넌트 렌더링 최적화
2-1. React.memo 이용
2-2. useState 개선
2-3. useReducer 개선
그 외 렌더링 최적화
2-4. react-virtualized
2-1. React.memo 이용
컴포넌트의 리렌더링을 방지할 때는 7.Lifecycle method에서 다루었던 shouldComponentUpdate 라이프사이클을 사용하면 됩니다.
그러나 함수 컴포넌트에서는 라이프사이클 메서드의 사용이 불가능합니다.
그 대신, React.memo라는 함수를 사용합니다.
컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수 컴포넌트의 리렌더링 성능을 최적화할 수 있습니다.
React.memo 사용법
import React from 'react';
...
export default React.memo(컴포넌트명);
- React.memo 함수 사용 예시 - src/components/TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames'; // 조건부 스타일링을 위해 classnames를 이용함
import './TodoListItem.scss';
// TodoList에서 todo 값을 넘겨줌
const TodoListItem = ({ todo, onRemove, onToggle }) => {
const { id, text, checked } = todo;
return (
<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>
);
};
export default React.memo(TodoListItem); // React.memo 설정 시 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않음
- React.memo 함수 사용 예시2 - src/components/TodoList.js
import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
// App.js에서 todos={todos}로 props를 넘겨주었기 때문에 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 함
const TodoList = ({ todos, onRemove, onToggle }) => {
return (
<div className="TodoList">
{/* props로 받아온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링함
map을 사용하여 컴포넌트로 변환할 때는 key props를 전달해주어야 함(여기서는 key값에 각 항목마다 가지고 있는 고유값인 id를 넣어 줌)
그리고 여러 종류의 값을 전달해줘야 하는 todo데이터의 경우 통째로 전달하는 편이 나중에 성능 최적화 시 편리함
=> TodoListItem으로 이동 */}
{todos.map((todo) => (
<TodoListItem
todo={todo}
key={todo.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
};
export default React.memo(TodoList);
2-2. useState 개선
8. Hooks 포스팅에서 useState에 대해 공부한 적이 있습니다.
useState를 이용했을 때 성능 개선을 하는 코드를 짜봅시다.
useState의 성능 개선 핵심
setTodos(useState로 선언한 함수의 두 번째 파라미터)를 사용할 때,
그 안에 todos(기존의 useCallback 함수의 두 번째 파라미터 값) =>를 넣고,
useCallback 함수의 두 번째 파라미터 값은 지워줌
- useState 성능 개선 예시 - src/App.js
import { useState, useRef, useCallback } from 'react'; // 2-1. useState 이용
import TodoInsert from './components/TodoInstert';
import TodoTemplate from './components/TodoTemplate';
import TodoList from './components/TodoList';
// createBulkTodos 함수 - 데이터 2500개를 자동으로 생성해주는 함수
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); // 데이터를 직접 입력하기는 힘드므로, 데이터를 자동으로 생성하는 함수 호출
// useState(createBulkTodos())로 작성하면 리렌더링될 때마다 createBulkTodos 함수가 호출되지만
// useState(createBulkTodos)처럼 파라미터를 함수 형태로 넣어 주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos 함수가 실행됨
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(2501); // id값은 useRef를 사용하여 관리 => id 값은 렌더링되는 정보가 아니고, 새로운 항목을 만들 때 참조되는 값일 뿐이기 때문
// onInsert 함수 - App 컴포넌트에서 todos 배열에 새 객체를 추가하는 함수
const onInsert = useCallback(
// 컴포넌트의 성능을 아낄 수 있도록 useCallback으로 감싸줌(props로 전달해야 할 함수를 만들 때는 useCallback을 사용해 함수를 감싸줌)
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos((todos) => todos.concat(todo));
nextId.current += 1; // nextId 1씩 더하기
},
[], // useCallback 두 번째 파라미터: input(todos)이 바뀌거나 새로운 항목이 추가될 때 새로운 함수 생성
// 함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두 번째 파라미터 안에 포함시켜주어야 함
);
// onRemove 함수 - App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수
const onRemove = useCallback((id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
}, []);
const onToggle = useCallback((id) => {
setTodos((todos) =>
todos.map(
(todo) =>
// 배열 내장 함수 map을 사용해 특정 id를 가지고 있는 객체의 checked 값을 반전시켜 줌
// 특정 배열 원소를 업데이트해야 할 때 이렇게 map을 사용하면 짧은 코드로 쉽게 작성 가능
todo.id === id ? { ...todo, checked: !todo.checked } : todo, // todo.id와 현재 파라미터로 사용된 id 값이 같을 때는 새로운 객체를 생성하지만, id 값이 다를 때는 처음 받아왔던 상태 그대로를 반환함
),
);
}, []);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
{/* todos, onRemove를 TodoList의 props로 전달*/}
</TodoTemplate>
);
};
export default App;
2-3. useReducer 개선
8. Hooks 포스팅에서 useReducer에 대해 공부한 적이 있습니다.
useReducer을 사용하면 기존 코드를 많이 고쳐야 한다는 단점이 있지만,
상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있습니다.
아래의 예시에서는 todoReducer 함수를 추가하고, useReducer을 이용해 각 함수의 처리법을 변경해주었습니다.
- useReducer 성능 개선 예시 - src/App.js
import { useReducer, useRef, useCallback } from 'react'; // 2-1. useReducer 이용
import TodoInsert from './components/TodoInstert';
import TodoTemplate from './components/TodoTemplate';
import TodoList from './components/TodoList';
// createBulkTodos 함수 - 데이터 2500개를 자동으로 생성해주는 함수
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': //토글
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); // useReducer의 첫 번째 파라미터: 리듀서 함수, 두 번째 파라미터: undefined, 세 번째 파라미터: 초기 상태를 만들어주는 createBulkTodos -> 컴포넌트가 맨 처음 렌더링될 떄만 createBulkTodos 함수가 호출됨
// 원래는 첫 번째 파라미터에 리듀서 함수, 두 번째 파라미터에 초기 상태를 넣어줘야 함
// useState(createBulkTodos())로 작성하면 리렌더링될 때마다 createBulkTodos 함수가 호출되지만
// useState(createBulkTodos)처럼 파라미터를 함수 형태로 넣어 주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos 함수가 실행됨
// 고윳값으로 사용될 id
// ref를 사용하여 변수 담기
const nextId = useRef(2501); // id값은 useRef를 사용하여 관리 => id 값은 렌더링되는 정보가 아니고, 새로운 항목을 만들 때 참조되는 값일 뿐이기 때문
// onInsert 함수 - App 컴포넌트에서 todos 배열에 새 객체를 추가하는 함수
const onInsert = useCallback(
// 컴포넌트의 성능을 아낄 수 있도록 useCallback으로 감싸줌(props로 전달해야 할 함수를 만들 때는 useCallback을 사용해 함수를 감싸줌)
(text) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
dispatch({ type: 'INSERT', todo });
nextId.current += 1; // nextId 1씩 더하기
},
[], // useCallback 두 번째 파라미터: input(todos)이 바뀌거나 새로운 항목이 추가될 때 새로운 함수 생성
// 함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두 번째 파라미터 안에 포함시켜주어야 함
);
// onRemove 함수 - App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수
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} />
{/* todos, onRemove를 TodoList의 props로 전달*/}
</TodoTemplate>
);
};
export default App;
2-4. react-virtualized
react-virtualized?
- 렌더링 시, 낭비되는 자원을 쉽게 아낄 수 있는 라이브러리
- 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있음
- 스크롤되면 해당 스크롤 위치에서 보여 주어야 할 컴포넌트를 자연스럽게 렌더링 시킴
설치할 라이브러리
yarn add react-virtualized
설치를 완료했으면 react-virtualized에서 제공하는 List 컴포넌트를 이용해 TodoList 컴포넌트의 성능을 최적화합시다.
최적화를 하기 전에, 각 항목의 실제 크기를 px 단위로 알아내야합니다. 크롬 개발자 도구로 px을 알아내봅시다.
할일 1은 테두리가 없기 때문에 56px이기에 할 일 2로 크기를 잡아줍니다.
- react-virtualized 사용 예시 - src/components/TodoList.js
import React, { useCallback } from 'react';
import { List } from 'react-virtualized';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
// App.js에서 todos={todos}로 props를 넘겨주었기 때문에 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 함
const TodoList = ({ todos, onRemove, onToggle }) => {
// List 컴포넌트를 사용하기 위해 rowRenderer라는 함수를 새로 작성함
// 이 함수는 react-virtualized의 List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용하며,
// 이 함수를 list 컴포넌트의 props로 설정해주어야 함
const rowRenderer = useCallback(
({ index, key, style }) => {
// 이 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아와서 사용함
const todo = todos[index];
return (
<TodoListItem
todo={todo}
key={key}
onRemove={onRemove}
onToggle={onToggle}
style={style}
/>
);
},
[onRemove, onToggle, todos], // 배열 안에 onRemove, onToggle, todos를 넣은 경우: input(여기서는 onRemove, onToggle, todos)이 바뀌거나 새로운 항목이 추가될 때 새로운 함수 생성
// 함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두 번째 파라미터 안에 포함시켜주어야 함
// 받아온 onRemove, onToggle, todos를 넣어 반환해야하기 때문에 배열 안에 onRemove, onToggle, todos를 꼭 넣어주어야 함
);
return (
// List 컴포넌트를 사용할 때는 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 렌더링할 때 사용해야 하는 함수, 그리고 배열을 props로 넣어줘야 함
// 그러면 이 컴포넌트가 전달받은 props를 사용하여 자동으로 최적화 해줌
<List
className="TodoList"
width={512} // 전체 크기
height={513} // 전체 높이
rowCount={todos.length} // 항목 개수
rowHeight={57} // 항목 높이
rowRenderer={rowRenderer} // 항목을 렌더링할 때 쓰는 함수
list={todos} // 배열
style={{ outline: 'none' }} // List에 기본 적용되는 outline 스타일 제거
/>
// <div className="TodoList">
// {/* props로 받아온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링함
// map을 사용하여 컴포넌트로 변환할 때는 key props를 전달해주어야 함(여기서는 key값에 각 항목마다 가지고 있는 고유값인 id를 넣어 줌)
// 그리고 여러 종류의 값을 전달해줘야 하는 todo데이터의 경우 통째로 전달하는 편이 나중에 성능 최적화 시 편리함
// => TodoListItem으로 이동 */}
// {todos.map((todo) => (
// <TodoListItem
// todo={todo}
// key={todo.id}
// onRemove={onRemove}
// onToggle={onToggle}
// />
// ))}
// </div>
);
};
export default React.memo(TodoList);
TodoList의 개별 항목인 TodoListItem에 대해 style 설정도 해줍시다. 아래의 코드를 적용시키지 않으면 스타일이 이상하게 나옵니다.
- react-virtualized 사용 예시 - src/components/TodoListItem.js
import React from 'react';
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames'; // 조건부 스타일링을 위해 classnames를 이용함
import './TodoListItem.scss';
// TodoList에서 todo 값을 넘겨줌
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); // React.memo 설정 시 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않음
- react-virtualized 사용 예시 - src/components/TodoListItem.scss
.TodoListItem-virtualized {
& + & { // 컴포넌트 사이사이에 테두리를 제대로 쳐줌
border-top: 1px solid #dee2e6;
}
&:nth-child(even) { // 짝수 번째 항목의 배경 색을 하얗게 바꿈
background: #f8f9fa;
}
}
.TodoListItem {
padding: 1rem;
display: flex;
align-items: center; // 세로 중앙 정렬
// &:nth-child(even) {
// background: #f8f9fa;
// }
.checkbox {
cursor: pointer;
flex: 1; // 차지할 수 있는 영역 모두 차지
display: flex;
align-items: center ; // 세로 중앙 정렬
svg {
// 아이콘
font-size: 1.5rem; // -> size는 안 되는 이유?
}
.text {
margin-left: 0.5rem;
flex: 1; // 차지할 수 있는 영역 모두 차지
}
// 체크되었을 때 보여 줄 스타일
&.checked {
svg {
color: #22b8cf;
}
.text {
color: #adb5bd;
text-decoration: line-through;
}
}
}
.remove {
display: flex;
align-items: center;
color: #ff6b6b;
cursor: pointer;
&:hover {
color: #ff8787;
}
}
// // 엘리먼트 사이사이에 테두리를 넣어 줌
// & + & {
// border-top: 1px solid #dee2e6;
// }
}
TASTEYOM을 만들 때, 최적화와 렌더링에 대해 이렇게 해도 되나..? 싶었던 부분들이 react에서 해결이 되는 걸 깨달았습니다..!!!
이 책을 1회독을 마치면 바로 최적화랑 디자인을 손보고 싶네요 ㅎㅎ
읽어주셔서 감사합니다. 잘못된 정보는 댓글로 알려주세요!
'Study > React' 카테고리의 다른 글
13. Router(라우터) & SPA(Single Page Application) (0) | 2022.07.24 |
---|---|
12. 불변성 유지하기(immer) (0) | 2022.07.21 |
10. 간단한 투두리스트(TodoList) 만들기 (0) | 2022.07.19 |
9. 컴포넌트 스타일링(CSS) (0) | 2022.07.18 |
8. Hooks (0) | 2022.07.16 |