[번역] React useImperativeHandle() 은 어떻게 동작하나요?

Ted LeeTed Lee
4 min read

영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.
원문링크:
https://jser.dev/react/2021/12/25/how-does-useImperativeHandle-work


ℹ️React Internals Deep Dive 에피소드 12, 유튜브에서 제가 설명하는 것을 시청해주세요.

React@18.2.0기준, 최신 버전에서는 구현이 변경되었을 수 있습니다.

💬 역자 주석: Jser의 코멘트는 ❗❗로 표시 해뒀습니다.
그 외 주석은 리액트 소스 코드 자체의 주석입니다.
... 은 생략된 코드입니다.

useImperativeHandle()을 사용해본 적이 있나요? 내부적으로 어떻게 동작하는지 한번 알아보죠.

사용법

다음은 공식 사용 예시입니다.

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));
  return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);

위의 코드를 통해 이제 FancyInput에 ref를 첨부할 수 있습니다.

function App() {
  const ref = useRef();
  const focus = useCallback(() => {
    ref.current?.focus();
  }, []);
  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={focus} />
    </div>
  );
}

간단해 보이지만, 왜 이렇게 할까요?

ref.current만 업데이트하면 어떨까요?

ImperativeHandle()을 사용하는 대신 아래와 같이 ref.current만 업데이트하면 어떨까요?

function FancyInput(props, ref) {
  const inputRef = useRef();
  ref.current = () => ({
    focus: () => {
      inputRef.current.focus();
    },
  });
  return <input ref={inputRef} />;
}

사실 그냥 작동하지만 문제가 있습니다. FancyInput은 정리가 아닌 수락된 ref의 current만 설정합니다.

React 내부 심층 분석 11 - useRef()는 어떻게 작동할까요? 에서 설명한 것처럼, React는 요소에 연결된 ref를 자동으로 정리하지만 이제는 그렇지 않습니다.

렌더링 도중 ref가 변경되면 어떻게 하나요? 그러면 이전 ref가 여전히 ref를 보유하게 되므로 <FancyInput ref={inputRef} />를 사용하려면 정리해야 합니다.

이 문제를 어떻게 해결할 수 있을까요? 정리하는 데 도움이 될 수 있는 useEffect()가 있으므로 다음과 같은 방법을 시도해 볼 수 있습니다.

function FancyInput(props, ref) {
  const inputRef = useRef();
  useEffect(() => {
    ref.current = () => ({
      focus: () => {
        inputRef.current.focus();
      },
    });
    return () => {
      ref.current = null;
    };
  }, [ref]);
  return <input ref={inputRef} />;
}

하지만 잠깐만요, ref가 함수 ref가 아닌 RefObject인지 어떻게 알 수 있을까요? 그럼 확인해보겠습니다.

function FancyInput(props, ref) {
  const inputRef = useRef();
  useEffect(() => {
    if (typeof ref === "function") {
      ref({
        focus: () => {
          inputRef.current.focus();
        },
      });
    } else {
      ref.current = () => ({
        focus: () => {
          inputRef.current.focus();
        },
      });
    }
    return () => {
      if (typeof ref === "function") {
        ref(null);
      } else {
        ref.current = null;
      }
    };
  }, [ref]);
  return <input ref={inputRef} />;
}

이것은 실제로 useImperativeHandle()의 작동 방식과 매우 유사합니다. useImperativeHandle()이 레이아웃 이펙트라는 점을 제외하면, ref 설정은 useEffect()보다 빠른 useLayoutEffect()와 동일한 단계에서 이루어집니다.

유튜브에서 useLayoutEffect 에 대해 설명 하는 모습 보기

이제 소스 코드를 살펴봅시다

이펙트의 경우 마운트 및 업데이트가 있으며, useImperativeHandle()이 호출되는 시점에 따라 달라집니다.

이것은 mountImperativeHandle(),(원본 코드)의 단순화된 버전입니다.

function mountImperativeHandle<T>(
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    fiberFlags,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

또한 업데이트의 경우, 원본 코드

function updateImperativeHandle<T>(
  ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null
): void {
  // TODO: If deps are provided, should we skip comparing the ref itself?
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;
  return updateEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps
  );
}

다음 사항에 유의하세요.

  1. 내부적으로는 mountEffectImplupdateEffectImpl이 사용됩니다. useEffect()useLayoutEffect()여기여기에서 동일한 작업을 수행합니다.

  2. 두 번째 인수는 HookLayout으로 레이아웃 이펙트를 의미합니다.

퍼즐의 마지막 조각, imperativeHandleEffect()의 작동 방식입니다.(코드)

function imperativeHandleEffect<T>(
  create: () => T,
  ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void
) {
  if (typeof ref === "function") {
    const refCallback = ref;
    const inst = create();
    refCallback(inst);
    return () => {
      refCallback(null);
    };
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;
    const inst = create();
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}

완벽함을 위한 디테일을 제쳐두면, 실제로는 우리가 쓴 것과 매우 비슷해 보이죠?

마무리

useImperativeHandle() 은 마법이 아니며, 단지 ref 설정과 정리를 래핑할 뿐이며, 내부적으로는 useLayoutEffect()와 같은 단계에 있으므로 useEffect()보다 조금 더 빠릅니다.

(원본 게시일: 2021-12-25)

0
Subscribe to my newsletter

Read articles from Ted Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ted Lee
Ted Lee

Software engineer for web tech. Interested in sustainable growth as software engineer.