React - useMemo와 useCallback은 언제 사용할까?

치맨·2023년 6월 29일
1

React

목록 보기
7/11
post-thumbnail

목차

useMemo

useMemo란

memoization이란

컴퓨팅 에서 메모이제이션 또는 메모이제이션은 비용이 많이 드는 함수 호출 의 결과를 저장 하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램 속도를 높이는 데 주로 사용되는 최적화 기술 입니다.
위키피디아 참조

  • 이전에 계산한 값을 메모리에 저장하고, 그 값을 반복되는 연산에서 사용함으로써 중복되는 계산을 줄일 수 있습니다.

  • memoization에서 가장 흔하게 사용되는 예시는 피보나치 수열입니다.

  • 피보나치 수열을 구하는 재귀함수를 fib라고 하고, 5번째 값(fib(5))을 구해보겠습니다.

  • 아래의 사진과 같이 15번 호출되는걸 알 수 있습니다.

  • 그러나 fib(3)은 2번, fib(2)는 3번, fib(1)은 5번, fib(0)은 3번이 중복되어 계산이 됩니다. 총 15번의 계산 중 무려 11번을 중복해서 계산하게 됩니다.

  • 만약 이미 계산한 값을 재활용한다면 어떻게 될까요??

  • 아래의 사진과 같이 총 9번만 계산하면 됩니다. 만약 값이 5가 아닌 엄청 크게 된다면 시간복잡도가 엄청나게 증가하고, 성능적으로 문제가 발생할 수 있습니다.

  • 이처럼 하위 문제에 대한 정답을 계산했는지 확인해가며 하향식으로 문제를 자연스럽게 풀어나가는 방식을 메모이제이션(Memoization) 이라고 합니다.

useMemo 사용방법

  • 기본형태 : const cachedValue = useMemo(calculateValue, dependencies)

  • calculateValue : 캐시하려는 값을 계산하는 함수입니다. ()=>{} 형태
    순수해야 하고 인수를 사용하지 않아야 하며 모든 유형의 값을 반환해야 합니다. React는 초기 렌더링 중에 함수를 호출합니다.

  • dependencies : 어떤 값이 변경되었을 때 다시 연산해야 할지 알려주기 위한 배열을 넣어줍니다. 이를 dependencies array, 줄여서 deps라고 합니다.

useMemo 사용하기

useMemo 사용 전

  • Ex) 아래의 코드는 value1, value2 즉 2가지 state값이 있고, 각 값을 1씩 증가시키는 버튼이 각각 존재합니다.

  • 이때 버튼2를 클릭하면 value2라는 state값이 변화되고, state값이 변화함에 따라 re-rendering이 발생하고, re-rendering이 발생함에 따라 multiple()함수는 다시 연산이 됩니다.

import { useState } from 'react';
import MainPage from '../components/pages/MainPage';

const Average = () => {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);

  const handleClick1 = () => {
    setValue1((prev) => prev + 1);
    console.log('1번 버튼 클릭');
  };

  const handleClick2 = () => {
    setValue2((prev) => prev + 1);
    console.log('2번 버튼 클릭');
  };

  const multiple = () => {
    const mul = value1 * value1;
    console.log('복잡한 계산이 수행되었습니다', mul);
    return mul;
  };

  return (
    <MainPage>
      <div>
        <p>{multiple()}</p>
        <p>{value2}</p>
        <button type="button" onClick={handleClick1}>
          value1 +1
        </button>

        <button type="button" onClick={handleClick2}>
          value2 +1
        </button>
      </div>
    </MainPage>
  );
};

export default Average;
  • 아래의 영상과 같이 2번 버튼이 클릭이 되었는데도 복잡한 계산(multiple 함수)이 수행되는 걸 알 수 있습니다.

useMemo 사용 후

  • multiple 함수에 useMemo를 사용해보겠습니다.
const multiple = useMemo(() => {
    const sum = value1 * value1;
    console.log('복잡한 계산이 수행되었습니다', sum);
    return sum;
  }, [value1]);
  • 아래의 영상과 같이 2번 버튼이 클릭 될 때 복잡한 계산이 수행되지 않는 걸 확인할 수 있습니다.

useCallback

  • useCallback도 useMemo와 상당히 비슷한 함수입니다. 주로 렌더링 최적화에 사용하며 사용법도 아주 비슷합니다.

  • Ex) const cachedFn = useCallback(fn, dependencies)

  • 그러나 차이점은 useMemo는 값을 메모이제이션하고 useCallback은 함수 자체를 메모이제이션 합니다.


항상 useMemo와 useCallback을 사용하면 되는건가

  • 그렇다면 항상 useMemo 혹은 useCallback을 사용하면 될까? => 당연히 안됩니다.

  • 왜 why? 메모이제이션이라는게 특정 메모리에 값을 저장 하는것입니다. 즉 비용이 발생하는데 단순 연산처리가 메모이제이션으로 저장하는 비용보다 더 저렴한 경우, 메모이제이션 하는 경우가 더 낭비일 수 있기 때문입니다.

useCallback의 잘못된 사용

  • 아래의 두 코드에서 두번째 렌더시에 useCallback을 사용하지 않은 경우 dispense 함수가 garbage collected(메모리 공간을 없애고)가 되고, 새로운것이 생성됩니다.

  • 하지만 useCallback을 사용한 경우 원본 dispense 함수는 garbage collected 되지 않고, 메모리 관점에서 더 좋지 않은 결과를 얻게 되는 것입니다. 삭제되지 않고 계속 새로운 메모리에 공간을 만들기 때문

// useCallback 사용 O
const dispense = React.useCallback(candy => {
  setCandies(allCandies => allCandies.filter(c => c !== candy))
}, [])


// useCallback 사용 X 
const dispense = candy => {
  setCandies(allCandies => allCandies.filter(c => c !== candy));
};

그렇다면 언제 사용하는게 좋을까요

  1. 참조 동일성
  2. 계산적으로 비용이 많이 드는 계산

참조 동일성

  • 자바스크립트의 객체는 같은 값이라도 다른 참조를 하고 있기 때문에 {a}==={a} 의 결과는 false가 나타나게 됩니다.

  • 아래의 코드에서 useEffect는 이전 렌더링의 options의 참조값과 비교해 다르다면 useEffect의 콜백함수가 실행되는데, options 변수는 렌더링 될때마다 새로운 참조값을 가지게 되기 때문에 useEffect callback 함수는 bar나 baz가 변화할때마다 불리는 것이 아니라 매 렌더마다 불리게 되는것입니다

  function Foo({ bar, baz }) {
    const options = { bar, baz };

    React.useEffect(() => {
      buzz(options);
    }, [options]); 
    
    return <div>foobar</div>;
  }

  function Blub() {
    const bar = () => {};
    const baz = [1, 2, 3];
    return <Foo bar={bar} baz={baz} />;
  }
  • 이와같이 참조 동일성이 유지 되지 않는 object의 경우 useMemo(), useCallback()을 사용하여 불필요한 렌더링을 막을 수 있습니다.
function Foo({ bar, baz }) {
  
  React.useEffect(() => {
    const options = { bar, baz };
    buzz(options);
  }, [bar, baz]);
  
  return <div>foobar</div>;
}

function Blub() {
  const bar = React.useCallback(() => {}, []);
  const baz = React.useMemo(() => [1, 2, 3], []);
  return <Foo bar={bar} baz={baz} />;
}

계산적으로 비용이 많이 드는 계산

  • for문, while문과 같이 반복문이나 재귀함수를 사용하는 경우 연산하는 비용이 크기 때문에 이 경우 useCallback이나 useMemo를 사용하면 좋을 것 같습니다.
const calcValue = () => {
  let value = value1;
  for (let i = 0; i < 10000; i++) {
    for (let j = 0; j < 10000; j++) {
      value = value + i + j;
    }
  }
  return value;
};

const computedMemoValue = useMemo(calcValue, [value1]);

참조

profile
기본기가 탄탄한 개발자가 되자!

0개의 댓글