본문 바로가기
카테고리 없음

React 성능 최적화 완전정리 (렌더링, 메모이제이션, 리렌더링)

by ctrl-f 2025. 5. 1.

React는 컴포넌트 기반 UI 프레임워크로써 재사용성과 확장성이 뛰어나지만, 잘못된 상태 관리나 렌더링 구조로 인해 성능 문제가 발생하기 쉽습니다. 특히 대규모 애플리케이션이나 리스트 렌더링, 상태 변경이 빈번한 상황에서는 불필요한 리렌더링이 사용자 경험을 저해할 수 있습니다. 이 글에서는 React의 렌더링 구조를 이해하고, 메모이제이션 기법을 활용해 불필요한 렌더링을 줄이며, 상태 관리 전략을 통해 성능 최적화를 어떻게 달성할 수 있는지 단계적으로 정리합니다. 또한 면접에서 자주 묻는 성능 최적화 질문에도 대비할 수 있는 핵심 내용을 함께 담았습니다.

불필요한 렌더링을 줄이는 구조 이해

React는 상태(state)나 props가 변경되면 해당 컴포넌트를 자동으로 리렌더링합니다. 이 기본 구조는 UI를 최신 상태로 유지하는 데 탁월하지만, 예상치 못한 리렌더링이 반복되면 성능 저하의 원인이 됩니다. 특히 부모 컴포넌트가 리렌더링되면서 자식 컴포넌트까지 모두 다시 렌더링되는 현상은 불필요한 연산과 렌더링 시간을 초래할 수 있습니다.

이를 막기 위한 첫 번째 방법은 React.memo를 사용하는 것입니다. React.memo는 고차 컴포넌트(HOC)로, props가 변경되지 않으면 컴포넌트를 다시 렌더링하지 않습니다. 예를 들어 아래와 같이 사용할 수 있습니다:

const Child = React.memo(({ value }) => {
  console.log("Child render");
  return <div>{value}</div>;
});

이 코드는 부모 컴포넌트가 리렌더링되더라도 value가 변경되지 않으면 Child는 렌더링되지 않습니다. 이를 통해 불필요한 연산을 방지할 수 있습니다.

또한 컴포넌트를 분리할 때 렌더링 영향을 최소화하도록 구조화하는 것도 중요합니다. 상태는 필요한 최소한의 컴포넌트에만 위치시켜야 하며, 불필요하게 많은 자식에게 props를 전달하지 않는 것이 좋습니다. 리스트 렌더링에서는 key 속성을 정확히 설정해 React가 DOM 변경을 최소화할 수 있도록 해야 합니다.

이러한 렌더링 구조 이해는 성능 개선뿐 아니라, 면접에서 "React에서 불필요한 렌더링을 줄이기 위한 구조 설계 방법은?" 같은 질문에 직접적으로 대응할 수 있는 실력 기반이 됩니다.

useMemo, useCallback의 실전 활용법

React에서 제공하는 useMemouseCallback은 메모이제이션(Memoization) 기법을 위한 훅(Hook)으로, 계산 결과나 함수 객체를 캐싱하여 리렌더링 시 불필요한 연산을 줄여줍니다.

useMemo는 복잡한 계산이 포함된 값을 리렌더링마다 다시 계산하지 않도록 캐싱해주는 훅입니다. 예를 들어 다음과 같이 사용합니다:

const expensiveValue = useMemo(() => {
  return computeHeavy(data);
}, [data]);

이렇게 하면 data가 변경될 때만 computeHeavy 함수가 실행되며, 그 외에는 이전 값을 그대로 재사용합니다.

useCallback은 함수 객체를 메모이제이션합니다. 리렌더링 시 매번 새로운 함수를 생성하면 자식 컴포넌트가 React.memo를 사용하더라도 리렌더링될 수 있습니다. 이를 방지하기 위해 함수도 메모이제이션할 수 있습니다:

const handleClick = useCallback(() => {
  doSomething();
}, []);

이 함수는 의존성이 변경되지 않는 한 동일한 참조를 유지하므로 자식 컴포넌트의 리렌더링을 유발하지 않습니다.

하지만 useMemo와 useCallback은 무분별하게 사용하는 경우 오히려 복잡도를 증가시키고 성능 이점을 얻지 못할 수 있으므로, "비용이 큰 연산" 또는 "props 전달로 인한 렌더링 최적화"에 명확한 목적이 있을 때만 사용하는 것이 좋습니다.

면접에서는 "useMemo와 useCallback의 차이는?", "언제 써야 효과적인가요?" 같은 질문이 자주 등장합니다. 이때 단순히 정의를 외우는 것이 아니라, 실제 사례를 들고 설명하는 것이 신뢰를 높일 수 있습니다. 예를 들어 "무한 스크롤에서 페이지네이션 데이터를 필터링할 때 useMemo를 썼고, 클릭 이벤트 핸들러를 자식에 전달할 때 useCallback을 사용해 리렌더링을 줄였다"는 식의 실전 설명이 좋습니다.

상태 변화와 리렌더링의 상관관계

React에서의 상태 변화는 리렌더링을 유발하는 주요 원인 중 하나입니다. 하지만 모든 상태 변경이 꼭 전체 컴포넌트를 다시 그려야 하는 것은 아니며, 상황에 따라 최적화할 수 있습니다.

React는 Virtual DOM을 통해 실제 DOM 조작을 최소화하지만, 상태가 변경되면 전체 컴포넌트가 함수형으로 다시 실행되고, 그 결과에 따라 변경된 부분만 DOM에 반영됩니다. 따라서 상태를 효율적으로 관리하지 않으면, 예상하지 못한 리렌더링이 발생해 성능이 저하됩니다.

이를 방지하려면 상태를 컴포넌트의 계층 구조에 따라 전략적으로 배치해야 합니다. 예를 들어 부모 컴포넌트에 상태를 위치시키고 모든 자식에게 props로 넘기면, 자식도 상태가 바뀔 때마다 모두 렌더링됩니다. 반대로, 하위 컴포넌트가 독립적으로 상태를 가지면, 다른 부분과 무관하게 상태 업데이트가 가능해 리렌더링을 최소화할 수 있습니다.

또한 상태가 복잡하거나 다양한 컴포넌트에 공유되어야 할 경우, useReduceruseContext를 사용하는 것이 도움이 됩니다. 다만 useContext의 경우 모든 하위 컴포넌트가 리렌더링될 수 있으므로, context value를 메모이제이션하거나 context를 분리하여 과도한 렌더링을 피해야 합니다.

실무에서는 React Query, Zustand, Redux Toolkit 같은 상태 관리 라이브러리를 활용하여 전역 상태를 효율적으로 관리하면서도, 리렌더링 범위를 최소화하는 방식이 자주 쓰입니다. 면접에서 "상태 변화에 따른 리렌더링을 어떻게 최소화할 수 있나요?"라는 질문을 받았을 때, 위와 같은 원칙과 사례를 말할 수 있다면 매우 좋은 인상을 줄 수 있습니다.

React에서 성능 최적화를 위해 고려해야 할 요소는 단순히 코드 한 줄의 변경이 아니라, 컴포넌트 구조, 상태 관리, 메모이제이션, 그리고 렌더링 타이밍까지 전반적인 이해를 필요로 합니다. React.memo, useMemo, useCallback은 모두 강력한 도구이지만, 상황에 따라 적절하게 사용해야 진정한 효과를 발휘합니다. 또한 Virtual DOM의 작동 원리와 상태 변경 시 리렌더링 흐름을 명확히 이해하면, 예기치 않은 성능 저하를 방지할 수 있습니다. 면접에서는 이러한 개념을 코드 예시와 함께 설명하고, 실무에서 겪었던 최적화 사례를 공유하는 방식으로 접근하는 것이 좋습니다. 이 글을 통해 개념 이해와 면접 준비 모두 탄탄하게 다질 수 있길 바랍니다.

 

React 성능 최적화 기법