[리엑트를 다루는 기술] Chapter 11 : 컴포넌트 성능 최적화

iGhost·2021년 8월 30일
post-thumbnail

지금은 괜찮아, 하지만?

지금은 Todo들이 몇개 없어서 랜더링이 금방금방 되지만

만약 2500개가 된다면 어떻게 될까?

성능이 굉장히 느려진다 ⇒ 크롬 개발자 기구를 통해 알아보자

컴포넌트가 느려지는 이유

1. 자신이 전달받은 props가 변경될 때

2. 자신의 state가 바뀔 때

3. 부모 컴포넌트가 리렌더링될 때

4. forceUpdate 함수가 실행될 때

지금은 2500개의 todo중 하나의 todo1이 리랜더링 될때 App.js에서 리랜더링 되는것이니 그 하위에 있는 컴포넌트들까지 모두 리랜더링이 된다

⇒ 최적화를 해줘야함

리랜더링이 불필요한 컴포넌트들을 리팩토링 해줘야한다

React.memo()를 사용한 컴포넌트 성능 최적화

함수의 props가 바뀌지 않았다면 리랜더링 하지 않는다

TodoListitme.js

import React from ‘react‘;
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from ‘react-icons/md‘;
import cn from ‘classnames‘;
import./TodoListItem.scss‘;

const TodoListItem = ({ todo, onRemove, onToggle }) => {
  ()
};

export default React.memo(TodoListItem);
  • 이제 TodoListItem은 todo, onRemove,onToggle이 변경되지 않으면 리랜더링 하지 않아도 된다

⇒ 할일 1 빼고 나머지 애들은 리랜더링 안해도됨

하지만 할일1을 누르면 → app.js 의 todos가 바뀜 → onRemove, onToggle함수도 새롭게 바뀜 (배열 참조)

⇒ 다른 TodoListItem도 바뀐다

todos에 의해 변경은 이루어지는게 맞지만, todos의 상태에 의존하지 않게 하는방법은??

useState의 함수형 업데이트

useState에 새로운 배열을 넣는게 아니라(비동기처럼 작동), 이전 배열을 이용해 어떻게 업데이트를 해야할지를 넣는다면 (호출 순서대로 큐로 들어감)

state의 특성과 setState 함수형 업데이트

기존에는

state가 변경 ⇒ 해당 state를 의존하는 usecallback 을 실행 ⇒ setState에 해당 state를 이용한 변경이 이루어짐

함수형 업데이트로 한다면

state가 변경 ⇒ 하지만 usecallback은 더이상 state를 의존하지 않음 ⇒ state가 변경되도 리랜더링 하지 않음

이제는 usecallback으로 선언한 함수를 호출 하지 않는 이상 리랜더링 될 이유가 없다

함수형 업데이트 방식

// 기존
setState(prevNumber+1)
// 업데이트 방식
setState(prevNumber => prevNumber+1 )
  • 이렇게 하면, 최초 선언되때만 함수가 호출되고
  • 유지하다가 호출 할때만 사용됨

useReducer이용하기

import React, { useReducer, useRef, useCallback } from ‘react‘;
import TodoTemplate from./components/TodoTemplate‘;
import TodoInsert from./components/TodoInsert‘;
import TodoList from./components/TodoList‘;

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

function todoReducer(todos, action) {
  switch (action.type) {
    caseINSERT: // 새로 추가
      // { type: ‘INSERT‘, todo: { id: 1, text: ‘todo‘, checked: false } }
      return todos.concat(action.todo);
    caseREMOVE: // 제거
      // { type: ‘REMOVE‘, id: 1 }
      return todos.filter(todo => todo.id != = action.id);
    caseTOGGLE: // 토글
      // { type: ‘REMOVE‘, 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;

원래는 두번째 파라미터에 초기 상태를 넣어줘야하는데 ⇒ 빈칸으로 주고, 세번째 파라미터에 초기 상태를 만들어주는 함수를 넣어준다

⇒ 이렇게 하면 맨처음 랜더링 될때만 creatrBulkTodos 함수가 호출된다

즉, 그뒤에 리랜더링 될때는 모든 리스트를 랜더링 하지 않고, 업데이트만 이루어진다.


불변성의 중요성

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

기존 데이터를 수정할때, 직접 수정하는게 아니라, 새로운 배열을 만든 다음에 새로운 객체를 만들어 필요한 부분을 교체하는 방식으로 작동한다(리엑트 근본)

⇒ prpos가 바뀌었는지, 아닌지를 판단할수 있다

⇒ React,memo로 최적화 할수있다.

불변성을 지킨다 = 기존의 갑을 직접 수정하지 않으면서, 새로운 값을 만들어 내는것

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

const nextArrayBad = array; // 배열을 복사하는 것이 아니라 똑같은 배열을 가리킵니다.
nextArrayBad[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 // 새로운 값을 덮어 씁니다.
};
console.log(object === nextObjectGood); // 다른 객체이기 때문에 false

불변성을 지키는 이유?

객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.

불변성을 위해 객체를 복사할때 주의점

얕은 복사를 주의하자

  • 전개 연산자(스프레드 문법)을 이용하면 , 얕은 복사가 되어서 값이 변경되어도, 같은 객체를 가르킨다
const todos = [{ id: 1, checked: true }, { id: 2, checked: true }];
const nextTodos = […todos];

nextTodos[0].checked = false; //바꿨는데도, true, 바뀐거는 [0]안에 프로퍼티가 바꾼기거 [0]이 바뀐게 아니기 때문
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); // fals

근데 이렇게 하기에는 너무 복잡하다

imme함수를 사용하자!


추가적으로 TodoList 최적화 하기

import React from ‘react‘;
import TodoListItem from./TodoListItem‘;
import./TodoList.scss‘;

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

export default React.memo(TodoList);
  • 현재 프로젝트에는 영향이 없으나, 나중에 APP.js에서 다른 state가 설정되어, 리랜더링 될때는 방지함

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

현재 2500개의 데이터가 있지만, 사실상 화면에 보이는것은 아홉개 정도뿐이다

보이지 않는 2,491개의 컴포넌트들을 보이지 않음에도 불구 하고 렌더링일 되기 때문에 비효율적이다

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

TodoList.js

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

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={{ outline: ‘none‘ }} // List에 기본 적용되는 outline 스타일 제거
    />
  );
};

export default React.memo(TodoList);
  • List 컴포넌트를 사용하기 위해 rowRenderer라는 함수를 새로 작성해 주었다
  • 이 함수는 react-virtualized의 List 컴포넌트에서 각 TodoItem을 렌더링할 때 사용하며, 이 함수를 List 컴포넌트의 props로 설정해 주어야 한다.
  • 이 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아 와서 사용한다.

컴포넌트가 받은 props를 사용하여 자동으로 최적화 해준다

  • style이 없으니 추가해주자

TodoListItem

import React from ‘react‘;
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from ‘react-icons/md‘;
import cn from ‘classnames‘;
import./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>
  );
};

export default React.memo(
  TodoListItem,
  (prevProps, nextProps) => prevProps.todo === nextProps.todo,
);

SCSS까지 변경해주니 최적화가 잘되었다

5ms까지 줄였다!

profile
인터벌로 가득찬

0개의 댓글