10th 코드 로그 · React Hooks: useAutoCallback

허정석·2025년 7월 22일

TIL

목록 보기
10/19
post-thumbnail

useAutoCallback

🖥️ 구현 코드

import type { AnyFunction } from "../types";
import { useCallback } from "./useCallback";
import { useRef } from "./useRef";

export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
  // 매 렌더링마다 latestFnRef.current 값이 업데이트됩니다.
  const latestFnRef = useRef(fn);
  latestFnRef.current = fn;

  // dispatcherRef: "latestFnRef을 여는 행위"를 하는 함수를 담을겁니다. 해당 함수는 자체는 변하지 않음.
  const dispatcherRef = useRef<(...args: any[]) => any>();

  // "latestFnRef을 여는 행위"를 정의합니다.
  // 이 함수는 매 렌더링마다 새로 생성됩니다.
  // 따라서 항상 최신 스코프의 `latestFnRef`를 참조합니다.
  const dispatcher = (...args: any[]) => {
    return latestFnRef.current(...args);
  };

  // 새로 생성된 dispatcher 함수를 dispatcherRef의 내용물로 업데이트.
  dispatcherRef.current = dispatcher;

  // autoCallback: 이 함수의 참조는 절대 변하면 안 됩니다.
  // autoCallback은 오직 "dispatcherRef를 열어서 그 안의 dispatcher를 실행"하는 것입니다.
  const autoCallback = useCallback((...args: any[]) => {
    // 이 함수가 호출될 때, 그 시점의 dispatcherRef를 열고,
    // 그 안에 있는 최신 dispatcher를 실행합니다.
    return dispatcherRef.current?.(...args);
  }, []); // 참조 고정

  return autoCallback as T;
};

📘 work-log

work-log


🤖 Gemini 설명문

함수 구현 시 고려사항

1. 항상 최신 상태를 반영한다.

  • 컴포넌트의 state나 props가 바뀌면, 콜백 함수 안의 로직도 그 최신 값을 사용

2. 함수의 참조(주소값)는 절대 변하면 안 된다.

  • const cb = useAutoCallback(...) 으로 만든 cb 변수는, 컴포넌트가 리렌더링되어도 항상 동일한 함수여야 한다.
  • 불필요한 재실행과 리렌더링 방지.

왜 그냥 useCallback으로는 안 되는가?

  • 최신 상태 반영: 의존성 배열에 값 적용 ➡️ 값이 바뀔 때마다 새로운 함수 생성
  • 참조 고정: 의존성 배열에 빈 값 적용 ➡️ 최초 렌더링 시점만을 기억

두 목표를 동시에 달성할 수 없음.

해결 방법

1. useRef 활용

  // 매 렌더링마다 latestFnRef.current 값이 업데이트됩니다.
  const latestFnRef = useRef(fn);
  latestFnRef.current = fn;

  // dispatcherRef: "latestFnRef을 여는 행위"를 하는 함수를 담을겁니다. 해당 함수는 자체는 변하지 않음.
  const dispatcherRef = useRef<(...args: any[]) => any>();

2. "latestFnRef을 여는 행위" 정의

  // "latestFnRef을 여는 행위"를 정의합니다.
  // 이 함수는 매 렌더링마다 새로 생성됩니다.
  // 따라서 항상 최신 스코프의 `latestFnRef`를 참조합니다.
  const dispatcher = (...args: any[]) => {
    return latestFnRef.current(...args);
  };

  // 새로 생성된 dispatcher 함수를 dispatcherRef의 내용물로 업데이트.
  dispatcherRef.current = dispatcher;

3. useCallback 활용


  // autoCallback: 이 함수의 참조는 절대 변하면 안 됩니다.
  // autoCallback은 오직 "dispatcherRef를 열어서 그 안의 dispatcher를 실행"하는 것입니다.
  const autoCallback = useCallback((...args: any[]) => {
    // 이 함수가 호출될 때, 그 시점의 dispatcherRef를 열고,
    // 그 안에 있는 최신 dispatcher를 실행합니다.
    return dispatcherRef.current?.(...args);
  }, []); // 참조 고정

  return autoCallback as T;
  

코드 논리 흐름

  1. 리렌더링 (count=1):
    • latestFnRef.current는 count가 1인 함수로 업데이트됩니다.
    • dispatcher라는 새로운 함수가 생성됩니다. 이 함수는 count가 1인 latestFnRef를 알고 있습니다.
    • dispatcherRef.current는 이 새로운 dispatcher 함수로 업데이트됩니다.
  2. autoCallback 호출:
    • autoCallback은 최초에 만들어진 그 함수입니다.
    • autoCallback이 dispatcherRef.current?.(...args)를 실행합니다.
    • 이 코드가 실행되는 "지금 이 시점"에 dispatcherRef 상자를 엽니다.
    • 상자 안에는 무엇이 있나요? 1번 단계에서 막 업데이트된, count가 1인 dispatcher 함수가 들어있습니다.
    • 그 dispatcher가 실행되고, 그 dispatcher는 다시 latestFnRef.current를 실행합니다.
    • 결과적으로 count가 1인 최신 함수가 실행됩니다.

ref 상자의 내용물은 매 렌더링마다 최신 함수로 교체되므로,
autoCallback은 언제 호출되든 항상 최신 로직을 실행할 수 있었던 것입니다.
dispatcher 함수를 useRef 초기값이 아닌,
매 렌더링마다 생성하고 ref.current직접 할당한다는 점입니다.

➡️ useEffect 없이 클로저 문제를 우회(오래된 클로저)

❓ 오래된 클로저(Stale Closure)란?
클로저(함수의 "기억의 가방")가 과거 시점의 변수 값을 계속 기억하고 있어서,
이미 변경된 최신 값을 참조하지 못하는 현상

  • 왜 "오래된(Stale)" 인가?
    count state는 이미 1, 2, 3... 으로 바뀌며 "신선한" 상태가 되었습니다.
    하지만 logCount의 클로저는 여전히 count가 0이었던 "오래된" 과거의 상태에 머물러 있습니다.
  • 왜 이런 일이 발생하는가?
    • useCallback이나 useEffect 같은 훅에서 의존성 배열([])을 비워두면,
      React는 성능 최적화를 위해 해당 함수나 효과를 최초 렌더링 시 단 한 번만 생성하고 재사용합니다.
    • 이때 함수가 생성되면서 최초 렌더링 시점의 state와 props를 클로저로 포획하게 되고,
      그 이후로는 업데이트된 값을 알 방법이 없어지는 것입니다.

0개의 댓글