React Native 설명: Legacy Bridge에서 New Architecture까지
Source: Dev.to
웹 개발을 오래 해오셨다면 React를 사용해 프론트엔드를 구축하고 계실 가능성이 높습니다. 견고한 웹 앱을 배포하고, 사용자들이 만족하고, 그러다 모바일 앱이 필요하다는 것을 깨닫게 됩니다.
조사를 시작했지만 벽에 부딪히게 됩니다: Android는 Kotlin, iOS는 Swift가 필요합니다. 두 개의 별도 언어를 배워야 하고, 완전히 다른 툴링을 마스터해야 하며, 두 개의 독립된 코드베이스를 유지해야 할까요?
이는 완전한 악몽처럼 들립니다. 포기하려는 순간, 크로스‑플랫폼 개발의 세계에 발을 들여 React Native를 발견하게 됩니다 – 이미 알고 있는 JavaScript와 React를 사용해 모바일 앱을 작성할 수 있는 프레임워크로, 하나의 코드베이스만으로 Android와 iOS를 네이티브하게 타깃팅합니다.
갑자기 모바일 앱을 만드는 것이 제로부터 시작하는 것이 아니라, 이미 알고 있는 것을 확장하는 느낌이 듭니다.
Source: …
React Native란?
React Native는 오픈소스 프레임워크로, 개발자가 JavaScript 프로그래밍 언어와 React 라이브러리를 사용하여 여러 플랫폼(Android, iOS, macOS, Windows)용 네이티브 모바일 애플리케이션을 만들 수 있게 해줍니다.
- WebView 안에서 렌더링하는 하이브리드 프레임워크와 달리(즉, 모바일 앱 내부에 브라우저를 삽입하는 방식), React Native는 실제 플랫폼‑전용 네이티브 컴포넌트를 렌더링합니다.
예시
Hello World
위 코드는 HTML로 변환되지 않습니다.
-
**
View**는 다음으로 매핑됩니다:- Android에서는
ViewGroup - iOS에서는
UIView
- Android에서는
-
**
Text**는 다음으로 매핑됩니다:- Android에서는
TextView - iOS에서는
UITextView
- Android에서는
이들은 Kotlin이나 Swift로 직접 앱을 작성할 때 사용하는 것과 동일한 빌딩 블록입니다. 따라서 앱은 네이티브처럼 동작하여 보다 부드럽고 반응성이 뛰어난 사용자 경험을 제공합니다.
React (웹 라이브러리)가 네이티브 컴포넌트를 렌더링할 수 있는 방법?
예와 아니오. 핵심 React 라이브러리는 웹과 모바일 생태계 전반에 걸쳐 일관되지만, 렌더러는 다릅니다.
핵심 아이디어
- React Core: 상태를 관리하고(
useState,useEffect같은 훅을 통해) 리컨실리에이션을 통해 UI의 어떤 부분을 업데이트해야 하는지 결정합니다. - Renderer: 그 UI 설명을 플랫폼이 이해할 수 있는 형태(HTML DOM, Android 뷰, iOS 뷰)로 변환하는 방법을 알고 있습니다.
Diagram (conceptual)
┌─────────────┐ │ React Core │ ← manages state, reconciliation └─────┬───────┘ │ ┌─────▼───────┐ │ Renderer │ ← react‑dom (web) or react‑native (mobile) └─────┬───────┘ │ ┌─────▼───────┐ │ Platform UI │ ← DOM nodes or native views └─────────────┘
렌더러 상세
| 렌더러 | 대상 플랫폼 | 작동 방식 |
|---|---|---|
| react‑dom | 웹 브라우저 | 브라우저가 JavaScript를 네이티브로 이해하므로 DOM을 직접 동기식으로 조작합니다. |
| react‑native | Android & iOS | UI 설명을 JS Bridge를 통해 비동기적으로 전달합니다. 네이티브 측에서 이 설명을 받아 해당 네이티브 컴포넌트를 생성합니다. |
핵심 요점: React 자체는 아무것도 렌더링하지 않습니다. 렌더러가 컴포넌트를 DOM 노드로 만들지 네이티브 UI 뷰로 만들지를 결정합니다.
더 깊이 파보기: React Native 아키텍처
브리지 (레거시) 아키텍처
React Native v0.76 이전에 기본이었던 Legacy Architecture(레거시 아키텍처)라고도 불립니다.
1️⃣ 빌드 단계
- Metro Bundler가 모든 JavaScript 코드, React 컴포넌트 및 에셋을 수집합니다.
- 이를 JS Bundle이라는 단일 최적화 파일로 생성합니다.
- 이 번들은 빌드 시점에 네이티브 앱에 포함됩니다.
앱이 설치된 후에는 Metro가 더 이상 관여하지 않으며, 이후 일어나는 모든 작업은 런타임에 이루어집니다.
2️⃣ 런타임 (실행)
앱을 실행하면 세 개의 스레드가 시작됩니다:
| Thread | Responsibility |
|---|---|
| JavaScript Thread | JavaScript 엔진(JSC 또는 Hermes)을 실행합니다. JS Bundle을 실행하고, React 로직·상태를 보관하며 API 호출을 수행합니다. |
| UI Thread | OS의 네이티브 UI 스레드입니다. 초기 UI를 그리며 사용자 상호작용을 처리합니다. |
| Shadow Thread | Yoga 레이아웃 엔진을 실행합니다. 레이아웃을 계산하고 UI 스레드에 명령을 전달합니다. |
3️⃣ 상호작용 흐름 (예시)
- 사용자가 배경 색상을 변경해야 하는 버튼을 탭합니다.
- UI Thread가 탭 이벤트를 캡처합니다.
- 네이티브 UI는 JavaScript를 직접 호출할 수 없으므로, 이벤트를 JSON으로 직렬화하고 비동기 브리지를 통해 JavaScript 스레드로 보냅니다.
- JavaScript가 이벤트를 처리하고 상태를 업데이트한 뒤 재렌더링을 트리거합니다.
- React‑Native 렌더러가 새로운 UI 설명을 생성하고 브리지를 통해 다시 전달합니다.
- Shadow Thread(Yoga)가 필요에 따라 레이아웃을 재계산합니다.
- UI Thread가 변경 사항을 적용하여 네이티브 뷰 계층을 업데이트합니다.
요약
- React Native는 이미 알고 있는 JavaScript/React 지식을 그대로 활용해 모바일 앱을 작성할 수 있게 해줍니다.
- core React library는 상태와 리컨실리시에이션을 담당하고, renderer(react‑dom vs. react‑native)는 해당 UI를 어떻게 실제 화면에 구현할지 결정합니다.
- legacy (bridge) architecture에서는 JS 번들이 전용 JavaScript 스레드에서 실행되는 반면, 네이티브 UI와 레이아웃은 별도의 스레드에서 처리되며, 비동기 브리지를 통해 서로 통신합니다.
이 흐름을 이해하면 React Native가 왜 친숙한(React) 동시에 진정한 네이티브(플랫폼‑특화 UI 컴포넌트)처럼 느껴지는지 알 수 있습니다. 이 지식을 바탕으로 이제 성능 최적화, 새로운 Fabric 아키텍처에 대해 더 깊이 파고들거나 첫 번째 크로스‑플랫폼 앱을 직접 만들어 볼 수 있습니다!
JS 브리지
JS 브리지는 정보를 JavaScript 엔진(JSC)으로 보냅니다.
JSC는 JSON을 역직렬화하고 JavaScript 코드를 실행합니다(예: useState로 상태를 업데이트). 그 후 React는 이러한 변경을 기반으로 새로운 UI를 계산합니다.
JS Thread는 결과 명령을 다시 JSON으로 직렬화하여 비동기 브리지를 통해 전송합니다.
Shadow Thread는 이 JSON을 받아 Yoga 라이브러리가 명령을 읽고 각 요소에 대한 정확한 좌표와 속성을 계산한 뒤 레이아웃 정보를 UI Thread에 전달합니다. UI Thread는 최종적으로 업데이트된 화면을 그립니다.
비동기 브리지의 제한 사항
원래 비동기 브리지에는 몇 가지 단점이 있었으며, 가장 중요한 것은 브리지 자체와 JSON에 대한 의존성이었습니다.
1. 직렬화 병목 현상
- 모바일 플랫폼과 JavaScript는 직접 통신할 수 없으므로 JSON을 통해 데이터를 교환합니다.
- JSON을 문자열화하고 파싱하는 데 CPU 사이클이 소모되어 지연이 발생합니다.
- 많은 이벤트가 처리될 때 이 직렬화/역직렬화 단계가 네이티브 측에서 지연 및 프레임 드롭을 일으킬 수 있습니다.
2. 단일 비동기 브리지
- JavaScript가 네이티브 UI를 직접 수정할 수 없지만, 모든 UI‑관련 이벤트는 브리지를 거쳐야 합니다.
- 대량의 JSON 메시지가 브리지를 혼잡하게 만들어 다음과 같은 문제를 초래합니다:
- 프레임 드롭
- 끊김 있는 애니메이션
- 대규모 리스트를 렌더링할 때 발생하는 “흰 화면” 글리치
3. 단일 JavaScript 스레드
- 대부분의 애플리케이션 로직이 JavaScript 스레드에서 실행됩니다.
- 무거운 연산이 이 스레드를 차단하여 앱 전체의 성능이 저하됩니다.
React Native는 메인 스레드 차단을 피하기 위해 모든 작업을 비동기적으로 수행하며, 각 이벤트를 프라미스 또는 콜백으로 변환합니다.
핵심 팀은 JSON 브리지를 최적화하는 것만으로는 근본적인 성능 문제를 해결할 수 없다는 것을 깨닫고, 비동기 브리지를 JavaScript Interface (JSI)—C++ 기반 레이어로 교체했습니다. JSI는 네이티브 API를 직접 호출하여 직렬화 및 메시지 전달이 필요 없게 합니다.
Hermes – Meta가 만든 오픈소스 JavaScript 엔진 – 은 React Native에 최적화되어 있으며 JSI를 사용해 네이티브 플랫폼과 직접 통신합니다. 또한 다음과 같은 이점을 제공합니다:
- 더 빠른 Time‑to‑Interactive (TTI)
- 더 작은 앱 번들 크기
- 감소된 메모리 사용량
Hermes는 기존 JSC를 대체했으며 React Native 0.70부터 기본 엔진이 되었습니다.
JavaScript Interface (JSI)
JSI는 JavaScript가 네이티브 코드와 직접 통신할 수 있게 해주는 가벼운 C++ 레이어입니다.
- No message passing – 기존 브리지의 비동기 큐가 사라졌습니다.
- Synchronous calls – JavaScript가 C++ 호스트 객체에 대한 참조를 보유하고 해당 메서드를 직접 호출할 수 있습니다.
Example
// JavaScript
nativeObjectRef.setProperty(value); // Calls C++ method synchronously
레거시 아키텍처에서는 네이티브 모듈(예: Bluetooth, 카메라)이 시작 시점에 즉시 로드되어 사용되지 않더라도 메모리에 상주했습니다. 이는 실행 시간과 메모리 사용량을 증가시켰습니다.
Turbo Modules(새 아키텍처)를 사용하면 모듈이 필요할 때만 지연 로드되어 메모리 사용량을 줄이고 성능을 향상시킵니다.
Fabric 시스템
Fabric은 네이티브 측에서 JSI와 Yoga 레이아웃 엔진을 활용하는 새로운 렌더링 시스템입니다.
레거시 방식에서는 큰 리스트를 렌더링하기 위해 방대한 JSON 페이로드를 직렬화하고, 이를 큐에 넣으며 UI 스레드가 프레임을 놓치지 않기를 바라는, 본질적으로 비동기적이고 취약한 과정을 거쳐야 했습니다.
Fabric을 사용하면 JavaScript가 JSI를 통해 C++ 메서드를 호출하여 네이티브 뷰(예: iOS의 UIView, Android의 ViewGroup)를 동기적으로 생성하고 업데이트합니다.
Fabric 렌더 파이프라인
| 단계 | 설명 |
|---|---|
| 렌더 단계 | React가 JavaScript를 실행하고 React 엘리먼트 트리를 구축한 뒤, C++를 사용해 이를 React 섀도우 트리로 변환합니다. |
| 커밋 단계 | Yoga 라이브러리가 섀도우 트리를 처리하고 정확한 레이아웃 좌표를 계산하여 Yoga 트리를 생성합니다. |
| 마운트 단계 | 네이티브 측은 Yoga 트리 정보를 기반으로 화면에 레이아웃을 렌더링합니다. |
Fabric의 내부 작동 방식은 이 글의 범위를 벗어나며, 향후 게시물에서 다룰 수 있습니다.
요약
이제 다음에 대해 확실히 이해했습니다:
- 레거시 아키텍처가 어떻게 작동했는지.
- 비동기 JSON 브리지가 왜 성능 병목 현상이 되었는지.
- React Native 핵심 팀이 C++ 기반 JSI, Turbo Modules, 그리고 Fabric 렌더링 시스템으로 이러한 문제를 어떻게 해결했는지.
뭐가 기다리고 있나요? 바로 앱을 만들어 보세요!