LODY/정리

리액트 네이티브 백과사전 / React Native 브라운필드 통합

React Native 브라운필드 통합

들어가며

그린필드(Greenfield)는 앱 전체가 React Native로 시작하는 구조다. 브라운필드(Brownfield)는 기존 Native 앱에 React Native를 모듈 단위로 통합하는 방식이다.

두 방식은 iOS 부트스트랩의 출발점이 다르다. 그린필드는 @main에서 즉시 RCTHost가 올라오지만, 브라운필드는 사용자가 RN 화면에 진입하는 시점까지 초기화를 미룬다.


그린필드 vs 브라운필드

그린필드브라운필드
Entry point@main + RCTReactNativeFactory.startReactNative()기존 AppDelegate, RN은 별도 초기화
RN 초기화 시점앱 시작 즉시RN 화면 첫 진입 시 (lazy) 또는 앱 시작 시 preload
UIWindowRN이 Window 전체 소유Native ViewController 안에 RN View 임베드
내비게이션React Navigation 등 JS 기반Native 내비게이션이 RN ViewController를 포함
Feature flag불필요#if RN_ENABLED 조건부 컴파일

브라운필드 진입점: RNBaseViewController

기존 Native 앱에서 RN 화면을 여는 방법은 RCTRootView를 Native UIViewController에 임베드하는 것이다.

// Generic Props 타입으로 JS에 전달할 데이터를 타입 안전하게 관리한다
class RNBaseViewController<Props: Encodable>: UIViewController {
  private let moduleName: String
  private let initialProps: Props
 
  override func viewDidLoad() {
    super.viewDidLoad()
 
    #if RN_ENABLED
    let delegate = ReactNativeDelegate()
    let factory = RCTReactNativeFactory(delegate: delegate)
    delegate.dependencyProvider = RCTAppDependencyProvider()
 
    // Native ViewController가 RN Root View를 소유한다
    let rootView = factory.rootViewFactory.view(
      withModuleName: moduleName,
      initialProperties: initialProps.asParams()
    )
    view = rootView
    #else
    let label = UILabel()
    label.text = "React Native is disabled"
    view.addSubview(label)
    #endif
  }
}

구체적인 RN 화면은 이 base를 상속한다:

class TransportReviewViewController: RNBaseViewController<TransportReviewProps> {
  init(orderId: String, token: String) {
    super.init(
      moduleName: "TransportReviewScreen",
      initialProps: TransportReviewProps(orderId: orderId, token: token)
    )
  }
}
 
// 기존 Native 내비게이션에서 일반 ViewController처럼 push한다
let vc = TransportReviewViewController(orderId: order.id, token: authToken)
navigationController?.pushViewController(vc, animated: true)

부트스트랩 차이

그린필드에서는 앱 시작 시 RCTHost가 즉시 초기화되고 JS Thread·Hermes·번들 로드가 모두 완료된다. 브라운필드에서는 이 시퀀스가 RN ViewController가 viewDidLoad()를 호출하는 시점까지 미뤄진다.


TurboModule: Native ↔ JS 양방향 통신

브라운필드에서는 기존 Native 기능(딥링크 처리, 토큰 갱신, 화면 닫기 등)을 JS에서 호출할 필요가 있다. Codegen을 통해 TurboModule을 생성한다.

TypeScript 스펙이 단일 소스 오브 트루스다:

// NativeSendy.ts — Codegen 입력
import { TurboModule, TurboModuleRegistry } from 'react-native';
 
export interface Spec extends TurboModule {
  closeCurrentComponent(params: CloseParams): Promise<void>;
  executeDeepLink(url: string): Promise<void>;
  refreshToken(): Promise<void>;
}
 
export default TurboModuleRegistry.get<Spec>('NativeSendy');

Codegen이 이 스펙으로부터 C++ 헤더(NativeSendySpec.h)를 자동 생성한다. iOS 구현체는 이 헤더를 채운다:

// RCTNativeSendy.mm — JSI Bridge 구현
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeSendySpecJSI>(params);
}

JS에서의 호출은 JSI를 통해 C++ → Objective-C++ → Swift 구현체까지 동기적으로 전달된다.


#if RN_ENABLED: 조건부 컴파일

브라운필드의 장점 중 하나는 RN을 점진적으로 도입하거나 비활성화할 수 있다는 점이다. RN_ENABLED 플래그를 끄면 RN 관련 코드가 컴파일에서 제외되어 완전한 Native 앱으로 빌드된다. RN 번들 없이도 앱이 동작해야 하는 상황(초기 도입 단계, A/B 테스트 등)에 유용하다.

📖 용어 설명

  • 그린필드(Greenfield): 처음부터 React Native로 만드는 앱. UIWindow 전체가 RN 소유
  • 브라운필드(Brownfield): 기존 Native 앱에 RN을 통합. 특정 화면만 RN으로 구성
  • RNBaseViewController: Native ViewController가 RN Root View를 소유하는 브라운필드 패턴
  • Codegen: TypeScript 스펙 → C++ 헤더를 자동 생성하는 도구. TurboModule의 타입 안전성을 보장
  • #if RN_ENABLED: RN 코드를 조건부로 포함/제외하는 컴파일 플래그