들어가며
React Native New Architecture 렌더링 파이프라인에서 setCount(count + 1) 한 줄이 UIView.frame 업데이트까지 이어지는 여정을 살펴봤다.
그런데 그 여정이 시작되려면 전제 조건이 있다. JS Thread가 살아있어야 하고, Hermes가 번들을 실행하고 있어야 하며, Fabric 렌더링 환경이 준비되어 있어야 한다.
@main부터 첫 화면이 그려지기까지 무슨 일이 일어나는지 살펴보자.
@main → AppDelegate
iOS 앱은 @main 어노테이션이 붙은 클래스에서 시작된다.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [...]
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = RCTReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "RNSendy",
in: window,
launchOptions: launchOptions
)
return true
}
}factory.startReactNative() 한 줄이 React Native 전체 초기화 시퀀스를 트리거한다.
RCTReactNativeFactory → RCTHost
RCTReactNativeFactory는 New Architecture의 진입점이다. 내부적으로 RCTHost를 생성하고 시작한다. RCTHost는 다음을 총괄한다:
- JS Thread 생성 및 생명주기 관리
- Hermes 엔진 초기화
- Bundle 로드 및 실행
- Fabric Surface 초기화 및 첫 렌더 트리거
Thread 구성
RCTHost.start() → RCTInstance → RCTJSThreadManager가 JS 전용 직렬 Thread를 생성한다. React Native는 세 개의 Thread 위에서 동작한다.
| Thread | 이름 / 타입 | 담당 작업 |
|---|---|---|
| Main Thread | main / UI Thread | UIKit 조작, RCTMountingManager, CALayer, AppDelegate |
| JS Thread | com.facebook.react.JavaScript / Serial queue | JS 실행, React Fiber, JSI 호출, Fabric Commit |
| Shadow Queue | com.facebook.react.ShadowQueue / Concurrent queue | Yoga layout, Shadow Tree 구성, MountItem 생성 |
JS Thread가 Serial queue인 이유는 JavaScript 자체가 싱글 Thread 언어이기 때문이다. 모든 JS 코드는 한 번에 하나씩 순차적으로 실행된다. Fiber의 Concurrent Mode도 실제 병렬 실행이 아니라 JS Thread 위에서의 인터리빙(interleaving) 이다.
Hermes 엔진
Hermes는 Meta가 React Native를 위해 만든 JS 엔진이다. V8이나 JavaScriptCore와 달리 모바일 환경에 특화되어 설계됐다.
핵심 설계 원칙: AOT 컴파일
일반 JS 엔진은 런타임에 소스 코드를 파싱 → AST 생성 → bytecode 컴파일 순서로 처리한다. Hermes는 이 과정을 빌드 타임으로 옮긴다.
빌드 타임 (CI/CD):
JS 소스 → hermesc (Hermes 컴파일러) → Hermes bytecode (.hbc)
런타임 (앱 실행):
Hermes bytecode → HermesRuntime → 즉시 실행
Release 빌드의 main.jsbundle은 JS 소스가 아니다. hermesc가 생성한 Hermes bytecode 바이너리다. 파일 시그니처(c0 c0 ac 1d)로 구분할 수 있다.
| JavaScriptCore (Old) | Hermes | |
|---|---|---|
| 컴파일 시점 | 런타임 (앱 실행 시) | 빌드 타임 |
| 콜드 스타트 | 느림 (파싱 + 컴파일 비용) | 빠름 (bytecode 직접 실행) |
| 메모리 | 상대적으로 높음 | 낮음 (불필요한 최적화 제거) |
| TTI | 느림 | 빠름 |
JS Thread에서 HermesRuntime이 초기화되면 JSI를 통해 C++ 객체가 JS global에 등록된다:
global.nativeFabricUIManager— Fabric C++ 함수들 (ShadowNode 생성·조작)global.nativePerformanceNow— 고정밀 타이머global.RCTDeviceEventEmitter— Native 이벤트 에미터
이 시점부터 JS에서 global.nativeFabricUIManager.createNode(...) 같은 동기 C++ 호출이 가능해진다.
Bundle 로드
func bundleURL() -> URL? {
#if DEBUG
// Metro dev server → http://localhost:8081/index.bundle
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
// 앱 번들 내장 Hermes bytecode
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}Debug 모드 — Metro Bundler
Metro는 React Native 전용 JS 번들러다. npx react-native start 로 실행되며 localhost:8081에서 번들을 서빙한다. 변경사항은 WebSocket을 통해 실시간으로 반영된다(Fast Refresh).
Release 모드 — Hermes bytecode
react-native bundle 명령이 두 단계를 실행한다:
1. Metro가 모든 JS 모듈을 하나의 JS 파일로 번들링
- import → require() 변환
- 모듈 ID 정수화 (경로 문자열 대신 숫자 ID 사용)
2. hermesc가 번들 JS → Hermes bytecode로 AOT 컴파일
main.bundle.js → main.jsbundle (Hermes bytecode)
번들 실행 중 AppRegistry.registerComponent('RNSendy', () => App)이 호출되면 컴포넌트가 등록되고 Surface 준비를 대기한다.
RCTFabricSurface.start(): 첫 렌더 트리거
Main Thread에서 RCTFabricSurface가 시작되면 JS 쪽으로 AppRegistry.runApplication()을 호출한다. 이때부터 React Fiber의 첫 Render Phase가 시작되고, 렌더링 파이프라인이 동작한다.
Thread 관계
📖 용어 설명
- RCTReactNativeFactory: New Architecture의 iOS 진입점.
RCTHost를 생성하고 초기화 시퀀스를 조율한다- RCTHost: JS Thread, Hermes, Surface 등 React Native 런타임 전체를 관리하는 최상위 객체
- RCTJSThreadManager: JS 전용 Serial Thread를 생성하고 관리한다
- HermesRuntime: Hermes JS 엔진의 C++ 런타임. JSI를 통해 Native 객체를 JS에 노출한다
- Hermes bytecode: 빌드 시 사전 컴파일된 VM 전용 실행 파일. 런타임 파싱·컴파일 비용이 없다
- RCTFabricSurface: React Native 컴포넌트 트리가 렌더링되는 단위.
start()호출이 첫 렌더를 트리거한다