영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.
원문링크: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} />;
}
이제 여기서 두 가지 퍼즐을 풀어보겠습니다.
최초 렌더링과 리렌더링에서
useRef()
는 어떻게 작동할까요?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;
}
이것은 매우 간단합니다
current
프로퍼티를 가진 ref 객체를 만들고mountWorkInProgressHook()
으로 새로운 훅을 만들고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 요소로 설정하는 방법인데, 여기에는 두 가지 하위 질문이 포함되어 있습니다.
어떻게
ref
가 연결(attached)되는가어떻게
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. 요약
조정하는 동안, ref 변경/생성은
flags
의 파이버에 표시(mark)됩니다.커밋하는 동안, 리액트는
flags
를 확인하여 ref를 분리/연결 합니다.useRef()
는 ref 객체만 보유하는 간단한 훅입니다.