[React] React 렌더링 성능 최적화

2026-02-14 hit count image

React 렌더링 성능 최적화의 필요성, React.memo, shouldComponentUpdate, PureComponent 등의 최적화 API, 참조 문제와 useCallback/useMemo를 활용한 Props 참조 최적화, 불변성, 성능 측정 방법, React Compiler에 대해서 공유합니다.

react

개요

이전 블로그 포스트에서는 React의 렌더링이 언제 발생하고, 어떤 구조로 렌더링되는지에 대해 자세히 살펴보았습니다. 만약, React의 렌더링에 대해서 자세히 알고 싶으신 분들은 다음 링크를 참고해 주시기 바랍니다.

이번 블로그 포스트에서는 렌더링 최적화에 대해 자세히 다루겠습니다.

렌더링 성능 최적화의 필요성

렌더링은 React의 정상적인 동작입니다. 문제는 “불필요한” 렌더링 작업입니다. 불필요한 렌더링이 렌더링 최적화의 대상이 됩니다.

불필요한 렌더링이란 렌더링의 출력이 변경되지 않은 경우(출력된 Fiber 오브젝트가 이전 렌더링의 Fiber 오브젝트와 같은 내용인 경우)의 렌더링을 말합니다.

소프트웨어에서 최적화하는 방법은 다음과 같습니다.

  1. 같은 작업을 더 빠르게 실행한다 (고속화)
  2. 같은 작업을 스킵한다

React에서는 불필요한 렌더링을 스킵하여 렌더링 작업을 줄이는 것으로 최적화합니다.

  • React의 렌더링 최적화 기본 원칙

컴포넌트의 렌더링 출력은 현재 props와 state에 완전히 기반해야 하며, props/state가 변경되지 않았다면, 출력도 동일해야 합니다. - React 컴포넌트의 순수성

이런 기본 원칙에 의거하여, 우리는 Props와 State가 변경되지 않았다는 것을 알 수 있다면, 렌더링 결과는 이전과 동일하다고 예측할 수 있고, 렌더링을 스킵해도 문제가 없다는 것이 React 렌더링 최적화의 전제가 됩니다.

렌더링 최적화 방법

React에서는 렌더링을 스킵하는 3가지 주요 API를 제공하고 있습니다. (함수 컴포넌트 1개, 클래스 컴포넌트 2개)

함수 컴포넌트

1. React.memo()

props가 변경되었는지 확인하는 HOC(High Order Component)입니다.

const MemoizedComponent = React.memo(MyComponent);

props가 변경되지 않으면 렌더링을 스킵합니다.

클래스 컴포넌트

2. shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
  return !shallowEqual(this.props, nextProps) ||
         !shallowEqual(this.state, nextState);
}

함수의 실행 결과가 false를 반환하면 렌더링을 스킵합니다.

3. PureComponent

shouldComponentUpdate를 자동으로 구현하는 기본 클래스입니다.

class MyComponent extends React.PureComponent {
  // ...
}

얕은 동등성(Shallow Equality)

모든 메서드는 **“얕은 동등성”**을 사용하여 값을 비교하게 됩니다. 그래서 다음과 같은 문제가 발생할 수 있습니다.

  • 오브젝트에서 필드가 변해도 참조가 같으면, 같은 것으로 인식한다 (참조가 변하지 않는 문제)
  • 값이 같아도 새로운 오브젝트라면 참조가 다르므로, 다른 것으로 인식한다 (참조가 변하는 문제)

최적화에서 참조의 문제

React에서는 기본적으로 부모가 렌더링이 되면 자식은 무조건 렌더링하기 때문에, 새로운 prop 참조는 특별히 문제가 되지 않습니다.

function ParentComponent() {
  const handleClick = () => {
    console.log('Button clicked');
  };

  const data = { a: 1, b: 2 };

  return <NormalChildComponent onClick={handleClick} data={data} />;
}
  • 모든 렌더링마다 새로운 함수와 오브젝트 참조가 생성됩니다
  • NormalChildComponent는 어차피 렌더링되므로 문제가 없습니다

하지만 memo를 사용하여 자식 컴포넌트의 렌더링을 최적화 시킨 경우, 부모 컴포넌트의 렌더링에 의해 다음과 같이 새로운 참조가 생성되고 자식 컴포넌트의 불필요한 리렌더링이 발생되게 됩니다.

const MemoizedChildComponent = React.memo(ChildComponent);

function ParentComponent() {
  const handleClick = () => {
    console.log('Button clicked');
  };

  const data = { a: 1, b: 2 };

  return <MemoizedChildComponent onClick={handleClick} data={data} />;
}

문제점:

  • 렌더링마다 새로운 onClickdata 참조가 생성됩니다
  • 새로운 참조가 props 변경을 나타냅니다
  • MemoizedChildComponent는 항상 리렌더링됩니다
  • React.memo()가 무의미해집니다!

Props 참조 최적화

함수 컴포넌트에는 이런 참조 문제를 해결하기 위해 다음과 같이 2가지 훅(Hook)을 제공하고 있습니다.

  • useCallback: 함수의 메모이제이션
function ParentComponent() {
  const memoizedCallback = useCallback(() => {
    console.log('Clicked');
  }, []); // 의존성 배열이 비어있으면 절대 변경되지 않음

  return <MemoizedChild onClick={memoizedCallback} />;
}
  • useMemo: 데이터의 메모이제이션
function ParentComponent() {
  const memoizedData = useMemo(() => ({ a: 1, b: 2 }), []);

  return <MemoizedChild data={memoizedData} />;
}

useCallbackuseMemo를 사용하여, 자식 컴포넌트에 전달하는 Props의 참조가 변하지 않도록 하여, 자식 컴포넌트에서 의도한 랜더링 최적화를 수행할 수 있습니다.

function ParentComponent() {
  const memoizedCallback = useCallback(() => {
    console.log('Clicked');
  }, []);

  const memoizedData = useMemo(() => ({ a: 1, b: 2 }), []);

  return <MemoizedChild onClick={memoizedCallback} data={memoizedData} />;
}

모든 것을 메모이제이션(최적화)해야 하는가?

React에서 랜더링 최적화에 대한 주제에서 항상 나오는 질문입니다. 여기에 대한 답은 **“아니다”**입니다.

그 이유는 다음과 같이 메모이제이션 자체에도 오버헤드가 있기 때문입니다.

  • Props 비교에 시간이 걸립니다
  • 메모리를 사용합니다
  • 코드가 복잡해집니다

메모이제이션이 가치 있는 경우

React 공식 문서에는 다음과 같이 메모이제이션이 언제 가치가 있는지 설명하고 있습니다.

“memo를 통한 최적화는, 컴포넌트가 동일한 props로 자주 리렌더링되고, 리렌더링 로직의 비용이 높은 경우에만 가치가 있습니다.”

랜더링 최적화 가이드라인

메모이제이션의 오버헤드와 공식 문서에서의 메모이제이션이 가치가 있는 경우를 생각하면, 다음과 같은 렌더링 최적화 가이드라인을 만들 수 있습니다.

  1. 컴포넌트가 동일한 props로 자주 리렌더링되는 경우
  2. 리렌더링 로직의 비용이 높은 경우 (복잡한 계산, 많은 자식 컴포넌트)
  3. 성능 문제를 실제로 측정한 경우

React를 사용하는 앱에서 발생하는 대부분의 성능 문제는 리렌더링과 관계가 없을 가능성이 높습니다. 렌더링을 최적화하기 전에, 로직에 문제가 없는지 확인하거나, 불필요하게 State를 업데이트하고 있지 않은지를 확인하는 것이 좋습니다.

React에서의 불변성(Immutability)

React에서는 기본적으로 얕은 동등성 체크를 사용하므로, 데이터의 내용이 변해도 참조가 변하지 않으면 리렌더링이 실행되지 않는 문제가 발생합니다.

따라서 다음과 같이 참조가 아닌 값이 변경되어 얕은 동등성 체크 실패하는 경우가 발생할 수 있습니다.

// ❌ 나쁜 예시: 변경을 감지할 수 없음
const [todos, setTodos] = useState(someTodosArray);

const handleClick = () => {
  todos[3].completed = true;
  setTodos(todos); // 같은 참조이므로 React가 변경을 감지할 수 없음
};

이런 문제를 피하기 위해서는 다음과 같이 새로운 참조를 생성하고, 값을 변경하여 React에게 명확하게 참조가 변경 된 것을 알려야 합니다.

// ✅ 좋은 예시: 새로운 참조를 생성
const handleClick = () => {
  const newTodos = todos.slice();
  newTodos[3].completed = true;
  setTodos(newTodos); // 새로운 참조로 변경이 감지됨
};

React와 그 에코시스템은 불변 업데이트를 전제로 하고 있습니다. 따라서 불변성을 지키지 않는 다면, 버그 발생 위험이 커집니다.

React 컴포넌트 렌더링 성능 측정

앞서 랜더링 최적화 가이드라인에서 성능 문제를 실제로 측정한 경우에만 렌더링 최적화를 수행해야 한다고 이야기했습니다. React에서는 렌더링의 횟수를 측정할 수 있는 React DevTools Profiler를 제공하고 있습니다.

React에서 렌더링 최적화를 수행하기 전에 반드시 이 툴을 사용하여 현재의 렌더링 상태를 분석한 후, 최적화 후의 내용과 비교하여 렌더링 최적화가 의미가 있는지 확인해야 합니다.

<StrictMode>

React는 개발 중에는 기본적으로 <StrictMode>가 동작하며, <StrictMode> 내의 컴포넌트를 이중 렌더링합니다.

이중 렌더링을 하는 이유는 React가 렌더링 중의 부작용을 감지하기 위해서 입니다.

따라서, console.log를 사용하여 렌더링을 확인하는 경우, 실제 렌더링 횟수와 다를 수 있습니다. 그래서, 렌더링 최적화를 위해 렌더링 횟수를 파악하기 위해서는 반드시 React DevTools Profiler를 사용해야 합니다.

React 19의 React Compiler

React 19에서 React Compiler가 추가되었습니다. React Compiler는 React 앱을 자동으로 최적화하는 새로운 빌드 타임 도구입니다.

React Compiler를 사용하지 않는 경우, 다음과 같이 메모이제이션을 활용하여 렌더링을 최적화합니다.

import { useMemo, useCallback, memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
  const processedData = useMemo(() => {
    return expensiveProcessing(data);
  }, [data]);

  const handleClick = useCallback((item) => {
    onClick(item.id);
  }, [onClick]);

  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} onClick={() => handleClick(item)} />
      ))}
    </div>
  );
});

하지만, 이 메모이제이션은 실제로 최적화되지 않았습니다. 다음과 같은 부분에서 랜더링을 할 때마다 새로운 참조가 발생하기 때문입니다.

<Item key={item.id} onClick={() => handleClick(item)} />

이처럼 기존의 렌더링 최적화는 많은 신경을 써야했고, 조금만 긴장을 풀면 아주 작은 실수로 렌더링 최적화가 동작하지 않는 문제가 발생하였습니다.

이런 문제를 해결하기 위해 React에서는 React Compiler를 만들었으며, React Compiler를 사용하면 다음과 같이 렌더링 최적화를 위해 아무것도 하지 않아도 자동으로 렌더링이 최적화됩니다.

function ExpensiveComponent({ data, onClick }) {
  const processedData = expensiveProcessing(data);

  const handleClick = (item) => {
    onClick(item.id);
  };

  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} onClick={() => handleClick(item)} />
      ))}
    </div>
  );
}

React Compiler는 React 17, 18, 19를 지원합니다.

만약, 렌더링 최적화에 어려움을 겪고 있다면, React Compiler를 도입해 보시면 좋을 거 같습니다.

완료

이번 블로그 포스트에서는 React의 렌더링 최적화에 대해서 알아보았습니다. 또, React Compiler라는 React에서 새로운 렌더링 최적화의 미래를 살펴보았습니다.

이제 여러분도 React Compiler를 도입해서 편하게 렌더링을 최적화해 보시기 바랍니다. 또 React Compiler를 도입할 수 없으시다면, 이 블로그를 통해 React 렌더링 최적화에 대해 명확히 이해하고 수행해 보시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS