본문 바로가기
기술 개발/React

컴포넌트 성능 최적화

by 쪼짱 2023. 1. 10.
728x90
반응형
SMALL

컴포넌트는 4가지와 같은 상황에서 리렌더링이 발생한다.

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

컴포넌트의 개수가 많지 않다면 모든 컴포넌트를 리렌더링해도 느려지지 않지만,

약 2000개가 넘어가면 성능이 저하된다.

따라서, 컴포넌트 리렌더링 성능을 최적화해 주는 작업을 진행해야한다.

 

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

- React.memo를 쓰기 전과 후 성능 비교

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

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

쓰기 전 vs 후

Render duration을 보면 React.memo를 쓰기 전에는 773.5ms 였고, 쓴 후에는 590.2ms로 줄어 든 것을 볼 수 있다.

 

2. useState의 함수형 업데이트 or useReducer 사용

- useState의 함수형 업데이트를 사용했을 때 성능 비교

// App.js
import { useCallback, useRef, useState } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

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

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

  const nextId = useRef(2501);

  // 추가
  const onInsert = useCallback(
    text => {
      const todo = {
        id: nextId.current,
        text,
        checked: false,
      }
      // 코드 수정
      setTodos(todos => todos.concat(todo))
      nextId.current += 1;
    },
    [],
  );

  // 삭제
  const onRemove = useCallback(
    id => {
      // 코드 수정
      setTodos(todos => todos.filter(todo => todo.id !== id));
    },
    [],
  )

  // 수정
  const onToggle = useCallback(
    id => {
      // 코드 수정
      setTodos(
        todos =>
          todos.map(todo =>
            todo.id === id ? { ...todo, checked: !todo.checked } : todo,
          )
      )
    },
    []
  )

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

export default App;

590.2ms에서 25.5ms로 줄었다.

 

- useReducer 사용했을 때 성능 비교

// App.js
import { useCallback, useReducer, useRef } from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';

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':
      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;
  }
}

function App() {
  // const [todos, setTodos] = useState(createBulkTodos)
  const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);

  const nextId = useRef(2501);

  // 추가
  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>
  )
}

export default App;

590.2ms에서 27ms로 줄었다.

 

결론적으로는 useState의 함수형으로 업데이트 하거나 useReducer을 사용하여 성능을 최적화할 수 있다.

useReducer은 기존 코드를 많이 고쳐야하는 단점이 있지만, 상태를 업데이트하는 로직을 모아 컴포넌트의 밖에 둘 수 있다는 장점이 있다.

개인적으론, useState의 함수형으로 업데이트 하는 것이 편한 것 같다.

 

3. react-virtualized를 통한 렌더링 최적화

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

yarn을 이용해서 설치부터 해준다.

$ yarn add react-virtualized

- react-virtualized를 사용했을 때 성능 비교

// TodoList.js
import React, { useCallback } from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss'
// 코드 수정
import {List} from 'react-virtualized'

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에 기본 적용되는 스타일  
    />
  );
};

export default React.memo(TodoList);

이 부분만 수정하면 style이 다 깨진다.

TodoList안에 있는 TodoListItem도 같이 스타일을 수정해주어야한다.

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

// 코드 수정
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);

react-virtualized를 사용 전에는 25.5ms였는데, 사용 후에 15.3ms로 렌더링 속도가 빨라졌다.

Intersection Observer로 무한 스크롤 구현하는 방법을 알고 있었는데, 다음에는 react-virtualized로 무한스크롤을 도전해봐야겠다.

 


참고

도서 - 리액트를 다루는 기술

https://yoon-dumbo.tistory.com/entry/React-virtualized-%EB%A1%9C-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%B6%80%EB%B6%84%EB%A7%8C-%EB%A0%8C%EB%8D%94%EB%A7%81-%ED%95%98%EC%97%AC-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8B%9C%ED%82%A4%EA%B8%B0

https://velog.io/@kimjh96/react-virtualized-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94

 

728x90
반응형
LIST

'기술 개발 > React' 카테고리의 다른 글

날짜 라이브러리 비교(Day.js, Moment.js, date-fns, Luxon)  (0) 2023.02.10
Data Fetching Library: React-Query, SWR  (0) 2023.01.31
React hooks (useRef)  (0) 2023.01.08
React hooks (useCallback)  (0) 2023.01.08
React Hooks (useMemo)  (0) 2023.01.06