2 분 소요

React 렌더링 최적화 (React.memo, useMemo, useCallback)

한 눈에 보는 결론

  • useMemo는 결과 값을 캐싱한다.
  • useCallback은 함수를 캐싱한다.
  • React.memo는 고차 컴포넌트로, 컴포넌트의 리렌더링을 최소화한다.

성능 최적화 필수성

React로 개발할 때, 누구나 한 번씩 고민하게 되는 주제. 그것은 바로 성능 최적화다.

사용자 경험을 향상시키고, 불필요한 렌더링을 줄여 앱의 효율을 높이는 것.

성능 저하의 주요 원인

성능 저하를 일으키는 원인은 다양하지만 그 중 주요 원인을 알아보자.

  1. 불필요한 렌더링. state가 변경될 때, 변화가 없는 컴포넌트까지 불필요하게 렌더링 되는 것.
  2. 복잡한 계산식. 복잡한 계산과 데이터 처리는 앱의 자원을 많이 소모하여, 사용자 경험을 저해하는 것. (ex. 입력 폼)

이 외에도 다양한 원인이 있지만, 주제를 벗어나지 않도록 마무리 짓고, 주요 원인을 해결할 수 있는 방법을 설명해보겠다.

React.memo

React의 고차 컴포넌트이다.

컴포넌트가 리렌더링될 때, 해당 컴포넌트의 propsstate가 변경되면 React는 기본적으로 컴포넌트를 리렌더링한다. 이러한 이유 때문에 부모 컴포넌트가 리렌더링할 때, 리렌더링이 필요없는 자식 컴포넌트도 함께 리렌더링되며 자원 낭비를 하는 경우가 생긴다.

이럴 때, React.memo를 사용하면 불필요한 렌더링을 방지할 수 있다.

React.memo로 래핑된 컴포넌트는 이전에 렌더링된 결과를 기억하고, 이전 props와 현재 props를 비교하여 변경 여부를 판단한다. 만약 props가 동일하다면 이전에 렌더링된 결과를 재사용하여 불필요한 리렌더링을 방지한다. 

useMemo

useMemo훅은 무거운 계산 작업의 결과를 메모리에 저장해 애플리케이션의 효율성을 높이는 데 도움을 준다.

첫 번째 인자로 콜백 함수를, 두 번째 인자로 의존성 배열을 받는다. 의존성 배열 내의 값이 변경되면 콜백 함수가 다시 실행되어 계산이 이루어지고, 그 결과가 메모리에 저장된다. 변경이 없다면 이전에 메모리에 저장된 값을 재사용한다.

import React, { useMemo } from 'react';

const getCalculatePrice = (max) => {
  // 가격 계산 로직
};

const App = ({ max }) => {
  // 의존성 배열 내의 값이 변경되면 getCalculatePrice 함수가 재실행된다.
  const price = useMemo(() => getCalculatePrice(max), [max]);

  return (
    <div>{max}</div>
  );
};

// App 컴포넌트는 동일

useCallback

useCallback 훅은 함수를 메모이제이션하여 컴포넌트의 불필요한 재렌더링을 방지한다. 특히 함수를 하위 컴포넌트에 props로 전달할 때 유용하게 사용된다.

함수를 메모리에 저장하고, 의존성 배열 내의 값이 변경될 때만 함수를 새로 생성한다. 이를 통해 컴포넌트가 재렌더링되어도 동일한 함수 참조를 유지할 수 있어서 성능을 개선할 수 있다.

  • 이벤트 핸들러 함수를 자식 컴포넌트에 props로 전달할 때
  • 렌더링 최적화가 필요한 대규모 리스트나 테이블에서 항목의 이벤트 처리
  • 업데이트가 잦은 stateprops에 의존하는 함수
import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onIncrement }) => {
  console.log('ChildComponent 렌더링...');
  return <button onClick={onIncrement}>증가</button>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>카운터: {count}</p>
      <ChildComponent onIncrement={handleIncrement} />
    </div>
  );
};

export default ParentComponent;

훅을 적용하기 전에는 부모가 재렌더링될 때마다 핸들러 함수가 새로 생성되어 자식도 불필요하게 재렌더링되었다. 훅을 적용한 후에는 핸들러 함수의 참조가 변경되지 않기에 자식의 재렌더링이 방지되어 전체적인 성능이 향상된다.

useCallback의 의존성 배열을 로 뒀기에, 컴포넌트가 처음 렌더링될 때 한 번만 생성되고, 절대 재생성되지 않는다.

카테고리:

업데이트: