내부 구조: React

발행: (2025년 12월 12일 오후 10:44 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

소개

React를 사용하기 시작한 순간부터 나는 이것을 하고 싶었다: React가 어떻게 동작하는지 이해하는 것. 이것은 소스 코드를 세세히 살펴보는 것이 아니라, React 내부의 핵심 패키지와 통합에 대한 개요이다. 이 연구를 통해 React를 더 잘 이해하고 문제를 자신 있게 디버깅할 수 있게 되었다. 여러분도 더 나은 시각을 얻길 바란다.

레이어 구조

React는 단독 패키지로서는 그다지 유용하지 않다는 것을 알게 되었다. API 레이어 역할을 하며 react-reconciler 패키지에 크게 의존한다. 그래서 보통 react-dom 같은 렌더러와 함께 사용되는데, react-domreact-reconciler와 번들되어 있다.

따라서 React를 배우려면 react-reconciler와 그 핵심 의존성인 scheduler 패키지를 배워야 한다. 먼저 scheduler부터 살펴보자.

scheduler

scheduler 패키지는 작업을 작은 청크로 나누고 실행 우선순위를 관리함으로써 React의 동시성 기능을 가능하게 한다.

“동기 렌더링에서는 업데이트가 렌더링을 시작하면 사용자가 화면에서 결과를 볼 때까지 중단될 수 없다.”
— React v18.0 | 2022 년 3월 29일, React 팀

동시성 React가 나오기 전에는 업데이트가 동기적으로 진행돼 메인 스레드를 차단했으며, 이로 인해 UI가 응답하지 않거나 애니메이션이 끊기는 문제가 발생했다.

scheduler협력적 스케줄링을 구현함으로써 이를 해결한다: 작업이 자발적으로 호스트에 제어권을 반환해 메인 스레드가 다른 작업에 사용될 수 있게 한다. 작업은 짧은 간격(≈ 5 ms) 동안 실행되고, 마감 시간이 지났는지 확인한 뒤 필요하면 양보하고, 다음 차례를 기다린다.

협력적 스케줄링은 Message Loop가 관리하고, 실제 작업은 Work Loop가 담당한다.

작업, 두 개의 루프, 그리고 두 개의 큐

unstable_scheduleCallback 함수(이후 scheduleCallback이라 부름)는 동시(비동기) 작업을 예약하는 주요 방법이다.

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: { delay: number },
): Task {
  // implementation...
}

Task 객체

export opaque type Task = {
  id: number,
  callback: Callback | null,
  priorityLevel: PriorityLevel,
  startTime: number,
  expirationTime: number,
  sortIndex: number,
  isQueued?: boolean,
};

우선순위 레벨

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

scheduler는 이러한 레벨을 사용해 실행 우선순위를 관리한다.

scheduler는 두 개의 우선순위 큐(최소 힙 구현)를 유지한다:

목적
Task queue실행 준비가 된 작업을 저장한다.
Timer queue아직 실행 가능하지 않은 작업(startTime이 미래인 경우)을 저장한다.

작업이 생성될 때 startTime(언제 실행 가능해지는지)과 expirationTime(최대 실행 시점)이 부여된다.

  • startTime > currentTime이면 Timer queue에 들어간다.
  • 그렇지 않으면 바로 Task queue에 들어간다.
startTime 계산
// 즉시 실행 작업
startTime = currentTime; // 지금 바로 사용 가능

// 지연 작업
startTime = currentTime + delay; // 미래에 사용 가능
expirationTime 계산
expirationTime = startTime + timeout;

timeoutPriorityLevel에 따라 달라지며, ImmediatePriority-1 ms, LowPriority는 대략 10 000 ms까지이다.

sortIndex
  • Timer queue에서는 sortIndexstartTime과 같다.
  • Task queue에서는 sortIndexexpirationTime과 같다.

우선순위 레벨 예시

우선순위 레벨일반적인 트리거
ImmediatePriority (Sync)현대 React에서는 거의 사용되지 않음
UserBlockingPriorityonScroll, onDrag, onMouseMove
NormalPrioritystartTransition(), useDeferredValue (대부분의 scheduler 작업 기본값)
IdlePriority화면 밖/숨겨진 콘텐츠 렌더링

동기 작업은 scheduler를 우회하며 queueMicrotask API를 사용해 큐에 넣는다.

Work Loop

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        // 작업이 아직 만료되지 않았고 마감 시간에 도달함.
        break;
      }
    }
    // ... 콜백 실행 ...
    currentTask = peek(taskQueue);
  }
}

Work Loop는 Task queue에서 작업을 꺼내 콜백을 실행한다. 마감 시점에 도달하면 호스트에 양보해야 한다. 또한 advanceTimers를 통해 Timer queue에 있던 작업을 Task queue로 승격한다. 작업이 할당된 시간보다 오래 실행되면 callback계속 콜백으로 교체돼 scheduler가 나중에 재개할 수 있다. 취소된 작업은 꺼낼 때 버려진다.

Message Loop

Work Loop가 양보하면 React는 이를 다시 시작할 메커니즘이 필요하다. 이것이 Message Loop이다. React는 비표준 setImmediate API를 선호하고, 사용할 수 없을 경우 MessageChannel이나 setTimeout(브라우저가 아닌 환경)을 폴백한다.

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
const schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

performWorkUntilDeadline이 Work Loop를 시작한다. Work Loop가 양보하기로 결정하면 schedulePerformWorkUntilDeadline()을 호출해 메시지를 포스트하고, 다음 틱에서 performWorkUntilDeadline이 트리거된다. MessageChannel을 사용하면 setTimeout이 부과하는 최소 4 ms 지연을 피할 수 있어 React가 작업을 더 정밀하게 스케줄링할 수 있다.

Back to Blog

관련 글

더 보기 »

React를 이용한 계산기

오늘 나는 React를 사용한 계산기 연습 프로젝트 중 하나를 완료했습니다. 이 React Calculator 애플리케이션은 기본 산술 연산을 수행합니다. 버튼을 지원합니다.