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

Ted LeeTed Lee
4 min read

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


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

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

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

전 당신이 useRef()를 꽤 많이 사용하셨을거라고 생각하는데요, 그 내부를 알아봅시다.

1. useRef() 소개

useRef()로 ref를 생성한 다음 프로그래밍 방식으로 .current 속성을 설정하거나 DOM 요소에서 ref 프로퍼티로 사용합니다.

function Component() {
  const ref = useRef(null);
  return <div ref={ref} />;
}

이제 여기서 두 가지 퍼즐을 풀어보겠습니다.

  1. 최초 렌더링과 리렌더링에서 useRef()는 어떻게 작동할까요?

  2. ref={ref}는 어떻게 작동할까요?

2. useRef()는 어떻게 동작할까요?

앞에서 다룬 것처럼 최초 렌더링과 리렌더링에 사용되는 mountRef()updateRef()를 직접 조사해 봅시다.

2.1 mountRef()

function mountRef<T>(initialValue: T): {| current: T |} {
  const hook = mountWorkInProgressHook();
  const ref = { current: initialValue };
  hook.memoizedState = ref;
  return ref;
}

이것은 매우 간단합니다

  1. current 프로퍼티를 가진 ref 객체를 만들고

  2. mountWorkInProgressHook()으로 새로운 훅을 만들고

  3. ref를 위해 memoizedState에 ref를 설정해줍니다.

memoizedState의 네이밍은 무시해도 됩니다. 이는 state 따위를 보관하기 위한 훅의 내부 이름일 뿐입니다.

mountRef()는 매우 간단합니다. updateRef()의 경우 다시 ref 객체를 반환하면 되겠죠? 계속 진행하겠습니다.

2.2 updateRef()

function updateRef<T>(initialValue: T): {| current: T |} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

맞습니다, 너무 간단하죠.

이전 동영상들에서 언급했듯이 updateWorkInProgressHook() 은 내부 커서를 사용하여 파이버의 훅 목록을 살펴보는 것입니다. 최초 렌더링의 경우 목록이 비어 있으므로 매번 새로운 훅을 생성합니다. 리렌더링의 경우 이미 훅이 있으므로 해당 훅을 사용합니다.

3. ref={ref}는 어떻게 동작하나요?

useRef() 는 매우 간단한데, 프로그래밍 방식으로 current를 설정하는 것은 해당 프로퍼티의 값만 변경하는 것이므로, useState() 와는 달리 업데이트를 트리거하지 않습니다.

더 흥미로운 질문은 DOM 요소로 설정하는 방법인데, 여기에는 두 가지 하위 질문이 포함되어 있습니다.

  1. 어떻게 ref가 연결(attached)되는가

  2. 어떻게 ref가 분리(detached)되는가

3.1 어떻게 ref가 연결(attached)되나요?

제가 어떻게 알아냈는지에 대한 자세한 내용은 제 동영상을 참조하실 수 있으며, 여기서는 중요한 부분만 나열했습니다.

커밋 단계에서, commitAttachRef()commitLayoutEffectOnFiber() 내부에서 호출됩니다.

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    // Moved outside to ensure DCE works with this flag
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === "function") {
      let retVal;
      retVal = ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}

HostComponent인 DOM 요소의 경우 여기에서 DOM 노드가 설정되어 있는 것을 볼 수 있습니다. 또한 콜백 ref 또는 ref 객체를 허용합니다.

이는 ref의 연결(attaching)이 레이아웃 이펙트와 같은 단계에서 발생한다는 것을 의미하며, 여기에 콜백 ref가 사용된다면 useEffect보다 더 빨리 연결될 수 있으며, 이는 React 고급 패턴 - Ref 를 통한 재사용 가능한 동작 훅을 이해하는 데 도움이 될 수 있습니다.

3.2 어떻게 ref가 분리(detached) 되나요?

이 함수 아래에는 분리 함수가 있습니다.

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === "function") {
      currentRef(null);
    } else {
      currentRef.current = null;
    }
  }
}

콜백 ref 또는 ref 객체도 지원하는 것을 볼 수 있습니다. commitDetachRef()를 검색해보면, 일관성을 유지하기 위해 먼저 분리해야 하므로 commitLayoutEffectOnFiber()보다 훨씬 빠른 commitMutationEffectsOnFiber()에서 발생하는 것을 볼 수 있습니다.

3.3 React는 flags를 통해 연결 또는 분리의 필요 여부를 알 수 있습니다.

위의 함수들이 호출되기 전에 몇 가지 확인할 사항들이 있습니다.

if (finishedWork.flags & Ref) {
  commitAttachRef(finishedWork);
}
if (flags & Ref) {
  const current = finishedWork.alternate;
  if (current !== null) {
    commitDetachRef(current);
  }
}

이들은 동일한 플래그 Ref를 사용하며, Ref 플래그가 설정되어 있으면 ref가 변경되었음을 의미하며, 분리를 위해 current를 확인하고 null이 아닌 경우 이전 파이버가 더 이상 사용되지 않으므로 이전 파이버의 ref를 분리합니다.

Ref는 언제 설정되나요? markRef()(소스)에서 수행됩니다.

function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    // Schedule a Ref effect
    workInProgress.flags |= Ref;
    if (enableSuspenseLayoutEffectSemantics) {
      workInProgress.flags |= RefStatic;
    }
  }
}

ref 생성 및 ref 변경을 확인하는 것을 볼 수 있습니다.

markRef()는 조정(reconciliation) 내부에 있는 updateHostComponent()에서 호출됩니다.(소스)

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {
  pushHostContext(workInProgress);
  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress);
  }
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  let nextChildren = nextProps.children;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);
  if (isDirectTextChild) {
    // We special case a direct text child of a host node. This is a common
    // case. We won't handle it as a reified child. We will instead handle
    // this in the host environment that also has access to this prop. That
    // avoids allocating another HostText fiber and traversing it.
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // If we're switching from a direct text child to a normal child, or to
    // empty, we need to schedule the text content to be reset.
    workInProgress.flags |= ContentReset;
  }
  markRef(current, workInProgress); // ❗❗
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

좋습니다, 이제 refs에서 무슨 일이 일어나는지 알 수 있습니다.

4. 요약

  1. 조정하는 동안, ref 변경/생성은 flags의 파이버에 표시(mark)됩니다.

  2. 커밋하는 동안, 리액트는 flags를 확인하여 ref를 분리/연결 합니다.

  3. useRef() 는 ref 객체만 보유하는 간단한 훅입니다.

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

I love TDD. Tool Driven Developement. Software engineer for web tech. I'm interested in how tech work.