안녕하세요~~~ 오랜만에 React Native 포스팅으로 만나뵙습니다!! 🥹🥹
React Native에 새롭게 등장한 새로운 아키텍처 대해 아시나요? 오늘은 새로운 아키텍쳐에 대해 알아보기 전에~~~ 구 아키텍처의 흐름에 대해 먼저 알아보려고 합니다.
React Native는 React 와 동일한 메커니즘을 사용할 수 있도록 하여 Android, iOS에 맞는 API를 사용하여 어플리케이션에 UI를 그리게 됩니다.
JavaScript로 React, JSX를 통해서 컴포넌트를 구성하지만, 실제로는 우리가 작성한 JavaScript 코드가 실행되는 영역과 UI를 그려주는 네이티브 영역은 분리되어 있습니다.
어떻게 React Native 에서는 우리가 작성한 JSX 표현식을 가지고 네이티브 UI를 그리고 있을까요? 🤔🤔
React Native 프로젝트를 실행하면 2개의 스레드가 동시에 동작합니다.
(*스레드: 명령어를 실행하여 처리하는 주체로, 가장 작은 작업의 단위로 볼 수 있다.)
React Native는 JS 스레드와 Native 스레드가 서로 소통할 수 있도록 통로를 만들어 두었습니다. 이 통로를 Bridge라고 합니다. Bridge는 두 스레드 사이에서 서로 메세지-큐 방식으로 소통합니다.
React Native에서는 JSX를 통해 컴포넌트를 작성하게 되면 JSON으로 직렬화한 문자열을 Bridge를 통해 Native 스레드로 전달하게 되며, Native 스레드에서는 다시 역직렬화를 하여 네이티브 API 형식에 맞춰 UI를 그려주게 됩니다.
🤓: 예를들어, React Native에서 View, Text의 컴포넌트를 호출하면,
iOS에서는 UIView와 UILable을,
Android에서는 android.view.ViewGroup, android.view.TextView를 호출하게 됩니다.
반대로 Native 스레드에서 JS 스레드로 이벤트를 전달하는 경우도 있습니다. 🤩
유저와 화면과의 interaction이 발생하면 Native 스레드에서 JSON으로 직렬화한 문자열을 Bridge를 통해 JS 스레드의 이벤트로 들어오게 됩니다.
🤓: 예를들어, iOS 혹은 Android 기기에서 버튼을 클릭 시, React Native의 버튼 컴포넌트(ex. TouchableOpacity)의 onPress로 이벤트가 들어오게 됩니다. 위의 예시로 보자면 "클릭!"의 문자열이 로그로 찍히겠죠? 😊
단순히 View와 Text만 작성해서 추가할 뿐만 아니라, 컴포넌트의 사이즈, 색상 그리고 위치는 어떻게 할 것인지와 같은 스타일 코드를 작성하게 됩니다. React Native 에서는 이러한 레이아웃 스타일을 Native 스레드에 보내기 위해 레이아웃 엔진인 Yoga를 사용하고 있습니다.
Yoga는 C++로 작성된 레이이웃 엔진으로 다양한 프로그래밍 언어에서 사용할 수 있는 오픈소스 라이브러리입니다. 기본적으로 React Native 와 함께 작동하기 위해서 설계 되었다고 합니다. React Native는 각 플랫폼에 맞는 UI 구성 요소에 대한 세부사항을 알 필요없이 Yoga를 통해 다양한 화면 크기, 해상도 및 유형에 최적화 된 인터페이스를 만들 수 있게 됩니다.
Yoga는 일반적으로 iOS 네이티브 기본 구성요소인 Auto Layout과 UIStackView로 레이아웃을 구성하는 것보다 빠르게 렌더링 된다고 합니다.
(* 하지만 단순하게 비교한 것이며 복잡한 구성에 따라 벤치마킹 결과도 다를 수 있습니다.)
(* Flexlayout이 Yoga 입니다. 자세한건 여기를 참고해주세요.)
단순히 일부 네이티브 영역의 렌더링 속도만 빨라서는 크게 의미는 없습니다. React Native에서는 단순히 레이아웃을 그리는 것 뿐만 아니라 더 다양한 것들을 구성하고 브릿지라는 메커니즘을 통해 네이티브와 소통하기 때문에, Yoga를 사용한다고 해서 네이티브보다 더 빠르게 작동하는 것은 아닙니다.
이러한 Yoga는 JS 스레드와 Native 스레드를 연결해주는 Bridge에서 작동하고 있습니다.
React 로직을 호스트 플랫폼에 렌더링하기 위한 일련의 작업을 렌더 파이프라인이라고 합니다.
렌더 파이프라인은 일반적으로 세 가지 단계로 나눌 수 있습니다.
1. Render Phase
2. Commit Phase
3. Mount Phase
아래의 코드는 렌더 파이프에 의해 어떻게 작동될까요?
<View
style={{
width: 200,
height: 200,
backgroundColor: "blue"
}}
>
<Text>Hello World</Text>
</View>
1. Render Phase
React는 JavaScript로 React Element Tree 생성
View, Text 컴포넌트와 1:1 관계를 가진 React Element Tree를 생성합니다.
Yoga는 이에 맞는 React Shadow Tree 생성
Yoga는 생성된 React Element Tree의 레이아웃을 가진 React Shadow Tree를 생성합니다.
React Shadow Tree는 React Shadow Node로 구성되어 있습니다.
React Shadow Node: 마운트할 Component를 나타내는 객체
렌더러는 업데이트의 성능을 높이기 위해, React Shadow Tree를 복제하여 복제된 Tree에서 React Shadow Node를 업데이트 합니다. (자세한 내용은 다음에 포스팅 하겠습니다!!^^)
2. Commit Phase
React Native에서는 React Element에서 시작되는 각 React Shadow Node 의 스타일이 필요합니다. Yoga는 React Shadow Node의 위치과 크기를 계산합니다.
3. Mount Phase
크기와 위치가 계산된 레이아웃 정보를 가진 React Shadow Node를 Host View Tree로 변환하여 UI로 렌더링합니다.
이러한 Bridge 방식 프레임워크에는 단점이 있습니다.
Bridge를 통해 렌더링에 필요한 데이터들을 주고 받는 모든 과정은 비동기로
작동합니다. 이벤트에 대해서 응답할 수 없거나, 이미 요청해버린 렌더링이나 작업들에 대해선 중간에 취소할 수 없습니다. 또한 이 과정에서 모두 JSON 직렬화와 역직렬화가 일어나기 때문에 불필요한 오버헤드가 발생하게 됩니다.
그리고 Bridge는 단일 스레드이므로 우선순위가 없습니다. Bridge는 단일 스레드이고, 코드가 모두 직렬화되어 전달되므로 중요한 부분에 우선순위 없이 코드를 전달하게 됩니다.
예를 들어, 무거운 작업이 필요한 많은 수의 항목을 가진 리스트를 만든다고 가정해봅시다. 사용자가 엄청나게 빠르게 리스트를 스크롤 하면, 스크롤 도중 중간 중간에 빈 리스트 화면이 보이게 됩니다.
아래와 같은 과정을 통해서요.
- JS 스레드 -> Native 스레드: 리스트 렌더링 요청
- 사용자가 엄청 빠르게 리스트 스크롤
- Native 스레드 -> JS 스레드 : 스크롤 이벤트 전송
// 4번을 받기 전에 빈 화면을 보게 됩니다.- JS 스레드 -> Native 스레드 : 리스트 렌더링 요청
이러한 일련의 과정이 빈번하고, 무거운 요소들을 그리고, 동기적인 기능들이 중간에 배치된다면 스크롤 도중 빈 영역을 만나게 될 수 있습니다.
이러한 문제를 개선하기 위해서 React Native 팀에서는 몇 년전 부터 새로운 아키텍쳐에 대해서 발표했고, 꾸준히 업데이트 하고 있습니다. 새로운 아키텍쳐는 불필요하게 오버헤드를 불러오는 Bridge를 없애기로 결정하였습니다.
그러면 Bridge 없이 어떻게 두 영역에서 소통할 수 있을까요?
정답은 바로바로~~~ 다음 게시물에서 뵙겠습니다.^^
참고
https://reactnative.dev/architecture/render-pipeline#react-state-updates