[React] 메모이제이션 Hook으로 중복연산 피하기 (useCallback, useMemo)

profile image pIutos 2023. 3. 14. 15:08

메모이제이션(Memoization)은 프로그램이 동일한 계산을 반복할 때, 이전에 계산한 값을 메모리에 저장함으로써 중복되는 연산을 제거해서 프로그램 실행 속도를 빠르게하는 기술입니다.

리액트 함수형 컴포넌트에서는 이러한 메모이제이션을 돕기 위한 두가지 Hook을 제공합니다.

메모이제이션을 하지 않을 때

함수형 컴포넌트에서 상태값이 변경되면 해당 컴포넌트는 다시 렌더링합니다. 이때 컴포넌트 내부에 정의한 함수들도 다시 생성되고 실행되기 때문에 메모이제이션이 필요합니다.

아래와 같은 경우 메모이제이션 훅 useCallback과 useMemo를 사용할 수 있습니다.

  1. 렌더링 마다 함수가 새로 생성되어 참조값이 변하는 경우
  2. 실행되는 함수가 복잡한 연산을 수행하는 경우

이외에도 훅을 사용하는 다양한 경우가 있지만, 두가지 경우를 예시로 메모이제이션 훅에 대해 설명하겠습니다.

useCallback

useCallback(fn, deps);

useCallback 함수는 콜백 함수를 메모이제이션하고 반환합니다.

import { useCallback } from 'react';

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

코드에서 useCallback함수는 doSomething함수를 반환합니다. 그리고 렌더링시 반환된 함수가 저장된 memoizedCallback 함수는 의존성 배열안에 들어있는 값 a, b의 상태가 변경되었을 때만 생성됩니다.

const Example = () => {
  const [a, setA] = useState(0);
  const fetchApi = () => {
    fetch('www.example.com/api')
      .then((response) => response.json())
      .then((data) => console.log(data));
  };
  
  useEffect(() => {
    fetchApi().then(data => setA(data));
  }, [fetchApi]);
  
  return(
    <div>
      <span>{a}</span>
    </div>
  );
}

API를 가져오는 함수 fetchApi()와 이 함수를 useEffect를 통해 실행하는 코드를 작성해보았습니다. 위 예시에서 useEffect를 통해 api를 가져오면 setA가 실행되어 컴포넌트 Example이 리렌더링됩니다.

이때 fetchApi 함수는 다시 생성되어 이름은 같지만, 자바스크립트에서 다른 메모리값을 가지기 때문에 새로운 참조값으로 변경됩니다.

따라서 useEffect는 fetchApi가 변경되었다고 판단하기 때문에 api를 다시 호출하고, 또 리렌더링이 발생하여 호출을 반복하는 무한루프가 발생하게됩니다.

const Example = () => {
  const [a, setA] = useState(0);
  const fetchApi = useCallback(() => {
    fetch('www.example.com/api')
      .then((response) => response.json())
      .then((data) => console.log(data));
  }, []);
  
  useEffect(() => {
    fetchApi().then(data => setA(data));
  }, [fetchApi]);
  
  return(
    <div>
      <span>{a}</span>
    </div>
  );
}

이 경우 위와같이 useCallback 훅을 이용하면 상태 a가 바뀌어 리렌더링이 되어도 fetchApi의 참조값이 변경되지 않기 때문에 함수 재호출, 무한루프가 발생하지 않게됩니다.

useMemo

useMemo(() => fn, deps);

useMemo 함수는 콜백 함수의 반환값을 메모이제이션하고 반환합니다.

import { useMemo } from 'react';

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

코드에서 computeExpensiveValue() 함수의 반환값이 memoizedValue에 저장되며, 렌더링시 a와 b의 상태가 변경되었을 때만 선언됩니다.

const Example = () => {
  const [a, setA] = useState('');
  const [b, setB] = useState(0);
  const expensiveTask = () => {
    let i = 0;
    while(i < 10000000) i++;
    return a;
  };
  
  return(
    <div>
      <span>
        {expensiveTask()}
      </span>
      <span>
        {b}
      </span>
      <button onClick={setB(prev => prev + 1)} />
    </div>  
  );
}

계산이 오래걸리는 함수가 렌더링때마다 수행되는 코드를 위와같이 작성해보았습니다. 버튼을 클릭하면 상태 b의 값이 변경되어 리렌더링되는데, 렌더링시에 expensiveTask 함수도 호출되기 때문에 연산이 끝날때까지 렌더링이 지연됩니다.

이 경우 아래와 같이 useMemo 훅으로 결과값을 메모이제이션 할 수 있습니다.

const Example = () => {
  const [a, setA] = useState('');
  const [b, setB] = useState(0);
  const expensiveTask = useMemo(() => {
    let i = 0;
    while(i < 10000000) i++;
    return a;
  }, [a]);
  
  return(
    <div>
      <span>
        {expensiveTask()}
      </span>
      <span>
        {b}
      </span>
      <button onClick={setB(prev => prev + 1)} />
    </div>  
  );
}

이처럼 useMemo 훅을 이용하면 상태 a의 값이 변할 때만 함수가 호출됩니다. 따라서 상태 b가 변경되더라도 렌더링이 지연되지 않습니다.

참고로 useMemo 훅으로 전달된 함수는 렌더링 중에 실행됩니다. 따라서 useMemo 내부에는 렌더링 시 수행할 작업만 작성해야 합니다. 렌더링 이후 작업을 작성하고 싶다면 useEffect를 사용해야 합니다.

정리

useCallback은 콜백 함수 자체를, useMemo는 콜백 함수의 반환값을 메모이제이션 한다는 점에서 차이가 존재합니다. 두 훅을 이용하여 

useCallback(fn, deps)은 useMemo(() => fn, deps)와 같습니다.

참고

https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98

https://ko.reactjs.org/docs/hooks-reference.html#usememo

https://thisblogfor.me/react/hooks_memoization/