안녕하세요! delay100입니다. 이번 포스팅에서는 지난 10장에서 만들었던 투두리스트의 컴포넌트들에 대해 성능을 최적화 해봅시다. 이번 학습에서는 10장 프로젝트의 코드를 그대로 가져온 후 시작합니다.
대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요!
책 리액트를 다루는 기술, 개정판의 11장 내용을 다루고 있습니다.
이번 포스팅의 Github 링크
https://github.com/delay-100/study-react/tree/main/ch11/todo-app
GitHub - delay-100/study-react
Contribute to delay-100/study-react development by creating an account on GitHub.
github.com
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 라이프사이클을 사용하면 됩니다.
7. Lifecycle method(라이프사이클 메서드)
안녕하세요! delay100입니다. 이번 포스팅에서는 컴포넌트의 라이프사이클 메서드에 대해 공부해봅시다. 대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요! 책
delay100.tistory.com
그러나 함수 컴포넌트에서는 라이프사이클 메서드의 사용이 불가능합니다.
그 대신, 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에 대해 공부한 적이 있습니다.
8. Hooks
안녕하세요! delay100입니다. 이번 포스팅에서는 Hooks에 대해 공부해봅시다. 대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요! 책 리액트를 다루는 기술, 개정판
delay100.tistory.com
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에 대해 공부한 적이 있습니다.
8. Hooks
안녕하세요! delay100입니다. 이번 포스팅에서는 Hooks에 대해 공부해봅시다. 대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요! 책 리액트를 다루는 기술, 개정판
delay100.tistory.com
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 |