개발/React

[리액트를 다루는 기술] 컴포넌트 성능 최적화

hayo 2023. 3. 12. 21:05

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

→ 리액트 v17부터는 리액트 전용 개발자 도구인 ReactDevTools를 사용하여 자세한 성능 분석이 가능하다.

  1. 리액트 개발자 도구의 Profiler 탭 클릭 후
  2. 좌측 상단의 파란색 녹화 버튼 클릭
  3. 성능 분석하고 싶은 작업 실행
  4. 작업 완료 후, 파란색 녹화 버튼을 다시 한번 클릭하면 성능 분석 결과가 나타남
    • Render duration: 리렌더링에 소요된 시간. **소요 시간은 컴퓨터 환경에 따라 다를 수 있음
    • Profiler 탭 상단에 있는 랭크 차트 아이콘을 클릭하면,
      리렌더링된 컴포넌트를 오래 걸린 순으로 정렬하여 나열해 줌

 

3. 느려지는 원인 분석

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

→ 리렌더링이 불필요할 때는 리렌더링을 방지해 주어야 한다!

 

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

→ 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수 컴포넌트의 리렌더링 성능을 최적화!

  • 컴포넌트의 리렌더링을 방지하는 방법
    1. 클래스 컴포넌트: shouldComponentUpdate 라이프사이클 메서드 사용
    2. 함수형 컴포넌트: React.memo 함수 사용
import React from 'react';

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

export default React.memo(TodoListItem);
//컴포넌트를 만들고 나서 'React.memo'로 감싸주기만 하면 된다!
  • 위와 같이 React.memo로 감싸주면 TodoListItem 컴포넌트는 todo, onRemove, onToggle이 바뀔 때만 리렌더링 된다!

 

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

  • 함수가 계속 만들어지는 상황을 방지하는 방법
    1. useState의 함수형 업데이트 기능을 사용하는 방법
    2. useReducer를 사용하는 방법

▶ useState의 함수형 업데이트

→ setState 함수(useState의 setter 함수)를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣어주는 것함수형 업데이트라고 한다.

더보기

setTodos를 사용할 때 그 안에 todos => 만 앞에 넣어 주면 된다!

// App.js
import React 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] = React.useState(createBulkTodos);
  
  //고유값으로 사용될 id
  const nextId = React.useRef(4);
  
  const onInsert = React.useCallback(text => {
    const todo = { id: nextId.current, text, checked: false };
    
    setTodos(todos => todos.concat(todo));
    nextId.current += 1; // nextId 1씩 증가 시키기
  },[]);
  
  const onRemove = React.useCallback(id => {
    setTodos(todos => todos.filter(todo => todo.id !== id));
  },[]);
  
  const onToggle = React.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;

▶ useReducer 사용하기

더보기
// App.js
import React 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: 'REMOVE', id: 1 }
      return todos.map(todo =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo
      );
    default:
      return todos;
  }
}

const App = () => {
  const [todos, dispatch] = React.useReducer(todoReducer, undefined, createBulkTodos);
  
  //고유값으로 사용될 id
  const nextId = React.useRef(2501);
  
  const onInsert = React.useCallback(text => {
    const todo = { id: nextId.current, text, checked: false };
    
    dispatch({ type: 'INSERT', todo });
    nextId.current += 1; // nextId 1씩 증가 시키기
  },[]);
  
  const onRemove = React.useCallback(id => {
    dispatch({ type: 'REMOVE', id });
  },[]);
  
  const onToggle = React.useCallback(id => {
    dispatch({ type: 'TOGGLE', id });
  },[]);
  
  
  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </TodoTemplate>
  )
}

export default App;

 

6. 불변성의 중요성

  • 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 '불변성을 지킨다'고 한다
  • 불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다
    → React.memo에서 서로 비교하여 최적화 하는 것이 불가능!
  • 전개 연산자(... 문법)를 사용하여 객체나 배열 내부의 값을 복사할 때는 얕은 복사(shallow copy)를 한다.
    즉, 내부의 값이 완전히 새로 복사되는 것이 아니라 가장 바깥쪽의 값만 복사된다.
    그러므로, 내부의 값이 객체 혹은 배열이라면 내부의 값 또한 따로 복사를 해주어야 한다!

    → 배열 혹은 객체의 구조가 복잡해질수록 불변성을 유지하면서 업데이트하는 것이 까다로워진다.
        이렇게 복잡한 상황에서는 immer라는 라이브러리의 도움을 받으면 편하게 작업할 수 있다.

 

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

→ 리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트리스트로 사용되는 컴포넌트 모두 최적화해야 한다.

// TodoList.js
import React from 'react';
import TodoListItem from './TodoListItem';
import './TodoList.scss';

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

export default React.memo(TodoList);
  • 상기의 코드와 같이 코드를 짤 경우, props를 지정해 주었으므로 불필요한 리렌더링이 발생하지 않는다.
  • App 컴포넌트에 다른 state가 추가되어, 그로 인한 리렌더링이 발생할 경우를 대비하여 React.memo를 사용해서 최적화한 코드!

 

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

  • 리액트 컴포넌트 리렌더링 성능을 최적화하는 방법
    1. 필요할 때만 리렌더링!
    2. 화면에 보이지 않는 정보를 미리 렌더링하지 않고, 보여져야할 때 렌더링 하도록 설정
  • 2번 방법은 react-virtualized 라이브러리를 사용하여 쉽게 구현할 수 있다.
    • yarn을 사용하여 라이브러리 설치: yarn add react-virtualized
    • TodoList 수정 ← 하기 전에, 각 항목의 실제 크기를 px 단위로 알아야 한다! (그 수치를 List에 적용해야 함)
// TodoList.js
import React from 'react';
import { List } from 'react-virtualized';
import TodoListItem from './TodoListItem';
import './TodoList.scss';

const TodoList = ({ todos, onRemove, onToggle }) => {
  
  const rowRender = React.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} // 항목 높이
      rowRender={rowRender} // 항목을 렌더링할 때 쓰는 함수
      list={todos} // 배열
      style={{ outline: 'none' }} // List에 기본 적용되는 outline 스타일 제거
    />
  )
};

export default React.memo(TodoList);

9. 정리

리액트 컴포넌트의 렌더링은 기본적으로 빠르기 때문에 개발할 때 최적화 작업에 너무 큰 스트레스를 받거나 모든 컴포넌트에 React.memo를 작성할 필요는 없다.
다만, 리스트와 관련된 컴포넌트를 만들 때 보여줄 항목이 100개 이상이고 업데이트가 잦다면 최적화할 필요가 있다!

 

 


참고자료

  • 리액트를 다루는 기술 [김민준 저]