Post

React Native 새로운 아키텍처(New Architecture)

0.76부터 완전히 적용된 RN New Architecture에 대해 알아보자

React Native 새로운 아키텍처(New Architecture)

Old Architecture?


이 전까지의 아키텍처는 한마디로 브릿지 방식 이다.

즉, 지금까지 리액트 네이티브는 자바스크립트와 리액트를 사용하여 크로스 플랫폼 형태의 모바일 앱을 만들순 있지만, 실제 디바이스에서 렌더링을 담당하는 부분은 iOS, Android 각 플랫폼의 “네이티브 컴포넌트” 를 사용해서 이뤄진다.

한마디로 개발자는 자바스크립트 및 리액트를 통해 어떤 인터페이스를 호출하는지에 대한 요구값을 “브릿지” 에 전달하면 브릿지는 네이티브 모듈에 전달하고, 결과값을 전달 받아 UI를 표현한다.

그러나 브릿지 통신은 결정적인 단점이 있는데 바로 “무조건적인 비동기적 통신” 이다. 일반적으로 비동기적 통신은 네트워크 요청(서버 API 호출) 이나 혹은 시간이 비교적 오래 걸리는 병렬 처리 작업에서 효율적으로 알려져 있다. 하지만 브릿지 통신은 간단한 계산 작업, UI 호출과 같은 작업에도 무조건 비동기적으로 통신이 이루어진다.

쉽게 표현하자면, 브릿지 방식은 모든 요청을 우편으로 보내는 것과 같다. 복잡하고 간단한 작업까지 모두 편지를 쓰고, 우체국에 가고, 상대방이 답장을 보내기까지 기다려야 한다.

이에 브릿지 통신의 단점으로는 다음과 같은 것들이 존재한다.

  • “병목 현상 유발” : 작업 요청이 길거나 큰 객체 처리가 많아질 경우, JSON 직렬화가 잦기 때문에 “병목 현상” 을 유발한다. 이로 인해 60FPS 이상을 안정적으로 달성하기 어려워지며 사용자 입장에서 버벅거림을 전달할 수 있다.
  • “쓰레드 간 동기화되지 않음” : 자바스크립트와 네이티브 레이어가 “완전히 동기화되지 않는 문제” 도 존재한다. 서로 다른 쓰레드에서 양방향 통신이 이루어지다 보니 늦은 반응으로 인해 실제 나타나야 할 공간에 빈 공간이 표시되거나 혹은 중간에 상태 렌더링으로 인해 의도치 않게 UI가 변경되는 버그 등이 발생한다.
  • “단일 쓰레드에서 계산” : 기존 아키텍처는 자바스크립트 스레드가 모든 작업(UI 업데이트, 사용자 입력 처리 등)을 혼자 처리하기 때문에 작업량이 많아지면 병목 현상에 의한 블로킹(blocking) 문제가 발생할 수 있었다. 이에 따라 사용자 입력과 같은 긴급한 업데이트를 처리할 수 없었고, 레이아웃 효과를 읽어 툴팁의 위치를 업데이트하는 등 레이아웃을 동기적으로 읽을 수 없었다.

비동기화로 인한 렌더링의 문제 예시

이 모든 문제는 “리액트의 동시(concurrent) 기능을 적절하게 접목하지 못하게 만들었다.”

New Architecture?


기존 브릿지 방식의 오래된 아키텍처를 보완하기 위해 Meta는 2018년부터 주요 시스템을 완전히 재설계하려고 노력했다.

재설계에 해당 하는 큰 요소로 다음 네 가지를 포함한다.

  1. “새로운 네이티브 모듈 시스템” : JSI / Fabric / Turbo Modules / CodeGen
  2. “새로운 렌더러” : Fabric
  3. “이벤트 루프”
  4. “브릿지 제거”

물론 실제 사용하는 개발자는 이러한 내부 시스템 작동 방식을 전혀 신경 쓰지 않아도 되지만, 재설계를 통해 성능을 개선하고 일부 새로운 기능을 제공한다.

이 아키텍처의 장점은 모든 작업이 “백그라운드 스레드” 에서 수행되기 때문에 업데이트를 렌더링하거나 네이티브 모듈 함수 호출을 처리하는 동안 “메인 스레드가 차단되지 않는다는 것 (다중 쓰레드로 동작)” 이다.

또한, 실제 앱을 사용하는 유저는 최대한 네이티브 앱처럼 즉각적인 피드백과 부드러운 UI 전환을 느껴지기 기대한다. 기존 브릿지 방식의 아키텍처는 비동기식 업데이트만 가능했기 때문에 이러한 부분에 허들이 작용했으나, 새로운 아키텍처는 “비동기식과 동기식 업데이트를 모두 허용하도록 설계되어 있다.”

JavScript Interface (JSI)


Old Architecture에서의 브릿지 통신 방식은 자바스크립트 쓰레드와 네이티브 쓰레드 간의 통신을 가능한게 한다.

달리 말하자면, “자바스크립트와 네이티브는 서로를 인식하지 못한다는 것을 의미하며 자바스크립트 쓰레드에서 네이티브 쓰레드의 메소드를 직접 호출할 수가 없다는 뜻과 같다.”

New Architecture에서 브릿지는 자바스크립트 엔진이 네이티브 영역의 메소드를 직접 invoke/call 할 수 있는 “C++” 로 가볍게 작성되었고 “범용 레이어인 JSI라는 모듈로 대체된다.” 범용이란, 기존 아키텍처에서 사용하던 JavaScriptCore, Hermes 엔진 외 Chakra, V8과 같은 다른 자바스크립트 엔진을 사용할 수 있다는 의미이다. C++로 작성되었기 때문에 iOS, Android 뿐만 아니라 Windows, macOS를 포함한 모든 플랫폼에서 작동하기에 일관된 렌더링 기능을 제공하고 각 플랫폼에 대해 별도로 구현할 필요성이 없어진다.

JSI에서 자바스크립트가 네이티브 메소드를 직접 호출할 수 있게 하는 방법


결론적으로 자바스크립트는 네이티브 모듈을 직접 건드릴 수 있는 “참조” 를 가지게 되고, JSI를 통해 이 참조의 메소드를 직접 호출할 수 있다.

네이티브 메소드는 JSI를 통해 C++ Host Objects를 거쳐 자바스크립트에 노출된다. 이는 자바스크립트가 네이티브 메소드를 직접 참조하여 호출할 수 있다.

Fabric vs 기존 렌더러


Fabric이란, UI Manager를 대체할 새로운 렌더링 시스템이다.

Fabric의 장점을 알아보기 전에 현재 RN에서 UI가 어떻게 렌더링되는지 알아보자.

  1. “ReactElementTree(JavaScript)” : 앱이 실행되면 React 코드를 실행하고 자바스크립트로 ReactElementTree를 생성
  2. “ReactShadowTree(C++)” : ReactElementTree를 기반으로 렌더러는 C++로 ReactShadowTree를 생성
  3. “HostViewTree(Native)” : ReactShadowTree는 UI Element의 레이아웃을 계산하는 데 사용되며 결과가 나오면 Native Elements로 구성된 HostViewTree로 변환 (ex. iOS에서 <View /> -> UIView 변환)

이 방식의 문제점은 다음과 같다.

  • “비효율성” : 쓰레드 간 모든 통신이 브릿지를 통해 이루어지다 보니 전송 속도가 느리고 불필요한 데이터 복사가 발생한다.
  • “쓰레드 간 동기화 되지 않음” : 자바스크립트 쓰레드와 네이티브 쓰레드가 동기화되지 않아 디바이스에서 60FPS 이상의 프레임을 따라갈 수 없음 (ex. 방대한 목록이 있는 FlatList)
  • “두 개의 계층/DOM 노드를 유지” : 자바스크립트와 네이티브가 동일한 목적이지만 서로 다른 언어로 각 영역에서도 노드를 관리하고 있음. 즉, 두 개의 계층에 각각 저장되고 각각 관리되고 있으며 이는 동기화 되지 않는 문제를 일으킴.

이를 위해 “Fabric” 이라는 새로운 렌더링 시스템이 개발되었다.

JSI에서 살펴보았듯이 자바스크립트는 C++로 작성된 참조를 통해 네이티브 모듈에 직접 접근할 수 있게 되었는데 여기에 UI 메소드도 포함된다. 이를 통해 브릿지로 비효율적인 양방향 통신이 없어도 직접 통신하기에 위 문제점을 보완할 수 있게 된다. “API 요청과 같은 작업은 비동기적으로 통신이 이루어져도 스크롤, 제스처와 같은 단순한 요청은 메인 쓰레드와 네이티브 쓰레드에서 “동기적”으로 실행할 수 있기 때문에” UI에 즉각적인 피드백을 수용하여 부드러운 동작을 표현할 수 있게된다.

또한, 자바스크립트와 네이티브가 “동일한 데이터를 참조” 하기 때문에 데이터를 복사하지 않고 immutable 객체 로 관리된다. 이로 인해 자바스크립트 쓰레드와 네이티브 쓰레드 간에 양쪽에 모두 공유되어 상호 작용이 가능하다. 즉, 기존 아키텍처에서 두 개의 계층/DOM 노드를 유지하여 메모리 소모를 유발했던 방식과 달리 하나의 값을 두 영역 간에 공유되기 때문에 메모리 소모를 줄이는 데 도움이 된다.

즉, Fabric은 자바스크립트 쓰레드에서 모든 작업을 담당하는 기존 아키텍처와 달리, 자바스크립트와 네이티브 간에 동시에 작업을 지원하는 “다중 쓰레드” 로 동작하기에 가능하다. 또한, 여러 동시성 업데이트 시 우선순위를 부여하여 처리하기 때문에 UI가 더욱 응답성이 좋아지고 끊김이 없는 경험을 제공할 수 있다.

Turbo Modules


기존 아키텍처는 앱을 실행하기 전에 자바스크립트에서 사용하는 모든 네이티브 모듈 (ex. 블루투스, 지역, 스토리지 등) 을 초기화해야만 했다. 이는 사용자가 필요로 하는 모듈이 아니더라도 시작할 때 무조건적으로 초기화해야 한다.

“Turbo Modules” 은 이러한 단점을 보완했다. 역시나 자바스크립트는 필요할 때만 각 모듈을 로드할 수 있기 때문에 RN으로 만들어진 앱을 시작할 때 시작 시간이 크게 개선된다.

CodeGen


자바스크립트는 동적 타입 언어이고 JSI는 C++로 작성된 정적 타입의 언어이다. 따라서, 두 언어 사이의 원활한 의사소통을 보장할 필요가 있으며, New Architecture에서 정적 타입 검사기가 포함되었는데 이 것이 바로 “CodeGen” 이다. 크로스 플랫폼 앱에서 발생하는 가장 일반적인 크래시 원인 중 하나는 경계 간 타입 오류이다. CodeGen은 이러한 문제를 해결하고 보일러 플레이트 코드를 자동으로 생성해준다.

즉, 타입이 지정된 자바스크립트를 source of truth 로 사용함으로써, CodeGen은 Fabric과 Turbo Modules에서 사용하는 인터페이스 요소를 정의한다. 또한, 런타임 대신 빌드 타임에 더 많은 네이티브 코드를 생성한다.

도움글


[번역] React Native New vs. Old Architecture

(번역) 리액트 네이티브의 새로운 아키텍처가 출시되었습니다