React Native의 작동원리

koreanhole·2021년 4월 17일
8

React Native앱은 크게 두가지 부분으로 구성되어 있다. Native부분과 JavaScript부분이 그것이다. Native부분은 iOS에서는 Objective-C/Swift, Android에서는 Java/Kotlin이 담당하는 곳으로 UI를 렌더한다. UI스레드(메인 스레드)라 불리는 것이 UI를 생성한다. 반면 JavaScript 부분에는 JS스레드가 있다. 이곳은 말그대로 자바스크립트 엔진을 통해 자바스크립트 코드가 실행되는 곳이다. 비지니스 로직을 포함하여 뷰를 언제, 어떻게 표시할지와 같은 React관련 자바스크립트 코드가 실행되는 스레드이다.

Native Bridge

Native부분인 Main thread와 JavaScript부분인 JS thread는 Native Bridge라는 것을 통해 소통한다. 이것은 클라이언트 - 서버가 통신하는것과 비슷하다. 클라이언트가 메인 스레드, 서버가 JS스레드인 것이다. 아무튼, 각 부분에서 전달되는 정보는 JSON object형태로 변환되어서 전달된다.

결국은 React Native앱에서 가장 중요하다고 볼 수 있는 부분이 Native Bridge이다. 이 부분이 병목현상이 가장 많이 생기며 좋은 성능의 React Native앱을 위해서는 Native Bridge를 건너는 횟수를 최소한으로 해야한다.

deadline

아이폰의 경우 1초에 60프레임을 표시할 수 있다. 이 말은 1프레임을 표시하기 위해 최대 1/60초(16.67ms)가 필요하다. React Native앱은 16.67ms안에 하나의 프레임을 생성해야 사용자가 봤을 때 애니메이션이 끊기지 않는 것이다. 만약 JS스레드의 한 이벤트 루프동안 16.67ms안에 작업을 처리하지 못하면 Native Bridge에 병목현상이 생기게 되고 프레임이 뚝뚝 끊기게 된다. 안드로이드의 경우는 1초에 90프레임, 120프레임까지 표시하는 경우도 있으니 각각 11.11ms, 8.33ms안에 하나의 프레임을 생성해야 하니 deadline이 더 긴박하다.

JS thread

React Native어플리케이션을 실행하기 위해 webpack과 비슷하게 JavaScript코드들을 하나의 번들 파일로 만든다.(main.bundle.js) 이후 JS스레드가 번들된 자바스크립트 코드를 실행하며 이벤트 루프가 끝날때마다 메인 스레드로 변경사항에 관한 정보를 전달한다. 전달되는 정보는 어떤 뷰를 표시할 건지와 같은 정보이다. ([ [2,3,[2,'Text',{...}]] [2,3,[3,'View',{...}]] ]) 다행히도 React의 효율적인 Diffing 알고리즘 덕분에 각 배치마다 UI의 최소한의 변경사항 정보만 Native Bridge를 건너게 할 수 있다.

Diffing Algorithm

React에서 우리가 선언적으로 정의하는 UI는 실제 DOM노드가 아니라 경량화된 자바스크립트 객체인 virtual DOM이다. React는 이 virtual DOM을 사용하여 최소한의 변경사항을 파악하여 새롭게 렌더하게 된다. 이때 사용하는 알고리즘이 diffing 알고리즘이다.

React에서는 트리의 레벨 별 비교 작업을 수행하며 이 작업의 시간복잡도는 O(n)에 가깝다고 한다. 보통 어플리케이션에서 컴포넌트 트리의 레벨이 변경되는 경우는 많이 없다는 점에 기인한 heuristic한 방법을 사용한다.

또한 React에서 사용자가 정의한 컴포넌트를 사용하는 경우가 많은데, 이 경우에는 diffing알고리즘은 같은 클래스인 경우에만 수행되며 다른 클래스를 갖고 있다면 비교작업을 수행하지 않고 그 컴포넌트를 추가한다. 이로 인해 두 컴포넌트를 비교하기 위한 시간을 소모하지 않는다.

Rendering

컴포넌트 안에서 setState 를 호출할 때마다 React는 이 컴포넌트를 dirty하다고 표시하고 매 event loop가 끝날때마다 dirty component를 확인하고 다시 렌더링 작업을 진행한다. 이 과정에서 자식 컴포넌트들에 대해 virtual DOM이 다시 만들어지며 비록 상태가 변경되지 않았더라도 render 메소드가 실행된다. 만약 이것이 실제 DOM이었다면 막대한 비용이 들었을 테지만 가상 DOM이기 때문에 비용이 그렇게 크지는 않다. 그리고 실제로 컴포넌트의 상태 변경이 루트 컴포넌트에서 진행되지 않으며 보통 일부 컴포넌트에서만 진행되는 '지역성'을 띄고 있기에 실제 비교횟수가 그렇게 많지도 않다.

만약 하위 컴포넌트들의 리렌더링을 직접 방지하고 싶다면 shouldComponentUpdate() 메서드를 사용하여 직접 React에 리렌더링 여부를 알려줘도 된다. 이로 인해 성능상의 이점을 챙겨갈 수 있다.

Main Thread

메인 스레드는 어플리케이션이 실행되자마자 시작된다. 앱을 로드하고 JS스레드를 실행시킨다. JS스레드의 매 이벤트 루프가 끝날때마다 Native Bridge를 통해 보내오는 메시지들을 받아 해석한 후 UI를 화면에 표시한다. 이 과정에서 shadow thread는 JS스레드로 부터 넘어오는 정보를 활용하여 화면의 layout을 계산한다. 웹에서의 reflow와 비슷하다. 또한 사용자들이 기기에 내리는 UI이벤트 명령들을 받고 Native Bridge를 경유해 JS스레드로 넘겨준다. 이때의 UI이벤트들은 press나 touch와 같은 이벤트이다.

React Native앱의 실행과정

위에서 살펴본 Main thread, JS thread, Native Bridge의 실행을 순차적으로 정리하면 다음과 같다.

  1. 앱이 시작되면서 Main thread가 실행되고 메인 스레드는 JS스레드를 실행시키고 자바스크립트 번들을 로드한다.
  2. JS스레드가 실행되면서 React는 virtual DOM을 생성하고 diffing알고리즘을 통해 변경사항을 Native Bridge를 경유하여 shadow스레드로 전달한다.
  3. shadow스레드는 변경사항 메시지를 통해 화면의 레이아웃을 계산하고 계산이 끝난 레이아웃의 파라미터나 객체를 메인 스레드로 보낸다.
  4. 메인스레드가 UI를 화면에 표시한다.
  5. 사용자가 화면에 입력한 UI이벤트 정보들이 Native Bridge를 경유하여 JS스레드로 보내진다.
  6. UI이벤트 메시지를 활용하여 JS스레드에서 비즈니스 로직들이 실행되고 React는 다시 virtual DOM을 생성하며 변경사항을 다시 Native Bridge를 경유하여 shadow thread로 전달된다.
  7. 3, 4, 5, 6과정이 반복된다.

참고자료

Performance Overview · React Native

Performance Limitations of React Native and How to Overcome Them

Investigating React Native

How React Native Works ? | Codementor

React's diff algorithm

Bridging in React Native

React Made Native Easy

1개의 댓글

comment-user-thumbnail
2023년 4월 21일

글로 잘 정리해주셔서 정말 감사합니다.

답글 달기