React의 성능 최적화 방법

승민·2025년 4월 3일
1

면접 대비

목록 보기
9/31
post-thumbnail

React에서 렌더링(Rendering)이란 컴포넌트 함수를 호출하여 React Element를 반환하는 과정입니다. 이때 Virtual DOM을 생성하고, 실제 DOM과 비교하여 변경된 부분만 반영합니다. 하지만 이과정에서 불필요한 렌더링이 발생하면 성능 저하가 일어날 수 있기 때문에, 이를 최적화하는 기법이 필요합니다.

React의 렌더링 과정

React의 렌더링 과정은 크게 두 단계로 나뉩니다.

  1. Render Phase
    컴포넌트를 호출하여 새로운 Virtual DOM을 생성하고, 기존 Virtual DOM과 비교하여 변경된 부분을 찾습니다.

  2. Commit Phase
    변경된 부분만 실제 DOM에 반영합니다.
    재조정이 필요 없는 경우, 이 과정은 생략됩니다(Skip됨).
    React는 또한 componentDidMount와 componentDidUpdate와 같은 생명주기 메서드를 호출함

실제 DOM 반영 과정

React는 Commit Phase 이후 브라우저의 렌더링 과정에서 리플로우와 리페인트 과정을 거칩니다.

  1. 리플로우(Reflow)
  • 요소의 위치, 크기, 레이아웃이 변경될 때 발생하는 과정입니다.
  • 노드의 추가/제거, 크기 변경, 폰트 변경 등이 발생하면 브라우저는 문서의 배치를 다시 계산해야 합니다.
  • 성능에 큰 영향을 미치므로 최소화하는 것이 중요합니다.
  1. 리페인트(Repaint)
  • 리플로우 이후 또는 스타일(색상, 배경, 테두리 등)이 변경될 때 발생하는 과정입니다.
  • 레이아웃 변경 없이 픽셀을 다시 그리는 과정입니다.
  • 리플로우보다 비용이 적지만, 빈번한 발생은 성능 저하를 초래할 수 있습니다.

React에서는 Virtual DOM을 활용하여 불필요한 리플로우 및 리페인트를 최소화하는 최적화 기법을 제공합니다.

React의 렌더링 최적화 방법

크게 보면 메모이제이션과 코드 스플리팅 두 가지 기법이 있습니다.

메모이제이션

React에서 메모이제이션을 활용하면 불필요한 렌더링을 방지하고 성능을 최적화할 수 있습니다.
대표적인 메모이제이션 기법으로는 React.memo, useMemo, useCallback이 있습니다.

React.memo
React.memo는 컴포넌트의 props가 변경되지 않으면, 렌더링을 건너뛰도록 합니다.

import React from "react";

const ChildComponent = React.memo(({ value }) => {
  console.log("ChildComponent 렌더링");
  return <div>{value}</div>;
});

export default ChildComponent;

React.memo는 얕은 비교를 통해 props가 변경되지 않았을 경우, Render Phase를 건너뜁니다.

단, 참조 타입은 새로운 객체가 생성될 경우 값이 달라졌다고 판단하므로 주의가 필요합니다.

useMemo
useMemo는 값 자체를 메모이제이션하여 렌더링 시 불필요한 계산을 방지합니다.

import { useMemo } from "react";

const ParentComponent = ({ item }) => {
  const memoizedItem = useMemo(() => item, [item]);
  return <ChildComponent item={memoizedItem} />;
};

useMemo는 비용이 큰 연산 결과를 저장하고, 동일한 의존성 배열 값을 유지하면 다시 계산하지 않습니다.

객체를 props로 넘길 때도 useMemo를 사용하면 부모 컴포넌트가 리렌더링되어도 객체의 참조 값이 유지되어 불필요한 렌더링을 방지할 수 있습니다.

useCallback
useCallback은 함수를 메모이제이션하여, 같은 함수가 매번 새롭게 생성되지 않도록 합니다.

import { useCallback } from "react";

const ParentComponent = ({ onClick }) => {
  const memoizedCallback = useCallback(() => {
    console.log("Button clicked");
  }, []);

  return <ChildComponent onClick={memoizedCallback} />;
};

useCallback은 함수의 참조 값이 유지되므로, props로 넘길 때 불필요한 렌더링을 방지할 수 있습니다.

React.memo와 함께 사용하면 최적화 효과를 극대화할 수 있습니다.

메모이제이션 기법을 무조건 사용하면 좋을까?

그렇지 않습니다.

메모이제이션 자체도 비용이 발생해서 무조건적으로 적용하는 것은 성능을 오히려 저하시킬 수 있습니다. 따라서 다음과 같은 경우 먼저 고려한 후 적용하는 것이 좋습니다.

  1. 불필요한 상태 변경을 최소화할 수 있도록 코드 구조를 개선하기
  2. 성능 문제를 실제로 겪고 있는 부분에서만 메모이제이션을 적용하기
  3. 참조 타입(props로 객체나 함수를 전달하는 경우)에 한해 useMemo나 useCallback을 활용하기

코드 스플리팅

React.lazy
React.lazy를 사용하면 동적 import를 통해 컴포넌트를 나누어 렌더링할 수 있습니다.
이를 통해 애플리케이션의 초기 로딩 시간을 단축, 필요한 시점에서만 컴포넌트를 로드하여 성능 최적화가 가능합니다.

React.Suspense
아직 렌더링이 준비되지 않은 컴포넌트가 있을 경우, fallback을 통해 로딩 화면을 보여줄 수 있습니다.
주로 Route 컴포넌트에서 사용되며, 해당 경로로 이동할 때 비로소 필요한 컴포넌트를 로드하는 방식입니다.

사용 방법

import { Suspense, lazy } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);

주의할 점

  • 페이지 이동 시마다 설정된 로딩 컴포넌트가 표시될 수 있으므로, 사용자 경험을 고려하여 적용해야 합니다.
  • 코드 스플리팅은 네트워크 요청을 동반하기 때문에, 너무 작은 단위로 나누면 오히려 성능이 저하될 수도 있습니다.

결론

React의 렌더링을 최적화하는 방법에는 메모이제이션코드 스플리팅이 있으며, 이를 적절히 활용하면 성능을 향상시킬 수 있습니다.

메모이제이션 : React.memo, useMemo, useCallback을 활용한 렌더링 최소화
코드 스플리팅 : React.lazy, Suspense를 이용한 코드 스플리팅으로 초기 로딩 최적화
주의 사항 : 불필요한 최적화는 오히려 성능을 저하시킬 수 있으므로, 실제 성능 문제가 발생하는 부분에서만 적용

성능 최적화는 무조건 적용하는 것이 아니라, 필요할 때 적절하게 활용하는 것이 중요합니다.

참고자료

[10분 테코톡] 앨버의 리액트 렌더링 최적화
React - React Deverloper Tool을 이용한 성능 최적화

0개의 댓글