Frontend/React

리액트 성능 최적화의 핵심, React.memo로 불필요한 리렌더링 완벽 방어하기

미니임 2026. 3. 1. 22:20

 

리액트는 선언적 UI 라이브러리로서 매우 효율적으로 동작하지만, 때로는 우리의 의도보다 더 자주 '열일'을 하곤 합니다. 특히 부모 컴포넌트가 업데이트될 때마다 자식 컴포넌트들이 아무런 변화가 없음에도 불구하고 다시 그려지는 현상은 대규모 애플리케이션에서 성능 저하의 주범이 됩니다. 오늘은 이 불필요한 렌더링의 고리를 끊어낼 수 있는 강력한 도구, React.memo에 대해 깊이 있게 살펴보겠습니다.


왜 우리는 '메모이제이션'에 주목해야 하는가?

현대 웹 애플리케이션은 수백 개의 컴포넌트가 거대한 트리를 형성합니다. 리액트의 기본 렌더링 메커니즘은 매우 단순합니다. "부모가 변하면 자식도 변한다." 하지만 실제 비즈니스 로직에서는 부모의 상태(State)가 변하더라도 특정 자식 컴포넌트가 전달받는 프로프(Props)는 그대로인 경우가 많습니다.

이때 발생하는 것이 바로 **불필요한 리렌더링(Unnecessary Re-render)**입니다. 돔(DOM)에 직접 반영되지 않더라도 리액트 내부적으로 가상 돔(Virtual DOM)을 비교하는 연산이 반복되면, 저사양 기기나 복잡한 대시보드 환경에서는 눈에 띄는 프레임 드랍이 발생하게 됩니다.


React.memo: 컴포넌트를 기억하는 기술 (Deep Dive)

React.memo는 고차 컴포넌트(Higher Order Component, HOC)입니다. 컴포넌트가 동일한 Props로 동일한 결과를 렌더링해낸다면, 리액트는 마지막으로 렌더링된 결과를 재사용합니다.

💡 일상 속의 비유: 요리 레시피 북

당신이 식당의 셰프라고 가정해 봅시다. 손님이 "A 세트"를 주문할 때마다 매번 레시피 북을 처음부터 끝까지 정독하며 재료를 준비한다면 매우 비효율적일 것입니다. 대신, **"A 세트의 구성품이 바뀌지 않았다면 이전에 만들어둔 표준 조리법을 그대로 사용한다"**는 규칙을 세우는 것이 바로 React.memo의 원리입니다.

작동 원리 (Shallow Comparison)

React.memo는 기본적으로 **얕은 비교(Shallow Comparison)**를 수행합니다.

  • 원시 타입(String, Number, Boolean): 값이 같으면 리렌더링을 방지합니다.
  • 참조 타입(Object, Array, Function): 메모리 주소값이 다르면 값이 내용적으로 같더라도 리렌더링이 발생합니다.

실전 예제: 이커머스 장바구니 품목 리스트

단순한 카운터 예제가 아닌, 실제 이커머스에서 흔히 볼 수 있는 '장바구니 리스트' 상황을 가정해 보겠습니다. 전체 금액을 계산하는 부모 컴포넌트가 업데이트될 때, 개별 상품 아이템(ProductItem)이 불필요하게 리렌더링되는 것을 막아보겠습니다.

1. 최적화되지 않은 코드의 문제점

부모 컴포넌트의 totalDiscount 상태가 변할 때마다, 리스트 내의 모든 ProductItem이 다시 렌더링됩니다.

2. React.memo를 적용한 최적화 솔루션

JavaScript
 
import React, { useState, useMemo } from 'react';

// [Hands-on] 개별 상품 아이템 컴포넌트
// React.memo로 감싸 Props가 변경되지 않으면 리렌더링을 방지합니다.
const ProductItem = React.memo(({ id, name, price }) => {
  console.log(`🚚 상품 렌더링 중: ${name}`);
  
  return (
    <div style={{ border: '1px solid #eee', padding: '10px', margin: '5px' }}>
      <strong>{name}</strong> - {price.toLocaleString()}원
    </div>
  );
});

// 메인 장바구니 컴포넌트
const ShoppingCart = () => {
  const [items] = useState([
    { id: 1, name: '고성능 게이밍 마우스', price: 75000 },
    { id: 2, name: '기계식 키보드', price: 120000 },
    { id: 3, name: '4K 모니터링 암', price: 45000 },
  ]);
  
  const [couponCode, setCouponCode] = useState('');

  return (
    <div style={{ padding: '20px' }}>
      <h2>🛒 내 장바구니</h2>
      
      {/* 쿠폰 입력 필드: 이 값이 변해도 ProductItem은 리렌더링되지 않아야 함 */}
      <div style={{ marginBottom: '20px' }}>
        <input 
          type="text" 
          placeholder="쿠폰 번호를 입력하세요" 
          value={couponCode}
          onChange={(e) => setCouponCode(e.target.value)}
        />
        <p>입력 중인 쿠폰: {couponCode}</p>
      </div>

      <div className="product-list">
        {items.map(item => (
          <ProductItem 
            key={item.id} 
            id={item.id} 
            name={item.name} 
            price={item.price} 
          />
        ))}
      </div>
    </div>
  );
};

export default ShoppingCart;

💡 코드 포인트 레슨

  • React.memo(Component): 컴포넌트를 이 함수로 감싸는 것만으로도 성능 최적화의 첫걸음을 뗄 수 있습니다.
  • 독립적 상태 분리: 위 예제에서 couponCode가 바뀔 때 부모인 ShoppingCart는 리렌더링되지만, ProductItem은 React.memo 덕분에 가상 돔 비교 단계를 스킵합니다.

트러블슈팅: "왜 메모를 썼는데도 리렌더링이 되나요?"

가장 흔히 겪는 실수 중 하나는 Props로 함수나 객체를 직접 전달하는 경우입니다.

JavaScript
 
// ❌ 위험한 패턴: 리렌더링 시마다 새로운 함수가 생성되어 memo가 깨짐
<ProductItem onClick={() => addToCart(item.id)} />

자바스크립트에서 () => {}는 매번 새로운 참조값을 만듭니다. React.memo는 "어? 전달받은 함수(객체)가 이전과 다른데?"라고 판단하여 다시 렌더링을 수행합니다. 이를 해결하려면 useCallback이나 useMemo를 병행하여 **참조 동일성(Referential Identity)**을 유지해야 합니다.


트레이드오프 (Trade-offs): 모든 곳에 써야 할까?

기술에는 공짜 점심이 없습니다. React.memo를 남용할 경우 다음과 같은 문제가 발생할 수 있습니다.

  1. 비교 비용 발생: Props가 아주 빈번하게 바뀌는 컴포넌트에 사용하면, "비교 연산" + "렌더링 연산"이 이중으로 발생하여 오히려 손해일 수 있습니다.
  2. 메모리 사용량: 이전 결과물을 메모리에 저장해두어야 하므로 메모리 점유율이 미세하게 상승합니다.

추천 전략: * 컴포넌트의 Props가 복잡하지 않으면서 부모는 자주 바뀌는데 자식은 그대로인 경우.

  • 렌더링 시 복잡한 로직(계산량 과부하)이 포함된 컴포넌트.

요약 및 제언

React.memo는 리액트 성능 최적화의 "방패"와 같습니다. 하지만 무분별하게 방패를 들기보다는, 먼저 **컴포넌트 구조를 효율적으로 설계(Composition)**하고, 그 이후에도 발생하는 병목 지점에 전략적으로 배치하는 것이 시니어 개발자의 접근 방식입니다.

여러분의 프로젝트에서 프로파일러(React DevTools Profiler)를 켜보세요. 유독 노랗게 변하며 자주 깜빡이는 컴포넌트가 있다면, 그곳이 바로 React.memo가 투입될 골든 타임입니다.

어떤 상황에서 메모이제이션이 가장 큰 효과를 보았나요? 혹은 메모를 적용했음에도 성능 개선이 미미했던 경험이 있다면 구조적 설계의 관점에서 다시 고민해 볼 가치가 있습니다.

반응형