Dandy Now!
  • [React.js] 메모이제이션 완벽 가이드: memo, useCallback, useMemo와 Profiler 활용
    2025년 05월 25일 21시 09분 54초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    React 메모이제이션 완벽 가이드: memo, useCallback, useMemo와 Profiler 활용

    React 애플리케이션의 성능 최적화는 사용자 경험을 크게 좌우하는 중요한 요소이다. 특히 컴포넌트의 불필요한 리렌더링은 애플리케이션의 성능을 저하시키는 주요 원인 중 하나이다. 이 글에서는 React의 메모이제이션 기법들과 Profiler를 활용한 성능 분석 방법을 실제 예제를 통해 살펴본다.

    React Profiler 소개

    React Profiler는 컴포넌트 렌더링 성능을 측정하고 분석할 수 있는 도구이다. 개발자 도구의 Profiler 탭뿐만 아니라, 코드 내에서 직접 사용할 수 있는 Profiler 컴포넌트도 제공한다.

    Profiler 컴포넌트 사용법

    import { Profiler } from 'react';
    
    function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) {
      console.log({
        id,                // Profiler tree의 "id" 속성
        phase,             // "mount" 또는 "update"
        actualDuration,    // 실제 렌더링에 소요된 시간
        baseDuration,      // 최적화 없이 전체 서브트리를 렌더링하는 데 걸리는 시간
        startTime,         // React가 이번 업데이트 렌더링을 시작한 시점
        commitTime,        // React가 이번 업데이트를 커밋한 시점
        interactions       // 이번 업데이트에 해당하는 상호작용들의 집합
      });
    }
    
    <Profiler id="CommentItem" onRender={onRender}>
      <CommentItem />
    </Profiler>

    Profiler의 onRender 콜백 함수는 프로파일링되는 컴포넌트가 업데이트될 때마다 호출된다. 이를 통해 실시간으로 렌더링 성능을 모니터링할 수 있다.

    메모이제이션이 필요한 이유

    React에서 상태가 변경되면 해당 컴포넌트와 그 하위 컴포넌트들이 모두 리렌더링된다. 이는 대부분의 경우 필요한 동작이지만, 때로는 불필요한 리렌더링으로 인해 성능 문제가 발생할 수 있다.

    예제 코드를 살펴보면, Memo 컴포넌트에서 1초마다 새로운 댓글이 추가된다. 이때 기존의 댓글 항목들도 함께 리렌더링되는데, 실제로는 새로운 댓글만 렌더링되면 충분하다.

    React.memo: 컴포넌트 메모이제이션

    React.memo는 고차 컴포넌트(Higher-Order Component)로, props가 변경되지 않은 경우 컴포넌트의 리렌더링을 방지한다.

    memo 사용 전후 비교

    memo 사용 전:

    function CommentItem({ title, content, likes }) {
      console.log(`Rendering: ${title}`);
      return (
        <div>
          <span>제목: {title}</span>
          <p>내용: {content}</p>
          <span>Likes: {likes}</span>
        </div>
      );
    }

    memo 사용 후:

    import { memo } from 'react';
    
    function CommentItem({ title, content, likes }) {
      console.log(`Rendering: ${title}`);
      return (
        <div>
          <span>제목: {title}</span>
          <p>내용: {content}</p>
          <span>Likes: {likes}</span>
        </div>
      );
    }
    
    export default memo(CommentItem);

    memo를 적용하면 props가 변경되지 않은 CommentItem 컴포넌트들은 리렌더링되지 않는다. 하지만 주의할 점이 있다.

    memo의 한계: 함수 props 문제

    부모 컴포넌트가 리렌더링될 때마다 함수가 재생성되므로, 함수를 props로 전달하는 경우 memo의 효과가 사라진다.

    function Comments({ commentList }) {
      // 매번 새로운 함수가 생성됨
      const handleClick = () => {
        alert("clicked");
      };
    
      return (
        <div>
          {commentList.map((comment) => (
            <CommentItem
              key={comment.title}
              handleClick={handleClick}  // 항상 새로운 함수 참조
            />
          ))}
        </div>
      );
    }

    useCallback: 함수 메모이제이션

    useCallback은 함수를 메모이제이션하여 의존성 배열이 변경되지 않는 한 동일한 함수 참조를 반환한다.

    useCallback 사용법

    import { useCallback } from 'react';
    
    function Comments({ commentList }) {
      const handleClick = useCallback(() => {
        alert("clicked");
      }, []); // 의존성 배열이 빈 배열이므로 함수가 한 번만 생성됨
    
      return (
        <div>
          {commentList.map((comment) => (
            <CommentItem
              key={comment.title}
              handleClick={handleClick}  // 항상 동일한 함수 참조
            />
          ))}
        </div>
      );
    }

    useCallback의 효과

    useCallback을 사용하면 handleClick 함수가 컴포넌트 리렌더링 시에도 동일한 참조를 유지한다. 따라서 memo로 감싼 CommentItem 컴포넌트들은 props가 실제로 변경되지 않는 한 리렌더링되지 않는다.

    주의사항: useCallback을 사용할 때는 의존성 배열을 정확히 명시해야 한다. 의존하는 값이 있다면 반드시 배열에 포함시켜야 한다.

    const handleClick = useCallback((id) => {
      console.log(`Clicked item ${id}`);
      // someStateValue를 사용하는 경우
      doSomethingWith(someStateValue);
    }, [someStateValue]); // 의존성 배열에 포함

    useMemo: 값 메모이제이션

    useMemo는 계산 비용이 높은 값을 메모이제이션하여 불필요한 재계산을 방지한다.

    useMemo 사용 전후 비교

    useMemo 사용 전:

    function CommentItem({ title, content, likes }) {
      // 컴포넌트가 리렌더링될 때마다 실행됨
      const rate = (() => {
        console.log(`rate 계산 중: ${title}`);
        return likes > 10 ? "좋음" : "보통";
      })();
    
      return (
        <div>
          <span>제목: {title}</span>
          <span>평가: {rate}</span>
        </div>
      );
    }

    useMemo 사용 후:

    import { useMemo } from 'react';
    
    function CommentItem({ title, content, likes }) {
      const rate = useMemo(() => {
        console.log(`rate 계산 중: ${title}`);
        return likes > 10 ? "좋음" : "보통";
      }, [likes, title]); // likes나 title이 변경될 때만 재계산
    
      return (
        <div>
          <span>제목: {title}</span>
          <span>평가: {rate}</span>
        </div>
      );
    }

    useMemo의 효과

    useMemo를 사용하면 likestitle이 실제로 변경되지 않는 한 rate 값이 재계산되지 않는다. 이는 복잡한 계산이나 객체 생성 비용을 줄이는 데 효과적이다.

    실제 성능 개선 사례 분석

    제공된 예제 코드를 통해 각 최적화 기법의 효과를 분석해보자.

    1. 기본 상황 (최적화 없음)

    // 모든 CommentItem이 매번 리렌더링됨
    console.log(`actualDuration: comment1: 0.234ms`);
    console.log(`actualDuration: comment2: 0.198ms`);
    console.log(`actualDuration: comment3: 0.201ms`);
    console.log(`rate check comment1`);
    console.log(`rate check comment2`);
    console.log(`rate check comment3`);

    2. memo 적용 후

    // 새로 추가된 댓글만 렌더링됨
    console.log(`actualDuration: comment4: 0.156ms`);
    console.log(`rate check comment4`);

    3. useCallback 추가 후

    // handleClick 함수 참조가 동일하므로 memo 효과 유지
    // 기존 댓글들은 리렌더링되지 않음

    4. useMemo 추가 후

    // rate 계산도 메모이제이션되어 불필요한 계산 방지
    // 클릭 시에도 rate가 재계산되지 않음

    메모이제이션 사용 시 주의사항

    1. 과도한 메모이제이션 피하기

    메모이제이션에도 비용이 발생한다. 간단한 계산이나 자주 변경되는 값에 대해서는 메모이제이션이 오히려 성능을 저하시킬 수 있다.

    // 좋지 않은 예: 간단한 계산을 메모이제이션
    const simpleSum = useMemo(() => a + b, [a, b]);
    
    // 좋은 예: 복잡한 계산을 메모이제이션
    const expensiveCalculation = useMemo(() => {
      return heavyComputationFunction(data);
    }, [data]);

    2. 의존성 배열 정확히 명시하기

    의존성 배열을 잘못 명시하면 예상치 못한 버그가 발생할 수 있다.

    // 잘못된 예: userId가 변경되어도 함수가 업데이트되지 않음
    const fetchUser = useCallback(() => {
      return api.getUser(userId);
    }, []); // userId를 의존성에 포함하지 않음
    
    // 올바른 예
    const fetchUser = useCallback(() => {
      return api.getUser(userId);
    }, [userId]);

    3. 참조 동일성 이해하기

    객체나 배열을 메모이제이션할 때는 참조 동일성을 고려해야 한다.

    // 매번 새로운 객체가 생성됨
    const config = { option1: true, option2: false };
    
    // 올바른 방법
    const config = useMemo(() => ({
      option1: true,
      option2: false
    }), []);

    성능 측정과 최적화 전략

    1. Profiler를 활용한 성능 측정

    실제 성능 개선 효과를 확인하기 위해서는 측정이 필요하다. React Profiler를 활용하여 최적화 전후의 렌더링 시간을 비교할 수 있다.

    function onRender(id, phase, actualDuration) {
      console.log(`${id} - ${phase}: ${actualDuration}ms`);
    }
    
    <Profiler id="CommentList" onRender={onRender}>
      <Comments commentList={comments} />
    </Profiler>

    2. 병목 지점 식별

    성능 문제가 발생하는 지점을 정확히 파악한 후 최적화를 적용해야 한다. 모든 컴포넌트에 메모이제이션을 적용하는 것보다는 실제로 성능 문제가 되는 부분에 집중하는 것이 효과적이다.

    3. 단계적 최적화

    1. React.memo: 불필요한 컴포넌트 리렌더링 방지
    2. useCallback: 함수 props로 인한 memo 무효화 방지
    3. useMemo: 비용이 높은 계산 최적화

    이 순서대로 단계적으로 적용하면서 성능 개선 효과를 측정하는 것이 좋다.

    결론

    React의 메모이제이션 기법들은 애플리케이션의 성능을 크게 개선할 수 있는 강력한 도구이다. React.memo는 컴포넌트의 불필요한 리렌더링을 방지하고, useCallback은 함수 참조의 안정성을 보장하며, useMemo는 비용이 높은 계산을 최적화한다.

    하지만 이러한 기법들을 무분별하게 사용하는 것은 오히려 성능을 저하시킬 수 있다. Profiler를 활용하여 실제 성능을 측정하고, 병목 지점을 정확히 파악한 후 적절한 최적화 기법을 선택적으로 적용하는 것이 중요하다.

    성능 최적화는 사용자 경험 개선의 핵심 요소이다. 올바른 메모이제이션 기법을 통해 더 빠르고 반응성 좋은 React 애플리케이션을 구축할 수 있다.

    728x90
    반응형
    댓글