useCallback 의 closure 로 인한 메모리 누수 가능성

https://schiener.io/2024-03-03/react-closures

import { useState, useCallback } from "react";

class BigObject {
  public readonly data = new Uint8Array(1024 * 1024 * 10);
}

export const App = () => {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const bigData = new BigObject(); // 10MB of data

  const handleClickA = useCallback(() => {
    setCountA(countA + 1);
  }, [countA]);

  const handleClickB = useCallback(() => {
    setCountB(countB + 1);
  }, [countB]);

  // This only exists to demonstrate the problem
  const handleClickBoth = () => {
    handleClickA();
    handleClickB();
    console.log(bigData.data.length);
  };

  return (
    <div>
      <button onClick={handleClickA}>Increment A</button>
      <button onClick={handleClickB}>Increment B</button>
      <button onClick={handleClickBoth}>Increment Both</button>
      <p>
        A: {countA}, B: {countB}
      </p>
    </div>
  );
};

Initial Render

IncrementA 클릭 시

IncrementB 클릭 시

//... 반복

문제점

해결 방법

  1. 클로저 스코프를 최대한 작게 잡기 (클로저 주변의 함수 크기를 줄이기)

    • 컴포넌트를 더 작게 만들기
      • 스코프 내의 들어갈 변수의 수를 줄여준다.
    • 커스텀 훅을 만들기
      • 그러면 모든 콜백이 훅 함수 내의 스코프만 가질 수 있고, 주로 함수 인자만 스코프 내에 가진다는 것을 의미.
  2. 다른 클로저, 특히 메모이제이션된 클로저를 캡쳐하지 않기.

    • 서로 호출하는 함수를 만들면, 첫번째 useCallback 을 추가하는 순간, 컴포넌트 스코프 내에서 호출되는 모든 함수들이 메모이제이션 되는 연쇄반응이 일어난다.
  3. 필요하지 않을 때에는 메모이제이션하지 않기.

  4. 큰 객체에는 useRef 를 사용하기

    • 객체의 생명주기를 직접 관리하고, 적절히 정리해야함을 의미할 수 있다.