Expo Modules API v2 네이티브 확장
Swift/Kotlin 네이티브 모듈 작성, 이벤트 브릿지, 테스트 전략
핵심 요약
- SDK 56은 "별도 패키지 생성·연결"에서 "앱 안에서 inline module로 실험 후 필요하면 독립 모듈로 승격"하는 흐름으로 이동했습니다.
- inline module은 experimental이라
experiments.inlineModules.watchedDirectories에 등록한 뒤npx expo prebuild로 native 프로젝트를 다시 생성합니다. - 재사용·버전 관리가 필요하면
create-expo-module --local또는 standalone module, 화면 자체가 native UI면 Expo UI custom view를 우선합니다. expo-type-information으로 Swift module에서 TypeScript interface를 생성해 JS에서any로 새지 않게 contract를 고정합니다.- native 모듈을 바꾸면 OTA가 아니라 runtimeVersion·binary release 대상이 될 수 있으므로, 별도 승인 흐름과 secret/MDM managed config 주입 규칙을 둡니다.
SDK 56에서 달라진 점
SDK 56의 Expo Modules 흐름은 "별도 패키지를 만들고 연결한다"에서 앱 안에서 네이티브 기능을 먼저 실험하고, 필요하면 독립 모듈로 승격한다로 바뀌었습니다.
| 기능 | 상태 | 프로덕션 의미 |
|---|---|---|
| Inline modules | SDK 56 experimental | Kotlin·Swift 파일을 앱 디렉터리에 두고 autolinking |
expo-type-information | SDK 56 신규 | Swift module에서 TypeScript interface 생성 |
create-expo-module 개선 | SDK 56 | local/standalone module, platform 추가, non-interactive scaffolding 강화 |
| Kotlin compiler plugin | SDK 56 | Android Expo Modules reflection 비용 감소 |
| iOS JSI layer 개선 | SDK 56 | Swift에서 JSI로 직접 연결되는 호출 경로 단순화 |
선택 기준
| 상황 | 권장 |
|---|---|
| 앱 하나에서만 쓰는 얇은 native capability | Inline module |
| 여러 앱에서 재사용하거나 버전 관리가 필요한 capability | create-expo-module --local 또는 standalone module |
| 이미 Android/iOS SDK가 있는 vendor wrapper | Expo Modules API로 wrapper 작성 |
| 화면 자체가 native UI여야 함 | Expo UI custom view/modifier 우선 |
| host native app에 Expo를 넣는 brownfield | expo-brownfield와 host Turbo Module 등록 |
Inline module 기준선
{
"expo": {
"experiments": {
"inlineModules": {
"watchedDirectories": ["app"]
}
}
}
}설정을 바꾼 뒤에는 npx expo prebuild로 native 프로젝트를 다시 생성합니다. Inline module 파일명과 class/module
이름은 충돌 없이 맞춰 둡니다.
package app
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class DevicePolicyModule : Module() {
override fun definition() = ModuleDefinition {
Name("DevicePolicy")
AsyncFunction("isManagedDevice") {
// MDM SDK 호출부
false
}
}
}internal import ExpoModulesCore
public class DevicePolicyModule: Module {
public func definition() -> ModuleDefinition {
Name("DevicePolicy")
AsyncFunction("isManagedDevice") {
return false
}
}
}TypeScript contract
네이티브 모듈이 JS에서 any로 새는 순간 운영 리스크가 커집니다. SDK 56의 type generation 도구를 쓰거나,
안정된 interface 파일을 직접 둡니다.
import { requireNativeModule } from 'expo';
type DevicePolicyModule = {
isManagedDevice(): Promise<boolean>;
};
export default requireNativeModule<DevicePolicyModule>('DevicePolicy');테스트 전략
- native module public API는 TypeScript contract test를 둡니다.
- Android는 Robolectric/JUnit, iOS는 XCTest로 permission·error branch를 검증합니다.
- JS wrapper는 native unavailable 상태를 mock해 fallback UX를 테스트합니다.
- EAS Build preview profile에서 실제 signing·entitlement·permission prompt를 확인합니다.
- module 변경이 OTA로 해결되지 않는 native runtime 변경인지 PR template에서 체크합니다.
운영 규칙
- native capability는
runtimeVersion변경을 요구할 수 있으므로 EAS Update와 별도 승인 흐름을 둡니다. - 비밀 키나 tenant 설정은 native source에 넣지 말고 EAS secret 또는 MDM managed config로 주입합니다.
- inline module이 두 앱 이상에서 복제되면 local Expo module로 승격합니다.
- package를 공개할 계획이면 semver, changelog, config plugin migration을 함께 관리합니다.