[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개를 자동으로 생성했다.

주의 ❗❗

👉 useState의 기본값에 함수를 넣은 이유는 useState(createBulkTodos())라고 작성하면 리렌더링될 때마다 createBulkTodos 함수가 호출되지만, useState(createBulkTodos)처럼 파라미터를 함수 형태로 넣을 경우 컴포넌트가 처음 렌더링될 때만 함수가 실행된다.

2. 크롬 개발자 도구를 통한 성능 모니터링

  • 데이터 2000개가 렌더링되었을 때 항목 중 하나를 체크할 경우 느려지는게 느껴짐.
  • 하지만 느낌만으로 성능을 분석하는 것은 정확하지 않고, 초 단위까지 확인해야함.
    👉 크롬 개발자 도구의 Performance 탭을 사용하여 초 단위까지 측정 가능
  • 크롬 개발자 도구의 Performance 탭을 열어 다음과 같이 녹화 버튼으로 녹화를 시작한 후, 항목을 체크한 다음 화면에 변화가 반영되면 Stop 버튼을 눌러 성능 분석 결과를 확인해보자.

  • 위의 그림과 같이 2000개밖에 안되는 데이터를 처리되는데 대략 0.7초나 걸린다는 것은 성능이 매우 나쁘다는 의미이다.

2.1 느려지는 원인 ✍

  • 컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.
  1. 자신이 전달받은 props가 변경될 때
  2. 자신의 state가 바뀔 때
  3. 부모 컴포넌트가 리렌더링될 때
  4. forceUpdate 함수가 실행될 때
  • 2 절의 상황을 분석해 보면, 할 일1 항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링된다.
    👉 즉, 부모 컴포넌트리렌더링되었기 때문에 TodoList 컴포넌트가 리렌더링되고 그 안의 다른 모든 컴포넌트도 리렌더링된다.
  • 이러한 상황은 할 일 1 항목만 리렌더링되도록 하고 불필요한 리렌더링을 방지해주어야 한다.

3. 컴포넌트 성능 최적화

  • [React] 8. 라이프사이클 개념에서 살펴보았던 shouldComponentUpdate 라는 라이프 사이클을 사용하면 된다.
  • 하지만, 함수형 컴포넌트에서는 라이프 사이클 메서드를 사용할 수 없다.
    👉 하지만, React.memo를 사용하여 컴포넌트 성능을 최적화 할 수 있다.
  • 아래와 같이 TodoListItem.jsReact.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 배열이 업데이트되면 onRemoveonToggle 함수도 새롭기 바뀌기 때문에 바뀌지 않도록 해주어야 한다.
  • onRemoveonToggle 함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 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 절에서 성능 측정한 방법으로 다시 한번 확인한 결과 성능이 훨씬 향상된 것을 확인했다.


Tip ❗❗

👉 현재 yarn start를 통해 개발 서버를 구동하고 있는데, 개발 서버를 통해 보이는 리액트 애플리케이션은 실제 프로덕션에서 구동될 때보다 처리속도가 느리다고한다.❗
✍ 지금은 소규모 프로젝트이기 때문에 차이가 그렇게 크지 않지만, 프로덕션 모드로 구동해 보고 싶다면 다음과 같이 명령어를 입력하여 확인해보자.

$ yarn build
$ yarn global add serve
$ serve -s build

위의 명령어를 통해 개발 서버프로덕션에서 구동될 때를 비교하면서 성능을 확인해보는 것도 좋을 것 같다.✍😊

end

좋은 웹페이지 즐겨찾기