React Native의 렌더링 프로세스

이영훈·2025년 12월 16일

React Native

목록 보기
2/3

React Native 렌더러는 React 로직을 호스트 플랫폼에 렌더링하기 위해 일련의 작업을 거친다. 이 작업 순서를 ‘렌더 파이프라인’이라고 하며, 여기서 말하는 호스트 플랫폼이란 Android, iOS등 네이티브 플랫폼을 뜻한다.

렌더링 파이프라인은 크게 세 가지 단계로 나뉜다.

1. Render

  • React는 자바스크립트에서 React 요소 트리를 생성하는 로직을 실행합니다. 이 트리에서 렌더러는 C++로 React Shadow Tree를 생성한다.

2. Commit

  • React Shadow Tree가 완전히 생성된 후, 렌더러가 커밋을 트리거한다.
  • 이로 인해 React Element 트리와 새로 생성된 React Shadow Tree가 모두 '다음 트리'로 승격하게 된다. 이 과정은 레이아웃 정보 계산도 일정에 포함한다.

3. Mount

  • 레이아웃 계산 결과를 바탕으로 React Shadow Tree는 Host View Tree로 변환된다.

Host View Tree

  • 호스트 플랫폼(Ex. Android, iOS)에서 뷰의 트리 표현
  • 예를 들어 Android에서 Host View는 Host View Tree의 구성 요소인 android.view.ViewGroup, android.widget.TextView 등의 인스턴스이다.
  • 각 호스트 뷰의 크기와 위치는 Yoga로 계산한 LayoutMetrics를 기반으로 하며, 각 호스트 뷰의 스타일과 내용은 React Shadow Tree의 정보를 기반으로 한다.

위 렌더 파이프라인의 단계는 서로 다른 스레드에서 발생할 수 있다. 이게 무슨 소리인지 이해하려면 RN의 스레딩 모델 개념을 알아야 하므로, Threading Model에 대해서 살펴보자.


이제 초기 렌더링 또는 상태가 업데이트 됐을 때의 렌더링 프로세스가 어떻게 진행되는지 더 자세히 알아보자.

1. Initial Render - 초기 렌더링

아래 예제 코드를 렌더링한다고 해보자.

function MyComponent() {
  return (
    <View>
      <Text>Hello, World</Text>
    </View>
  );
}

// <MyComponent />

여기서 <MyComponent />는 React 엘리먼트이다.

React는 이 React 엘리먼트를 재귀적으로 실행하면서, 더 이상 쪼갤 수 없는 최종 React Host Component가 될 때까지 변환한다.(이 과정에서 함수 컴포넌트라면 해당 함수를 호출하고, 클래스 컴포넌트라면 render 메서드를 호출한다.)

React Host Component❓
실제 호스트 플랫폼(IOS, Android)에 연결되는 컴포넌트

  • Ex. <View>, <Text>

이 과정을 모두 거치고 나면, 모든 노드가 View, Text와 같은 React Host Component로만 이루어진 React Element 트리가 만들어진다.

이제 초기 렌더링 시에 각 단계가 어떻게 진행되는지 더 자세히 알아보자.

1. Render

위에서 말했던 React가 React Element를 하나씩 변환해나가는 과정에서, 각 React Element가 실행될 때 렌더러는 동시에 React Shadow Node를 동기적으로 생성한다.

단, 이 Shadow Node가 모든 컴포넌트에 대해 생성되는 것은 아니다.

  • React Host Component(Ex: <View>, <Text>)에 대해서만 생성된다.
  • React Composite Component(예: <MyComponent>)에 대해서는 생성되지 않는다.

위 예제를 기준으로 하면 아래와 같다.

  • <View> → ViewShadowNode 객체 생성
  • <Text> → TextShadowNode 객체 생성
  • <MyComponent> → 이에 해당하는 Shadow Node는 생성되지 않음

React가 두 개의 React Element 사이에 부모 - 자식 관계를 생성할 때마다, 렌더러는 이에 대응되는 React Shadow Node 사이에도 동일한 부모 – 자식 관계를 생성한다.

위 예제 기준으로 렌더 페이즈를 거친 결과는 다음과 같다.

React 쉐도우 트리가 완성된 후, 렌더러는 React Element Tree의 커밋을 트리거한다.


2. Commit

커밋 단계는 두 가지 작업으로 구성된다.

  • 레이아웃 계산 (Layout Calculation)
  • 트리 승격 (Tree Promotion)

2-1 레이아웃 계산

이 단계에서는 각 React Shadow Node의 위치와 크기를 계산한다.

React Native에서는 이를 위해 Yoga 엔진을 호출하여 각 React Shadow Node의 레이아웃을 계산한다.

이 계산에는 다음 정보들이 필요하다.

  • 각 React Shadow Node가 가지고 있는 스타일 정보
    • 이 스타일들은 JavaScript에서 정의된 React Element로부터 전달된다.
  • React Shadow Tree의 루트 노드에 대한 레이아웃 제약 조건
    • 화면 전체에서 사용 가능한 공간이 얼마나 되는지를 의미하며, 이 값에 따라 자식 노드들이 차지할 수 있는 영역이 결정된다.


즉, 이 단계는 “각 View와 Text가 화면의 어디에, 어떤 크기로 배치될지”를 계산하는 과정이다.

2-2 트리 승격

이 단계에서는 새롭게 만들어진 React Shadow Tree를 다음에 마운트될 트리(next tree)로 승격시킨다.

새 React Shadow Tree는 마운트에 필요한 모든 정보를 이미 갖추고 있으며, 현재 React Element Tree의 가장 최신 상태를 정확히 반영하고 있다.

이렇게 승격된 next tree는 UI 스레드의 다음 틱(tick)에 실제로 화면에 마운트된다.

tick❓
일반적으로 소프트웨어와 특히 UI 스레드에서 시간의 단위를 나타내는 용어다.

  • React에서의 “tick”이란 UI 업데이트가 일어나는 주기를 의미한다.
  • React는 화면에 변경 사항을 적용하기 위해 비동기적으로 작업을 큐에 넣고, 그 큐의 작업이 실행되는 순간을 "tick"이라고 한다.
  • 따라서 이벤트 루프와 관련이 있으며, UI 스레드는 이러한 "tick"을 통해 변경사항을 애플리케이션에 반영한다.

3. Mount

Mount 단계에서는 레이아웃 정보가 포함된 React Shadow Tree를 실제 화면에 보이는 네이티브 View 트리로 변환한다. 이 단계가 끝나면 화면에 실제 픽셀이 그려진다.

아래와 같은 React 코드가 있다고 해보자.

<View>
  <Text>Hello, World</Text>
</View>

React Native 렌더러는 각 React Shadow Node에 대해 대응되는 호스트 뷰를 생성하고 화면에 마운트한다.

Android에서는 아래와 같이 진행된다.

  • → android.view.ViewGroup 생성
  • → android.widget.TextView 생성
  • "Hello, World" 문자열이 TextView에 설정됨

iOS에서는 아래와 같이 진행된다.

  • → UIView
  • → UILabel 또는 Text 관련 UIKit 컴포넌트
  • NSLayoutManager를 통해 텍스트 레이아웃 처리

각 호스트 뷰는 React Shadow Node의 props를 사용하도록 설정되며, 크기와 위치는 계산된 레이아웃 정보를 바탕으로 구성된다.


더 자세히 살펴보면, 마운트 단계는 다음 세 단계로 구성된다.

3-1. Tree Diffing

이 단계에서는 이전에 화면에 그려져 있던 트리와 새로 그릴 트리를 C++을 통해 비교한다.

그 결과는 호스트 뷰에서 수행해야 할 mutation에 해당하는 createViewupdateViewremoveViewdeleteView 등으로 이루어진 최소 단위 명령 목록이 되며, 이 단계에서 View Flattening도 수행된다.

View Flattening❓
불필요한 Host View를 만들지 않도록 구조를 평탄화하는 작업이다.
자세한 내용은 공식문서 링크를 참고하자.

3-2. Tree Promotion (Next Tree → Rendered Tree)

이 단계에서는 “다음 트리”를 “이전에 렌더링된 트리”로 승격하여 다음 마운트 단계에서 적절한 트리에 대한 차이를 계산한다.

즉, “이제 이 트리가 현재 화면 상태”라고 상태를 교체하는 작업이다.

3-3. View Mounting

이 단계에서는 Diffing 단계에서 만든 명령들을 실제 네이티브 View에 적용하며, 호스트 플랫폼의 UI 스레드에서 실행된다.


2. React State Updates - 리액트 상태 업데이트

위에서는 초기 렌더링 시 RN이 어떤 과정을 거쳐서 최종적으로 실제 화면에 렌더링하는지에 대한 과정을 살펴봤다.

하지만 우리가 훨씬 더 많이 겪는 상황은 초기 렌더링보다는 상태가 변경됐을 때 일 것이다. 이번에는 리액트의 상태가 변경됐을 때 어떤 렌더링 프로세스를 거치는지 자세히 살펴보자.

다음과 같은 컴포넌트가 있다고 해보자.

function MyComponent() {
  return (
    <View>
      <View
        style={{backgroundColor: 'red', height: 20, width: 20}}
      />
      <View
        style={{backgroundColor: 'blue', height: 20, width: 20}}
      />
    </View>
  );
}

초기 렌더링이 완료된 상태는 다음과 같을 것이다.

여기서 다음과 같이 기존 ‘red’ 색상에서 ‘yellow'로 변경되었다면, React Native에서는 어떻게 처리할까?

<View>
  <View
    style={{backgroundColor: 'yellow', height: 20, width: 20}}
  />
  <View
    style={{backgroundColor: 'blue', height: 20, width: 20}}
  />
</View>

상태가 업데이트되면, 렌더러는 이미 화면에 마운트되어 있는 호스트 뷰를 업데이트하기 위해, 개념적으로 React Element Tree를 다시 계산해야 한다.

여기서 “개념적으로 다시 계산”이라는 뜻은 기존에 그려진 화면을 직접 수정하는 것이 아니라, “새로운 상태의 React Element Tree의 모습”을 다시 계산한다는 의미이다.

하지만 스레드 안전성을 보장하기 위해(여러 스레드가 동시에 접근할 수 있으므로) React Element Tree와 React Shadow Tree는 불변이어야 한다.

따라서 기존 Element Tree, Shadow Tree를 직접 변경 하지 않고 새로운 props, styles, children이 반영된 새로운 트리 복사본을 생성한다.

이제 각 단계별로 그 과정을 자세히 살펴보자.

1. Render

React가 새로운 상태를 반영한 새로운 Element Tree를 만들 때, 변경의 영향을 받는 모든 Element와 Shadow Node를 복제해야 한다.

이 작업이 끝나면, 새로 만들어진 Shadow Tree가 커밋 단계로 넘어갈 준비를 하게 된다.

RN 렌더러는 불변성으로 인한 성능 비용을 줄이기 위해 구조적 공유를 활용한다.

변경된 상태를 포함하기 위해 React Element가 복제될 때, 해당 Element에서 루트까지 이어지는 경로상에 있는 Element들만 복제하며, React는 props, style, children에 대한 업데이트가 필요한 경우에만 React Element를 복제하기 때문에 상태가 변경되지 않은 Element들은 이전 트리와 새로운 트리에서 그대로 공유된다.

따라서 위 예시에서는 React가 다음과 같은 과정을 통해 새로운 트리를 생성한다.

  1. CloneNode(노드 3, {backgroundColor: 'yellow'}) → 노드 3'
  2. CloneNode(노드 2) → Node 2'
  3. AppendChild(노드 2', 노드 3')
  4. AppendChild(노드 2', 노드 4)
  5. CloneNode(노드 1) → Node 1'
  6. AppendChild(노드 1', 노드 2')

이 연산이 완료되면, Node 1'은 새로운 React Element Tree의 루트가 된다.

여기서 이전에 렌더링된 트리를 T라고 하고, 새로운 트리를 T'라고 해보자.

여기서 T와 T'가 모두 노드 4를 공유한다는 점을 주목하자. 구조적 공유는 성능을 향상시키고 메모리 사용량을 줄인다.


2. Commit

React가 새로운 Element Tree와 Shadow Tree를 생성한 이후에는 반드시 커밋해야 한다.

커밋 단계는 초기 렌더링과 같이 두 가지 단계를 거친다.

2-1 Layout Calculation: 레이아웃 계산

이 단계는 초기 렌더링 시 레이아웃 계산과 유사하다.

중요한 차이점은 레이아웃 계산 과정에서 공유되고 있던 Shadow Node가 복제될 수 있다는 점이다.

공유된 Shadow Node의 부모 노드에 레이아웃 변경이 발생하면, 해당 Shadow Node의 레이아웃 역시 변경될 수 있기 때문이다.

2-2 Tree Promotion(New Tree → Next Tree): 트리 승격

이 단계는 초기 렌더링 시의 트리 승격과 동일한 방식으로 동작한다.


3. Mount

3-1. Tree Promotion (Next Tree → Rendered Tree): 트리 승격

이 단계에서는 “다음 트리”를 원자적으로 “이전에 렌더링된 트리”로 승격시킨다. 이를 통해 다음 마운트 단계에서 올바른 트리를 기준으로 diff를 계산할 수 있게 된다.

3-2. Tree Diffing

이 단계에서는 “이전에 렌더링된 트리”(T)와 “다음 트리”(T') 사이의 차이를 계산한다. 그 결과로 호스트 뷰에 적용해야 할 원자적인 변경 연산들의 목록(UpdateView, CreateView 등으로 이루어진 최소 단위 명령 목록)이 생성된다.

예를 들어 위 예제에서의 연산은 다음과 같다

UpdateView(**Node 3**, {backgroundColor: 'yellow'})

diff는 현재 마운트되어 있는 모든 트리에 대해 새 트리로 계산할 수 있다. 또한 렌더러는 중간에 생성되었던 일부 트리 버전을 건너뛸 수도 있다.

3-3. View Mounting

이 단계에서는 mutation 연산을 해당하는 호스트 뷰에 적용한다. 위 예시에서는 View 3의 backgroundColor가 노란색으로 업데이트되면서 최종적으로 상태 변경이 완료된다.

profile
안녕하세요 프론트엔드 개발자 이영훈입니다.

0개의 댓글