리렌더링(Re-rendering)을 피하는 방법 5가지

Soyeon·2025년 3월 17일
2
post-thumbnail

프로젝트를 진행하면서 불필요한 렌더링이 너무 많이 일어나는 것을 알게 되었다.

예를 들어, 폼에 값이 입력될 때마다 다른 컴포넌트가 렌더링된다거나, 헤더와 메뉴 탭도 불필요하게 계속 렌더링되는 문제가 발생했다.

리액트의 성능을 최적화할 수 있는 방법에는 여러가지가 있지만, 먼저 리렌더링이 발생하는 과정부터 이해한 후, 이를 어떻게 적용할지 고민해볼 필요가 있다.

먼저, 리렌더링이 일어나는 과정을 알아보자

React 는 컴포넌트의 state 나 부모로부터 받은 props 가 변경되는지 감시한다.

변경이 감지되면, React 는 "새로운 가상 DOM"을 만들어서 기존의 DOM과 비교하는데, 이 때 변경된 부분만 실제 DOM에 업데이트하려는 과정이 일어난다. 이를 Reconciliation(조화단계)이라고 한다. (공식문서)

렌더링이 반복될수록 Virtual DOM 연산이 많아질 것이며, 이는 최적화되지 않은 DOM 업데이트가 많아지면서 브라우저 성능에도 영향을 줄 수 있다.

반복되는 렌더링이 성능 저하를 유발하는 이유

  1. 불필요한 연산 증가
    컴포넌트에서 필터링, 정렬, 데이터 변환, 등등 이런 연산을 수행하는데 리렌더링될 때마다 이런 연산이 다시 일어난다. 즉, 같은 결과를 매번 계산하는 비효율이 발생한다.

  2. 자식 컴포넌트도 함께 렌더링
    자식 컴포넌트의 변화가 없더라도 부모 컴포넌트가 리렌더링되면, 함께 렌더링될 수 있다.
    특히, props로 객체, 배열, 함수가 전달되면 새로운 값으로 인식되어 리렌더링된다.

  3. 가상 DOM 비교 비용이 증가
    Virtual DOM 비교(diffing) 과정에서 변경된 부분을 감지하기 위한 비용이 증가하여 성능이 저하될 수 있다.


리렌더링 피하는 방법

이러한 리렌더링을 언제, 어떻게 피할 수 있을지 자세하게 알아보자.

useMemo = 값 메모이제이션

붎필요한 연산을 방지하고 싶을 때

  • 메모이제이션, 최초로 한번 계산했을 때 결과값을 메모리 어딘가에 보관하고, 이 연산이 필요해지면 저장되어 있는 값을 돌려주는 방법이다.
  • 즉, 특정 값이 변경될 때만 연산을 수행하고, 변경되지 않으면 이전 값을 반환한다.
const expensiveFunction = (num: number) => {
  console.log("비용이 큰 연산...");
  return num * 2;
};

const MyComponent = ({ number }: { number: number }) => {
  const memoizedValue = useMemo(() => expensiveFunction(number), [number]);
  
  return <div>{memoizedValue}</div>;
};

useCallback = 함수 메모이제이션

불필요한 함수 생성을 방지하고 싶을 때,
자식 컴포넌트에게 props로 함수를 넘길 때,
useEffect의 종속성 배열에서 함수가 계속 바뀌는 것을 막고 싶을 때

  • 이벤트 핸들러나 콜백 함수를 기억해서 불필요한 함수 생성을 방지한다.
  • 이전 렌더에서 만든 함수를 기억하고, 종속성([])이 바뀔 때만 새로운 함수를 만든다.
// 함수 생성 방지
const MyComponent = () => {
  const handleClick = useCallback(() => {
    console.log("Button clicked!");
  }, []);
  
  return <button onClick={handleClick}>Click Me</button>;
};

React.memo = 컴포넌트 리렌더링 방지

부모 컴포넌트가 리렌더링될시, 자식 컴포넌트도 함께 렌더링되는 것을 막고 싶을 때

  • props가 변경되지 않으면 해당 컴포넌트를 다시 렌더링하지 않는다.
  • 고차 컴포넌트(Higher Order Component, HOC)
const Parent = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Child />
    </div>
  );
};

// React.memo 사용
const Child = React.memo(() => {
  console.log("🔄 Child Re-render");
  return <div>I'm a child component</div>;
});

useRef = 상태가 아닌 값 유지

렌더링될 때마다 초기화되는 것을 막고 싶을 때,
DOM 요소에 직접 접근해야 할 때,

  • 상태가 아니라 렌더링과 무관한 값을 유지할 수 있다.
const MyComponent = () => {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  renderCount.current++; // 값이 유지됨

  return (
    <div>
      <p>Render Count: {renderCount.current}</p>
      <button onClick={() => setCount(count + 1)}>Click</button>
    </div>
  );
};

컴포넌트 구조 최적화하기 = 상태 관리

위의 함수를 적절히 사용하는 것도 중요하지만, 컴포넌트 구조 자체를 최적화하는 과정도 중요하다.

  1. 상태를 최소한의 컴포넌트에서만 관리하기

    • 컴포넌트가 갖고 있는 state가 변경되면 그 컴포넌트는 무조건 리렌더링된다.
  2. Context API 사용하기

    • 불필요한 props의 전달은 리렌더링을 유발한다. (Prop Drilling)
  3. 컴포넌트를 memo + useCallback 으로 최적화하기

  1. 리스트의 key를 최적화하여 Virtual DOM 비교 비용 줄이기

    • 리스트를 렌더링할 때 key가 없거나 index를 key로 사용하면 불필요한 리렌더링이 발생할 수 있다.

렌더링에 대해 다시 정리하면서, 단지 리렌더링을 방지하는 방법만 알기 보다 virtual DOM과 조화 단계가 뭔지 이해하는 것이 중요한걸 알게 되었다.

추후에는 프로젝트에서 직접 적용해보며 어떻게 개선되는지 정리해봐야겠다.

profile
탄탄한 개발자로 살아남기🗿

2개의 댓글

comment-user-thumbnail
2025년 3월 17일

최적화 할 수 있는 방법에 이런 방법도 있군요!

답글 달기
comment-user-thumbnail
2025년 3월 23일

상황에 따라 여러가지 방법으로 불필요한 랜더링을 줄여서 최적화를 할수있겠네요!

답글 달기