한우의 개발일기

useRef() 파해치기 본문

React

useRef() 파해치기

한우코딩 2025. 3. 18. 14:45

https://github.com/Bookiwi-hub/flow/blob/deep-dive/apps/reader/src/hooks/useAsync.ts

위에 훅을 공부 하던중 비동기 처리를 ref 를 이용하여 위에 훅으로 사용할 수 있는 훅을 만든것을 보았는데 왜 ref를 사용하고 ref 의 동작원리를 잘 모르겠어서 ref에 대해서 공부해 보았습니다.

React의 useRef() 훅 내부 동작 분석

React의 훅 중 하나인 useRef()의 내부 동작 원리를 깊이 살펴보려고 합니다. React를 사용하다 보면 useRef()를 DOM 요소에 접근하거나 re-render 사이에 값을 유지하기 위해 자주 사용하게 되는데요, 이 훅이 어떻게 작동하는지 내부 메커니즘을 이해하면 더 효과적으로 활용할 수 있습니다.

useRef()는 무엇인가요?

간단히 말해 useRef().current 속성을 가진 변경 가능한 객체를 반환하는 훅입니다. 이 객체는 컴포넌트의 전체 생명주기 동안 유지되며, 컴포넌트가 재렌더링되어도 그 값이 보존됩니다.

가장 흔한 사용법은 다음과 같습니다

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

이 코드에서 두 가지 중요한 부분이 있습니다:

  1. useRef()를 호출하여 ref 객체를 생성하는 부분
  2. JSX에서 ref={myRef}와 같이 사용하여 DOM 요소와 연결하는 부분

이 두 가지 작업이 내부적으로 어떻게 이루어지는지 React의 소스 코드를 파헤쳐 봅시다.

useRef()의 내부 구현

React의 소스 코드를 살펴보면, useRef() 훅은 초기 렌더링과 업데이트(리렌더링) 단계에서 각각 다른 함수를 사용합니다.

초기 렌더링 시: mountRef()

function mountRef<T>(initialValue: T): {| current: T |} {
  const hook = mountWorkInProgressHook();
  const ref = { current: initialValue };
  hook.memoizedState = ref;
  return ref;
}
  1. { current: initialValue } 형태의 객체를 생성합니다
  2. 이 객체를 새로운 훅의 memoizedState에 저장합니다
  3. 생성한 객체를 반환합니다

여기서 mountWorkInProgressHook()은 React의 내부 함수로, 새로운 훅 인스턴스를 생성하고 컴포넌트의 파이버(fiber)에 연결하는 역할을 합니다.

리렌더링 시: updateRef()

function updateRef<T>(initialValue: T): {| current: T |} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}
  1. 현재 처리 중인 훅을 가져옵니다(updateWorkInProgressHook())
  2. 이전에 저장했던 ref 객체를 그대로 반환합니다

이것이 바로 useRef()가 컴포넌트의 리렌더링 사이에서도 동일한 객체 참조를 유지하는 방식입니다. 그저 기존 객체를 재사용합니다.

실제 DOM 요소와의 연결: ref={myRef}

이제 더 흥미로운 부분입니다. JSX에서 ref={myRef}와 같이 사용하면 React는 어떻게 실제 DOM 요소를 myRef.current에 연결할까요?

이 과정은 React의 렌더링 및 커밋 단계에서 복잡한 과정을 통해 이루어집니다.

1. Ref 변경 감지

먼저 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;
    // ...
  }
}

이 함수는 다음과 같은 경우에 파이버의 flagsRef 플래그를 설정합니다

  • 새로운 ref가 생성되었을 때
  • 기존 ref가 다른 ref로 변경되었을 때

이 플래그는 나중에 커밋(Commit Phase) 단계에서 ref를 처리해야 함을 React에게 알려주는 신호역할을 합니다.

2. Ref 연결 (Attaching)

커밋 단계에서는 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;
    }
  }
}

여기서 중요한 부분을 살펴보면

  1. DOM 요소(HostComponent)의 경우, getPublicInstance(instance)를 통해 실제 DOM 노드를 가져옵니다
  2. ref가 함수라면 해당 함수를 호출하고(콜백 ref), 객체라면 .current 속성에 DOM 노드를 할당합니다

이 과정이 리액트 내부를 살펴보면 useLayoutEffect 훅과 동일한 단계에서 실행되는데, 이는 ref가 DOM에 연결되는 시점이 useEffect보다 빠르다는 것입니다, 이 특성은 DOM 요소를 다루는 커스텀 훅을 만들 때 매우 유용합니다.

useEffect: 브라우저가 화면을 그린 후에 비동기적으로 실행됩니다.
useLayoutEffect: 브라우저가 화면을 그리기 전에 동기적으로 실행됩니다. (이 부분에 대해서는 학습후 포스팅을 하겠습니다)

3. Ref 분리 (Detaching)

컴포넌트가 언마운트되거나 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()를 더 효과적으로 활용할 수 있습니다

1. DOM 측정하기

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에서 수행하는 것이 더 안전합니다.

2. 이전 값 기억하기

컴포넌트의 이전 상태를 기억해야 할 때

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의 파이버 아키텍처와 밀접하게 연결되어 동작합니다. 이러한 내부 메커니즘을 이해하면 다음과 같은 이점이 있습니다:

  1. 성능 최적화 ref를 사용하여 불필요한 리렌더링을 방지할 수 있습니다.
  2. DOM 조작 시점 제어 DOM 요소에 접근하는 적절한 시점(useLayoutEffect vs useEffect)을 선택할 수 있습니다.
  3. 고급 패턴 구현 ref 기반의 커스텀 훅을 더 효과적으로 설계할 수 있습니다.

다만, ref를 사용할 때는 다음 사항에 주의해야 합니다

  • 직접적인 DOM 조작은 최소화하자 React의 선언적 패러다임을 벗어나는 작업이 될 수 있습니다.
  • ref.current 변경은 리렌더링을 트리거하지 않는다 상태 관리에는 useStateuseReducer를 사용하세요.
  • 초기 렌더링 시에는 ref.current가 null일 수 있다 항상 null 체크를 해주세요.

useRef()는 단순해 보이지만, 그 내부에는 복잡하네요

다음 포스팅에서는 useAysnc 훅이 ref를 통해서 어떻게 동작하는지 알아보겠습니다.

'React' 카테고리의 다른 글

useEffect vs useLayoutEffect  (0) 2025.03.21
useAsync 파해치기  (0) 2025.03.18
리액트의 초기 마운트  (0) 2025.02.13
리액트의 내부개요  (0) 2025.02.13
React-hook-form에서 watch 와 useWatch  (0) 2024.11.11