관리 메뉴

거니의 velog

(2) 컴포넌트 성능 최적화 2 본문

React_리액트 응용

(2) 컴포넌트 성능 최적화 2

Unlimited00 2023. 12. 7. 08:29

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

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

* React.memo의 사용법은 매우 간단하다. 컴포넌트를 만들고 나서 감싸 주기만 하면 된다. TodoListItem 컴포넌트에 다음과 같이 React.memo를 적용해 보자.

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

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

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

export default React.memo(TodoListItem);

* 정말 간단하지요? 이제 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링을 하지 않는다.


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

* React.memo를 사용하는 것만으로 컴포넌트 최적화가 끝나지 않는다. 현재 프로젝트에서는 todos 배열이 업데이트 되면 onRemove와 onToggle 함수도 새롭게 바뀌기 때문이다. onRemove와 onToggle 함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다. 이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 두 가지이다. 첫 번째 방법은 useState의 함수형 업데이트 기능을 사용하는 것이고, 두 번째 방법은 useReducer를 사용하는 것이다.


(1) useState의 함수형 업데이트

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

* 예시를 한번 확인해 보자.

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

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

* 그럼 이제 onToggle, onRemove 함수에서 useState의 함수형 업데이트를 사용해 보자. 이 과정에서 onInsert 함수도 함께 수정해 볼 것이다.

[App.js]

import React, { useCallback, useRef, useState } 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: `할 일 ${i}`,
      checked: false,
    });
  }
  return array;
}

const App = () => {
  const [todos, setTodos] = useState(createBulkTodos);

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

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

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

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

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
    </TodoTemplate>
  );
};

export default App;

* setTodos를 사용할 때 그 안에 todos => 만 앞에 넣어 주면 된다. 정말 간단하다.

* 이제 코드를 저장하고, 조금 전 했던 것과 똑같이 크롬 개발자 도구의 Perfomance 탭에서 성능을 측정해 보자.

* 이전보다 성능이 훨씬 향상된 것을 확인할 수 있다. 1.02초에서 0.059초로 줄었다.

[개발 환경에서의 성능]

* 현재 yarn start를 통해 개발 서버를 구동하고 있는데, 개발 서버를 통해 보이는 리액트 애플리케이션은 실제
  프로덕션에서 구동될 때보다 처리 속도가 느리다. 실제 프로덕션 모드에서는 에러 시스템 및 Timing이 비활성화되어
  처리 속도가 훨씬 더 빠르다. 지금은 소규모 프로젝트이기 때문에 프로덕션 모드일 때와 개발 모드일 때의 차이가
  그렇게 크지 않지만, 프로젝트의 규모가 커질 수록 차이가 커진다.
[프로덕션 모드]

* 프로덕션 모드로 구동해 보고 싶다면 프로젝트 디렉터리에서 다음 명령어를 입력한다.

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

(2) useReducer 사용하기

* useState의 함수형 업데이트를 사용하는 대신, useReducer를 사용해도 onToggle과 onRemove가 계속 새로워지는 문제를 해결할 수 있다.

* 코드를 다음과 같이 수정해 보자.

[App.js]

import React, { useCallback, useReducer, useRef, useState } 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: `할 일 ${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(4);

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

  const onRemove = useCallback(
    (id) => {
      dispatch({ type: 'REMOVE', id });
    },
    [todos],
  );

  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를 사용하는 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있다. 성능상으로는 두 가지 방법이 비슷하기 때문에 어떤 방법을 선택할지는 여러분의 취향에 따라 결정하면 된다.