본문 바로가기

웹 성능 최적화

React 리렌더링 최적화 완벽 가이드 - 성능 개선 실전 테크닉

웹 성능 최적화 React 리렌더링 최적화 완벽 가이드 {} Performance

React 리렌더링 최적화 완벽 가이드 - 성능 개선 실전 테크닉

React 앱이 느려지는 가장 큰 원인은 불필요한 리렌더링이에요. 컴포넌트가 실제로 변경되지 않았는데도 다시 렌더링되면서 성능 저하가 발생하죠. 이 글에서는 실전에서 바로 적용할 수 있는 리렌더링 최적화 기법을 알려드릴게요.

React 리렌더링이 발생하는 3가지 상황

React 컴포넌트는 다음 경우에 리렌더링돼요. 첫째, 자신의 state가 변경될 때, 둘째, 부모 컴포넌트가 리렌더링될 때, 셋째, 사용 중인 context 값이 변경될 때입니다. 특히 부모 컴포넌트의 리렌더링은 자식 컴포넌트에 props가 변경되지 않아도 연쇄적으로 리렌더링을 유발해요.
문제는 대부분의 경우 자식 컴포넌트는 실제로 업데이트가 필요 없는데도 렌더링된다는 거예요. 예를 들어 카운터를 증가시키는 버튼이 있는 부모 컴포넌트 안에 정적인 헤더 컴포넌트가 있다면, 카운터가 변경될 때마다 헤더도 불필요하게 리렌더링되죠.

React.memo로 props 비교 최적화하기

React.memo는 컴포넌트를 메모이제이션해서 props가 변경되지 않으면 리렌더링을 건너뛰게 해요. 고차 컴포넌트(HOC) 방식으로 동작하며, 얕은 비교(shallow comparison)를 통해 props의 변경 여부를 판단합니다.

const Header = React.memo(({ title }) => {
  console.log('Header 렌더링');
  return <h1>{title}</h1>;
});

function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <Header title="내 앱" />
      <button onClick={() => setCount(count + 1)}>
        카운트: {count}
      </button>
    </>
  );
}

이제 버튼을 클릭해도 Header는 리렌더링되지 않아요. title prop이 변경되지 않았기 때문이죠. 하지만 객체나 함수를 props로 전달할 때는 주의가 필요해요.

useMemo와 useCallback으로 참조 동일성 유지

React.memo를 사용해도 객체나 함수를 props로 넘기면 매번 새로운 참조가 생성되어 최적화가 무효화돼요. 이때 useMemo와 useCallback을 사용하면 참조를 유지할 수 있어요.
useMemo는 계산 비용이 큰 값을 메모이제이션하고, useCallback은 함수를 메모이제이션해요. 의존성 배열에 있는 값이 변경될 때만 새로운 값이나 함수를 생성하죠.

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

  const expensiveValue = useMemo(() => {
    return items.reduce((acc, item) => acc + item.price, 0);
  }, [items]);

  const handleClick = useCallback(() => {
    console.log('클릭됨');
  }, []);

  return <ChildComponent value={expensiveValue} onClick={handleClick} />;
}

useCallback은 useMemo(() => fn, deps)의 문법 설탕이에요. 함수를 반환하는 경우에 더 간결한 문법을 제공하죠.

key 속성과 컴포넌트 구조 최적화

리스트를 렌더링할 때 key를 인덱스로 사용하면 아이템 순서가 변경될 때 불필요한 리렌더링이 발생해요. 고유한 ID를 key로 사용해야 React가 정확히 어떤 아이템이 변경되었는지 추적할 수 있어요.
또한 state를 적절한 위치에 배치하는 것도 중요해요. state가 변경되면 해당 컴포넌트와 모든 자식이 리렌더링되므로, state를 가능한 한 하위 컴포넌트에 두는 게 좋아요. 전역 상태 관리가 필요한 경우에만 상위로 끌어올리세요.
컴포넌트를 작게 쪼개는 것도 효과적이에요. 자주 변경되는 부분과 정적인 부분을 분리하면 변경 범위를 최소화할 수 있죠.

최적화 시 주의사항

과도한 최적화는 오히려 코드 복잡도를 높이고 유지보수를 어렵게 만들어요. React.memo, useMemo, useCallback은 메모리를 사용하고 비교 연산 비용이 발생하므로, 실제 성능 문제가 있는 곳에만 적용하세요.
React DevTools Profiler로 렌더링 성능을 측정하고, 병목 지점을 찾아서 최적화하는 게 바람직해요. 성능 문제 없이 잘 작동하는 코드라면 굳이 최적화할 필요가 없답니다.

결론

React 리렌더링 최적화의 핵심은 불필요한 렌더링을 제거하는 거예요. React.memo로 컴포넌트를 메모이제이션하고, useMemo와 useCallback으로 참조 동일성을 유지하며, 적절한 컴포넌트 구조 설계로 변경 범위를 최소화하세요. 다만 실제 성능 문제가 있는 경우에만 선택적으로 적용하는 것을 잊지 마세요.