React 톺아보기 - 2.1 (주석을 담아서)

adultlee·2024년 1월 2일
0
post-thumbnail

해당 시리즈는 제 다른글과 비교했을때 특히나 "메모"의 성격이 짙습니다.
그럼에도 최대한 정제해서 작성해보려 노력하겠습니다. 잘 부탁드립니다

해당 시리즈는 원문의 버전인 v16.12.0 버전 함수형 컴포넌트와 브라우저 환경을 기준으로 진행중이었으나, FiberNode의 변경점(v18.2.0)이 많은 만큼, 이에 대해서 가볍게 추가하고 진행합니다

현재 링크로 남기는 react의 버전은 18.2.0 을 의미합니다. 추후 버전업데이트가 되어 main 버전이 이동하는 경우 내용이 일부 변경될 수 있습니다.

React FiberNode

react FiberNode


function FiberNode( // 원문과는 다르게 인자에서 type 추론을 진행합니다. 
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // Fiber Node의 유형을 나타냅니다. 함수형 컴포넌트, 클래스 컴포넌트, 호스트 컴포넌트 등을 의미합니다. 
  this.key = key; // React에서 key 속성을 의미합니다. 리스트에서 요소들을 구별할때 사용됩니다. 재조정 과정에서 중요한 역할을 수행합니다. 
  this.elementType = null; // +) 기존과는 다르게 새롭게 추가되었습니다. React 요소의 타입입니다. 예를 들어, <MyComponent />에서 elementType은 MyComponent가 됩니다.
  this.type = null; // 컴포넌트의 구체적인 타입 정보를 담습니다. elementType과 유사하지만, 고차 컴포넌트나 다른 추상화를 다룰 때 차이가 나타날 수 있습니다.
  this.stateNode = null; // Fiber Node와 연관된 실제 DOM 노드나 React 컴포넌트 인스턴스를 가리킵니다. 클래스 컴포넌트의 경우 컴포넌트 인스턴스가 여기에 해당합니다.

  // Fiber
  this.return = null; // 부모 FiberNode
  this.child = null; // 부모입장에서 첫번째 자식노드
  this.sibling = null; // 자신의 바로 "다음" 형제 노드를 칭함
  this.index = 0; // 자신의 형제들 중에서 몇번째 형제인지

  this.ref = null; // +) Dom Node나 컴포넌트 인스턴스의 상태와 업데이트를 관리합니다. 
  this.refCleanup = null; // 참조 정리를 위해서 사용되는 속성입니다

  this.pendingProps = pendingProps; // 컴포넌트에 전달될 예정인 속성 workInProgress 작업이 종료되지 않았음, 아직 pending으로 관리
  this.memoizedProps = null; // Render phase가 종료된 이후 사용되었던 Props
  this.updateQueue = null; // 상태 업데이트나 이펙트(effect)를 위한 업데이트 큐입니다.
  this.memoizedState = null; // 컴포넌트의 메모된(캐시된) 상태입니다.
  this.dependencies = null; // 컴포넌트와 관련된 다양한 의존성들(예: Context)을 관리합니다.

  this.mode = mode; // 컴포넌트의 렌더링 모드(예: Concurrent Mode)를 나타냅니다.

  // Effects
  this.flags = NoFlags; // Fiber Node의 현재 또는 예정된 작업을 나타내는 플래그(예: 업데이트, 마운트)입니다.

  this.subtreeFlags = NoFlags; // 하위 트리에 대한 플래그입니다.
  this.deletions = null; //삭제되어야 할 자식 노드들을 나타냅니다.

  this.lanes = NoLanes; // 우선 순위 레벨을 표시하는 데 사용되는 레인(lanes). 여기서 레인은 작업의 우선 순위와 관련된 개념입니다.
  this.childLanes = NoLanes; // 앞서 언급한 Concurrent Mode에서 비동기 렌더링에서 진행되는 작업의 우선순위에 따라 변경됩니다.

  this.alternate = null; // 더블 버퍼링 구조의 각 tree 를 관리합니다
}

현시점 우리가 확인하고 공부할 속성들입니다. 앞서서 언급한 링크 에서 좀 코드를 확인하실 수 있습니다.

this.tag

this.tag의 정체는 쉽게 찾을 수 있었습니다. 인자로 받는 tag의 타입을 살펴보니, 아래와 같은 위치에 정의되어 있었습니다.

https://github.dev/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.js

this.tag 속성은 React의 FiberNode에서 컴포넌트 또는 엘리먼트의 유형을 나타냅니다. 각각의 숫자는 다른 유형의 컴포넌트나 엘리먼트를 식별하는 데 사용됩니다. 예를 들어, FunctionComponent는 0, ClassComponent는 1로 정의되어 있습니다. 이러한 태그는 React가 각 노드를 어떻게 처리할지 결정하는 데 사용됩니다. 예를 들어, FunctionComponent는 함수 컴포넌트를, ClassComponent는 클래스 기반 컴포넌트를 나타냅니다. 이를 통해 React는 렌더링, 상태 관리, 라이프사이클 메소드 처리 등을 각 컴포넌트 유형에 맞게 조절합니다.

나머지 다른 WorkTag에 대해서도 가볍게 설명을 추가합니다.

export const FunctionComponent = 0; // 함수 컴포넌트, React 함수를 사용하여 정의됨
export const ClassComponent = 1; // 클래스 컴포넌트, React 클래스를 사용하여 정의됨
export const IndeterminateComponent = 2; // 초기 렌더링 시 결정되지 않은 컴포넌트 유형
export const HostRoot = 3; // React 애플리케이션의 루트 노드
export const HostPortal = 4; // 다른 DOM 노드로의 포털
export const HostComponent = 5; // 실제 DOM 엘리먼트를 나타냄
export const HostText = 6; // 텍스트 노드
export const Fragment = 7; // React.Fragment, 여러 자식을 그룹화하지만 별도의 노드는 생성하지 않음
export const Mode = 8; // React 모드(예: StrictMode, ConcurrentMode)
export const ContextConsumer = 9; // Context API의 소비자
export const ContextProvider = 10; // Context API의 제공자
export const ForwardRef = 11; // Ref를 하위 컴포넌트에 전달
export const Profiler = 12; // 성능 측정을 위한 Profiler 컴포넌트
export const SuspenseComponent = 13; // 데이터 로딩 등을 위한 Suspense 컴포넌트
export const MemoComponent = 14; // React.memo로 감싸진 컴포넌트
export const SimpleMemoComponent = 15; // 최적화를 위한 메모 컴포넌트
export const LazyComponent = 16; // 코드 분할을 위한 Lazy 컴포넌트
export const IncompleteClassComponent = 17; // 아직 완성되지 않은 클래스 컴포넌트
export const DehydratedFragment = 18; // 서버 사이드 렌더링에서 사용되는 Fragment
export const SuspenseListComponent = 19; // 여러 Suspense 컴포넌트를 관리하는 SuspenseList
export const ScopeComponent = 21; // 특정 범위의 상호작용을 위한 컴포넌트
export const OffscreenComponent = 22; // 화면 밖의 컴포넌트(예: 숨김 처리)
export const LegacyHiddenComponent = 23; // 이전 버전의 숨김 처리를 위한 컴포넌트
export const CacheComponent = 24; // 캐싱을 위한 컴포넌트
export const TracingMarkerComponent = 25; // 트레이싱과 성능 측정을 위한 마커 컴포넌트
export const HostHoistable = 26; // 호스트 환경 최적화를 위한 컴포넌트
export const HostSingleton = 27; // 단일 인스턴스의 호스트 컴포넌트

this.key

this.key는 React에서 key를 의미합니다. Render

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const key = element.key; // 가져온 element의 key를 저장합니다.
  let child = currentFirstChild; // 현재 Fiber의 첫 번째 자식을 가져옵니다. 그 후 while 문의 최하단에서 child를 갱신합니다. (다음 형제)
  while (child !== null) {
    // key 값이 같은 자식을 찾습니다.
    if (child.key === key) {
      const elementType = element.type; // element의 타입을 확인합니다.
      if (elementType === REACT_FRAGMENT_TYPE) {
        // Fragment 타입인 경우 처리 로직입니다.
        if (child.tag === Fragment) {
          deleteRemainingChildren(returnFiber, child.sibling); // 형제 노드들을 삭제합니다.
          const existing = useFiber(child, element.props.children); // children을 바탕으로 clone 된 Fiber를 생성합니다. .
          existing.return = returnFiber; // return 포인터를 설정합니다.
          return existing;
        }
      } else {
        // Fragment가 아닌 다른 타입의 경우 처리 로직입니다.
        if (
          child.elementType === elementType || // 같은 타입인 경우
          (__DEV__
            ? isCompatibleFamilyForHotReloading(child, element) // 개발 모드에서 핫 리로딩 체크
            : false) ||
          (typeof elementType === 'object' &&
            elementType !== null &&
            elementType.$$typeof === REACT_LAZY_TYPE &&
            resolveLazy(elementType) === child.type)
        ) {
          deleteRemainingChildren(returnFiber, child.sibling); // 형제 노드들을 삭제합니다.
          const existing = useFiber(child, element.props); // 기존 Fiber를 재사용합니다.
          existing.ref = coerceRef(returnFiber, child, element); // ref를 설정합니다.
          existing.return = returnFiber; // return 포인터를 설정합니다.
          return existing;
        }
      }
      deleteRemainingChildren(returnFiber, child); // 일치하지 않으면 현재 자식들을 삭제합니다.
      break;
    } else {
      deleteChild(returnFiber, child); // 일치하지 않는 자식을 삭제합니다.
    }
    child = child.sibling; // 다음 형제 노드로 이동합니다.
  }
  // 지금까지, key가 다름을 바탕으로 fiber를 제거 했습니다. 
  // 새로운 Fiber를 생성하는 로직입니다.
  if (element.type === REACT_FRAGMENT_TYPE) {
    const created = createFiberFromFragment(
      element.props.children,
      returnFiber.mode,
      lanes,
      element.key,
    );
    created.return = returnFiber;
    return created;
  } else {
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}

다음 함수를 확인해보면, key 값의 차이를 확인한 후 그에 따라서 현재 fiber 노드를 변경하고 있습니다.
이때 변경된 fiber는 workInProgress 에 반영되고 있는데, 이를 통해서 Render phase 에서 key의 변화에 따라 workInProgress 가 실시간으로 변경되고 있음을 확인할 수 있습니다.

또한 React를 학습하며 Fiber Archtecture에서는 왜 첫번째 자식만 child로 받고 나머지 형제들을 sibling으로 받는지 이해할 수 없었으나, 해당 로직을 살펴보니, 한개의 node에서 형제 단위 depth에서 비교 및 삭제 변경 로직을 진행하는 것이 훨씬 이해하기 쉽고 변경되는 depth가 적을 것 같다는 생각이 들었습니다.

아래는 해당 함수에서 추가적으로 사용된 함수들을 가져왔습니다.

// useFiber
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
    // We currently set sibling to null and index to 0 here because it is easy
    // to forget to do before returning it. E.g. for the single child case.
    const clone = createWorkInProgress(fiber, pendingProps); // workInProgress 에 반영됨
    clone.index = 0;
    clone.sibling = null;
    return clone;
  }

// deleteRemainingChildren
  function deleteRemainingChildren( // 제거 되지 않은 남은 자식들을 모두 제거합니다. 
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
  ): null {
    if (!shouldTrackSideEffects) {
      // Noop.
      return null;
    }

    // TODO: For the shouldClone case, this could be micro-optimized a bit by
    // assuming that after the first child we've already added everything.
    let childToDelete = currentFirstChild;
    while (childToDelete !== null) {
      deleteChild(returnFiber, childToDelete);
      childToDelete = childToDelete.sibling;
    }
    return null;
  }

맺음말

지금까지 FiberNode의 tag 와 key 속성에 대해서 알아보았습니다.

아직 FiberNode의 모든 속성을 이해하진 못했지만, 18버전으로 버전업 하면 반영된 FiberNode의 새로운 속성과 변경점들을 공부하다보면, React를 좀 더 깊숙히 이해할 수 있으리라 생각합니다.

0개의 댓글