React를 다루는 기술 12 - 컴포넌트 성능향상

3 minute read

React.memo를 이용한 성능 최적화

의도치 않은 컴포넌트의 리렌더링이 많이 발생할 시 성능의 저하를 보이게 되는데 컴포넌트의 리렌더링이 발생하는 케이스는 이하의 4개가 있다.

  • 자신이 받은 props의 변화
  • 자신의 state가 변화
  • 부모 컴포넌트가 리렌더링
  • forceUpdate 함수를 실행했을 때

이러한 4가지의 조건을 통해 자신, 부모 혹은 자식 컴포넌트가 리렌더링되게 되는데 이를 방지하기 위해 클래스 컴포넌트에서는 라이프사이클 메서드인 shouldComponentUpdate를 사용하여 렌더링을 강제로 막을 수 있다.
함수형 컴포넌트에서는 이와 비슷한 역할을 하는 React.memo를 통해 리렌더링 성능을 최적화 할 수 있다.

import React from 'react';

const App = ({ todo, onRemove }) => {
  [...]
}

export default React.memo(App);

위와 같이 컴포넌트를 만들고 React.memo로 감싸주면 이후 props로 받은 todo, onRemove가 변경되었을 때만 리렌더링되게 된다.

함수가 불필요하게 바뀌지 않게 하기

컴포넌트에서 사용하는 함수는 내부에서 참조하는 props나 state값이 변경되면 새로 만들어지게 된다.
값이 바뀔 때마다 함수가 일일히 반응해야하는 경우라면 이러한 상황이 당연하지만 값의 변경과 별개로 특정 트리거를 통해서만 함수가 실행이 되는 케이스라면 이는 불필요한 리소스 소모이다.

  1. useState의 함수형 업데이트
const [number, setNumber] = useState(0);

const onIncrease = useCallback(
  () => setNumber(number + 1),
  [number]
)

위와 같이 state의 값을 변경하는 함수에서 인자로 state값을 사용하면 state값을 업데이트하는 과정에서 최신 상태의 state값을 참조하기 때문에 계속 함수가 새로 만들어지게된다.
이를 막기 위해 업데이트 함수의 파라미터로 직접 값을 넣어주는 것이 아니라 함수를 넣어서 useCallback의 배열에 number를 빼도록 하는 방향으로 성능을 개선할 수 있다.

const [number, setNumber] = useState(0);

const onIncrease = useCallback(
  () => setNumber(prevNumber => prevNumber + 1),
  []
)
  1. useReducer를 사용하기
function reducer(number, action) {
  switch (action.type) {
    case 'INCREASE':
      return action.number + 1
  }
}

const App = () => {
  const [number, setNumber] = useReducer(reducer, undefined, {number: 1});

  const onIncrease = useCallback(value => {
    dispatch({ type: 'INCREASE', value });
  }, [])
}

위처럼 업데이트 로직을 useReducer를 이용해 따로 함수로 빼고 useCallback 함수에서는 reducer로 액션과 이전값을 전달해주기만 하면 함수형 업데이트와 마찬가지로 성능을 개선할 수 있다.

둘의 차이점은 useReducer는 함수형 업데이트에 비해 업데이트 로직을 바깥에 빼놓을 수 있다는 장점이 있는 정도로 선호하는 방법을 사용하면 된다.

불변성 유지

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

React에서 배열의 값을 변경할 때보면 항상 map, filter 함수처럼 새로운 배열을 생성한 뒤 업데이트 로직에 넣어주는걸 볼 수 있다.
이는 React 컴포넌트에서 상태를 업데이트 할 때 불변성을 유지하는 것이 아주 중요하기 때문인데 이는 방금 전에 살펴보았던 React.memo와 관련이 있다.
React.memo는 props의 값이 바뀌었는지의 여부에 따라 컴포넌트의 리렌더링을 최적화해주는 기능을 가지고 있다.
때문에 값의 변화를 쉽게 확인할 수 있어야 최적화를 할 수 있는데 불변성이 지켜지지 않으면 이 값의 변화를 눈치채지 못 하여 서로 비교하여 최적화를 하는 것이 불가능해진다.

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 todos = [{id: 1, checked: true}, {id: 2, checekd: false}];
const nextTodos = [...todos];

nextTodos[0].checked = false;
console.log(todos[0] === nextTodos[0]) // true

nextTodos[0] = {
  ...nextTodos[0],
  checked: false
}
console.log(todos[0] === nextTodos[0]) // false

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

웹페이지를 만들 때 화면에 보이지 않는 수많은 오브젝트로 인해 성능이 저하되는 케이스가 간혹 존재한다.
이를 해결하기 위해 react-virtualized라는 라이브러리를 사용하여 화면상에 표시되지 않는 오브젝트에 lazy loading을 걸어주어 렌더링하지 않고 크기만 차지하도록 할 수 있다.

이 라이브러리를 사용할 때에 주의할 점은 화면상에 보이지 않는 각 항목의 실제 px크기를 알아야한다는 점이다.
이를 위해 우선 react-virtualized 없이 어플리케이션을 실행하고 크롬의 개발자 도구와 같은 툴을 이용하여 해당 오브젝트의 크기를 확인하고 진행하면 된다.

import React, { useCallback } from 'react';
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}
    />
  )
}

export default React.memo(TodoList);

react-virtualized의 List 컴포넌트를 사용하기 위해 rowRenderer 함수를 작성했는데 이 함수는 List 컴포넌트에서 각각의 TodoListItem을 렌더링 할 때 사용하며 이 함수를 List 컴포넌트의 props로 넣어주어야한다.
그리고 파라미터로 index, key, style 값을 객체타입으로 받아야한다.
이를 통해 List 컴포넌트는 전달받은 props를 이용해 자동으로 최적화를 진행한다.

출처자료

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=209354690 (리액트를 다루는 기술(개정판))

Categories:

Updated: