본문 바로가기
카테고리 없음

[React] 오픈소스 살펴보기_Hook의 update

by 2__50 2023. 12. 15.
공부한 내용을 정리한 글입니다 
내용에 오류가 있거나 더 좋은 의견이 있다면 댓글로 남겨주세요.
배움에 큰 도움이 됩니다. 🖋

 

 

Hook의 update


current와 workInProgress /  https://icon-library.com/icon/icon-for-update-28.html

 

 

 

지난 시간hook이 호출된 순서대로 연결리스트에 추가돼 순서가 보장되는 것을 살펴보았다. 

오늘은 hook이 상태를 변경하고 컴포넌트를 리렌더링하는 과정을 알아보자.

 

 

// reconciler > ReactFiberHooks.js


function dispatchAction(fiber, queue, action) {
  const alternate = fiber.alternate

  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // Render phase update
  } else {
    // Idle update
  }
}

 

 

dispatchAction 함수는 hook의 queue에 업데이트를 추가하고 해당 컴포넌트의 fiber에 작업이 예약됐음을 알려주는 함수다. 업데이트가 컴포넌트 렌더링 중 발생한 것인지, 렌더링이 완료된 이후 발생한 것인지에 따라 업데이트하는 방식이 다르다.

 


렌더링이 완료된 이후 발생한 업데이트 (Idle update)


//reconciler > ReactFiberHooks.js


function dispatchAction(fiber, queue, action) {
// const alternate = fiber.alternate
  if (...) {
    // Render phase update
  } else {
    /* ... */
    // queue.last = update

    // 컴포넌트에서 업데이트가 발생한 적이 있는지 확인
    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // 최적화 로직..
    }
    /*...*/
  }
}

 

 

 

일반적으로 컴포넌트의 상태가 변경되면 react는 해당 컴포넌트를 다시 렌더링한다. 그러나 렌더링이 완료된 이후 발생한 업데이트의 경우, 불필요한 렌더링을 방지해 성능을 최적화하기 위해 추가적인 체크가 이뤄진다.

 

 

  • 현재 컴포넌트의 업데이트로 인해 work가 스케줄링 되어있지 않은 상태인가?
    이미 다른 작업을 진행중인지 확인하는 것이다. 만약 다른 작업이 예약돼 있다면, 렌더링이 이미 진행 중이므로 추가적인 렌더링을 예약하지 않는다.
  • action의 결괏값이 현재 상태 값과 동일한가?
    상태에 실제로 변경이 발생했는지 검사한다.

만약 두 조건 모두에 해당되면 변경된 내용이 없는 것이므로 추가적인 작업 없이 중단된다.
변경이 있다면 react는 컴포넌트의 상태를 업데이트하고 이로 인해 발생한 작업(work)을 스케줄링해 컴포넌트를 리렌더링한다.

 


useEffect나 이벤트 핸들러에서 발생한 상태 변경 등이 '현재 렌더링 중이 아닌 다른 상황에서 발생한 업데이트'에 해당된다.
이 때 생성된 업데이트 객체는 해당 컴포넌트의 루트*로 스케줄된다. 업데이트는 컴포넌트의 루트에서 해당 컴포넌트의 fiber까지 전파되는 성질이 있기 때문이다.

 

* fiber 트리의 최상위 노드

 

 

위 업데이트는 현재 렌더링 중인 작업과는 독립적으로 처리되며, 해당 컴포넌트가 다시 렌더링될 때 적용된다.

 

 

컴포넌트 렌더링 중 발생한 업데이트  (Render phase update)


 

// reconciler > ReactFiberHooks.js


function dispatchAction(fiber, queue, action) {
  // const alternate = fiber.alternate
  if (...) {
    didScheduleRenderPhaseUpdate = true; // renderWithHooks()에게 컴포넌트 재실행을 알려줄 플래그

    const update = {
      expirationTime: renderExpirationTime,
      action,
      suspenseConfig: null,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map(); // update 임시 저장소
    }

    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    /*idle update..*/
  }
}

 

 

 

컴포넌트 렌더링 중에 추가 업데이트가 발생하면, 업데이트 객체는 dispatchAction() 함수를 통해 해당 컴포넌트의 업데이트 queue에 추가된다. 이때 renderPhaseUpdates라는 임시 저장소를 사용해, 추가된 업데이트를 관리한다.

 

유휴 상태(Idle update)에서 넘어 온 경우에도 마찬가지다. 이미 작업이 진행 중이므로 Idle update 때처럼 작업 스케줄링이나 성능 최적화에 대한 처리는 필요 없다. 대신, 컴포넌트 렌더링 중 발생한 업데이트가 더 이상 발생하지 않을 때까지 컴포넌트를 계속해서 재호출해 action을 소비한다.

 

 

// reconciler > ReactFiberHooks.js


export function renderWithHooks(...) {
  /*...*/
  if (didScheduleRenderPhaseUpdate) {
    do {
      didScheduleRenderPhaseUpdate = false
      // 무한 루프 방지와 업데이트 구현체에게 Render phase update를 알려주는 플래그
      numberOfReRenders += 1

      //이하 훅 업데이트 구현체에서 Render phase update를 소비하는데 필요한 변수들을 설정
      nextCurrentHook = current !== null ? current.memoizedState : null
      nextWorkInProgressHook = firstWorkInProgressHook
      currentHook = null
      workInProgressHook = null

      ReactCurrentDispatcher.current = HooksDispatcherOnUpdate // 업데이트 구현체 주입

      children = Component(props, refOrContext) // 컴포넌트 재호출
    } while (didScheduleRenderPhaseUpdate)

    renderPhaseUpdates = null // Render phase update 저장소 초기화
    numberOfReRenders = 0
  }
  /*...*/
}

 



이 과정에서 didScheduleRenderPhaseUpdate 플래그를 통해 추가 업데이트가 발생했음을 표시하고, numberOfReRenders 체크를 통해 무한 루프를 방지한다.

 

 

 

정리하기


dispatchAction() 함수는 hook이 상태를 변경할 때 컴포넌트를 리렌더링하는데, 렌더링 중 혹은 이후에 발생한 업데이트 여부에 따라 업데이트를 구분한다.

렌더링 이후의 업데이트는 불필요한 작업을 방지하기 위한 조건을 검사하며, 컴포넌트의 상태 변경 여부에 따라 작업을 스케줄링해 컴포넌트를 리렌더링한다.

 

렌더링 중 발생한 업데이트는 임시 저장소 renderPhaseUpdates를 활용해 컴포넌트를 계속해서 재호출하며 업데이트를 소비한다. 이 때 중복 호출을 방지하기 위해 didScheduleRenderPhaseUpdate 플래그를, 무한 루프를 방지를 위해 numberOfReRenders를 체크한다.

 

 

 

 

 

참고


https://goidle.github.io/react/in-depth-react-hooks_1/

 

React 톺아보기 - 03. Hooks_1 | Deep Dive Magic Code

모든 설명은 v16.12.0 버전 함수형 컴포넌트와 브라우저 환경을 기준으로 합니다. 버전에 따라 코드는 변경될 수 있으며 클래스 컴포넌트는 설명에서 제외됨을 알려 드립니다. 각 포스트의 주제는

goidle.github.io

 

댓글