[Todo-List] 4. 컴포넌트 성능 개선 (1)
Todo-List 프로젝트는 소규모 프로젝트라서 추가되어 있는 데이터가 매우 적기 때문에 속도나 성능 측면에서 문제가 없을 수 있다.
👉 하지만, 데이터가 무수히 많아진다면? 🤦♂️ 바로 느려지는 것을 체감할 수 있을 것이다.
이번에는 임의적으로 `랙(lag)`을 발생시켜 문제점을 확인해보자.✍
또한, 정확한 소스 코드는 내 github를 확인하면 된다.
1. 많은 데이터 렌더링
- App 컴포넌트를 다음과 같이 수정하여
랙
을 발생시켜 보자.
//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;
- 데이터를 직접 추가하기 보단 createBulkTodos라는 함수를 만들어서 데이터
2000개
를 자동으로 생성했다.
주의 ❗❗
랙
을 발생시켜 보자.//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)
처럼 파라미터를 함수 형태로 넣을 경우 컴포넌트
가 처음 렌더링될 때만 함수가 실행된다.
2. 크롬 개발자 도구를 통한 성능 모니터링
- 데이터
2000개
가 렌더링되었을 때 항목 중 하나를 체크할 경우 느려지는게 느껴짐.
- 하지만
느낌
만으로 성능을 분석하는 것은 정확하지 않고, 초 단위까지 확인해야함.
👉 크롬 개발자 도구의 Performance
탭을 사용하여 초 단위까지 측정 가능
- 크롬 개발자 도구의
Performance
탭을 열어 다음과 같이 녹화 버튼으로 녹화를 시작한 후, 항목을 체크한 다음 화면에 변화가 반영되면 Stop
버튼을 눌러 성능 분석 결과를 확인해보자.
2000개
가 렌더링되었을 때 항목 중 하나를 체크할 경우 느려지는게 느껴짐.느낌
만으로 성능을 분석하는 것은 정확하지 않고, 초 단위까지 확인해야함.👉 크롬 개발자 도구의
Performance
탭을 사용하여 초 단위까지 측정 가능Performance
탭을 열어 다음과 같이 녹화 버튼으로 녹화를 시작한 후, 항목을 체크한 다음 화면에 변화가 반영되면 Stop
버튼을 눌러 성능 분석 결과를 확인해보자.- 위의 그림과 같이
2000개
밖에 안되는 데이터를 처리되는데 대략 0.7초나 걸린다는 것은 성능이매우 나쁘다
는 의미이다.
2.1 느려지는 원인 ✍
- 컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.
- 자신이 전달받은
props
가 변경될 때 - 자신의
state
가 바뀔 때 - 부모 컴포넌트가
리렌더링
될 때 forceUpdate
함수가 실행될 때
2 절
의 상황을 분석해 보면,할 일1
항목을 체크할 경우 App 컴포넌트의state
가 변경되면서 App 컴포넌트가리렌더링
된다.
👉 즉, 부모 컴포넌트가리렌더링
되었기 때문에 TodoList 컴포넌트가리렌더링
되고 그 안의 다른 모든 컴포넌트도리렌더링
된다.- 이러한 상황은
할 일 1
항목만리렌더링
되도록 하고 불필요한리렌더링
을 방지해주어야 한다.
3. 컴포넌트 성능 최적화
- [React] 8. 라이프사이클 개념에서 살펴보았던
shouldComponentUpdate
라는 라이프 사이클을 사용하면 된다.
- 하지만,
함수형 컴포넌트
에서는 라이프 사이클 메서드를 사용할 수 없다.
👉 하지만, React.memo를 사용하여 컴포넌트
성능을 최적화 할 수 있다.
- 아래와 같이
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
이 바뀌지 않으면 리렌더링을 하지 않음.
3.1 onToggle, onRemove 함수 바뀌지 않게 하기
- Todo-List 프로젝트에서는
todos
배열이 업데이트되면 onRemove
와 onToggle
함수도 새롭기 바뀌기 때문에 바뀌지 않도록 해주어야 한다.
onRemove
와 onToggle
함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 todos
를 참조하기 때문에 todos
배열이 바뀔 때마다 함수가 새로 만들어진다.
👉 함수
가 계속해서 만들어지는 상황은 useState
의 함수형 업데이트 기능을 사용하는 것과, useReducer
를 사용하는 방법이 있다.
3.1.1 useState 함수형 업데이트
- 기존에
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 절
에서 성능 측정한 방법으로 다시 한번 확인한 결과 성능이 훨씬 향상된 것을 확인했다.
shouldComponentUpdate
라는 라이프 사이클을 사용하면 된다.함수형 컴포넌트
에서는 라이프 사이클 메서드를 사용할 수 없다.👉 하지만, React.memo를 사용하여
컴포넌트
성능을 최적화 할 수 있다.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 절
에서 성능 측정한 방법으로 다시 한번 확인한 결과 성능이 훨씬 향상된 것을 확인했다.Tip ❗❗
👉 현재 yarn start
를 통해 개발 서버를 구동하고 있는데, 개발 서버를 통해 보이는 리액트 애플리케이션은 실제 프로덕션
에서 구동될 때보다 처리속도가 느리다고한다.❗
✍ 지금은 소규모 프로젝트이기 때문에 차이가 그렇게 크지 않지만, 프로덕션
모드로 구동해 보고 싶다면 다음과 같이 명령어를 입력하여 확인해보자.
$ yarn build
$ yarn global add serve
$ serve -s build
위의 명령어를 통해 개발 서버
와 프로덕션
에서 구동될 때를 비교하면서 성능을 확인해보는 것도 좋을 것 같다.✍😊
end
Author And Source
이 문제에 관하여([Todo-List] 4. 컴포넌트 성능 개선 (1)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@daekyeong/Todo-List-4.-컴포넌트-성능-개선-1저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)