React: 메모이제이션

Bora Im·2023년 9월 14일
0

메모이제이션(memoization)

  • 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술
  • 동적 계획법의 핵심이 되는 기술
  • 값비싼 함수 호출의 결과를 캐싱하고 동일한 입력이 다시 발생할 때 캐싱된 결과를 반환하는 프로그래밍 기술
  • 동일한 결과를 불필요하게 다시 계산하지 않고, 캐시된 결과를 반환
  • 리액트 메모이제이션 훅 : useMemo, useCallback
  • 이미 함수가 실행되어 연산된 값이 있다면, 함수를 다시 호출하지 않고 기존에 연산된 값을 재활용하는 방식

재귀를 이용한 피보나치 함수

// 총 6가지 경우의 n : f(5)~f(0)
// n이 2보다 작을 경우(0,1) 그대로 반환
let a = 0;
const fib = (n) => {
  a += 1;
  console.log(a +'번째 연산: n =', n);
  if (n < 2) return n
  return fib(n - 1) + fib(n - 2)
}
fib(5); // -> 5

fib(5)
fib(4) + fib(3)
fib(3) + fib(2) + fib(2) + fib(1)
fib(2) + fib(1) + fib(1) + fib(0) + fib(1) + fib(0) + 1
fib(1) + fib(0) + 1 + 1 + 0 + 1 + 0 + 1
1 + 0 + 1 + 1 + 0 + 1 + 0 + 1 = 5
// 

메모이제이션 피보나치 함수

// 이미 연산한 값은 배열에 각 단계의 값을 저장
// fib
let a = 0;
const memo = [0, 1];
const fib = (n) => {
  a += 1;
  console.log(a +'번째 연산: n =', n);

  if (memo[n] || n <= 1) return memo[n]

  const result = fib(n - 1) + fib(n - 2)
  memo[n] = result

  return result
}
fib(5); // -> 5
memo // -> (6) [0, 1, 1, 2, 3, 5]

fib(5)
fib(4)                            + fib(3) // memo[5] = 3 + 2 = 5 -> memo = [0, 1, 1, 2, 3, 5]
fib(3)                   + fib(2) + 2      // memo[4] = 2 + 1 = 3 -> memo = [0, 1, 1, 2, 3]
fib(2)          + fib(1) + 1      + 2      // memo[3] = 1 + 1 = 2 -> memo = [0, 1, 1, 2]
fib(1) + fib(0) + 1      + 1      + 2      // memo[2] = 1 + 0 = 1 -> memo = [0, 1, 1]

useMemo

메모이제이션된 값을 반환하는 리액트 훅
직전에 연산된 값이 있다면, 다시 연산을 하지 않고, 해당 값을 반환

useMemo(값을 연산하고 반환하는 함수, 의존성 배열)

  • 의존성이 변경되었을 때에만 메모이제이션된 값을 다시 계산한다.
  • 기존에 매 렌더링마다 실행되었던 복잡한 계산을 방지해준다. (배열이 없는 경우 매 렌더링 마다 새로운 값을 계산)
// 문제 상황
const [val1, setVal1] = useState(0)
const [val2, setVal2] = useState(0)
const val3 = val1 * val1
console.log('val3:', val3)
return (
  <>
    <div>val1: {val1}</div>
    <div>val2: {val2}</div>
    <div>val3: {val3}</div>
    <button onClick={() => setVal1(v => v+1)}>Add val1</button>
    <button onClick={() => setVal2(v => v+1)}>Add val2</button>
  </>
);

val3val1에만 의존하는데 val2가 변경됐을 때도 컴포넌트가 리렌더링 되어 연산이 재실행

// val3에 useMemo 적용
const val3 = useMemo(() => {
  console.log('val3:', val1 * val1)
  return val1 * val1
}, [val1])
  • 연산을 최소화 하고 효율적인 코드를 위해서 유용하다.
  • 계산에 대한 입력이 변경되지 않으면 업데이트되지 않는다.
  • 계산에 대한 입력이 일정하게 유지되는 경우에만 유용하다.
  • 결과에 액세스할 때마다 계속 연산을 수행해야 하기 때문에 잦은 변동이 있는 경우 오히려 성능이 악화될 수 있다.
  • 값을 재활용하기 위해 따로 메모리를 사용하기 때문에 불필요한 값까지 메모이제이션 해버리면 오히려 메모리를 낭비할 수 있다.

UseCallback

메모이제이션된 콜백 함수, 즉 이미 생성된 함수를 반환하는 리액트 훅
불필요한 렌더링을 방지하기 위해 참조의 동일성을 보장하거나, 자식 컴포넌트에 의존적인 콜백 함수를 전달할 때 유용

useCallback(생성된 함수를 반환하는 함수, 의존성 배열)

  • 첫번째 인자로 넘긴 함수를, 두번째 인자로 넘긴 의존성 배열내의 값이 변경되기 전까지 저장하고 재사용
  • 컴포넌트가 다시 렌더링 되더라도, 해당 함수가 의존하고 있는 값들이 바뀌지 않는다면 함수를 새로 생성하지 않고 기존 함수를 계속 반환
  • 함수와는 상관없는 상태 값이 변할 때, 함수 컴포넌트에서 불필요하게 함수를 업데이트하는 것을 방지

함수 동등성

  • 자바스크립트에서 함수는 객체로 취급이 되기때문에, 함수를 동일하게 만들어도 메모리 주소가 다르면 다른 함수로 간주한다.
  • 특정 함수를 다른 함수의 인자로 넘기거나, 자식 컴포넌트의 props로 넘길 때 함수의 참조가 달라서 예상하지 못한 성능 문제가 생길 수 있다.
  • 이때, useCallback을 이용해 함수를 특정 조건이 변경되지 않는 이상 재생성하지 못하게 제한하여 함수 동등성을 보장할 수 있다.
// 문제 상황
const [data, setData] = useState(null);

const fetchData = () =>
  fetch("https://jsonplaceholder.typicode.com/todos/${id}")
    .then((response) => response.json())
    .then((data) => setData(data));

useEffect(() => {
  fetchData();
}, [fetchData]);

컴포넌트가 렌더링 될 때마다 fetchData 함수가 새로운 참조값으로 변경
fetchData 함수 변경 useEffect -> 재렌더링 -> 무한루프

// useCallback 적용
const fetchData = useCallback(
  () =>
    fetch("https://jsonplaceholder.typicode.com/todos/${id}")
      .then((response) => response.json())
      .then((data) => setData(data)),
  [id]
);

컴포넌트가 다시 렌더링 되더라도 fetchData 참조값 동일하게 유지
useEffect에 의존성 배열 값에 있는 fetchData 함수는 id 값이 변경되지 않는 한, 재호출되지 않는다.


사용하지 말아야 할 경우

  • 의존성 배열에 완전히 새로운 객체나 배열을 전달

useMemo

  • DOM에서 다른 컴포넌트를 렌더링하지 않는 컴포넌트

useCallback

  • 연산이 복잡하지 않은 함수, 간단한 일반 함수
  • 단순히 함수 내부에서 setStatedispatch 함수 등을 호출하는 경우

사용해야 하는 경우

  • useMemo는 계산 집약적인 작업에 가장 적합하며, useCallback은 의존성이 거의 없는 가벼운 기능에 가장 적합하다.
  • 자식 컴포넌트에서 useEffect가 반복적으로 트리거 되거나, 무한 루프에 빠질 위험이 있을 때

useMemo

  • 연산 혹은 처리량이 매우 많아서 렌더링의 문제가 되는 경우
  • 사용자의 입력값이 map 혹은 filter 등을 사용하여 이후 렌더링에서도 동일한 참조를 사용할 가능성이 높을 경우
  • 리액트 상위 트리에서, 부모가 리렌더링 될 때 자식 컴포넌트까지의 렌더링 전파를 막고 싶을 때
    ㄴ자식 컴포넌트가 메모이제이션 컴포넌트일 경우, 메모이제이션된 props를 사용해 필요한 부분만 리렌더링 할 수 있다.
  • ref 함수를 부수작용(side effect)와 함께 전달하거나, ref로 wrapper 함수를 만들 때

useCallback

  • 자식 컴포넌트에 함수를 props로 넘길 때, 불필요한 렌더링이 일어나는 경우
  • 외부에서 값을 가져오는 api를 호출하는 경우
  • 함수 자체가 매우 복잡하거나, 다시 계산하는데 비용이 많이 드는 경우

cf. React.memo

HOC(Higher-Order Components): 컴포넌트를 인자로 받아 새로운 컴포넌트를 다시 반환해주는 함수
일반 컴포넌트는 인자로 받은 props를 UI에 활용하는 반면,
인자로 받은 컴포넌트를 새로운 별도의 컴포넌트로 만든다.
const MyComponent = React.memo((props) => { return (컴포넌트 렌더링 코드) });

  • 컴포넌트가 같은 props를 받을 때 같은 결과를 렌더링한다면 React.memo를 사용하여 불필요한 컴포넌트 렌더링을 방지
  • 컴포넌트에 같은 props가 들어온다면 리액트는 컴포넌트 렌더링 과정을 스킵하고 마지막에 렌더링된 결과를 재사용
  • props로 들어온 object가 같은 값을 'reference(참조)'하고 있는지 얕은 비교(shallow compare)

React.memo는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo는 hook이기 때문에 오직 함수형 컴포넌트 안에서만 사용 가능

참고자료

useMemo와 useCallback는 왜, 언제 사용할까? | Hayeon Dev Blog
useMemo와 useCallback의 차이
https://velog.io/@vvsogi/React-useMemo-useCallback의-사용-이유와-사용법
React.memo와 useMemo 차이점
https://velog.io/@uoayop/useCallback-useMemo-함수와-연산된-값-재사용

0개의 댓글

관련 채용 정보