useRef의 내부 동작 원리

우혁·2024년 9월 3일
18

React

목록 보기
6/19

useRef란?

리액트의 훅 중 하나로, 함수형 컴포넌트에서 DOM 요소나 값을 참조하기 위해 사용된다.

주요 기능

1. DOM 요소 접근: 특정 DOM 요소에 대한 참조를 생성할 수 있다. 이를 통핸 컴포넌트가 렌더링된 후 해당 요소에 직접 접근할 수 있다.

2. 값 저장: 일반적인 상태와 다르게 useRef()로 저장된 값은 컴포넌트가 리렌더링 되어도 유지가 되고, 값이 업데이트되어도 리렌더링을 일으키지 않는다.


useRef의 내부 동작 원리

최초 렌더링, mountRef

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

1. mountWorkInProgressHook()으로 새로운 훅 생성

2. current 프로퍼티를 가진 ref 객체 생성, 초기 값은 initialValue로 할당

3. hook.memoizedStateref를 할당하여, 해당 훅이 리렌더링될 때 이 상태를 유지하도록 한다. 이는 리액트의 내부 훅 상태 관리 방식이다.

리렌더링, updateRef

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

1. updateWorkInProgressHook 현재 진행 중인 렌더링 과정에서 어떤 훅이 호출되고 있는지를 추적한다. 이를 통해 각 훅이 자신의 상태를 올바르게 업데이트하고 유지할 수 있다.

2. hook.memoizedState를 반환한다. 여기서 memoizedState는 이전에 저장된 ref 객체이다.


어떻게 ref가 연결되는지

// finishedWork는 현재 처리 중인 컴포넌트의 정보를 담고 있는 Fiber 객체이다.
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) { // ref가 null이 아닌 경우에만 실행
    const instance = finishedWork.stateNode; // 컴포넌트의 인스턴스 가져오기
    let instanceToUse;
    
    // 태그에 따라 어떤 인스턴스를 사용할 지 결정
    switch (finishedWork.tag) { 
      case HostHoistable:
      case HostSingleton:
      // HostComponent는 실제 DOM 요소를 나타내는 컴포넌트이다.
      // <div>, <span> 같은 HTML 요소
      case HostComponent: 
        // 공개 인스턴스 가져오기
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        // 원래 인스턴스 사용
        instanceToUse = instance;
    }

    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    
    if (typeof ref === 'function') {
      if (shouldProfile(finishedWork)) {
        try {
          startLayoutEffectTimer();
          finishedWork.refCleanup = ref(instanceToUse);
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        finishedWork.refCleanup = ref(instanceToUse);
      }
    } else {
      ref.current = instanceToUse;
    }
  }
}

commitAttachRef()함수는 리액트의 내부에서 컴포넌트가 커밋될 때 참조를 설정하는 역할이다.

1. ref 가져오기

  • 현재 컴포넌트의 ref를 가져온다. 만약 refnull이라면 다음 단계를 진행한다.

2. 인스턴스 결정

  • 현재 컴포넌트의 인스턴스를 가져온다. 이 인스턴스는 stateNode라는 속성에 저장되어 있다.

3. DOM 노드 설정

  • 만약 현재 컴포넌트가 HostComponents라면, getPublicInstance를 통해 DOM 노드를 가져온다. 이는 실제 DOM 요소에 대한 참조를 설정하는 과정이다.

4. 콜백 ref

  • ref가 함수일 경우, 함수를 호출하여 인스턴스를 설정한다. 이러한 경우에 리액트는 이 참조를 렌더링 후 DOM이 업데이트된 직후에 설정하며, 이는 useLayoutEffect가 실행되기 전에 발생한다.

즉 컴포넌트가 화면에 렌더링되기 전에 참조가 설정되므로, useLayoutEffectuseEffect보다 더 빠르게 접근할 수 있다.

  • 콜백 ref의 예시 코드
function InputComponent(){
  // 콜백 ref 함수
  const setRef = useCallback((element: HTMLInputElement) => {
    if (element) {
	   element.focus(); // 인풋 포커스
    } 
  }, []);

  return (
    <input
      type="text"
      ref={setRef} // 콜백 ref 사용
      placeholder="인풋 포커스 테스트"
    />
  );
}
  • 콜백 refuseEffect 비교

콘솔 창을 확인해보면 콜백 ref 포커스 시점이 더 빠르고 useEffect가 늦게 포커스 이벤트를 주어서 useEffect쪽 인풋에 포커스가 되어있는 것을 볼 수 있다.

💡 기본 인스턴스(Default Instance)와 공개 인스턴스(Public Instance)의 차이

기본 인스턴스(Default Instance)

  • 리액트 컴포넌트가 생성될 때 내부에서 사용되는 인스턴스를 말한다.
    이 인스턴스는 리액트의 생명주기 메서드와 상태 관리 등을 포함하고 있다.

  • 리액트의 내부 로직에서 사용되며, 일반적으로 사용자 코드에서는 직접 접근하지 않는다.

공개 인스턴스(Public Instance)

  • 사용자가 직접 접근할 수 있는 인스턴스이다. 즉, 컴포넌트의 메서드나 속성에 접근할 수 있는 방법을 제공한다.

  • 주로 DOM 요소에 대한 참조를 제공하는데, 이는 다른 라이브러리나 코드에서 해당 DOM 요소를 조작하거나 상태를 확인할 수 있게 해준다.


어떻게 ref가 분리되는지

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref; // 현재 Fiber 노드의 ref를 가져온다.
  if (currentRef !== null) {
    if (typeof currentRef === "function") {
      currentRef(null);
    } else {
      currentRef.current = null;
    }
  }
}
  • 함수형 ref 처리: currentRef가 함수인 경우, 리액트는 이 함수를 호출하여 null을 전달한다.

  • 객체형 ref 처리: currentRef가 객체인 경우, 이 객체의 current 속성을 null로 설정하여 참조를 제거한다.
    이는 클래스형 컴포넌트나 useRef 훅을 사용할 때의 일반적인 처리 방식이다.

이 함수는 리액트의 렌더링 과정에서 컴포넌트가 언마운트되거나 업데이트될 때, 해당 컴포넌트의 참조를 안전하게 제거하는 역할을 한다.

💡 ref가 언마운트 전에 null로 재설정되는 이유

  • ref가 계속 DOM 요소를 참조하고 있으면, 해당 요소가 DOM에서 제거되어도 메모리에서 해제되지 않을 수 있다.
  • 컴포넌트가 언마운트된 후에도 ref가 이전 DOM 요소를 가리키고 있다면, 이는 잘못된 상태를 나타낸다.

flags를 통한 연결 또는 분리의 필요 여부 확인

💡 위 연결, 분리 함수들은 호출되기 전에 flags를 통해 필요 여부를 확인한다.

if (finishedWork.flags & Ref) { // 1. 플래그 검사
  commitAttachRef(finishedWork); // 2. 참조 연결
}
if (flags & Ref) { // 3. 현재 작업의 플래그 검사
  const current = finishedWork.alternate; // 4. 이전 상태 가져오기
  if (current !== null) { // 
    commitDetachRef(current); // 5. 참조 분리
  }
}

1. 플래그 검사

  • finishedWork 객체의 flags 속성에 Ref 플래그가 설정되어 있는지를 검사한다.
    Ref 플래그가 설정되어 있다면, 해당 컴포넌트에 참조가 있다는 것을 의미한다.

2. 참조 연결

  • 만약 Ref 플래그가 설정되어 있다면, finishedWork에 대한 참조를 추가한다.
    이 함수는 finishedWorkref를 적절히 설정하여 컴포넌트가 마운트될 때 참조를 연결한다.

3. 현재 작업의 플래그 검사

  • 현재 작업의 플래그에 Ref가 있는지 검사한다. 현재 컴포넌트의 상태가 변경되었음을 나타낸다.

4. 이전 상태 가져오기

  • finishedWork의 이전 상태를 가져온다(이전 Fiber 노드를 참조). 이 노드는 주로 현재 상태와 비교하기 위해 사용된다.

5. 참조 분리

  • 이전 상태에 대한 참조를 안전하게 제거하여 메모리 누수를 방지한다.

ref 생성 및 변경 확인

// current: 현재 Fiber 노드, 이전 상태를 나타낸다.
// workInProgress: 현재 작업 중인 Fiber 노드
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;
    }
  }
}

markRef()는 조정(reconciliation)단계 내부에 있는updateHostComponent()에서 호출된다.

  • 첫 번째 조건: currentnull이고 refnull이 아닐 때
    ↪ 이전 상태가 없고 현재 상태에 ref가 새로 추가된 경우

  • 두 번째 조건: currentnull이 아니고 이전 상태의 ref와 현재 상태 ref가 다를 때
    ↪ ref가 변경된 경우

이 두가지 조건 중 하나라도 참인 경우 workInProgressflagsRef 플래그를 설정한다.

이는 이후에 참조 효과를 예약하기 위해 필요한 작업이다.

여기서 참조 효과는 리액트에서 ref가 생성되거나 변경될 때 발생하는 작업을 의미한다.


정리하기

  • useRef()ref 객체만 보유하는 간단한 훅이다.

  • 조정하는 동안, ref의 생성 및 변경은 flags의 Fiber에 표시(mark)된다. ➔ markRef()

  • 커밋하는 동안, 리액트는 flags를 확인하여 ref를 연결 및 분리 작업을 실행한다.
    commitAttachRef(), commitDetachRef()


🙃 도움이 되었던 자료들

How does useRef() work?
useRef- 리액트 공식 문서(v18.3.1)

profile
🏁

0개의 댓글