프로그래밍/ReactJS

React 성능 최적화의 핵심: `useMemo` 훅 완벽 이해하기

shimdh 2025. 10. 13. 13:34
728x90

React 애플리케이션의 성능은 사용자 경험에 직결되는 중요한 요소입니다. 특히 대규모 데이터나 복잡한 UI를 다룰 때, 불필요한 재렌더링은 앱의 속도를 떨어뜨리고 사용자에게 피로감을 줄 수 있습니다. 이러한 문제를 해결하기 위해 React는 useMemo 훅을 제공합니다. 이 훅은 계산 비용이 높은 작업의 결과를 메모리에 저장하여 재사용함으로써 성능을 크게 향상시킬 수 있습니다.

이 글에서는 useMemo 훅의 기본 개념부터 작동 원리, 실전 사용 사례, 그리고 주의할 점까지 자세히 탐구해 보겠습니다. 초보자부터 중급 개발자까지, useMemo를 마스터하고 React 앱을 더 효율적으로 만드는 팁을 얻어 가세요!

728x90

메모이제이션이란? useMemo의 배경 이해하기

useMemo를 제대로 활용하려면 먼저 '메모이제이션(Memoization)' 개념을 알아야 합니다. 메모이제이션은 컴퓨터 과학에서 자주 등장하는 최적화 기법으로, 함수의 입력값에 대한 결과를 미리 계산해 캐싱(저장)하는 방식입니다. 동일한 입력이 다시 들어오면 실제 계산을 건너뛰고 캐시된 결과를 즉시 반환합니다. 이는 반복적인 계산을 줄여 시간과 리소스를 절약합니다.

간단한 예로 피보나치 수열을 들어보죠. 피보나치 함수 fib(n)fib(n-1) + fib(n-2)로 정의되는데, 메모이제이션 없이 구현하면 동일한 fib(5) 같은 중복 계산이 폭발적으로 증가합니다. 하지만 메모이제이션을 적용하면:

const fibCache = {};
function fib(n) {
    if (fibCache[n]) return fibCache[n];  // 캐시 확인
    if (n <= 1) return n;
    fibCache[n] = fib(n - 1) + fib(n - 2);  // 계산 후 캐싱
    return fibCache[n];
}

이처럼 fib(10)을 한 번 계산한 후, 재호출 시 저장된 값을 바로 사용합니다. useMemo는 이 원리를 React 컴포넌트의 렌더링 사이클에 적용합니다. 컴포넌트가 재렌더링될 때마다 불필요한 계산을 피하고, 상태나 props 변화에만 반응하도록 돕죠. 결과적으로 앱의 부드러운 반응성을 유지할 수 있습니다.

useMemo 훅의 작동 방식과 기본 구문

useMemo는 React가 컴포넌트 렌더링 중에 특정 값을 계산하고, 그 결과를 메모리에 저장합니다. 종속성(dependencies)이 변하지 않으면 저장된 값을 재사용하므로, 불필요한 재계산을 막아줍니다.

기본 구문은 다음과 같습니다:

import { useMemo } from 'react';

const memoizedValue = useMemo(() => {
    // 비용이 많이 드는 계산 로직 (예: 복잡한 데이터 처리)
    return computedValue;  // 계산 결과 반환
}, [dependencies]);  // 종속성 배열: 이 값들이 변할 때만 재계산
  • 첫 번째 인수 (콜백 함수): 계산 로직을 담당합니다. 이 함수가 실행되어 반환된 값이 메모됩니다. 렌더링 시마다 실행되지 않고, 종속성에 따라 결정됩니다.
  • 두 번째 인수 (종속성 배열): 배열 안의 값들이 이전 렌더링과 동일하면 메모된 값을 반환합니다. 하나라도 바뀌면 콜백 함수를 재실행합니다. 빈 배열 []을 사용하면 컴포넌트 마운트 시 한 번만 계산됩니다.

이 메커니즘 덕분에 useMemo는 렌더링 비용을 줄이고, 특히 함수형 컴포넌트에서 강력한 최적화 도구가 됩니다.

useMemo는 언제 사용해야 할까? 실전 시나리오 탐구

useMemo는 만능이 아닙니다. 오히려 과도한 사용은 오버헤드를 초래할 수 있으니, 다음 경우에 집중적으로 적용하세요:

  1. 비용이 많이 드는 계산: 대량 데이터 필터링, 정렬, 또는 복잡한 수학 연산처럼 CPU를 많이 소모하는 작업. 매 렌더링마다 실행되면 앱이 느려지기 쉽습니다.
  2. 참조 타입의 안정성 유지: 객체 {}나 배열 [] 같은 참조 타입은 매 렌더링마다 새 인스턴스가 생성됩니다. 이는 자식 컴포넌트의 props 비교를 실패시켜 불필요한 재렌더링을 유발합니다. useMemo로 이를 안정화하면 React.memo와 결합해 더 큰 효과를 볼 수 있습니다.
  3. 자식 컴포넌트 최적화: 부모 컴포넌트의 빈번한 재렌더링에도 자식 컴포넌트가 영향을 받지 않도록 할 때. 예를 들어, 콜백 함수나 객체를 props로 전달할 때 유용합니다.

실용적인 예시 1: 대량 데이터 필터링 최적화

대규모 아이템 목록을 필터링하는 컴포넌트를 보죠. items 배열이 10,000개라면 매 키 입력마다 필터링은 성능 killer가 됩니다.

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

const ItemList = ({ items }) => {
    const [filterTerm, setFilterTerm] = useState('');

    // useMemo로 필터링 메모이제이션
    const filteredItems = useMemo(() => {
        console.log('항목 필터링 중...');  // 이 로그가 필터 변경 시에만 출력됨
        return items.filter(item => 
            item.name.toLowerCase().includes(filterTerm.toLowerCase())
        );
    }, [items, filterTerm]);  // items나 filterTerm 변경 시 재계산

    return (
        <div>
            <input
                type="text"
                placeholder="필터 검색..."
                value={filterTerm}
                onChange={(e) => setFilterTerm(e.target.value)}
            />
            <ul>
                {filteredItems.map((item) => (
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    );
};

useMemo 없이 구현하면 입력 시마다 필터링이 재실행되지만, 여기서는 종속성 변경 시에만 실행됩니다. 실제 앱에서 Chrome DevTools의 Performance 탭으로 차이를 확인해 보세요!

실용적인 예시 2: 객체 참조 안정화

자식 컴포넌트에 복잡한 config 객체를 전달할 때:

const Parent = () => {
    const [count, setCount] = useState(0);

    const expensiveConfig = useMemo(() => ({
        threshold: 100,
        options: computeExpensiveOptions(),  // 비용 높은 계산
        rules: [/* 복잡한 규칙 배열 */]
    }), []);  // 빈 배열: 마운트 시 한 번만 계산

    return (
        <div>
            <button onClick={() => setCount(count + 1)}>카운트: {count}</button>
            <Child config={expensiveConfig} />  {/* 참조 안정: 재렌더링에도 동일 객체 */}
        </div>
    );
};

const Child = React.memo(({ config }) => {
    // config 참조가 변하지 않으므로 불필요한 재렌더링 방지
    return <div>{config.threshold}</div>;
});

이렇게 하면 부모의 count 변경에도 Child가 재렌더링되지 않습니다.

useMemo 사용 시 중요한 고려 사항

useMemo는 강력하지만, 함정도 있습니다. 다음을 유의하세요:

  1. 오버헤드 관리: 종속성 배열 비교와 메모리 할당에 약간의 비용이 들며, 작은 함수에는 오히려 손해입니다. React DevTools Profiler로 실제 병목을 확인 후 적용하세요.
  2. 코드 복잡성 피하기: 훅이 많아지면 디버깅이 어려워집니다. KISS(Keep It Simple, Stupid) 원칙을 따르고, 최적화는 "작고 빠른"부터 시작하세요.
  3. 종속성 배열 실수 방지: ESLint의 react-hooks/exhaustive-deps 규칙을 활성화해 누락된 deps를 잡아내세요. 무시할 deps는 명시적으로 주석 처리.

결론: useMemo로 더 나은 React 앱 만들기

useMemo는 React의 렌더링 효율성을 높이는 핵심 훅입니다. 메모이제이션을 통해 비용 높은 계산을 최소화하고, 참조 안정성을 유지함으로써 앱의 속도와 사용자 만족도를 끌어올립니다. 하지만 성능 프로파일링 없이 맹신하지 말고, 데이터에 기반한 최적화를 실천하세요.

728x90