렌더, 커밋, 마운트
이 문서는 현재 활발히 배포 중인 새로운 아키텍처를 참조합니다.
React Native 렌더러는 React 로직을 호스트 플랫폼에 렌더링하기 위해 일련의 작업을 거칩니다. 이 작업 시퀀스를 렌더 파이프라인이라고 하며, 초기 렌더링과 UI 상태 업데이트 시에 발생합니다. 이 문서에서는 렌더 파이프라인과 각 시나리오에서의 차이점을 설명합니다.
렌더 파이프라인은 크게 세 단계로 나눌 수 있습니다:
- 렌더: React는 제품 로직을 실행하여 JavaScript에서 React 엘리먼트 트리를 생성합니다. 이 트리에서 렌더러는 C++로 React 섀도우 트리를 만듭니다.
- 커밋: React 섀도우 트리가 완전히 생성된 후, 렌더러는 커밋을 트리거합니다. 이는 React 엘리먼트 트리와 새로 생성된 React 섀도우 트리를 "다음 트리"로 승격시켜 마운트할 준비를 합니다. 또한, 레이아웃 정보 계산을 스케줄링합니다.
- 마운트: 레이아웃 계산 결과가 반영된 React 섀도우 트리는 호스트 뷰 트리로 변환됩니다.
렌더 파이프라인의 각 단계는 서로 다른 스레드에서 발생할 수 있습니다. 자세한 내용은 스레딩 모델 문서를 참고하세요.
초기 렌더링
다음과 같은 코드를 렌더링한다고 가정해 보겠습니다.
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent />
위 예제에서 <MyComponent />
는 React 엘리먼트입니다. React는 이 _React 엘리먼트_를 재귀적으로 터미널 React 호스트 컴포넌트로 축소합니다. 이를 위해 React는 엘리먼트를 호출하거나(또는 JavaScript 클래스로 구현된 경우 render
메서드를 호출) 더 이상 축소할 수 없을 때까지 반복합니다. 이제 여러분은 React 호스트 컴포넌트로 구성된 _React 엘리먼트 트리_를 갖게 됩니다.
Phase 1. 렌더링
엘리먼트 축소 과정에서 각 _React Element_가 호출될 때, 렌더러는 동기적으로 React Shadow Node를 생성합니다. 이는 _React Host Components_에만 해당하며, React Composite Components에는 적용되지 않습니다. 위 예제에서 <View>
는 ViewShadowNode
객체를 생성하고, <Text>
는 TextShadowNode
객체를 생성합니다. 특히, <MyComponent>
를 직접 나타내는 _React Shadow Node_는 존재하지 않습니다.
React가 두 React Element Nodes 사이에 부모-자식 관계를 생성할 때마다, 렌더러는 해당 React Shadow Nodes 사이에 동일한 관계를 생성합니다. 이렇게 _React Shadow Tree_가 조립됩니다.
추가 정보
- React Shadow Node 생성 및 두 React Shadow Nodes 사이의 부모-자식 관계 생성 작업은 동기적이며 스레드 안전한 작업으로, React(JavaScript)에서 렌더러(C++)로 실행되며 일반적으로 JavaScript 스레드에서 수행됩니다.
- React Element Tree(및 그 구성 요소인 React Element Nodes)는 영구적으로 존재하지 않습니다. 이는 React에서 "fibers"에 의해 구체화된 일시적인 표현입니다. 호스트 컴포넌트를 나타내는 각 "fiber"는 JSI를 통해 _React Shadow Node_에 대한 C++ 포인터를 저장합니다. 이 문서에서 "fibers"에 대해 더 알아보세요.
- _React Shadow Tree_는 불변입니다. _React Shadow Node_를 업데이트하려면 렌더러는 새로운 _React Shadow Tree_를 생성합니다. 그러나 렌더러는 상태 업데이트를 더 효율적으로 수행하기 위해 복제 작업을 제공합니다. 자세한 내용은 React State Updates를 참조하세요.
위 예제에서 렌더링 단계의 결과는 다음과 같습니다:
_React Shadow Tree_가 완성된 후, 렌더러는 _React Element Tree_의 커밋을 트리거합니다.
Phase 2. Commit
커밋 단계는 **레이아웃 계산(Layout Calculation)**과 트리 승격(Tree Promotion) 두 가지 작업으로 구성됩니다.
- 레이아웃 계산: 이 작업은 각 _React Shadow Node_의 위치와 크기를 계산합니다. React Native에서는 Yoga를 호출하여 각 _React Shadow Node_의 레이아웃을 계산합니다. 실제 계산에는 JavaScript의 _React Element_에서 비롯된 각 _React Shadow Node_의 스타일이 필요합니다. 또한, _React Shadow Tree_의 루트에 대한 레이아웃 제약 조건도 필요합니다. 이 제약 조건은 결과 노드가 차지할 수 있는 사용 가능한 공간의 양을 결정합니다.
- 트리 승격 (New Tree → Next Tree): 이 작업은 새로운 _React Shadow Tree_를 "다음 트리(next tree)"로 승격시킵니다. 이 승격은 새로운 _React Shadow Tree_가 마운트될 모든 정보를 가지고 있으며, _React Element Tree_의 최신 상태를 나타냄을 의미합니다. "다음 트리"는 UI 스레드의 다음 "틱(tick)"에서 마운트됩니다.
추가 정보
- 이 작업들은 백그라운드 스레드에서 비동기적으로 실행됩니다.
- 대부분의 레이아웃 계산은 C++ 내에서 완전히 실행됩니다. 그러나 일부 컴포넌트의 레이아웃 계산은 호스트 플랫폼(예:
Text
,TextInput
등)에 의존합니다. 텍스트의 크기와 위치는 각 _호스트 플랫폼_에 따라 다르며, 호스트 플랫폼 레이어에서 계산되어야 합니다. 이를 위해 Yoga는 _호스트 플랫폼_에 정의된 함수를 호출하여 컴포넌트의 레이아웃을 계산합니다.
Phase 3. Mount
마운트 단계는 React 상태 업데이트의 마운트 단계와 거의 동일합니다. 렌더러는 여전히 레이아웃을 다시 계산하고, 트리 차이를 수행하는 등의 작업을 해야 합니다. 자세한 내용은 위 섹션을 참고하세요.
React State Updates
_React Element Tree_의 상태가 업데이트될 때 렌더 파이프라인의 각 단계를 살펴보겠습니다. 초기 렌더링에서 다음과 같은 컴포넌트를 렌더링했다고 가정해 보겠습니다.
function MyComponent() {
return (
<View>
<View
style={{backgroundColor: 'red', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
);
}
초기 렌더링 섹션에서 설명한 내용을 적용하면 다음과 같은 트리가 생성될 것으로 예상할 수 있습니다.
Node 3은 빨간색 배경의 호스트 뷰에 매핑되고, Node 4는 파란색 배경의 호스트 뷰에 매핑됩니다. JavaScript 제품 로직에서 상태 업데이트의 결과로 첫 번째 중첩된 <View>
의 배경색이 'red'
에서 'yellow'
로 변경되었다고 가정해 보겠습니다. 새로운 _React Element Tree_는 다음과 같이 보일 수 있습니다.
<View>
<View
style={{backgroundColor: 'yellow', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
React Native는 이 업데이트를 어떻게 처리할까요?
상태 업데이트가 발생하면, 렌더러는 이미 마운트된 호스트 뷰를 업데이트하기 위해 개념적으로 _React Element Tree_를 업데이트해야 합니다. 하지만 스레드 안전성을 보장하기 위해 _React Element Tree_와 _React Shadow Tree_는 불변(immutable)이어야 합니다. 이는 현재의 _React Element Tree_와 _React Shadow Tree_를 변경하는 대신, 새로운 props, 스타일, 자식을 포함한 새로운 트리의 복사본을 생성해야 함을 의미합니다.
상태 업데이트 동안 렌더 파이프라인의 각 단계를 살펴보겠습니다.
Phase 1. Render_m5wcVF2cWKJycqbDVjA6eM
React가 새로운 상태를 반영한 _React Element Tree_를 생성할 때, 변경 사항에 영향을 받는 모든 _React Element_와 _React Shadow Node_를 복제해야 합니다. 복제가 완료되면 새로운 _React Shadow Tree_가 커밋됩니다.
React Native 렌더러는 불변성을 유지하면서도 오버헤드를 최소화하기 위해 구조적 공유를 활용합니다. 새로운 상태를 포함하기 위해 _React Element_를 복제할 때, 루트까지의 경로에 있는 모든 _React Element_가 복제됩니다. React는 props, 스타일, 또는 자식 요소에 업데이트가 필요한 경우에만 _React Element_를 복제합니다. 상태 업데이트로 변경되지 않은 _React Elements_는 이전 트리와 새로운 트리에서 공유됩니다.
위 예제에서 React는 다음과 같은 작업을 통해 새로운 트리를 생성합니다:
- CloneNode(Node 3,
{backgroundColor: 'yellow'}
) → Node 3' - CloneNode(Node 2) → Node 2'
- AppendChild(Node 2', Node 3')
- AppendChild(Node 2', Node 4)
- CloneNode(Node 1) → Node 1'
- AppendChild(Node 1', Node 2')
이 작업이 완료되면, **Node 1'**이 새로운 _React Element Tree_의 루트가 됩니다. 이전에 렌더링된 트리를 T, 새로운 트리를 **T'**로 지정해 보겠습니다:
T와 **T'**가 Node 4를 공유하는 방식을 주목하세요. 구조적 공유는 성능을 향상시키고 메모리 사용량을 줄입니다.
Phase 2. Commit_QpwxMv5E9jLHvTr77UwU9a
React가 새로운 _React Element Tree_와 _React Shadow Tree_를 생성한 후, 이를 커밋해야 합니다.
- 레이아웃 계산: 초기 렌더링 중의 레이아웃 계산과 유사합니다. 중요한 차이점은 레이아웃 계산이 공유된 _React Shadow Node_를 복제할 수 있다는 것입니다. 이는 공유된 _React Shadow Node_의 부모가 레이아웃 변경을 일으키면, 공유된 _React Shadow Node_의 레이아웃도 변경될 수 있기 때문입니다.
- 트리 승격 (새 트리 → 다음 트리): 초기 렌더링 중의 트리 승격과 유사합니다.
Phase 3. Mount_CTsfpREUry8X5FTEJrTkn7
- 트리 승격 (Next Tree → Rendered Tree): 이 단계에서는 "다음 트리"를 "이전에 렌더링된 트리"로 원자적으로 승격시켜, 다음 마운트 단계에서 올바른 트리에 대한 차이를 계산할 수 있도록 합니다.
- 트리 차이 계산: 이 단계에서는 "이전에 렌더링된 트리" (T)와 "다음 트리" (T') 사이의 차이를 계산합니다. 결과는 _호스트 뷰_에 수행될 원자적 뮤테이션 작업 목록입니다.
- 위 예제에서 작업은 다음과 같습니다:
UpdateView(**Node 3**, {backgroundColor: 'yellow'})
- 차이는 현재 마운트된 트리와 새로운 트리 사이에 대해 계산될 수 있습니다. 렌더러는 트리의 일부 중간 버전을 건너뛸 수 있습니다.
- 위 예제에서 작업은 다음과 같습니다:
- 뷰 마운팅: 이 단계에서는 원자적 뮤테이션 작업을 해당 _호스트 뷰_에 적용합니다. 위 예제에서는 View 3의
backgroundColor
만 업데이트됩니다 (노란색으로).
React Native 렌더러 상태 업데이트
_Shadow Tree_의 대부분 정보에서 React는 단일 소유자이자 단일 진실 공급원입니다. 모든 데이터는 React에서 시작되며 데이터는 단방향으로 흐릅니다.
하지만 한 가지 예외와 중요한 메커니즘이 있습니다: C++의 컴포넌트는 JavaScript에 직접 노출되지 않는 상태를 포함할 수 있으며, JavaScript가 진실 공급원이 아닙니다. C++과 _Host Platform_이 이 _C++ State_를 제어합니다. 일반적으로 이 기능은 _C++ State_가 필요한 복잡한 _Host Component_를 개발할 때만 관련이 있습니다. 대부분의 _Host Component_는 이 기능이 필요하지 않습니다.
예를 들어, ScrollView
는 이 메커니즘을 사용해 렌더러에게 현재 오프셋을 알립니다. 업데이트는 host platform, 특히 ScrollView
컴포넌트를 나타내는 호스트 뷰에서 트리거됩니다. 오프셋 정보는 measure와 같은 API에서 사용됩니다. 이 업데이트는 호스트 플랫폼에서 시작되며 React Element Tree에 영향을 미치지 않기 때문에, 이 상태 데이터는 _C++ State_에 보관됩니다.
개념적으로 C++ State 업데이트는 위에서 설명한 React State Updates와 유사합니다. 두 가지 중요한 차이점이 있습니다:
- React가 관여하지 않기 때문에 "렌더 단계"를 건너뜁니다.
- 업데이트는 메인 스레드를 포함한 모든 스레드에서 시작되고 발생할 수 있습니다.
Phase 2. Commit_QjUXbVms9dXBYiLThbwnxk
C++ State 업데이트를 수행할 때, 코드 블록은 ShadowNode
(N)의 업데이트를 요청하여 _C++ State_를 값 S로 설정합니다. React Native 렌더러는 N의 최신 커밋된 버전을 반복적으로 가져오고, 새로운 상태 S로 복제한 후, **N’**을 트리에 커밋하려고 시도합니다. 만약 React나 다른 C++ State 업데이트가 이 과정 중에 다른 커밋을 수행했다면, C++ State 커밋은 실패하고 렌더러는 커밋이 성공할 때까지 C++ State 업데이트를 여러 번 재시도합니다. 이는 진실의 원천 충돌과 경쟁 상태를 방지합니다.