[리액트공부] 11. 컴포넌트 성능 최적화

kkado·2022년 8월 5일
0

리다기

목록 보기
12/16
post-thumbnail

직전 챕터에서 일정관리 애플리케이션을 만들었었는데 다루는 데이터의 양이 적어서 이용에 불편을 느끼지 않겠지만 성능 면에서 개선의 여지가 많다.

우선 실제로 렉을 경험할 수 있게끔 많은 데이터를 렌더링해 보자.

function createBulkTodos() {
  const array = [];
  for(let i = 1; i <= 2500; i++)
  {
    array.push({
      id: i,
      text: `할 일 ${i}`,
      checked: false,
    });
  }
  return array;
}

기존의 TodoList 애플리케이션에서 위와 같은 코드를 추가하여 2500개의 리스트를 만든 후에 체크나 삭제 등을 수행하면 조금 느려진 모습을 확인할 수 있다.


컴포넌트가 느려지는 원인

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

위의 경우 항목을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링 되므로, 그 안의 많은 컴포넌트들도 부모 컴포넌트가 리렌더링되었기 때문에 리렌더링된다.

이 때, 내가 선택한 항목을 제외한 나머지 수많은 리스트들은 리렌더링될 필요가 없음에도 리렌더링되고 있으므로 지금처럼 성능의 저하가 발생한 것이다.

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

컴포넌트의 리렌더링을 방지할 때에는 shouldComponentUpdate 라이프사이클을 사용하면 되는데 함수형 컴포넌트에서는 라이프사이클을 사용할 수 없으므로, React.memo라는 함수를 사용한다.

이제 TodoListItem 컴포넌트에 React.memo를 적용해 보면 다음과 같다.

...생략
export default React.memo(TodoListItem);

이제 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링 되지 않는다. 그러나 현재 프로젝트에서는 todos 배열이 업데이트되면 onRemove와 onToggle 함수도 새롭게 바뀐다. 두 함수는 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다.

이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 두 가지이다. useState의 함수형 업데이트 기능을 사용하는 것이나, useReducer를 사용하는 것이다.

useState의 함수형 업데이트

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

const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text: text,
      checked: false,
    };
    setTodos((todos) => todos.concat(todo));
    nextId.current += 1;
  }, []);

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

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

이런 식으로 setTodos를 사용할 때 그 안에 todos => 만 넣어주면 된다. 그리고 다시 애플리케이션을 구동해 보면 성능 개선이 많이 이루어진 것을 볼 수 있다.

불변성

기존 데이터를 수정할 때 직접 수정하지 않고 새로운 배열을 만든 다음에 새로운 객체를 만들어서 필요한 부분을 교체해 주는 방식으로 구현했다.

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

다음의 코드를 보자.

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

const newBadArray = array; // 배열을 복사하는 것이 아니라 같은 배열을 가리킴
newBadArray[0] = 100;
console.log(newBadArray === array); // 완전히 같은 배열이므로 true

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

// true
// false

업데이트가 필요한 곳에서는 아예 새로운 배열 혹은 객체를 만들기 때문에 React.memo를 사용했을 때 props가 바뀌었는지 혹은 바뀌지 않았는지를 알아내서 리렌더링 성능을 최적화할 수 있다.

불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 변화를 감지하지 못한다. 비교를 해야 하는 원본 객체가 변화했기 때문이다.


추가로 전개 연산자(...)를 사용하여 객체나 배열의 값을 복사할 때는 얕은 복사(shallow copy)를 하게 되는데, 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽에 있는 값만 복사한다.

따라서 객체 내부에 또다른 객체 또는 배열이 있을 경우 따로 복사해 주어야 한다.

todoList 컴포넌트 최적화

todoList에도 export default React.memo(TodoList);를 해줄 수 있지만 todoList는 불필요한 렌더링이 발생하지 않는다. TodoList 컴포넌트가 리렌더링되는 유일한 경우는 todos 배열이 업데이트될 때이기 때문이다.


정리

아직 컴포넌트들을 최적화할 수준은 되지 못하지만... React.memo 를 사용하면 불필요한 렌더링을 줄일 수 있다고 배웠었는데 실습해 볼 수 있었다.

원래 react-virtualized 부분도 있었으나 공부해 본 결과 이해하기가 쉽지 않아서 패스...

profile
베이비 게임 개발자

0개의 댓글