ArkTS 기반 React Native 번들 로딩 아키텍처
HarmonyOS NEXT의 ArkTS 환경에서 React Native(RN) 번들을 로드하는 과정은 크게 세 가지 핵심 단계로 구성됩니다.
- RN 인스턴스(
RNInstance)의 생성 및 상태 관리 - RN 서드파티 컴포넌트 컨텍스트(
RNComponentContext) 초기화 - UI 렌더링 컨테이너(
RNApp또는RNSurface) 바인딩
RN 애플리케이션은 단일 번들(Single Bundle) 구조와 다중 번들(Multi Bundle) 구조로 나뉩니다. 다중 번들 구조는 화면 간 이동 시 번들을 동적으로 로드하거나, 하나의 화면을 여러 번들이 조합하여 렌더링하는 마이크로서비스 형태의 아키텍처를 지원합니다. 이 경우 번들 로딩 지연으로 인한 화이트 스크린(White Screen) 현상을 방지하기 위해 사전 로딩(Pre-loading) 전략이 필수적입니다.
번들 설정 및 로딩 컨트롤러 구현
다중 번들 환경을 효율적으로 관리하기 위해 번들 구성 데이터와 로딩 로직을 분리하여 캡슐화합니다. HarmonyOS NEXT에서는 Java 지원이 완전히 배제되었으므로, 네이티브 TurboModule 구현을 위해 CAPI 아키텍처(enableCAPIArchitecture)를 반드시 활성화해야 합니다.
// RnBundleConfig.ts
export class RnBundleConfig {
isDebugMode: boolean = false;
jsBundlePath: string = '';
resourcePath: string = '';
bundleKey: string = '';
}
// BundleLoadController.ts
export class BundleLoadController {
private static instanceMap: Map<string, any> = new Map();
private static coreContext: any = null;
public static async executeBundleLoad(config: RnBundleConfig): Promise<boolean> {
if (!this.coreContext || this.instanceMap.has(config.bundleKey)) {
return true;
}
const rnInstance = await this.coreContext.createAndRegisterRNInstance({
enableCAPIArchitecture: true, // HarmonyOS NEXT 필수 옵션
enableNDKTextMeasuring: true,
enableBackgroundExecutor: true,
assetsDest: config.resourcePath
});
const componentContext = new RNComponentContext(
RNOHContext.fromCoreContext(this.coreContext, rnInstance),
wrapBuilder(buildCustomComponent),
wrapBuilder(buildRNComponentForTag),
new Map()
);
const bundleProvider = config.isDebugMode
? new MetroJSBundleProvider()
: new ResourceJSBundleProvider(this.coreContext.uiAbilityContext.resourceManager, config.jsBundlePath);
await rnInstance.runJSBundle(bundleProvider);
const executionStatus = rnInstance.getBundleExecutionStatus(bundleProvider.getURL());
if (executionStatus === "DONE") {
this.instanceMap.set(config.bundleKey, rnInstance);
return true;
}
return false;
}
public static getInstance(key: string): any {
return this.instanceMap.get(key);
}
}
진입점 초기화 및 비동기 로딩
앱의 진입 페이지(aboutToAppear)에서 다중 번들을 비동기적으로 초기화합니다. 각 번들의 로딩 완료 상태와 현재 활성화된 번들 키를 상태 변수로 관리하여 UI 렌더링을 제어합니다.
// EntryPage.ets
@Entry @Component
struct EntryPage {
@StorageLink('RNOHCoreContext') coreContext: any = undefined;
@State isPrimaryLoaded: boolean = false;
@State isSecondaryLoaded: boolean = false;
@State activeBundleKey: string = 'primary';
private currentInstance: any = null;
private propsMap: Map<string, Record<string, Object>> = new Map();
aboutToAppear() {
if (!this.coreContext) return;
this.initializeBundles();
}
private async initializeBundles() {
const primaryConfig = new RnBundleConfig();
primaryConfig.jsBundlePath = 'primary/bundle.harmony.js';
primaryConfig.resourcePath = 'rawfile/primary/assets';
primaryConfig.bundleKey = 'primary';
const secondaryConfig = new RnBundleConfig();
secondaryConfig.jsBundlePath = 'secondary/bundle.harmony.js';
secondaryConfig.resourcePath = 'rawfile/secondary/assets';
secondaryConfig.bundleKey = 'secondary';
BundleLoadController.executeBundleLoad(primaryConfig).then((success) => {
this.isPrimaryLoaded = success;
if (success) this.currentInstance = BundleLoadController.getInstance('primary');
});
BundleLoadController.executeBundleLoad(secondaryConfig).then((success) => {
this.isSecondaryLoaded = success;
});
}
// ... build 메서드는 하단 참조
}
UI 컨테이너 선택: RNApp vs RNSurface
단일 번들 애플리케이션에서는 RNApp 컴포넌트를 사용하여 전체 UI를 호스팅할 수 있습니다. 그러나 다중 번들 환경, 특히 번들 간 동적 전환이나 복합 UI 페이징이 필요한 경우에는 RNSurface를 사용하는 것이 유연합니다. RNSurface는 상태 변수에 따라 특정 번들의 UI만 선택적으로 마운트하거나 여러 번들의 UI를 동시에 배치할 수 있습니다.
// EntryPage.ets (build 메서드)
build() {
Column() {
if (this.isPrimaryLoaded && this.activeBundleKey === 'primary') {
RNSurface({
surfaceConfig: {
appKey: 'primary',
initialProps: this.propsMap.get('primary')
},
ctx: new RNComponentContext(
RNOHContext.fromCoreContext(this.coreContext!, this.currentInstance),
wrappedCustomComponentBuilder,
wrapBuilder(buildRNComponentForTag),
new Map()
),
})
}
if (this.isSecondaryLoaded && this.activeBundleKey === 'secondary') {
RNSurface({
surfaceConfig: {
appKey: 'secondary',
initialProps: this.propsMap.get('secondary')
},
ctx: new RNComponentContext(
RNOHContext.fromCoreContext(this.coreContext!, this.currentInstance),
wrappedCustomComponentBuilder,
wrapBuilder(buildRNComponentForTag),
new Map()
),
})
}
}
.width('100%')
.height('100%')
}
네이티브 이벤트 기반 번들 전환
번들 간의 라우팅 및 전환은 RN에서 네이티브로 이벤트를 전달하고, 네이티브에서 @State 변수를 업데이트하여 트리거합니다. 이를 통해 현재 활성화된 번들 인스턴스와 전달할 초기 Props를 동적으로 갱신할 수 있습니다.
// EntryPage.ets (이벤트 리스너 등록)
aboutToAppear() {
// ... 초기화 로직
emitter.on('NAVIGATE_TO_SECONDARY_BUNDLE', (eventData) => {
const rawParams = eventData?.data?.param;
if (rawParams) {
try {
const parsedParams = JSON.parse(rawParams) as Record<string, Object>;
this.propsMap.set('secondary', parsedParams);
} catch (error) {
console.error('Props parsing failed:', error);
}
}
// 번들 전환 및 인스턴스 스와핑
this.activeBundleKey = 'secondary';
this.currentInstance = BundleLoadController.getInstance('secondary');
});
}
리소스 경로(assetsDest) 제한 사항 및 우회 전략
HarmonyOS NEXT 환경에서 다중 번들 로딩 시 assetsDest 속성을 통해 각 번들의 리소스 경로를 별도로 지정하더라도, 애플리케이션이 실제 리소스를 로드할 때는 기본 경로인 rawfile/assets/만 참조하는 제한 사항이 존재합니다.
이러한 플랫폼 레벨의 제약을 우회하기 위해서는 각 번들의 리소스를 동일한 rawfile/assets/ 디렉토리 내에서도 서로 다른 하위 폴더(예: rawfile/assets/primary/images, rawfile/assets/secondary/images)로 물리적으로 분리해야 합니다. 이 경우 RN JavaScript 코드 내부에서 참조하는 이미지 경로 또한 하위 디렉토리 구조에 맞게 일괄적으로 수정(Refactoring)하는 작업이 선행되어야 합니다.