공부블로그

11장. 컴포넌트 성능 최적화 본문

리액트/리액트를 다루는 기술

11장. 컴포넌트 성능 최적화

떠어영 2021. 1. 23. 16:57

11. 1 ) 많은 데이터 렌더링하기

createBulkTodos라는 함수를 만들고 useState의 초기값에 파라미터로 넣어서 많은 데이터를 렌더링해보자.

import React, { useState, useRef, useCallback } from 'react';
...

function createBulkTodos() {
  const array = [];
  for (let i = 1; i <= 2500; i++) {
    array.push({
      id: i,
      text: `할 일${i}`,
      checked: false,
    });
  }
  return array;
}
const App = () => {
  const [todos, setTodos] = useState(createBulkTodos);
  //기본값에 파라미터로 함수 형태를 넣어주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos함수 실행된다.

  //ref를 사용하여 변수 담기
  const nextId = useRef(2501);

  ...
export default App;

 

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

크롬 개발자 도구의 Performance 탭을 사용하여 정확히 몇 초가 걸리는지 측정할 수 있다. 

 

 

11. 3 ) 느려지는 원인 분석

컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.

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

앞에서 만들었던 일정 관리 애플리케이션을 분석해보면, '할 일1' 항목을 체크할 경우 App컴포넌트의 state가 변경되면서 App컴포넌트가 리렌더링된다.

부모 컴포넌트가 리렌더링되었기 때문에 TodoList 컴포넌트가 리렌더링되고, 또 그 안의 무수한 컴포넌트들이 리렌더링된다. 

 

'할 일1' 항목 이외의 2499개의 항목들도 다같이 리렌더링되고 있기 때문에 매우 느린것이다. 리렌더링이 불필요할 때는 리렌더링을 방지해주어야 한다.

 

 

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

컴포넌트의 리렌더링을 방지할 때는 7장에서 배운 shouldComponentUpdate라는 라이프사이클을 사용하면 된다.

하지만 함수형 컴포넌트에서는 사용할 수 없으므로 그대신 React. memo라는 함수를 사용한다. 컴포넌트의 props가 바뀌지 않았다면 리렌더링을 하지 않도록 설정한다.

 

TodoListItem 컴포넌트를 React.memo로 감싸준다. → export default React.memo( TodoListItem );

이제 TodoListItem컴포넌트는 todo, onRemove, onToggle (props)가 바뀌지 않으면 리렌더링하지 않는다.

 

 

11. 5 ) onToggle, onRemove 함수가 바뀌지 않게 하기

현재 프로젝트에서는 onRemove와 onToggle이 배열 상태를 업데이트하는 과정에서 최신상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다. 

함수가 계속 만들어지는 상황을 방지하기 위해서는 useState의 함수형 업데이트 기능을 사용하거나, useReducer를 사용할 수 있다. 

 

11. 5. 1 ) useState의 함수형 업데이트

 

기존의 setTodos함수처럼 새로운 상태를 파라미터로 넣어주는 대신, 업데이트 함수를 넣어준다.

App. js에서 onInsert, onRemove, onToggle 함수에서 setTodos함수를 사용할 때, 앞에는 todos=> 를 넣어주고, useCallback함수의 두번째 파라미터에는 빈 배열을 넣어준다.

 

11. 5. 2 ) useReducer 사용하기

 

useReducer를 사용할 때 원래는 두번째 파라미터에 초기 상태를 넣어주는데 그 대신 undefined를 넣고, 세번째 파라미터에 초기상태를 만들어 주는 createBulkTodos함수를 넣어준다.

이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos함수가 호출된다.

 

App. js

import React, { useReducer, useRef, useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
...
function todoReducer(todos, action) {
  switch (action.type) {
    case 'INSERT': //새로추가
      return todos.concat(action.todo);
    case 'REMOVE': //제거
      return todos.filter((todo) => todo.id !== action.id);
    case 'TOGGLE': //토글
      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);
  //기본값에 파라미터로 함수 형태를 넣어주면 컴포넌트가 처음 렌더링될 때만 createBulkTodos함수 실행된다.

  //ref를 사용하여 변수 담기
  const nextId = useRef(2501);

  const onInsert = useCallback(
    (text) => {
      //???
      const todo = {
        id: nextId.current,
        text,
        checked: false,
      };
      dispatch({ type: 'INSERT', todo }); 
      nextId.current += 1; //id+1
    },
    [], //todos가 바뀔때만 렌더링
  );

  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} />{' '}
      {/* props 넣어주기 */}
    </TodoTemplate>
  );
};
export default App;

11. 6 ) 불변의 중요성

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

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

불변성이 지켜져야 React. memo를 사용했을 때 객체 내부의 값이 바뀌었는지 알아내서 성능을 최적화할 수 있다.

 

추가로 전개 연산자 ( . . . ) 를 사용항여 객체나 배열 내부의 값을 복사할 때는 얕은 복사를 하게 된다. 따라서 내부의 값이 객체 혹은 배열이라면 내부의 값도 따로 복사해주어야 한다.

ex) const nextTodos = [ . . . todos ]; 

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

이렇게 복잡한 경우에는 immer라이브러리의 도움을 받을 수도 있다. immer는 다음 장에서 알아보자.

 

11. 7 ) TodoList 컴포넌트 최적화하기 

리스트에 관련된 컴포넌트를 최적화할 때는 리스트 아이템과 리스트, 이 두 컴포넌트를 최적화해주는 것이 필수적이다.

TodoList. js에서 export default React.memo( TodoList ); 를 작성해보자.

위의 코드는 사실 성능에 전혀 영향을 주지 않는다. 하지만 App컴포넌트에 state가 추가되어 해당 값들이 업데이트될 수도 있으므로 미리 최적화를 해주는 것이 좋다.

 

11. 8 ) react-virtualized를 사용한 렌더링 최적화

react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있다. 

$ yarn add react-virtualized로 라이브러리를 설치하자.

이 라이브러리에서 제공하는 List컴포넌트를 사용하여 성능을 최적화 시켜보자.

 

TodoList. js

import React from 'react';
import { List } from 'react-virtualized';
import TodoListItem from './TodoListItem'; //블러오기
import './TodoList.scss';
import { useCallback } from '../../node_modules/react/cjs/react.development';

const TodoList = ({ todos, onRemove, onToggle }) => {
  const rowRender = useCallback(
    //리스트 컴포넌트에서 각 TodoItem을 렌더링할 때 사용,
    //이 함수를 List컴포넌트의 props로 설정해야한다
    ({ index, key, style }) => {
      //파라미터에 객체타입으로 받아와서 사용
      const todo = todos[index];
      return (
        <TodoListItem
          todo={todo}
          key={key}
          onRemove={onRemove}
          onToggle={onToggle}
          style={style}
        />
      );
    },
    [onRemove, onToggle, todos],
  );

  return (
    <List //react-virtualized의 list컴포넌트 사용
      className="TodoList"
      width={512} //전체크기
      height={513} //전체 높이
      rowCount={todos.length} //항목 개수
      rowHeight={57} //항목 높이
      rowRenderer={rowRender} //항목을 렌더링할 때 쓰는 함수
      list={todos} //배열
      style={{ outline: 'none' }} //List에 기본 적용되는 outline스타일 제거
    />
  );
};
export default React.memo(TodoList);

rowRender라는 함수에서 파라미터로 index, key, style을 가져와 사용하고, 이 함수는 List컴포넌트의 props( rowRenderer )로 설정해준다.

TodoListItem. js에서는 <div className='TodoListItem-virtualized' style={style}> 로 props올 받아온style을 적용시켜 감싸준다.

마지막으로 TodoListItem.scss에서 .TodoLsitItem-virtualized를 이용해 스타일을 수정해준다.