11장 컴포넌트 성능 최적화

Posted by : at

Category : React


2021-07-06

11장 컴포넌트 성능 최적화


10장에서 만든 애플리케이션은 불편하지 않다. 하지만 데이터가 무수히 많아진다면 애플리케이션이 느려지는것을 체감할 수 있을 정도로 지연이 생긴다.

  1. 많은 데이터 렌더링하기
  2. 크롬 개발자 도구를 통한 성능 모니터링
  3. React.memo를 통한 컴포넌트 리렌더링 성능 최적화
  4. onToggle과 onRemove가 새로워지는 현상 방지하기
  5. react-virtualized 사용한 렌더링 최적화

다음과 같은 순서로 진행되고 정리한다.



11.1 많은 데이터 렌더링하기


App.js 중 일부분

// 많은 데이터 렌더링하기
function createBulkTodos() {
  const array = [];
  for (let i = 1; i <= 2500; i++) {
    array.push({
      id: i,
      text: `할 일 ${i}`,
      checked: false,
    });
  }
  return array;
}

createBulkTodos() 라는 함수를 만들어 할일에 대한 배열을 2500개까지 생성하도록한다. id는 1부터 2500까지 인덱스를 넣어주고 백틱 기호를 사용하여 ${i} 를 통해서 인덱스를 텍스트에 넣어주도록 한다. 그리고 배열을 반환하도록 한다.


// useState(함수이름) 을 통해서 처음 렌더링 시에 createBulkTodos가 실행되게 한다.
const [todos, setTodos] = useState(createBulkTodos);

다음으로 state를 생성하는 코드에서 todos 배열안에 createBulkTodos가 반환하는 배열이 들어가도록 useState(createBulkTodos)를 통해서 컴포넌트가 처음 렌더링 될 때 함수가 실행되도록 함수의 이름만 넣어주도록 한다.



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


성능을 분석하는데 있어서 느려졌다는 느낌으로는 충분하지 않다. 따라서 정확히 몇 초가 걸리는지 확인해야 하는데, 크롬 개발자 도의 Performance 탭을 사용하여 측정하면 된다. 개발자 도구의 녹화 버튼을 눌러 동작한 후 그 동작에 대한 성능을 측정하도록 한다.


성능 분석 결과에 Timings를 열어 보면 각 시간대에 컴포넌트의 어떤 작업이 처리되었는지를 확인할 수 있다.



11.3 느려지는 원인 분석


컴포넌트는 다음과 같은 상황에서 리렌더링된다.

  1. 자신이 전달받은 props가 변경될 때
  2. 자신의 state가 바뀔 때
  3. 부모 컴포넌트가 리렌더링 될 때
  4. forceUpdate 함수가 실행될 때

지금 프로젝트의 상황은 state가 변경되면서 리스트에 있는 컴포넌트들이 모두 리렌더링하는 상황으로 리렌더링이 불필요할 때는 방지해주는 작업이 필요하다.



11.4 React.memo를 사용하여 컴포넌트 성능 최적화


컴포넌트 리렌더링을 방지할 때는 shouldComponentUpdate라는 라이프사이클을 사용하면 된다. 함수형 컴포넌트에서는 라이프사이클 메서드를 사용할 수 없다. 따라서 React.memo라는 함수를 사용하도록 한다. 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화해 줄 수 있다.

TodoListItem.js

// React.memo로 감싸줌으로써 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않는다.
export default React.memo(
  TodoListItem,
  (prevProps, nextProps) => prevProps.todo === nextProps.todo
);



11.5 onToggle, onRemove 함수가 바뀌지 않게 하기


현재 프로젝트에서는 React.memo를 사용하는 것만으로는 컴포넌트 최적화가 끝나지 않는다. 현재 프로젝트에서는 todos 배열이 업데이트되면 onRemoveonToggle 함수도 새롭게 바뀌기 때문이다.

onRemoveonToggle 함수는 배열 상태를 업데이트 하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다. 이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 useState 함수형 업데이트 기능을 사용하는 것과 useReducer를 사용하는 것이다.



useState의 함수형 업데이트

기존에는 setTodos 함수를 사용할 때 새로운 상태로 파라미터를 넣어 주었다. setTodos를 사용할 때는 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수 있다. 이를 함수형 업데이트라고 부른다.


example.js

const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킨다.
const onIncrease = useCallback(
  () => setNumber((prevNumber) => prevNumber + 1),
  []
);

setNumber(number + 1) 하는 것이 아니라, 위 코드 처럼 어떻게 업데이트할지 정의해 주는 업데이트 함수를 넣어준다. 그러면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 number를 넣지 않아도 된다.


App.js 중 일부

const onRemove = useCallback((id) => {
  setTodos((todos) => todos.filter((todo) => todo.id !== id));
}, []);

다음과 같이 함수들을 todos(이전 상태 값)을 파라미터로 받아서 해당 값에 대한 연산을 수행한 후 상태를 업데이트 하도록 하고 뒤에 들어가는 배열은 비운다.


** 프로덕션 모드로 구동 시

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



useReducer 사용하기

App-Reduce.js

import React, { useCallback, useReducer, useRef, useState } from 'react';
import TodoInsert from 'components/Todo/TodoInsert';
import TodoList from 'components/Todo/TodoList';
import TodoTemplate from 'components/Todo/TodoTemplate';

// 많은 데이터 렌더링하기
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': // 토글
      // { type : 'TOGGLE', id : 1}
      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);

  // 고윳값으로 사용될 id
  // ref를 사용하여 변수 담기
  const nextId = useRef(2501);

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    dispatch({ type: 'INSERT', todo });
    nextId.current += 1; // nextId 1씩 더하기
  }, []);

  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} />
    </TodoTemplate>
  );
};

export default App;

useReducer를 사용할 때는 원래 두 번째 파라미터에 초기 상태를 넣어주어야 한다. 지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 사앹를 만들어주는 함수인 createBulkTodos를 넣어주었다. 이렇게 하면 컴포넌트가 맨 청므 렌더링 할때만 createBulkTodos 함수가 호출된다.


useReducer를 사용하는 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있다.



11.6 불변성의 중요성


리액트 컴포넌트에서 상태를 업데이트할 때 불변성을 지키는 것은 매우 중요하다.

App.js

const onToggle = useCallback((id) => {
  setTodos((todos) =>
    todos.map((todo) =>
      todo.id === id ? { ...todo, checked: !todo.checked } : todo
    )
  );
}, []);

기존 데이터를 수정할 때 직접 수정하지 않고 새로운 배열을 만든 다음에 새로운 객체를 만들어서 필요한 부분을 교체해 주는 방식으로 구현했다. 업데이트가 필요한 곳에서는 아예 새로운 배열 혹은 새로운 객체를 만들기 때문에 React.memo를 사용했을 때 props가 바뀌었는지 혹은 바뀌지 않았는지를 알아내서 리렌더링 성능을 최적화해 줄 수 있었다.

이렇게 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것은 불변성을 지킨다 라고 한다.

const array = [1, 2, 3, 4, 5];

const nextArrayBad = array; // 배열을 복사하는 것이 아니라 똑같은 배열을 가리킨다.
nextArray[0] = 100;
console.log(array === nextArrayBad); // 완전히 같은 배열이기 때문에 true

const nextArrayGood = [...array]; // 배열 내부에 값을 모두 복사
nextArrayGood[0] = 100;
console.log((array = nextArrayGood)); // 다른 배열이기 때문에 false

const object = {
  foo: 'bar',
  value: 1,
};

const nextObjectBad = object; // 객체가 복사되지 않고, 똑같은 객체를 가리킨다.
nextObjectBad.value = nextObjectBad.value + 1;
console.log(object === nextObjectBad); // 같은 객체이기 때문에 true

const nextObjectGood = {
  ...object, // 기존에 있던 내용을 모두 복사해서 넣는다.
  value: object.value + 1, // 새로운 값을 덮어 쓴다.
};

cosole.log(object === nextObjectGood); // 다른 객체이기 때문에 false

불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다. 전개 연산자( . . . 문법)를 사용하여 객체나 배열 내부의 값을 복사할 때는 얕은 복사(shallow copy)를 하게 된다. 즉, 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사된다. 따라서 내부의 값이 객체 혹은 배열이라면 내부의 값 또한 따로 복사해 주어야 한다.

const todos = [
  { id: 1, checked: true },
  { id: 2, checked: true },
];

const nextTodos = [...todos];

nextTodos[0].checked = false;
console.log(todos[0] === nextTodos[0]); // 아직까지는 똑같은 객체를 가리키고 있기 때문에 true

nextTodos[0] = {
  ...nextTodos[0],
  checked: false,
};

console.log(todos[0] === nextTodos[0]); // 새로운 객체를 할당해 주었기에 false

만약 객체 안에 있는 객체라면 불변성을 지키면서 새 값을 할당해야 하므로 다음과 같이 해 줘야 한다.

const nextComplexObject = {
  ...ComplexObject,
  objectInside: {
    ...ComplexObject.objectInside,
    enabled: false,
  },
};

console.log(complexObject === nextComplexObject); // false
console.log(complexObject.objectInside === nextComplexObject.objectInside); // false



11.7 TodoList 컴포넌트 최적화하기

TodoList.js

const TodoList = ({ todos, onRemove, onToggle }) => {
    return ( . . .);
};

export default React.memo(TodoList);

TodoList를 다음과 같이 수정한다. 위의 최적화 코드는 프로젝트 성능에 영향을 전혀 주지 않는다. 왜냐하면 TodoList 컴포넌트의 부모 컴포넌트인 App 컴포넌트가 리렌더링되는 유일한 이유가 todos 배열이 업데이트될 때이기 때문이다. 따라서 TodoList 컴포넌트는 불필요한 리렌더링이 발생하지 않는다. 하지만 state가 추가되어 해당 값들이 업데이트 될 때는 TodoList 컴포넌트가 불필요한 렌더링을 할 수 있기 때문에 React.memo를 통해서 최적화 해주었다.



11.8 react-virtualized를 사용한 렌더링 최적화


지금까지는 리액트 컴포넌트 리렌더링 성능을 최적화하는 방법을 알아보았다. 리렌더링 성능을 최적화할 때는 필요한 리렌더링만 하도록 설정하는게 좋을 것이다.


react-virtualized 를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있다. 그리고 만약 스크롤되면 해당 스크롤 위치에서 보여 주어야 할 컴포넌트를 자연스럽게 렌더링한다.

$ yarn add react-virtualized


다음으로 각 항목의 크기와 전체 크기를 재어 TodoList를 수정하도록 한다.

TodoList.js

import React, { useCallback } from 'react';
import TodoListItem from './TodoListItem';
import 'components/Scss/TodoList.scss';
import { List } from 'react-virtualized';

const TodoList = ({ todos, onRemove, onToggle }) => {
  const rowRenderer = useCallback(
    ({ index, key, style }) => {
      const todo = todos[index];
      return (
        <TodoListItem
          todo={todo}
          key={key}
          onRemove={onRemove}
          onToggle={onToggle}
          style={style}
        />
      );
    },
    [onRemove, onToggle, todos]
  );

  return (
    <List
      className="TodoList"
      width={512} // 전체크기
      height={513} // 전체 높이
      rowCount={todos.length} // 항목 개수
      rowHeight={57} // 항목 높이
      rowRenderer={rowRenderer} // 항목을 렌더링할 때 쓰는 함수
      list={todos} // 배열
      style= // List에 기본 적용되는 outline 스타일 제거
    />
  );
};

export default React.memo(TodoList);

List 컴포넌트를 사용할 때는 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 렌더링할 때 사용해야 하는 함수, 그리고 배열을 props로 넣어주어야 한다. 그러면 이 컴포넌트가 전달받은 props를 사용하여 자동으로 최적화 해준다.

TodoListItem

import React from 'react';
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from 'react-icons/md';

import cn from 'classnames';
import 'components/Scss/TodoListItem.scss';

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>
  );
};

// React.memo로 감싸줌으로써 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않는다.
export default React.memo(
  TodoListItem,
  (prevProps, nextProps) => prevProps.todo === nextProps.todo
);

About YeongJun
YeongJun

Hi I am YeongJun, a Web Developer and Designer.

Email : dudwns1045@naver.com

Website : http://gitbub.com/dudwns9331

About yeongjun

강원대학교 컴퓨터공학과 컴퓨터과학전공 R.O.T.C 60th 127 학군단, 프론트엔드 개발 준비중 :D

Categories
Useful Links