useState의 내부 동작 원리

우혁·2024년 12월 18일
28

React

목록 보기
19/19
post-thumbnail

useState란?

컴포넌트에 state 변수를 추가할 수 있는 React Hook이다.


useState의 내부 동작 원리

내부 동작 원리를 파악하는 소스 코드는 React v19.0.0 버전의 코드입니다.


컴포넌트 초기 렌더링: useState 훅 초기화

// mountState의 내부 구현
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook(); // 새로운 훅 객체 생성
  if (typeof initialState === "function") {
    // 초기 값이 함수인 경우
    // 초기 렌더링 시에만 이 함수가 호출되어 불필요한 계산을 방지한다.
    const initialStateInitializer = initialState;
    initialState = initialStateInitializer();
  }
   // 초기 상태 값을 훅의 memoizedState와 baseState에 저장
  hook.memoizedState = hook.baseState = initialState;
  
  // 상태 업데이트를 관리할 큐 객체 생성
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null, // 대기 중인 업데이트
    lanes: NoLanes, // 우선 순위
    dispatch: null, // 디스패치 함수
    lastRenderedReducer: basicStateReducer, // 마지막으로 렌더링된 리듀서
    lastRenderedState: (initialState: any), // 마지막으로 렌더링된 상태
  };
  hook.queue = queue;
  return hook; // 훅 객체 반환
}

// useState의 초기 렌더링(useState의 실제 인터페이스)
function mountState<S>(
  initialState: (() => S) | S // 초기 값
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState); // 훅 객체 생성
  const queue = hook.queue;
  // dispatchSetState 함수 생성(상태 업데이트 트리거)
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber, // 현재 렌더링 중인 파이버
    queue // 상태 업데이트를 관리하는 큐 객체
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch]; // 훅의 현재 상태와 디스패치 함수 반환
}

memoizedState와 baseState의 역할

  • memoizedState
    • 훅의 현재 상태 값을 저장한다.
    • 렌더링 사이에 상태 값을 기억하고 있어, 불필요한 재계산을 방지한다.
    • 컴포넌트가 리렌더링될 때 이 값을 사용하여 최신 상태를 표시한다.
  • baseState
    • 상태 업데이트의 기준이 되는 초기 상태를 저장한다.
    • 업데이트 큐가 처리될 때 이 값을 기준으로 새로운 상태를 계산한다.
    • 여러 업데이트가 큐에 쌓여있을 때, 이 값을 시작점으로 사용하여 최종 상태를 계산한다.

업데이트 큐 객체 설명

// 상태 업데이트 큐 객체는 React 상태 관리 시스템에서 중요한 역할을 한다.
const queue: UpdateQueue<S, BasicStateAction<S>> = {
  pending: null,
  lanes: NoLanes,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
};
  • pending
    • 대기 중인 상태 업데이트들의 연결 리스트를 저장한다.
    • 새로운 업데이트가 발생하면 이 리스트에 추가되고 처리될 때 순서대로 적용된다.
  • lanes
    • React의 우선순위 시스템에서 사용되는 비트 필드이다.
    • 각 업데이트의 우선순위를 나타내며, 이를 통해 React는 중요한 업데이트를 먼저 처리할 수 있다.
  • dispatch
    • 상태를 업데이트하는 함수이다.(useState의 두 번째 반환 값으로 상태 업데이트를 트리거 할 수 있다)
  • lastRenderedReducer
    • 마지막으로 사용된 리듀서 함수이다.
    • useState의 경우 기본적으로 basicStateReducer를 사용한다.
    • 이 리듀서는 새로운 상태 값을 계산하는 데 사용된다.
  • lastRenderedState
    • 마지막으로 사용된 상태 값이다.
    • 이전 렌더링의 결과를 저장하여 불필요한 리렌더링을 방지하는 데 사용된다.

상태 업데이트 함수 생성 및 호출 과정

function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane
): boolean {
  // 업데이트 객체 생성
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    // 현재 렌더링 중이라면
    // 렌더링 중에 업데이트가 발생하는 경우 업데이트 큐에 값을 넣는다.
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    // 렌더링 중이 아니라면
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 현재 파이버와 alternate 파이버에 진행 중인 업데이트가 없다면
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        try {
          // 마지막으로 렌더링된 리듀서를 사용해 새로운 상태를 즉시 계산
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // 새로운 상태가 현재 상태와 같다면 업데이트를 큐에 추가
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return false; // 리렌더링이 필요하지 않음
          }
        } catch (error) {}
      }
    }
    // 업데이트를 큐에 추가
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      // 파이버 트리의 업데이트 스케줄링
      // 트랜지션 관련 업데이트 처리
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true; // 리렌더링이 필요함
    }
  }
  return false; // 리렌더링이 필요하지 않음
}

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A
): void {
  const lane = requestUpdateLane(fiber); // 현재 업데이트의 우선순위 레인 요청
  // 실제 상태 업데이트 로직 수행
  const didScheduleUpdate = dispatchSetStateInternal(
    fiber,
    queue,
    action,
    lane
  );
}

렌더링 중에 상태 업데이트가 발생하거나, 새로운 상태 값이 현재 상태와 같다면 업데이트를 큐에 추가하지만 불필요한 리렌더링을 스케줄링하지 않아 성능을 향상시킨다.

위 상황들에 포함되지 않고 업데이트를 큐에 성공적으로 추가한다면 리렌더링을 스케줄링한다.


컴포넌트 리렌더링: 상태 업데이트 처리

function updateState<S>(
  initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
  // basicStateReducer: 새로운 상태를 반환하는 기본 리듀서
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: (I) => S
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook(); // 현재 작업중인 훅 가져오기
  return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}

function updateReducerImpl<S, A>(
  hook: Hook, // 현재 훅
  current: Hook, // 이전 훅
  reducer: (S, A) => S // 리듀서 함수
): [S, Dispatch<A>] {
  // 훅에 큐가 없으면 에러 발생(조건부 훅 호출 방지)
  const queue = hook.queue;
  if (queue === null) {
    throw new Error(
      "Should have a queue. You are likely calling Hooks conditionally, " +
        "which is not allowed. (https://react.dev/link/invalid-hook-call)"
    );
  }

  // 현재 리듀서 함수를 lastRenderedReducer로 설정
  queue.lastRenderedReducer = reducer;

  let baseQueue = hook.baseQueue;
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // 대기 중인 업데이트가 있으면 기존 baseQueue와 병합
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  const baseState = hook.baseState; // 이전 렌더링의 기본 상태
  if (baseQueue === null) {
    // baseQueue가 없으면 baseState 사용
    hook.memoizedState = baseState;
  } else {
    // baseQueue가 있다면 업데이트 처리를 위한 변수 초기화
    const first = baseQueue.next;
    let newState = baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast: Update<S, A> | null = null;
    let update = first;
    let didReadFromEntangledAsyncAction = false;

    do {
      // 각 업데이트의 우선순위 확인
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;

      // 오프 스크린 업데이트인지 현재 렌더링에서 건너뛰어야 하는지 결정
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);

      if (shouldSkipUpdate) {
        // 현재 렌더링에서 이 업데이트를 스킵해야하는 경우
        const clone: Update<S, A> = {
          lane: updateLane,
          revertLane: update.revertLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };

        // 업데이트의 복사본을 새로운 기본 큐에 추가
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        // 현재 렌더링 중인 파이버의 레인을 업데이트
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        const revertLane = update.revertLane;
        if (!enableAsyncActions || revertLane === NoLane) {
          // 비동기 액션이 확성화되거나 revertLane이 없는 경우
          if (newBaseQueueLast !== null) {
            // 적용할 업데이트를 새로운 기본 큐에 추가
            const clone: Update<S, A> = {
              lane: NoLane,
              revertLane: NoLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: (null: any),
            };
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }

          if (updateLane === peekEntangledActionLane()) {
            // 비동기 액션과 관련된 업데이트인 경우
            didReadFromEntangledAsyncAction = true;
          }
        } else {
          // 비동기 액션 관련 업데이트 처리
          if (isSubsetOfLanes(renderLanes, revertLane)) {
            update = update.next;
            if (revertLane === peekEntangledActionLane()) {
              didReadFromEntangledAsyncAction = true;
            }
            continue;
          } else {
            const clone: Update<S, A> = {
              lane: NoLane,
              revertLane: update.revertLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: (null: any),
            };
            if (newBaseQueueLast === null) {
              newBaseQueueFirst = newBaseQueueLast = clone;
              newBaseState = newState;
            } else {
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
            currentlyRenderingFiber.lanes = mergeLanes(
              currentlyRenderingFiber.lanes,
              revertLane
            );
            markSkippedUpdateLanes(revertLane);
          }
        }

        const action = update.action;
        if (shouldDoubleInvokeUserFnsInHooksDEV) {
          reducer(newState, action);
        }

        if (update.hasEagerState) {
          // 이미 계산된 상태가 있다면 계산된 상태를 사용
          newState = ((update.eagerState: any): S);
        } else {
          // 없다면 새로운 상태 계산
          newState = reducer(newState, action);
        }
      }
      update = update.next; // 다읍 업데이트로 이동
      // 모든 업데이트가 처리될 때까지 반복
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      // 새로운 기본 상태 설정
      newBaseState = newState;
    } else {
      // 기본 큐 설정
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    if (!is(newState, hook.memoizedState)) {
      // 상태가 변경 되었다면 업데이트 마킹
      markWorkInProgressReceivedUpdate();
      if (didReadFromEntangledAsyncAction) {
        // 비동기 액션이 있다면 처리
        const entangledActionThenable = peekEntangledActionThenable();
        if (entangledActionThenable !== null) {
          throw entangledActionThenable;
        }
      }
    }
    // 훅 상태 업데이트
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

  if (baseQueue === null) {
    // 모든 업데이트가 처리되면 큐의 레인 초기화
    queue.lanes = NoLanes;
  }

  // 훅에 memoizedState와 디스패치 함수를 반환
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

1. 초기 설정

  • 훅의 업데이트 큐를 확인하고 큐가 없으면 에러를 발생시킨다.(훅 조건부 호출 방지)

  • 현재 리듀서를 큐의 lastRenderedReducer로 설정한다.

    ➔ 리듀서: 현재 상태와 액션을 받아 새로운 상태를 반환하는 순수 함수이다.
    각 업데이트(액션)를 현재 상태에 적용하여 새로운 상태를 계산하는 역할을 한다.

2. 대기 중인 업데이트 처리

  • baseQueuependingQueue를 확인하고 대기중인 업데이트(pendingQueue)가 있다면 baseQueue와 병합한다.

    ➔ 렌더링 도중 새로운 업데이트가 발생할 수 있는데, 이러한 업데이트들(pendingQueue)을 기존의 처리되지 않은 업데이트들(baseQueue)과 병합함으로써 모든 업데이트가 순서대로 처리될 수 있도록 보장한다.

3. 기본 상태 설정

  • baseQueue가 없으면 baseState를 그대로 사용한다.

  • baseQueue가 있으면 업데이트 처리 과정을 시작한다.

    baseQueue가 없는 경우: 이전 렌더링에서 모든 업데이트가 처리되었음을 의미한다.
    따라서 단순히 baseState를 현재 상태로 사용한다.

    baseQueue가 있는 경우: 이전 렌더링에서 처리되지 않은 업데이트가 있음을 의미한다.
    이 경우 업데이트들을 현재 렌더링에서 처리해야 하므로 업데이트 처리 과정을 시작한다.

4. 업데이트 순회 및 처리

  • 각 업데이트의 우선순위를 확인한다.

  • 업데이트를 건너뛸지, 처리할지 결정한다.

  • 건너뛰는 업데이트는 새로운 기본 큐에 추가한다.

  • 처리하는 업데이트는 리듀서를 적용하여 새로운 상태를 계산한다.

    ➔ 건너뛰는 업데이트: 현재 렌더링의 우선순위보다 낮은 우선순위를 가진 업데이트를 말한다.
    이러한 업데이트는 현재 렌더링에서 처리되지 않고 다음 렌더링으로 미뤄진다.

5. 비동기 액션 처리

  • 얽혀있는(entangled) 비동기 액션과 관련된 업데이트를 확인하고 처리한다.

    ➔ 얽혀있는(entangled) 비동기 액션: React의 동시성 모드에서 사용되는 개념으로 비동기 작업의 결과가 현재 렌더링 과정과 얽혀있는(entangled) 상황을 나타낸다.

    ➔ 이러한 액션이 확인되면 React는 현재 비동기 작업의 결과를 기다리거나, 필요한 경우 현재 렌더링을 중단하고 다시 시작할 수 있다.

6. 새로운 상태 설정

  • 계산된 새로운 상태를 memoizedState로 설정한다.
  • 새로운 baseStatebaseQueue를 설정한다.

7. 상태 변경 확인

  • 새로운 상태가 이전과 다르다면 업데이트를 마킹한다.

8. 레인 초기화

  • 모든 업데이트가 처리되면 큐의 레인을 초기화한다.

9. 결과 반환

  • 최종 계산된 상태와 디스패치 함수를 반환한다.

정리하기

  1. 초기 렌더링 시, useState는 새로운 훅 객체를 생성하고 초기 상태를 설정한다. 이 과정에서 상태 업데이트를 관리할 큐 객체도 함께 생성된다.

  2. 상태 업데이트 함수가 호출되면 React는 업데이트 객체를 생성하고 현재 렌더링 상태를 확인한다. 만약 렌더링 중이 아니고 현재 진행 중인 업데이트가 없다면 새로운 상태를 즉시 계산한다.

  3. 리렌더링 과정에서 React는 이전에 대기 중이던 모든 업데이트를 처리한다. 각 업데이트는 우선순위에 따라 처리되거나 다음 렌더링으로 미뤄진다.

  4. 모든 업데이트가 처리된 후 최종 상태가 계산되어 컴포넌트가 새로운 상태로 리렌더링된다.

useState 훅은 Fiber 노드에 연결되어 있으며 이를 통해 React는 컴포넌트의 상태를 효율적으로 추적하고 업데이트할 수 있다.

Fiber 아키텍쳐는 작업을 작은 단위로 나눠 처리할 수 있어 브라우저의 메인 스레드를 차단하지 않고 렌더링을 수행할 수 있다.


useState와 React의 리렌더링 메커니즘 이해하기

아래 코드에서 버튼을 3번 클릭하면 App 컴포넌트와 A 컴포넌트는 각각 몇번 렌더링 할까?

function A() {
  console.log("A 컴포넌트 렌더링");
  return null;
}

function App() {
  const [state, setState] = useState(false);
  const counter = useRef(0);
  console.log(`현재 state: ${state}`);
  console.log("App 컴포넌트 렌더링");

  return (
    <div>
      <button
        onClick={() => {
          console.log(`-------\n버튼 ${++counter.current}회 클릭\n-------`);
          setState(true);
        }}
      >
        클릭
      </button>
      <A />
    </div>
  );
}

결과는 App 컴포넌트는 2번, A 컴포넌트는 1번 리렌더링이 발생한다.

첫 번째 버튼 클릭

  • state: false ➔ true
  • App 컴포넌트: 리렌더링 발생
  • A 컴포넌트: 리덴더링 발생

두 번째 버튼 클릭

  • state: true ➔ true
  • App 컴포넌트: 리렌더링 발생
  • A 컴포넌트: 리렌더링 X

세 번째 버튼 클릭

  • state: true ➔ true
  • App 컴포넌트: 리렌더링 X
  • A 컴포넌트: 리렌더링 X

여기서 드는 2가지 의문이 있다.

  1. 두 번째 버튼 클릭할 때, 상태 값 true 에서 true 로 업데이트하여 상태 변화가 없는데 왜 App 컴포넌트는 리렌더링이 발생할까?

  2. App 컴포넌트에서 리렌더링이 발생했을 때(두 번째 버튼 클릭) 왜 자식 컴포넌트인 A 컴포넌트는 리렌더링이 발생하지 않을까?


업데이트하였을 때 상태 변화가 없는데 왜 App 컴포넌트는 리렌더링이 발생할까?

💡 핵심 포인트

  • 초기 최적화 시도: dispatchSetState 함수에서 불필요한 리렌더링을 피하려고 시도한다.
  • Fiber 노드의 상태: Fiber 노드의 lanes라는 속성을 사용해 업데이트가 필요한지 판단한다.
  • 현재 Fiber와 대체 Fiber: 현재 Fiber와 대체(alternate) Fiber 두 버전을 관리한다.(더블 버퍼링)
  • 업데이트 큐잉: 상태 업데이트 시 현재 Fiber에 먼저 업데이트를 표시하고, 리렌더링 과정에서 대체 Fiber(workInProgress)에도 업데이트가 적용된다.
  • 리렌더링 과정: 실제 리렌더링은 beginWork 함수에서 시작되며 이 과정에서 lanes가 초기화된다.

첫 번째 버튼 클릭

  • setState(true) 가 호출되면 React는 현재 Fiber 노드에 업데이트를 표시한다.
  • 리렌더링이 발생하고 새로운 Fiber(workInProgress)가 생성된다.
  • beginWork 함수에서 lanes가 초기화된다.
  • 리렌더링 과정에서 현재 Fiber가 대체 Fiber로 교체된다.
    ➔ 상태가 falsetrue 로 변경되고 App 컴포넌트, A 컴포넌트 모두 리렌더링된다.

두 번째 버튼 클릭

  • 다시 setState(true) 가 호출된다.
  • 여전히 하나의 Fiber에 업데이트 표시가 남아있어 리렌더링이 발생한다.
  • 결과적으로 App 컴포넌트는 리렌더링되지만 상태 값은 변경되지 않는다.
  • React는 실제 상태 변경이 없음을 확인하고 자식 컴포넌트에 대해 최적화를 수행한다.
// 파이버에 업데이트 표시가 있어 NoLanes에 해당하지 않는다.
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)){
  // 이 조건에 해당하지 않는 경우, 즉시 상태 확인을 건너뛰고 else문 코드를 실행한다.
} else {
  // 두 번째 버튼 클릭 시에는 여기 코드가 실행
  // 업데이트를 큐에 추가
  const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    // 파이버 트리의 업데이트 스케줄링
    scheduleUpdateOnFiber(root, fiber, lane);
    entangleTransitionUpdate(root, queue, lane);
    return true; // 리렌더링이 필요함
  }
}

세 번째 버튼 클릭

  • 다시 setState(true) 가 호출 시 두 Fiber 모두 lanes가 NoLanes인 상태이다.
  • dispatchSetStateInternal 함수에서 새로운 상태를 즉시 계산하고 현재 상태와 비교한다.
  • 두 상태가 같다는 것을 확인하여 리렌더링을 예약하지 않는다.(bailout 최적화)
if (is(eagerState, currentState)) {
  // 새로운 계산한 상태(eagerState)와 현재 상태(currentState) 비교
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false; // 리렌더링이 필요하지 않음
}

📝 정리하기

  • 첫 번째 클릭에서는 상태가 실제로 변경되어 전체 리렌더링이 발생한다.
  • 두 번째 클릭에서는 Fiber 노드에 업데이트 표시가 남아있어 App 컴포넌트의 리렌더링이 발생하지만, 자식 컴포넌트는 최적화된다.(아래에서 자세히 설명)
  • 세 번째 클릭에서는 모든 Fiber 노드가 깨끗한 상태가 되어 완전한 최적화(bail out)가 이루어져 리렌더링이 발생하지 않는다.

App 컴포넌트에서 리렌더링이 발생했을 때 자식 컴포넌트인 A 컴포넌트는 리렌더링이 발생하지 않을까?

첫 번째 버튼 클릭

  • App 컴포넌트의 상태가 변경되어 리렌더링된다.
  • A 컴포넌트는 새로 생성되어 렌더링된다.

두 번째 버튼 클릭

  • App 컴포넌트는 리렌더링되지만 상태 값은 변경되지 않는다.
  • React는 A 컴포넌트의 props가 변경되지 않았음을 감지한다.
  • 따라서 A 컴포넌트의 리렌더링을 건너뛴다.

세 번째 클릭

  • App 컴포넌트의 리렌더링이 발생하지 않아 A 컴포넌트도 리렌더링되지 않는다.
// 불필요한 렌더링을 방지하는 중요한 최적화단계이다.
// 컴포넌트와 그 자식들의 상태를 효율적으로 관리하여 변경이 필요한 부분만 업데이트하도록 한다.
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null, // 현재 렌더링된 Fiber 노드
  workInProgress: Fiber, // 작업 중인 Fiber 노드
  renderLanes: Lanes // 현재 렌더링 중인 우선순위
): Fiber | null {
  if (current !== null) {
    // 현재 Fiber가 존재하면 이전 렌더링의 의존성을 새로 작업 중인 Fiber에 복사
    // 이는 불필요한 재계산을 방지한다.
    workInProgress.dependencies = current.dependencies;
  }

  // 현재 업데이트 레인 스킵 표시(이 업데이트가 처리되지 않았음을 표시)
  markSkippedUpdateLanes(workInProgress.lanes);

  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
   // 현재 렌더링 레인과 자식 컴포넌트의 레인 비교, 겹치는 부분(자식에게 작업이 없다면)이 없으면 최적화 진행
    if (enableLazyContextPropagation && current !== null) {
      // 지연 컨텍스트 전파가 활성화되어 있고 현재 Fiber가 존재하는 경우
      // 부모 컨텍스트 변경을 지연 전파
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        // 다시 자식 레인을 확인하고 여전히 작업이 없으면 null을 반환하여 렌더링을 건너뛴다.
        return null;
      }
    } else {
      // 지연 컨텍스트 전파가 비활성화되어 있다면 바로 null을 반환하여 렌더링을 건너뛴다.
      return null;
    }
  }

  // 자식 컴포넌트에 작업이 있는 경우 현재 Fiber의 자식들을 복제하고 첫 번째 자식 Fiber를 반환한다.
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

React는 기본적으로 부모 컴포넌트가 리렌더링될 때 모든 자식 컴포넌트를 리렌더링한다.

하지만 props가 변경되지 않은 경우, React는 자식 컴포넌트의 리렌더링을 최적화한다. 이는 React.memo나 PureComponent와 같은 최적화 기법을 사용하지 않아도 기본적으로 적용되는 동작이다.

이러한 최적화는 React의 재조정(reconciliation) 과정에서 이루어지며, 불필요한 렌더링을 방지하여 성능을 향상시킨다.

🤷‍♂️ React의 기본 최적화가 있는데 React.memo와 같은 최적화 기법을 왜 사용하는걸까?
➔ React.memo는 React의 기본 최적화를 보완하고 더 세밀하게 명시적인 성능 최적화를 가능하게 한다.

  • React의 기본 최적화는 얕은 비교만 수행하는데 React.memo를 사용하면 더 세밀한 비교 로직을 구현할 수 있어, 복잡한 props 구조에서도 불필요한 리렌더링을 방지할 수 있다.
  • 규모가 큰 애플리케이션에서 React.memo를 전략적으로 사용하면, React의 기본 최적화보다 더 효과적으로 성능을 향상시킬 수 있다.

🙃 도움이 되었던 자료들

useState - React 공식문서(v18.3.1)
How does useState() work internally in React?
Exploring React’s Fiber Architecture: A Comprehensive Guide
Why React Re-Renders
(번역) React는 내부적으로 re-render를 어떻게 처리할까?

profile
🏁

0개의 댓글