React 성능 최적화: 메모이제이션 기법으로 불필요한 리렌더링 방지하기

2025. 5. 28. 10:08·Frontend Development
반응형

이미지 출처 - https://www.geeksforgeeks.org/what-is-memoization-in-react/

"모든 최적화는 측정에서 시작한다. 성능 개선 작업은 실제 병목을 해결할 때만 가치가 있다."

여러분의 React 애플리케이션이 느리게 동작하나요? 컴포넌트가 필요 이상으로 자주 리렌더링되고 있지는 않나요? 이 글에서는 React의 메모이제이션(Memoization) 기법을 활용하여 애플리케이션의 성능을 효과적으로 개선하는 방법을 알아보겠습니다.

목차

  • React의 렌더링 메커니즘 이해하기
  • 불필요한 리렌더링이 발생하는 상황
  • 메모이제이션 기법 소개
  • React.memo로 컴포넌트 최적화하기
  • useMemo로 계산값 최적화하기
  • useCallback으로 함수 최적화하기
  • 성능 측정 및 병목 식별하기
  • 메모이제이션의 오해와 진실
  • 실제 사례로 보는 최적화 전략
  • 결론

React의 렌더링 메커니즘 이해하기

React에서 렌더링이란 무엇일까요? 간단히 말해, 컴포넌트의 현재 props와 state를 기반으로 UI를 그리는 과정입니다. React는 다음과 같은 상황에서 렌더링을 발생시킵니다:

  1. 컴포넌트의 state가 변경될 때
  2. 부모 컴포넌트가 리렌더링될 때
  3. 컴포넌트가 전달받는 props가 변경될 때
  4. context가 변경될 때

특히 주목해야 할 점은, 부모 컴포넌트가 리렌더링되면 기본적으로 모든 자식 컴포넌트도 리렌더링된다는 것입니다. 이는 React의 기본 동작이며, 많은 경우 성능 문제의 원인이 됩니다.

렌더링 과정을 자동차 엔진에 비유해볼 수 있습니다. 자동차를 운전할 때마다 엔진이 작동하듯, React 애플리케이션에서 상태가 변경될 때마다 렌더링 엔진이 작동합니다. 하지만 자동차가 정지해 있을 때도 엔진을 계속 돌린다면 연료가 낭비되듯, 변경이 없는 컴포넌트까지 리렌더링하는 것은 성능 낭비입니다.


불필요한 리렌더링이 발생하는 상황

다음과 같은 상황에서 불필요한 리렌더링이 자주 발생합니다:

  1. 부모 컴포넌트가 자주 업데이트되는 경우

    • 부모 컴포넌트의 상태가 변경될 때마다 모든 자식 컴포넌트가 리렌더링됩니다.
  2. 복잡한 계산이 렌더링마다 반복되는 경우

    • 데이터 필터링, 정렬 등의 연산이 매 렌더링마다 반복 실행됩니다.
  3. 동일한 함수가 매번 새로 생성되는 경우

    • 이벤트 핸들러 함수가 렌더링마다 새로 생성되어 자식 컴포넌트의 불필요한 리렌더링을 유발합니다.

실제 프로젝트에서 아래와 같은 코드를 본 적이 있을 것입니다:

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

  // 렌더링마다 새로운 함수 생성
  const handleClick = () => {
    console.log('Button clicked');
  };

  // 렌더링마다 계산 반복
  const expensiveCalculation = () => {
    console.log('Calculating...');
    return items.filter(item => item.active).map(item => item.value);
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Clicked {count} times
      </button>
      <ExpensiveComponent 
        data={expensiveCalculation()} 
        onClick={handleClick} 
      />
    </div>
  );
}

이 코드에서는 count 상태가 변경될 때마다 handleClick 함수가 새로 생성되고, expensiveCalculation 함수가 다시 실행되어 ExpensiveComponent가 불필요하게 리렌더링됩니다.


메모이제이션 기법 소개

메모이제이션(Memoization)은 이전에 계산한 결과를 저장해두고 동일한 입력이 들어오면 저장된 결과를 재사용하는 최적화 기법입니다. 이는 마치 주스를 만들 때, 매번 과일을 다시 갈지 않고 같은 재료로 만든 주스가 있다면 이미 만들어 둔 것을 제공하는 것과 유사합니다.

React에서는 세 가지 주요 메모이제이션 도구를 제공합니다:

  1. React.memo: 컴포넌트 자체를 메모이제이션
  2. useMemo: 계산 결과값을 메모이제이션
  3. useCallback: 함수를 메모이제이션

각 도구는 특정 상황에서 성능 향상에 도움이 되며, 적절히 사용하면 애플리케이션의 반응성을 크게 개선할 수 있습니다.


React.memo로 컴포넌트 최적화하기

React.memo는 고차 컴포넌트(Higher Order Component)로, 컴포넌트의 props가 변경되지 않으면 리렌더링을 방지합니다. 이는 특히 부모 컴포넌트가 자주 리렌더링되지만 자식 컴포넌트의 props는 거의 변경되지 않는 상황에서 유용합니다.

기본 사용법

const MyComponent = (props) => {
  /* 컴포넌트 로직 */
  return <div>{props.name}</div>;
};

// React.memo로 컴포넌트 감싸기
const MemoizedComponent = React.memo(MyComponent);

사용자 정의 비교 함수

기본적으로 React.memo는 props 객체의 얕은 비교(shallow comparison)를 수행합니다. 복잡한 비교 로직이 필요한 경우, 두 번째 인자로 사용자 정의 비교 함수를 전달할 수 있습니다:

const areEqual = (prevProps, nextProps) => {
  // true를 반환하면 리렌더링하지 않음
  // false를 반환하면 리렌더링함
  return prevProps.id === nextProps.id && 
         prevProps.name === nextProps.name;
};

const MemoizedComponent = React.memo(MyComponent, areEqual);

언제 사용해야 할까?

React.memo는 다음과 같은 상황에서 효과적입니다:

  • 컴포넌트가 같은 props로 자주 렌더링되는 경우
  • 컴포넌트가 렌더링될 때마다 복잡한 연산을 수행하는 경우
  • 컴포넌트가 큰 리스트 내부에서 렌더링되는 경우

useMemo로 계산값 최적화하기

useMemo는 복잡한 계산 결과를 메모이제이션하는 훅입니다. 특정 의존성이 변경될 때만 계산을 다시 수행하고, 그렇지 않으면 이전 계산 결과를 재사용합니다.

기본 사용법

import { useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  // 의존성 배열의 값이 변경될 때만 계산 실행
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

언제 사용해야 할까?

useMemo는 다음과 같은 상황에서 효과적입니다:

  • 계산 비용이 높은 연산이 있는 경우 (데이터 필터링, 정렬, 변환 등)
  • 복잡한 객체나 배열을 생성하는 경우 (참조 동일성 유지가 필요한 경우)
  • 렌더링 성능에 영향을 미치는 계산인 경우

useCallback으로 함수 최적화하기

useCallback은 함수를 메모이제이션하는 훅으로, 특정 의존성이 변경될 때만 함수를 새로 생성합니다. 이는 함수의 참조 동일성을 유지해야 하는 경우, 특히 자식 컴포넌트에 콜백 함수를 전달할 때 유용합니다.

기본 사용법

import { useCallback } from 'react';

function ParentComponent({ id }) {
  // id가 변경될 때만 새 함수 생성
  const handleClick = useCallback(() => {
    console.log(`Item ${id} clicked`);
    // 데이터 처리 로직
  }, [id]);

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

언제 사용해야 할까?

useCallback은 다음과 같은 상황에서 효과적입니다:

  • 함수가 React.memo로 감싼 자식 컴포넌트에 전달될 때
  • 함수가 다른 훅의 의존성 배열에 포함될 때
  • 함수가 복잡한 로직을 포함하거나 생성 비용이 높을 때

성능 측정 및 병목 식별하기

성능 최적화는 측정에서 시작합니다. React에서는 React DevTools의 Profiler를 사용하여 컴포넌트의 렌더링 시간과 빈도를 분석할 수 있습니다.

React DevTools Profiler 활용하기

  1. Chrome 또는 Firefox에 React DevTools 확장 프로그램 설치
  2. 개발자 도구에서 Profiler 탭 선택
  3. 녹화 버튼을 누르고 애플리케이션 사용
  4. 녹화 중지 후 결과 분석

Profiler는 각 컴포넌트의 렌더링 시간과 렌더링이 발생한 이유를 시각적으로 보여줍니다. 이를 통해 어떤 컴포넌트가 병목의 원인인지 식별할 수 있습니다.

최적화 우선순위 결정하기

성능 최적화는 모든 코드에 무차별적으로 적용하는 것이 아니라, 다음 기준에 따라 우선순위를 결정해야 합니다:

  1. 사용자 경험에 직접적인 영향을 미치는 부분
  2. 렌더링 시간이 오래 걸리는 컴포넌트
  3. 렌더링 빈도가 높은 컴포넌트
  4. 실제로 최적화했을 때 개선 효과가 큰 부분

메모이제이션의 오해와 진실

메모이제이션에 관한 몇 가지 일반적인 오해와 진실을 살펴보겠습니다.

오해: "메모이제이션은 항상 성능을 향상시킨다"

진실: 메모이제이션 자체도 비용이 듭니다. 메모리를 사용하고, 의존성 배열의 각 항목을 비교하는 작업이 필요합니다. 간단한 연산이나 자주 변경되는 값에 메모이제이션을 적용하면 오히려 성능이 저하될 수 있습니다.

오해: "가능한 많은 곳에 메모이제이션을 적용해야 한다"

진실: 메모이제이션은 실제 성능 병목이 있는 곳에만 선별적으로 적용해야 합니다. 불필요한 메모이제이션은 코드 복잡성만 증가시키고 성능 이점은 미미합니다.

잘못된 사용 예시와 올바른 사용 예시

잘못된 사용 (불필요한 메모이제이션)

// 간단한 함수의 불필요한 메모이제이션
const handleClick = useCallback(() => {
  console.log('Button clicked');
}, []);

// 자주 변경되는 값의 불필요한 메모이제이션
const currentTime = useMemo(() => new Date().toLocaleTimeString(), []);

올바른 사용 (효과적인 메모이제이션)

// 복잡한 계산의 효과적인 메모이제이션
const filteredAndSortedItems = useMemo(() => {
  return items
    .filter(item => item.active)
    .sort((a, b) => a.value - b.value);
}, [items]);

// 자식 컴포넌트에 전달되는 함수의 효과적인 메모이제이션
const handleItemSelect = useCallback((id) => {
  setSelectedItems(prev => [...prev, id]);
  analytics.trackSelection(id);
}, []);

실제 사례로 보는 최적화 전략

실제 애플리케이션의 성능 최적화 예시를 통해 메모이제이션 전략을 살펴보겠습니다.

예시 1: 대규모 데이터 목록 렌더링

function ProductList({ products, category, searchTerm }) {
  // 필터링 및 정렬 로직 메모이제이션
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products
      .filter(product => 
        product.category === category && 
        product.name.includes(searchTerm)
      )
      .sort((a, b) => a.price - b.price);
  }, [products, category, searchTerm]);

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductItem 
          key={product.id} 
          product={product} 
        />
      ))}
    </ul>
  );
}

// 개별 아이템은 React.memo로 최적화
const ProductItem = React.memo(({ product }) => {
  return (
    <li className="product-item">
      <h3>{product.name}</h3>
      <p>{product.price}원</p>
    </li>
  );
});

예시 2: 양방향 데이터 바인딩이 있는 폼

function ComplexForm({ onSubmit, initialData }) {
  const [formData, setFormData] = useState(initialData);

  // 제출 핸들러 메모이제이션
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    onSubmit(formData);
  }, [formData, onSubmit]);

  // 입력 변경 핸들러 메모이제이션
  const handleChange = useCallback((e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  }, []);

  // 폼 유효성 검사 결과 메모이제이션
  const validationResult = useMemo(() => {
    return validateForm(formData);
  }, [formData]);

  return (
    <form onSubmit={handleSubmit}>
      {/* 폼 필드 렌더링 */}
      <FormFields 
        data={formData} 
        errors={validationResult.errors}
        onChange={handleChange} 
      />
      <button type="submit" disabled={!validationResult.isValid}>
        제출
      </button>
    </form>
  );
}

// 폼 필드 컴포넌트 메모이제이션
const FormFields = React.memo(({ data, errors, onChange }) => {
  // 폼 필드 렌더링 로직
});

결론

React에서 메모이제이션 기법은 성능 최적화의 중요한 도구입니다. React.memo, useMemo, useCallback을 적절히 활용하면 불필요한 리렌더링을 방지하고 애플리케이션의 성능을 크게 향상시킬 수 있습니다.

하지만 기억해야 할 점은 모든 최적화에는 비용이 따른다는 것입니다. 메모이제이션도 메모리 사용과 비교 연산이라는 비용을 수반합니다. 따라서 실제 성능 병목이 발생하는 부분을 정확히 식별하고, 선별적으로 최적화하는 것이 중요합니다.

최적화 전략을 요약하자면 다음과 같습니다:

  1. React DevTools Profiler로 실제 성능 병목 식별하기
  2. 렌더링이 자주 발생하고 비용이 큰 컴포넌트에 React.memo 적용하기
  3. 복잡한 계산이 포함된 값에 useMemo 적용하기
  4. 자식 컴포넌트에 전달되는 콜백 함수에 useCallback 적용하기
  5. 과도한 최적화는 지양하고, 측정 가능한 성능 향상이 있는 경우에만 적용하기

성능 최적화는 개발 과정의 마지막 단계에서 진행하는 것이 바람직합니다. 먼저 코드를 명확하고 유지보수하기 쉽게 작성한 후, 실제 성능 문제가 발생할 때 필요한 부분을 최적화하는 방식으로 접근하세요.


참고 자료

  • React 공식 문서 - React.memo
  • React 공식 문서 - Hooks API Reference
  • React DevTools Profiler 소개
  • Kent C. Dodds - When to useMemo and useCallback
  • Dan Abramov - Before You memo()
  • React Performance Optimization
반응형
저작자표시 비영리 변경금지 (새창열림)

'Frontend Development' 카테고리의 다른 글

React의 동시성 모드(Concurrent Mode): 사용자 경험을 혁신하는 비밀 무기  (0) 2025.05.28
script 태그의 성능 최적화: async와 defer 속성 완벽 가이드  (0) 2025.05.28
CORS 완벽 가이드: 웹 개발자가 반드시 알아야 할 교차 출처 리소스 공유  (0) 2025.05.28
React의 Error Boundary: 안정적인 프론트엔드 애플리케이션을 위한 필수 요소  (2) 2025.05.28
JavaScript Promise 완벽 가이드: resolve()와 fulfilled 상태 이해하기  (0) 2025.05.28
'Frontend Development' 카테고리의 다른 글
  • React의 동시성 모드(Concurrent Mode): 사용자 경험을 혁신하는 비밀 무기
  • script 태그의 성능 최적화: async와 defer 속성 완벽 가이드
  • CORS 완벽 가이드: 웹 개발자가 반드시 알아야 할 교차 출처 리소스 공유
  • React의 Error Boundary: 안정적인 프론트엔드 애플리케이션을 위한 필수 요소
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (100) N
      • Frontend Development (39) N
      • Backend Development (21) N
      • Algorithm (33) N
        • 백준 (11) N
        • 프로그래머스 (17)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (3)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    frontend development
    tailwindcss
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
React 성능 최적화: 메모이제이션 기법으로 불필요한 리렌더링 방지하기
상단으로

티스토리툴바