ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • memo, useMemo, useCallback으로 React 성능 최적화하기
    Frontend 2020. 5. 17. 12:58

    https://medium.com/swlh/optimizing-react-performance-with-memo-usememo-and-usecallback-11fb34f4a3fa

     

    Optimizing React performance with memo, useMemo, and useCallback

    Optimizing component performance through memoization is one of the most underused techniques in React. Memoization is a somewhat advanced…

    medium.com

     

    - 아래 글은 위 글을 번역한 글입니다. -

     

     

    사진 출처: https://knowledgeone.ca/5-factors-influencing-memory-process/

     

     

    memoization을 통한 컴포넌트 성능 최적화는 React에서 가장 잘 안쓰이는 기술 중에 하나이다. Memoization은 React의 고급 기술 중에 하나이고, 95%의 경우에는 그닥 중요치 않다. React의 reconciliation 과정(React가 어떤 컴포넌트를 업데이트할 지 안할 지 여부를 결정하는 알고리즘적 방법론)과 가상 돔(virtual DOM, React가 DOM을 업데이트하는 방식)이 매우 빨라서 대부분의 경우 우리는 memoization을 통한 최적화가 얼마나 성능을 끌어올려주는 지 알아차리지 못한다. 대부분의 React 사용자들은 성능이 우리가 알아차릴 수 있을만큼 눈에 띄게 떨어졌을 때야 비로소 성능 최적화 도입을 고려해보기 시작한다.

     

    컴포넌트의 성능을 최적화하기 위해 성능이 느려질때까지 기다리는 것이 정말 좋은 방법인가?
    우리가 도로를 건설할 때 교통량 등을 고려하지 않고 그냥 원래의 기능만 하도록 하는 것이 옳은가?

    물론 아니다! 성능이 문제가 될 때까지 기다리지 말고 지금 바로 성능 최적화 기술을 배우고 컴포넌트를 만들 때 사용해보자.

     


    useMemo

    useMemo는 매 번 render할 때마다 메모리가 많이 소모되는 값들을 계산하지 않고 functional components를 최적화하는데 도움을 준다. useMemo는 dependency 리스트를 생성하는 것을 도와주며, 그 중 하나가 변경되면 바로 값을 계산한다. 예를 들어 다음과 같은 컴포넌트를 생각해보자.

     

    // useMemo를 사용하지 않은 컴포넌트
    import React from "react";
    
    const computeValueFromProp = (prop) => {
      // 에너지가 많이 소모되는 계산이 일어남
    }
    
    const ComponentThatRendersOften = ({ prop1, prop2 }) => {
      const valueComputedFromProp1 = computeValueFromProp(prop1);
      
      return (
        <>
           <div >{valueComputedFromProp1}</div>
           <div >{prop2}</div>
        </>
      );
    };

     

    위 컴포넌트가 다시 render되는 순간에 valueComputedFromProp을 매 번 다시 계산할 것이다. 컴포넌트가 매우 작으면 상관없겠지만 만약 이 컴포넌트가 더 많은 props를 가진 큰 컴포넌트인데, state가 업데이트되었다고 생각해보자. prop이나 state가 바뀌는 순간 마다, 이 컴포넌트는 값들을 다시 계산하거나 다시 render할 것이다. 이 값들을 다시 계산할 필요가 없는 경우에도 굳이 다시 계산하는 것은 매우 비효율적이다. 이상적으로 valueComputedFromProp1prop1이 변경되었을 때만 다시 계산되어야 하고, prop2가 변경되었을 때는 계산되지 않아야 한다. 그럼 이제 useMemo를 통해 prop1이 변경되었을 때만 값을 계산하도록 바꿔보자.

     

     

    // useMemo를 사용한 예제
    import React, { useMemo } from "react";
    
    const computeValueFromProp = (prop) => {
      // 에너지가 많이 소모되는 계산이 일어남
    }
    
    const ComponentThatRendersOften = ({ prop1, prop2 }) => {
    
    const valueComputedFromProp1 = useMemo(() => {
      return computeValueFromProp(prop1)
    }, [prop1]);
    
    return (
        <>
           <div >{valueComputedFromProp1}</div>
           <div >{prop2}</div>
        </>
      );
    };

     

    이제 valueComputedFromProp1은 prop1이 변경될 때만 다시 계산된다. 만약에 prop2가 업데이트된 상태로 re-render가 실행되었다면 valueComputedFromProp1은 마지막으로 계산된 값을 사용하고 다시 계산하지 않는다. 

     

    useMemo는 2개의 인자를 가진다.

    1. 계산된 값을 return하는 callback 함수
    2. useMemo에게 언제 다시 계산해야할지 알려 줄 dependency 리스트 배열

    useMemo의 두 번째 인자로 넣은 [prop1]은 prop1이 변경되었을 때만 계산을 다시 하라고 알려주기 위한 배열이다. 만약 우리가 prop1뿐만 아니고 prop2가 변경되었을 때도 다시 계산하는 것이 필요하다면 배열은 [prop1, prop2]가 될 것이다. 만약에 최초의 mount 시에만 계산을 하고 싶다면 빈 배열 []을 넣으면 된다.

     

    useMemo는 복잡한 값들을 가지고 있고 자주 re-render되는 함수형 컴포넌트들이 아주 큰 성능 향상을 할 수 있도록 도와준다.

     


    useCallback

    useCallback은 useMemo와 거의 비슷한 개념이다. 유일하게 다른 점이라면, useCallback은 "값(value)"을 기억(memoizing)하는 대신에 "함수(function)"를 기억한다. 아래 예제를 살펴보자.

     

    import React, { useState } from 'react';
    
    const ComponentThatRendersOften = ({ cb1, cb2 }) => {
      const [state, setState] = useState(...);
      
      const expensiveFunction = () => {
        // 에너지가 많이 소모되는 계산이 일어남
        ...    
        setState(...);
        cb1();
      };
      
      return (
        <button onClick={expensiveFunction} />
      );
    };

     

    useMemo와 같이 위 컴포넌트는 re-render될 때마다 expensiveFunction을 다시 생성한다. 만약 많은 prop들을 가지고 있거나 state 변화가 많아 re-render가 자주 일어난다면 위와 같이 매 번 다시 함수를 생성하는 것이 에너지를 많이 소모하는 일일 것이다. 이 때 우리는 이 함수를 컴포넌트 스코프(scope, 범위) 밖으로 옮겨서 매 번 다시 렌더하지 않도록 만들 수도 있다. 그러나 그렇게 할 경우에는 prop들과 state, state setters들을 매 번 넘겨줘야 하기 때문에 매우 번거롭고 가독성이 떨어질 수 있다. 그렇다면 이제 이 함수를 useCallback을 사용하여 우리가 필요할 때만 다시 생성하도록 만들어보자.

     

     

    import React, { useState, useCallback } from 'react';
    
    const ComponentThatRendersOften = ({ cb1, cb2 }) => {
      const [state, setState] = useState(...);
    
      const expensiveFunction = useCallback(() => {
        // 에너지가 많이 소모되는 계산이 일어남
        setState(...);
        cb1();
      }, [cb1]);
    
      return (
        <button onClick={expensiveFunction} />
      );
    };

     

    이제 이 컴포넌트는 expensiveFunction을 cb1이 변경된 후 re-render될 때만 다시 생성한다. (setState는 절대 변경되지 않으므로 dependency 배열에 넣을 필요가 없다.)

     

    useCallback2개의 인자를 가진다.

    1. 기억된 채로 리턴된 callback 함수
    2. useCallback에게 언제 함수를 재생성할 지 알려 줄 dependency 리스트 배열

    useMemo와 동일하게 useCallback의 두 번째 인자로 넣은 배열 [cb1]은 cb1이 변경되었을 때만 다시 재 생성하라고 알려주기 위한 목적으로 사용되었다. 만약에 cb2와 state가 변경되었을 때도 재생성하고 싶다면 [cb1, cb2, state]가 될 것이다. 만약 첫 mount 시에만 생성하고 싶다면 빈 배열을 넣으면 된다.

     

    useCallback또한 useMemo처럼 자주 변경되어 많은 re-render를 일으키는 함수가 포함된 함수형 컴포넌트에 사용하면 매우 큰 성능 향상을 일으킬 수 있다.

     


    memo

    다른 memoization 기술들과는 논외로, memo는 아마 가장 개념화하고 이해하기 어렵지만 가장 중요한 기술 중 하나일 것이다. 간단하게 말하면 memo는 우리의 컴포넌트가 항상 기본적으로 re-render되는 것을 막는다. 즉, 컴포넌트 내부의 state나 prop들이 얕게(shallowly) 변경되었을 때만 re-render되도록 한다. 아래 예제를 살펴보자.

     

    import React, { useState } from 'react';
    
    const Text = ({ text }) => {
      return <p >{text}</p>
    };
    
    const ParentComponent = () => {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
      
      return (
        <>
          <input onChange={(e) => setFirstName(e.target.value)} />
          <input onChange={(e) => setLastName(e.target.value)} />
          <Text text='Your name is:' />
          <Text text={firstName} />
          <Text text={lastName} />
        </>
      );
    };

     

    기본적으로 ParentComponent가 업데이트되면 3개의 Text 컴포넌트들이 re-render된다. 사용자가 각 input 박스에 얼마나 많은 글자들을 입력할지 생각해보자. 매 번 input에 입력되는 text가 바뀔 때마다 Text 컴포넌트가 re-render될 것이다. 위 예제는 매우 간단하지만, 많은 child 컴포넌트를 가지는 큰 컴포넌트에서 얼마나 많은 성능 이슈가 야기할 지 쉽게 추측할 수 있다. 이제 memo 기술을 이용해 이 컴포넌트를 최적화시키고 모든 기본 default re-render를 막아보자.

     

     

    import React, { useState, memo } from 'react';
    
    // 컴포넌트를 memo로 감싼다.
    const Text = memo(({ text }) => {
      return <p >{text}</p>
    });
    
    const ParentComponent = () => {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
      
      return (
        <>
          <input onChange={(e) => setFirstName(e.target.value)} />
          <input onChange={(e) => setLastName(e.target.value)} />
          <Text text='Your name is:' />
          <Text text={firstName} />
          <Text text={lastName} />
        </>
      );
    };

     

    이제 firstName과 lastName이 변경될 때마다 그것과 연관된 각각의 Text 컴포넌트만 업데이트된다. memo로 감싸진 Text 컴포넌트는 해당 컴포넌트의 prop들이 얕게(shallowly) 변경되었을 때만 다시 render된다.

     

    만약 firstName과 관련된 input 값이 변경되면, 그 값을 prop으로 받는 두 번째 Text 컴포넌트만 업데이트된다. 컴포넌트가 다시 업데이트할 지 말 지를 결정할 때, Text 컴포넌트는 현재의 text prop과 새로운 text prop을 비교하여 prop이 동일하면 update하지 않는다. 때문에 첫 번째와 세 번째 Text 컴포넌트는 전과 같은 prop을 가지고 있어 업데이트가 되지 않는다.

     

    여기까지 memo가 그렇게 많이 복잡해보이지는 않는다. 단지 컴포넌트가 기본적으로 re-render하는 특성을 컴포넌트의 prop이 얕게 변경되었을 때만 re-render하도록 변경할 뿐이다. 이제 우리는 얕은(shallowly)이라는 단어가 강조되었다는 것을 알아차릴 수 있다. memo의 가장 큰 특징이다. 어떤 컴포넌트를 업데이트할 지 결정하기 위해 prop들을 비교할 때 memo는 prop들을 얕게 비교한다. prop value가 변화할 때, 그에 따라 memo가 우리의 컴포넌트를 다시 re-render 하는지 안하는지 살펴보자.

     

     

    // 이러한 prop 변화는 memo가 항상 유효한 re-render를 일으킨다.
    Before: 'hi'
    After:  'bye'
    
    Before: 0
    After:  1
    
    Before: [1, 2, 3]
    After:  [1, 2, 4]
    
    Before: { a: 'apple', p: 'peach' }
    After:  { a: 'apple', p: 'plum' }
    
    Before: undefined
    After: null
    
    // 이러한 prop 변화는 memo가 re-render를 일으킨다고 보장하지 않는다.
    Before: { person: { name: { first: 'henry' } }
    After:  { person: { name: { first: 'john' } } }
    
    Before: [ { greeting: 'hi' } ]
    After: [ { greeting: 'bye' } ]

     

    memo는 1 레벨 깊이의 얕은 비교를 통한 업데이트만 보장하기 때문에 만약 컴포넌트가 (1 레벨 이상의) 깊은 prop 값들을 가지고 있다면 memo를 사용하지 않는 것이 좋다. 만약 우리 컴포넌트가 깊은 prop 구조를 가지고 있다면 memoize보다는 다음 예제와 같은 다른 방법을 고려해볼 수 있다.

     

    import React, { memo } from 'react';
    
    const ComponentWithPotentialComplexProps = ({ complexProp }) => {
      return (...)
    };
    
    const MemoizedComponent = memo(ComponentWithPotentialComplexProps);
    
    const ParentComponent = ({ prop ) => {
      return (
        <>
          <MemoizedComponent
            complexProp={{ animal: 'cat' }}
          />
          <ComponentWithPotentialComplexProps
            complexProp={prop}
          />
        </>
      );
    };

     

    위 예제의 첫 번째 경우에서 우리는 하드 코딩된 prop({ animal: 'cat' })이 절대 값을 바꾸지 않을 것이라는 것을 알고 있다. 따라서 우리는 그 값이 깊은 비교할 필요가 없다는 것을 알기 때문에 안전하게 memoize할 수 있다. 그러나 두 번째 경우에는 어떤 prop이 전달될 지 모르고 그 prop이 몇 차수일 지 모르기 때문에 memoize를 사용할 수 없다.

     


    When to use each

    memoization은 언제 사용해야 할 지 판단하는 것이 가장 어렵다. 아래는 memoization이 좋은 선택일 지 판단하는 데 도움을 줄 간단한 체크리스트이다. when to use 경우를 대부분 만족하고 when not to use 경우에 해당하지 않는다면 memoization을 사용하는 것이 좋다.

     

    useMemo

    What it does:

    dependency들이 변경되었을 때만 값(value)을 다시 계산하도록 기억(memoize)한다.

     

    When to use it:

    • 모든 re-render 시가 아니라 특정 prop이나 state가 변경되었을 때만 값을 다시 계산하고 싶을 때
    • 당신의 값 계산 프로세스가 매우 에너지 소모가 많을 때 (regex's, json 파싱, 큰 배열의 순회 등)
    • 함수형 컴포넌트일 때

    When to NOT use it:

    • 계산하기 간단한 값들일 때 (useMemo는 가독성을 떨어뜨릴 수 있다.)
    • 클래스 컴포넌트일 때
    • 해당 함수를 간단하게 컴포넌트 scope 밖으로 옮길 수 있을 때 (prop callback을 불러일으키거나 state 업데이트와 관련 없을 때)

    useCallback

    What it does:

    dependency들이 변경되었을 때만 함수(function)를 다시 생성하도록 기억(memoize)한다.

     

    When to use it:

    • 모든 re-render 시가 아니라 특정 prop이나 state가 변경되었을 때만 함수를 다시 생성하고 싶을 때
    • 함수가 re-render 시 마다 다시 생성되기에 너무 비용 소모가 큰 경우에
    • 함수형 컴포넌트일 때

    When to NOT use it:

    • 매 re-render 시마다 다시 생성해도 비용 소모가 크지 않을 때
    • 클래스 컴포넌트일 때
    • state나 prop 변화와 상관없는 함수여서 컴포넌트 scope 밖에 선언해도 될 때 (또는 한 두개의 state/prop과 관련있어 컴포넌트 scope 밖에 선언하고 인자로 쉽게 넘겨줄 수 있을 때)

    memo

    What it does:

    함수형 컴포넌트를 감싸서 해당 컴포넌트의 prop이나 state가 얕게(shallowly) 변경되었을 때만 re-render하도록 만든다.

     

    When to use it:

    • prop이 변경되었을때만 re-render하고 싶을 때 (내부 state 업데이트 시에는 여전히 re-render가 일어남)
    • 플랫한 prop을 가지고 있을 때 (1 레벨 초과하는 복잡한 object prop일 경우 사용하지 말 것)
    • 컴포넌트가 리액트 트리에서 중위에서 상위 레벨인 경우에
    • 컴포넌트가 자주 re-render되는 경우
    • 함수형 컴포넌트일 때

    When to NOT use it:

    • 클래스 컴포넌트일 때
    • prop이 복잡한 object이고, 깊은 변화(deep change) 시 re-render되는 상황일 경우
    • state나 prop 변화와 상관없는 함수여서 컴포넌트 scope 밖에 선언해도 될 때 (또는 한 두개의 state/prop과 관련있어 컴포넌트 scope 밖에 선언하고 인자로 쉽게 넘겨줄 수 있을 때)

     

    위의 3가지 memoization 기술은 단지 빠르고 최적화된 React 어플리케이션을 개발하는 데 도움을 줄 뿐만 아니라 우리의 React 기술을 다음 레벨로 끌어올려주는 데도 도움을 준다. 최적화 기회를 찾아내는 일은 기초 레벨의 React 개발자들과 차별점이 될 수 있다. memoizing 기술을 사용하여 우리와 우리의 사용자들이 성능이 향상된 어플리케이션을 사용할 수 있도록 하자!

     

    반응형

    COMMENT