LODY/정리

리액트 네이티브 백과사전 / React Native New Architecture의 내부 동작 원리

React Native New Architecture의 내부 동작 원리

들어가며

React Native에서 버튼을 탭하면 무슨 일이 일어날까?

const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <View style={styles.container}>
      <Text style={styles.text}>{count}</Text>
      <Button onPress={() => setCount(count + 1)} title="탭" />
    </View>
  );
};

익숙하게 생겼다. 비슷한 코드를 Web에서도 본 적 있을 것이다.

useState, Fiber Scheduler, Reconciler — 두 코드에서 React 자체는 완전히 동일하다. 달라지는 것은 딱 두 가지이다.

React DOMReact Native
Primitivediv, span, buttonView, Text, Button
RendererReactDOM → DOM APIFabric → UIKit / Android

React는 플랫폼을 모른다. Fiber가 변경사항을 계산한 뒤 Renderer에 "이걸 만들어라"고 위임할 뿐이다. Web이냐 Native냐는 어떤 Renderer를 붙이느냐의 차이이다. 이 글은 그 중 Native Renderer — Fabric — 의 내부를 따라간다.

setCount(count + 1) 한 줄. 화면의 숫자가 하나 올라간다.

단순해 보이지만, 이 한 줄이 실제 픽셀로 나타나기까지 놀랍도록 많은 Layer를 거친다.

  • Render Phase — Fiber Scheduler가 변경사항을 감지하고
  • Commit Phase — Fabric Renderer가 Shadow Tree를 재구성하며
  • Layout Phase — Yoga가 frame을 계산하고
  • Mount PhaseRCTViewComponentViewUIViewframe을 적용하고
  • Drawing Phase — CALayer drawRect:가 CPU 사이드 비트맵을 생성하고
  • Paint Phase — Render Server가 Metal GPU로 픽셀을 화면에 출력한다

각 Phase는 서로 다른 트리 표현 위에서 동작한다.

이 글의 목적은 그 여정을 처음부터 끝까지 — React 코드에서 UIKit의 addSubview()까지 — 추적하는 것이다.

전체 파이프라인을 먼저 보겠다. 각 Layer의 내부는 이 글을 읽고 나면 비로소 의미가 채워질 것이다.

📖 용어 설명

  • Fiber: React의 내부 자료구조. 컴포넌트 하나당 하나씩 생성되는 작업 단위로, rendering 작업을 잘게 쪼개 스케줄링할 수 있게 한다
  • Fabric: React Native의 새 Renderer. C++로 작성되어 있으며 JSI를 통해 동작한다
  • JSI (JavaScript Interface): JS 엔진에 C++ 객체를 직접 노출하는 Layer. JSON 직렬화 없이 동기 호출을 가능하게 한다
  • Shadow Tree: Fabric이 관리하는 C++ 컴포넌트 트리. UIView 계층의 설계도 역할
  • Yoga: Meta가 만든 CSS Flexbox 구현체 (C++). 각 컴포넌트의 frame을 계산한다
  • CALayer: UIView가 실제 rendering을 위임하는 Layer. Core Animation이 관리한다

Old Bridge가 가진 문제

New Architecture가 왜 만들어졌는지 이해하려면, Old Architecture가 어떤 구조였는지 먼저 봐야 한다.

Old Architecture에서 JavaScript와 Native 코드는 비동기 메시지 Bridge로 통신했다. JS Thread, Bridge Thread, Native Thread — 세 Thread가 JSON으로 직렬화된 메시지를 주고받는 구조였다.

이 구조에는 세 가지 근본적인 한계가 있었다.

첫째, 모든 통신이 비동기이다. UI 업데이트의 타이밍을 JavaScript에서 보장할 수 없다. 스크롤 이벤트에 즉각적으로 반응해야 하는 경우, Bridge 지연이 그대로 Frame drop으로 이어졌다.

둘째, 직렬화 비용이다. 모든 데이터는 JSON으로 직렬화되어 전달된다. 복잡한 props 업데이트가 많을수록 이 비용이 누적된다.

셋째, 메모리 복사이다. JS Thread와 Native Thread는 메모리를 공유하지 않다. 데이터를 전달할 때마다 복사가 발생한다.

New Architecture는 이 문제를 JSI로 해결한다. JSI를 사용하면 JS 코드가 C++ 함수를 직접, 동기적으로 호출할 수 있다.

📖 용어 설명

  • Old Bridge: React Native 0.68 이전의 통신 방식. JS ↔ Native 간 비동기 JSON 메시지 큐
  • JSON 직렬화: JavaScript 객체를 문자열로 변환하는 과정. Native에서 역직렬화 과정이 필요
  • Hermes: Meta가 만든 React Native용 JavaScript 엔진. AOT 컴파일로 앱 시작 시간을 단축한다
  • JSI: Old Bridge를 대체하는 C++ Layer. JS 엔진에 C++ 객체를 직접 노출하여 동기 호출을 가능하게 한다

Phase 1: React

JSX → ReactElement

우리가 작성하는 JSX는 빌드 시점에 React.createElement() 호출로 변환된다. Babel 또는 SWC가 처리하는 문법 변환이다.

// 우리가 작성하는 코드
<View style={styles.container}>
  <Text style={styles.text}>{count}</Text>
</View>
 
// 빌드 후 변환된 코드
React.createElement(
  View,
  { style: styles.container },
  React.createElement(Text, { style: styles.text }, count)
)

React.createElement()의 반환값은 ReactElement{ type, props, key } 형태의 평범한 JavaScript 객체이다. 이 시점에서 실제 View는 아직 만들어지지 않다. "이 View를 만들어 달라"는 설명서(description) 에 불과한다.

Fiber 트리

React는 내부적으로 이 ReactElement 트리를 Fiber 노드 트리로 관리한다. Fiber는 컴포넌트 하나당 하나씩 생성되는 작업 단위이다.

각 Fiber 노드가 추적하는 정보:

필드의미
type컴포넌트 타입 (View, Text, 함수 컴포넌트 등)
stateNode클래스 컴포넌트 instance 또는 Native 뷰 instance
memoizedState현재 hook 상태 (useState, useEffect 등의 linked list)
memoizedProps현재 rendering된 props
child / sibling / return트리 탐색 pointer
effectTag필요한 작업 종류 (Update, Placement, Deletion)

React는 항상 두 개의 Fiber 트리를 유지한다. current 트리는 현재 화면에 표시된 것이고, workInProgress 트리는 다음 rendering을 위해 구성 중인 것이다. rendering이 완료되면 workInProgresscurrent가 되고, 이전 current는 다음 workInProgress를 위해 재활용된다. 이를 double buffering 패턴이라고 한다.

Reconciliation

setCount(count + 1)이 호출되면 React Scheduler가 해당 컴포넌트의 업데이트를 큐에 등록한다. Scheduler는 우선순위에 따라 작업을 처리하며, 사용자 인터랙션(탭, 입력)은 가장 높은 우선순위를 가진다.

작업이 시작되면 Reconciler가 workInProgress 트리를 구성한다:

  1. beginWork(fiber): 각 Fiber를 top-down으로 순회한다. 컴포넌트 함수를 실행하고 이전 Fiber와 비교(diff)한다. 변경이 없는 서브트리는 건너뜁니다. count가 변경됐으므로 <Text> Fiber에 Update effect가 태깅된다.

  2. completeWork(fiber): 리프 노드에서 다시 root 방향으로 올라오며 effect를 수집한다. 각 노드의 변경 사항이 부모로 전파된다.

root에 도달하면 Reconciliation이 완료되고 Commit Phase가 시작된다.

Commit Phase → Fabric 진입

React는 플랫폼별 View 생성·업데이트 코드를 Host Config Interface로 추상화한다. Web 환경에서는 React DOM이 이 Interface를 구현하여 DOM을 조작하고, React Native 환경에서는 Fabric Renderer가 이 Interface를 구현한다.

Commit Phase는 세 단계로 구성된다:

단계설명
commitBeforeMutationEffects변경 전 스냅샷 획득 (getSnapshotBeforeUpdate)
commitMutationEffects실제 변형 — createInstance, commitUpdate, removeChild 등 Host Config 호출
commitLayoutEffects변형 직후 동기 처리 (useLayoutEffect, componentDidMount/Update)

commitMutationEffects 단계에서 effect 목록을 순회하며 Fabric의 commitUpdate()가 호출된다. 여기서 React의 관할이 끝나고 React Native Phase가 시작된다.

📖 용어 설명

  • ReactElement: React.createElement()가 반환하는 plain object. { type, props, key } 구조로 View의 설명서 역할
  • Fiber: React의 내부 작업 단위. 컴포넌트별 상태, 효과, 트리 관계를 추적한다
  • double buffering: 현재 화면(current)과 작업 중인 트리(workInProgress)를 분리해 유지하는 패턴. rendering 중 이전 화면을 유지한다
  • Reconciliation: 이전 Fiber 트리와 새 ReactElement 트리를 비교해 최소한의 변경만 적용하는 과정
  • Host Config: React가 플랫폼별 코드를 추상화하는 Interface. Web은 React DOM, RN은 Fabric이 구현한다
  • Commit Phase: Reconciliation 완료 후 실제 View에 변경사항을 적용하는 단계. 세 단계로 구성된다

Phase 2: React Native (Fabric + JSI)

JSI: 동기 C++ 호출

Commit Phase에서 commitUpdate()가 호출되면, 이 호출은 JSI를 통해 C++ 코드로 직접 전달된다.

Hermes는 JSI를 통해 C++ 객체를 global 네임스페이스에 노출한다. Fabric은 이 방식으로 global.nativeFabricUIManager를 등록하여 다음과 같은 메서드를 제공한다:

global.nativeFabricUIManager.cloneNodeWithNewProps(shadowNode, newProps)

이 JS 코드가 실행되면 JSI를 통해 C++ 함수가 동기적으로 호출된다. Old Bridge처럼 메시지를 큐에 넣고 기다리는 것이 아니라, 일반적인 함수 호출처럼 즉시 실행된다.

Fabric Renderer: Shadow Tree 구성

Fabric은 React의 Host Config를 C++로 구현한 Renderer이다. 각 React 컴포넌트는 C++ 측에 대응하는 ShadowNode를 가진다.

React 컴포넌트C++ ShadowNode
<View>ViewShadowNode
<Text>ParagraphShadowNode
<Image>ImageShadowNode
<ScrollView>ScrollViewShadowNode

Shadow Tree는 불변(immutable) 이다. props가 변경되면 해당 ShadowNode를 수정하는 것이 아니라 새 ShadowNode를 생성(clone)하여 새 트리를 구성한다. 이 설계는 두 가지 이점을 준다: 여러 Thread에서 락 없이 읽을 수 있고, 이전 트리와 새 트리를 안전하게 비교(diff)할 수 있다.

Yoga: 레이아웃 계산

새 Shadow Tree가 구성되면 Yoga가 레이아웃을 계산한다.

Yoga는 Meta가 C++로 구현한 CSS Flexbox 엔진이다. 각 ShadowNode는 내부적으로 YGNode(Yoga Node)를 가지며, flexDirection, justifyContent, width, height 같은 Flexbox 속성들이 여기 저장된다.

Yoga는 Shadow Tree를 top-down으로 순회하며 각 노드의 frame{ x, y, width, height } — 을 계산한다. 이 계산은 background thread에서 수행되므로 Main thread를 차단하지 않다. 이것이 New Architecture에서 스크롤 성능이 개선된 이유 중 하나이다.

Commit → MountItem 생성

레이아웃 계산이 완료되면 Fabric은 이전 Shadow Tree와 새 Shadow Tree를 diff한다. 변경된 노드만 추려 MountItem 리스트를 만든다.

MountItem은 iOS 측에 전달할 명령이다:

MountItem의미
CreateMountItem새 View를 생성하라
UpdatePropertiesMountItemprops를 업데이트하라
UpdateLayoutMountItemframe을 업데이트하라
InsertChildMountItem자식 View를 삽입하라
RemoveChildMountItem자식 View를 제거하라
DeleteMountItemView를 삭제하라

이 MountItem 리스트가 Main thread로 전달된다. Phase 2의 레이아웃 계산까지는 background thread에서 처리됐다.

📖 용어 설명

  • ShadowNode: 각 React 컴포넌트의 C++ 표현. props, 레이아웃 정보, 자식 관계를 가진다
  • Shadow Tree: ShadowNode들로 구성된 불변 트리. UIView 계층이 만들어지기 전의 설계도이다
  • 불변(immutable) 트리: 변경 시 기존 노드를 수정하지 않고 새 노드를 생성한다. 안전한 멀티Thread 접근이 가능한다
  • YGNode: Yoga의 레이아웃 노드. Flexbox 속성과 계산된 frame 정보를 저장한다
  • MountItem: Fabric이 iOS 측에 전달하는 View 조작 명령. "무엇을 어떻게 만들어라"를 표현한다

Phase 3: iOS

이제 MountItem 리스트가 Main thread에 도착했다. iOS 측에서 실제 UIView로 변환하는 과정이다.

RCTMountingManager

RCTMountingManager는 Main thread에서 MountItem을 처리하는 진입점이다. performTransaction()이 호출되면 MountItem 리스트를 순서대로 처리한다.

ComponentDescriptor: View Factory

각 React 컴포넌트 타입은 iOS 측에 대응하는 ComponentDescriptor가 있다. ComponentDescriptor는 해당 컴포넌트 타입의 View를 생성하는 Factory 역할을 한다.

CreateMountItem이 처리될 때의 흐름:

CreateMountItem { componentHandle: ViewComponentDescriptor }
    ↓
RCTComponentViewFactory
  .createComponentViewWithComponentHandle(handle)
    ↓
ViewComponentDescriptor
  → RCTViewComponentView instance 생성

<View>RCTViewComponentView, <Text>RCTParagraphComponentView처럼, 컴포넌트 타입마다 대응하는 UIView subclass가 있다.

RCTViewComponentView: UIView에 연결

RCTViewComponentViewUIView를 상속한 클래스이다. Fabric과 UIKit 사이의 연결 역할을 한다. RCTComponentViewProtocol을 구현하여 Fabric이 호출하는 세 가지 핵심 메서드를 제공한다.

updateProps() — React props → UIView property

UpdatePropertiesMountItem이 도착하면 updateProps()가 호출된다.

// RCTViewComponentView.mm (핵심 로직 요약)
- (void)updateProps:(Props::Shared const &)props
           oldProps:(Props::Shared const &)oldProps
{
    const auto &newViewProps = *std::static_pointer_cast<const ViewProps>(props);
 
    if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
        self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
    }
    if (oldViewProps.opacity != newViewProps.opacity) {
        self.layer.opacity = (float)newViewProps.opacity;
    }
    // borderRadius, transform 등...
}

updateLayoutMetrics() — Yoga frame → UIView.frame

UpdateLayoutMountItem이 도착하면 updateLayoutMetrics()가 호출된다. Yoga가 계산한 frame이 실제 UIView.frame에 적용된다.

- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics
         oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics
{
    // Yoga가 계산한 { x, y, width, height } 를 UIView에 직접 적용한다
    CGRect frame = RCTCGRectFromRect(layoutMetrics.frame);
    self.frame = frame;
}

mountChildComponentView:atIndex: — Shadow Tree 계층 → UIView 계층

InsertChildMountItem이 도착하면 mountChildComponentView:atIndex:가 호출된다. Shadow Tree의 계층 관계가 그대로 UIView 계층으로 미러링된다.

- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
                          index:(NSInteger)index
{
    [self insertSubview:childComponentView atIndex:index];
}

setCount(count + 1) 한 줄이 결국 UIView.frame = ...insertSubview:atIndex:로 이어졌다.

CALayer → Render Server

UIView.frame이 업데이트되면 내부적으로 CALayer의 geometry가 변경된다. UIView는 직접 rendering하지 않습니다 — 모든 rendering은 view.layer (CALayer)가 담당한다.

CALayer 변경사항은 다음 Runloop iteration에서 암묵적 CATransaction을 통해 커밋된다. 커밋된 transaction은 IPC(프로세스 간 통신)를 통해 Render Server — 앱과 독립적인 별도 프로세스 — 로 전송된다.

Render Server는 Metal 명령을 GPU에 전달하여 layer tree를 rendering한다. 그 결과물인 픽셀 비트맵이 화면에 표시된다.

앱을 디버깅할 때 실행을 일시 정지해도 애니메이션이 계속 동작하는 것을 본 적 있는가? Render Server가 앱과 별도의 프로세스에서 돌아가기 때문이다.

📖 용어 설명

  • RCTMountingManager: Main thread에서 MountItem을 처리하는 진입점
  • ComponentDescriptor: 컴포넌트 타입별 View 클래스를 instance화하는 Factory. <View>RCTViewComponentView
  • RCTViewComponentView: UIView를 상속한 RN의 Native 컴포넌트 뷰. Fabric ↔ UIKit Bridge 역할
  • updateLayoutMetrics(): Yoga가 계산한 frame을 UIView.frame에 직접 적용하는 메서드
  • CALayer: UIView의 rendering 백엔드. view.layer property로 접근하며 Core Animation이 관리한다
  • Render Server: 앱 외부에서 실행되는 rendering 전용 프로세스. Metal 명령으로 GPU 드로잉을 수행한다

정리

버튼 탭 한 번의 전체 여정을 통합 다이어그램으로 다시 봅니다. 처음에 봤던 다이어그램과 같지만, 이제 각 박스의 의미가 다를 것이다.

Phase 1: React

  • JSX는 빌드 시 React.createElement()로 변환되어 ReactElement (plain object) 생성
  • React는 내부적으로 Fiber 트리를 유지. current (현재 화면) / workInProgress (작업 중) 두 트리를 동시에 관리
  • setCount 호출 → Scheduler가 우선순위에 따라 업데이트 큐 등록 → Reconciler의 work loop 시작
  • Reconciler는 변경된 Fiber에만 effect 태깅. Commit Phase에서 Host Config (Fabric)을 호출

Phase 2: React Native (Fabric + JSI)

  • JSI는 JSON 직렬화 없이 JS → C++ 직접 동기 호출을 가능하게 하는 C++ Layer
  • Fabric은 불변 Shadow Tree를 유지. props 변경 시 ShadowNode를 복제하여 새 트리 생성
  • Yoga가 새 Shadow Tree의 Flexbox 레이아웃을 background thread에서 계산
  • Shadow Tree diff → MountItem 리스트 생성 → Main thread 전달

Phase 3: iOS

  • RCTMountingManager가 Main thread에서 MountItem을 처리
  • ComponentDescriptor가 컴포넌트 타입에 맞는 RCTViewComponentView instance 생성
  • updateLayoutMetrics(): Yoga frame → UIView.frame 직접 적용
  • mountChildComponentView:atIndex:: Shadow Tree 계층 → UIView 계층 (insertSubview:atIndex:)
  • UIView.layer (CALayer) 변경 → CATransaction → Render Server (IPC) → Metal GPU → 화면