Todo-List 프로젝트는 소규모 프로젝트라서 추가되어 있는 데이터가 매우 적기 때문에 속도나 성능 측면에서 문제가 없을 수 있다.
👉 하지만, 데이터가 무수히 많아진다면? 🤦♂️ 바로 느려지는 것을 체감할 수 있을 것이다.
랙
을 발생시켜 보자.//App.js
function createBulkTodos() {
const array = [];
for(let i = 1; i <= 2000; i++) {
array.push({
id: i,
text: `할 일 ${i}`,
checked: false
});
}
return array;
}
const App = () => {
(...)
};
export default App;
2000개
를 자동으로 생성했다.👉 useState
의 기본값에 함수를 넣은 이유는 useState(createBulkTodos())
라고 작성하면 리렌더링될 때마다 createBulkTodos
함수가 호출되지만, useState(createBulkTodos)
처럼 파라미터를 함수 형태로 넣을 경우 컴포넌트
가 처음 렌더링될 때만 함수가 실행된다.
2000개
가 렌더링되었을 때 항목 중 하나를 체크할 경우 느려지는게 느껴짐.느낌
만으로 성능을 분석하는 것은 정확하지 않고, 초 단위까지 확인해야함.Performance
탭을 사용하여 초 단위까지 측정 가능Performance
탭을 열어 다음과 같이 녹화 버튼으로 녹화를 시작한 후, 항목을 체크한 다음 화면에 변화가 반영되면 Stop
버튼을 눌러 성능 분석 결과를 확인해보자.2000개
밖에 안되는 데이터를 처리되는데 대략 0.7초나 걸린다는 것은 성능이 매우 나쁘다
는 의미이다.props
가 변경될 때state
가 바뀔 때리렌더링
될 때forceUpdate
함수가 실행될 때2 절
의 상황을 분석해 보면, 할 일1
항목을 체크할 경우 App 컴포넌트의 state
가 변경되면서 App 컴포넌트가 리렌더링
된다.리렌더링
되었기 때문에 TodoList 컴포넌트가 리렌더링
되고 그 안의 다른 모든 컴포넌트도 리렌더링
된다.할 일 1
항목만 리렌더링
되도록 하고 불필요한 리렌더링
을 방지해주어야 한다.shouldComponentUpdate
라는 라이프 사이클을 사용하면 된다.함수형 컴포넌트
에서는 라이프 사이클 메서드를 사용할 수 없다.컴포넌트
성능을 최적화 할 수 있다.TodoListItem.js
를 React.memo를 사용하여 수정했다.//TodoListItem.js
import React from "react";
import {
MdCheckBox,
MdRemoveCircleOutline,
MdCheckBoxOutlineBlank,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';
const TodoListItem = ({ todo, onRemove, onToggle }) => {
(...)
};
export default React.memo(TodoListItem);
export
과정에서 React.memo로 감싸주었기 때문에 컴포넌트
의 props
가 바뀌지 않았다면, 리렌더링하지 않는다.todo
, onRemove
, onToggle
이 바뀌지 않으면 리렌더링을 하지 않음.todos
배열이 업데이트되면 onRemove
와 onToggle
함수도 새롭기 바뀌기 때문에 바뀌지 않도록 해주어야 한다.onRemove
와 onToggle
함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 todos
를 참조하기 때문에 todos
배열이 바뀔 때마다 함수가 새로 만들어진다.함수
가 계속해서 만들어지는 상황은 useState
의 함수형 업데이트 기능을 사용하는 것과, useReducer
를 사용하는 방법이 있다.setTodos
함수를 사용할 때는 새로운 상태를 파라미터로 넣어주었는데, setTodos
를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트
를 어떻게 할지 정의해 주는 업데이트를 함수형 업데이트
라고 한다.예제
를 살펴보자.//Example
const [number, setNumber] = useState(0);
//prevNumbers는 현재 number 값
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1), []);
setNumber(number+1)
을 하는 것이 아니라 위 코드처럼 어떻게 업데이트할지 정의해주면 된다.useCallback
을 사용할 때 두 번째 파라미터로 넣는 배열에 number
를 넣지 않아도 된다.App.js
를 다음과 같이 정했다.//App.js
import React, { useState, useRef, useCallback } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';
function createBulkTodos() {
const array = [];
for(let i = 1; i <= 2000; 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; // id 값 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 =>
만 앞에 넣었고, useCallback
의 두 번째 파라미터를 넣지 않았다.2 절
에서 성능 측정한 방법으로 다시 한번 확인한 결과 성능이 훨씬 향상된 것을 확인했다.👉 현재 yarn start
를 통해 개발 서버를 구동하고 있는데, 개발 서버를 통해 보이는 리액트 애플리케이션은 실제 프로덕션
에서 구동될 때보다 처리속도가 느리다고한다.❗
✍ 지금은 소규모 프로젝트이기 때문에 차이가 그렇게 크지 않지만, 프로덕션
모드로 구동해 보고 싶다면 다음과 같이 명령어를 입력하여 확인해보자.
$ yarn build
$ yarn global add serve
$ serve -s build
위의 명령어를 통해 개발 서버
와 프로덕션
에서 구동될 때를 비교하면서 성능을 확인해보는 것도 좋을 것 같다.✍😊