11장 컴포넌트 성능 최적화


할 일 목록을 2500개 늘리면 리렌더링 시간이 상당히 오래걸림

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

React DevTools로 녹화해서 리렌더링 시간 측정할 수 있다.

느려지는 원인 분석

리렌더링 발생하는 상황

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

할일 1을 체크할 경우 App 컴포넌트의 state가 변경되면서 App 컴포넌트가 리렌더링 되고, 자식 컴포넌트인 TodoList와 TodoListItem도 모두 연쇄적으로 리렌더링 된다.

React.memo 를 이용해서 컴포넌트 성능 최적화

클래스 컴포넌트에서는 shouldComponentUpdate 라이프사이클 사용하면 된다.
함수 컴포넌트엣는 React.memo함수 사용한다.

export default React.memo(TodoListItem);

todo, onRemove, onToggle 이 바뀌지 않으면 리렌더링 되지 않음
그냥 react.memo 로 감싸주면 된다는데 이렇게 해도 리렌더링 되던디.
→ todos 업데이트 되면 onRemove, onToggle이 업데이트 되서 React.memo 효과 제대로 못봄

함수들도 바뀌지 않게 해주면된다.

함수가 바뀌지 않게 하기

todos 배열이 업데이트 되면 onRemove 와 onToggle 함수도 새롭게 바뀜. 업데이트하는 과정에서 최신 상태의 todos 참조하기 위해서 함수가 새로 만들어진다.

이를 해결하기 위한 2가지 방법. 1. useState 함수형 업데이트 2. useReducer

즉, 정적인 값을 만들어 주는게 아니라. 동적으로 값을 만들어내는 함수를 전달해주는 것.

useState의 함수형 업데이트

세터 함수에 state 값을 전달하는게 아니라 state값을 어떻게 업데이트 할지 함수로 전달.
두번째 인자로 빈 배열 줘야한다.

const onInsert = useCallback(
    (text) => {
      setTodos((todos) => [
        ...todos,
        { id: nextId.current, text: text, cehcked: false },
      ]);
      nextId.current += 1;
    },
    [todos]
  );
  const onRemove = useCallback(
    (id) => {
      setTodos((todos) => todos.filter((todo) => todo.id != id));
    },
    [todos]
  );
  const onToggle = useCallback((id) => {
    setTodos((todos) =>
      todos.map((todo) =>
        id == todo.id ? { ...todo, checked: !todo.checked } : todo
      )
    );
  });

근데 왜 안되지.
→ useCallback 두번째 인수로 빈배열 [ ] 주니까 잘 된다.
함수형 업데이트 사용 안할 떄 빈배열 주면 재대로 작동안함. 이유가 뭘까.
→ 첫 렌더링 시에만 생성하는데 todos 가 고정값을 전달되서

List 컴포넌트 최적화

리스트에 관련된 컴포넌트 최적활 할때는 리스트 내부에서 사용되는 컴포넌트도 최적화 해야하고, 리스트로 사용되는 컴포넌트도 최적화 해줘야 좋다.

const TodoList = ({ todos, onRemove, onToggle }) => {
  return (
    <div className="TodoList">
      {todos.map((todo) => (
        <TodoListItem
          todo={todo}
          key={todo.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
};

export default React.memo(TodoList);

App가 리랜더링 되는 이유가 todos 배열 업데이트 밖에 없어서 TodoList 컴포넌트에 React.memo 적용해도 효과가 없지만, App 컴포넌트에 다른 state가 추가될 경우에는 필요해 진다.

useReducer

함수형 업데이트 대신에, useReducer사용해도 된다.

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);
  const nextId = useRef(4);
  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    dispatch({ type: 'INSERT', todo });
    nextId.current += 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>
  );
};

useReducer사용할 때 2번째 파라미터에 초기 상태 넣어줘야하는데, 대신에 undefined 넣고 3번째에 초기 상태 만들어주는 함수 createBulkTodos 넣어줌 → 이렇게 하면 맨 처음 랜더링될 때만 createBulkTodos 함수 호출

useReducer 사용하면 상태 업데이트 하는 로직을 컴포넌트 바깥에 따로 뺄 수 있다는 장점이 있음.

불변성의 중요성

값을 직접 수정하지 않고 새로운 값을 만들어 내는 것을 불변성을 지킨다고 한다.
왜 지켜야하느냐?
불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.
→ prev 와 next 의 값 비교를 할 수가 없다.

객체 배열 복사할 때, 최외 값 만 복사되는 얕은 복사 이루어질 수 있다.
→ 깊은 복사 해줘야한다. immer 라이브러리 써라.

react-virtualized

화면에 나오는 부분만 렌더링 하는 방법.

yarn add react-virtualized

TodoList

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

TodoListItem

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

const TodoListItem = ({ todo, onRemove, onToggle, style }) => {
  const { id, text, checked } = todo;
  return (
    <div className="TodoList-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,
);