https://github.com/Bookiwi-hub/flow/blob/deep-dive/apps/reader/src/hooks/useAsync.ts
위에 훅을 공부 하던중 비동기 처리를 ref 를 이용하여 위에 훅으로 사용할 수 있는 훅을 만든것을 보았는데 왜 ref를 사용하고 ref 의 동작원리를 잘 모르겠어서 ref에 대해서 공부해 보았습니다.
React의 훅 중 하나인 useRef()의 내부 동작 원리를 깊이 살펴보려고 합니다. React를 사용하다 보면 useRef()를 DOM 요소에 접근하거나 re-render 사이에 값을 유지하기 위해 자주 사용하게 되는데요, 이 훅이 어떻게 작동하는지 내부 메커니즘을 이해하면 더 효과적으로 활용할 수 있습니다.
간단히 말해 useRef()는 .current 속성을 가진 변경 가능한 객체를 반환하는 훅입니다. 이 객체는 컴포넌트의 전체 생명주기 동안 유지되며, 컴포넌트가 재렌더링되어도 그 값이 보존됩니다.
가장 흔한 사용법은 다음과 같습니다
function MyComponent() {
const myRef = useRef(null);
return <div ref={myRef} />;
}
이 코드에서 두 가지 중요한 부분이 있습니다:
1. useRef()를 호출하여 ref 객체를 생성하는 부분
2. JSX에서 ref={myRef}와 같이 사용하여 DOM 요소와 연결하는 부분
이 두 가지 작업이 내부적으로 어떻게 이루어지는지 React의 소스 코드를 파헤쳐 봅시다.
React의 소스 코드를 살펴보면, useRef() 훅은 초기 렌더링과 업데이트(리렌더링) 단계에서 각각 다른 함수를 사용합니다.
function mountRef<T>(initialValue: T): {| current: T |} {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}
{ current: initialValue } 형태의 객체를 생성합니다memoizedState에 저장합니다여기서 mountWorkInProgressHook()은 React의 내부 함수로, 새로운 훅 인스턴스를 생성하고 컴포넌트의 파이버(fiber)에 연결하는 역할을 합니다.
function updateRef<T>(initialValue: T): {| current: T |} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
updateWorkInProgressHook())이것이 바로 useRef()가 컴포넌트의 리렌더링 사이에서도 동일한 객체 참조를 유지하는 방식입니다. 그저 기존 객체를 재사용합니다.
이제 더 흥미로운 부분입니다. JSX에서 ref={myRef}와 같이 사용하면 React는 어떻게 실제 DOM 요소를 myRef.current에 연결할까요?
이 과정은 React의 렌더링 및 커밋 단계에서 복잡한 과정을 통해 이루어집니다.
먼저 React는 ref가 변경되었는지 확인하는 markRef() 함수를 사용합니다:
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
workInProgress.flags |= Ref;
// ...
}
}
이 함수는 다음과 같은 경우에 파이버의 flags에 Ref 플래그를 설정합니다
이 플래그는 나중에 커밋(Commit Phase) 단계에서 ref를 처리해야 함을 React에게 알려주는 신호역할을 합니다.
커밋 단계에서는 commitAttachRef() 함수가 호출됩니다
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;
// ...
}
if (typeof ref === "function") {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
여기서 중요한 부분을 살펴보면
getPublicInstance(instance)를 통해 실제 DOM 노드를 가져옵니다.current 속성에 DOM 노드를 할당합니다이 과정이 리액트 내부를 살펴보면 useLayoutEffect 훅과 동일한 단계에서 실행되는데, 이는 ref가 DOM에 연결되는 시점이 useEffect보다 빠르다는 것입니다, 이 특성은 DOM 요소를 다루는 커스텀 훅을 만들 때 매우 유용합니다.
useEffect: 브라우저가 화면을 그린 후에 비동기적으로 실행됩니다.
useLayoutEffect: 브라우저가 화면을 그리기 전에 동기적으로 실행됩니다. (이 부분에 대해서는 학습후 포스팅을 하겠습니다)
컴포넌트가 언마운트되거나 ref가 변경될 때는 기존 ref와 DOM 요소의 연결을 해제해야 합니다
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === "function") {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
이 함수는 간단하게
1. ref가 함수라면 null을 인자로 호출합니다
2. ref가 객체라면 .current 속성을 null로 설정합니다
이런 내부 동작 원리를 이해하면 useRef()를 더 효과적으로 활용할 수 있습니다
DOM 요소의 크기나 위치를 측정해야 할 때
function MeasureExample() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<>
<div ref={divRef}>측정할 요소</div>
<p>너비: {dimensions.width}px, 높이: {dimensions.height}px</p>
</>
);
}
여기서 useLayoutEffect를 사용한 이유는 ref.current가 DOM 요소에 연결되는 시점이 useLayoutEffect 실행 시점과 동일하기 때문입니다. 따라서 DOM 측정 작업은 useEffect보다 useLayoutEffect에서 수행하는 것이 더 안전합니다.
컴포넌트의 이전 상태를 기억해야 할 때
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 사용 예시
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>현재: {count}, 이전: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
이 패턴은 useRef()가 리렌더링 사이에 값을 유지하는 특성을 활용한 것입니다.
React의 useRef() 훅은 겉으로 보기에 단순하지만, 내부적으로는 React의 파이버 아키텍처와 밀접하게 연결되어 동작합니다. 이러한 내부 메커니즘을 이해하면 다음과 같은 이점이 있습니다:
다만, ref를 사용할 때는 다음 사항에 주의해야 합니다
useState나 useReducer를 사용하세요.useRef()는 단순해 보이지만, 그 내부에는 복잡하네요
다음 포스팅에서는 useAysnc 훅이 ref를 통해서 어떻게 동작하는지 알아보겠습니다.