Skip to main content
Version: Next

Android 네이티브 모듈

info

Native Module과 Native Components는 기존 아키텍처에서 사용하던 안정적인 기술이다.
새로운 아키텍처가 안정화되면 앞으로 지원이 중단될 예정이다. 새로운 아키텍처에서는 Turbo Native ModuleFabric Native Components를 사용해 유사한 결과를 달성한다.

안드로이드 네이티브 모듈에 오신 것을 환영합니다. 네이티브 모듈이 무엇인지 이해하려면 먼저 네이티브 모듈 소개를 읽어 보세요.

캘린더 네이티브 모듈 생성하기

이 가이드에서는 CalendarModule이라는 네이티브 모듈을 생성해 JavaScript에서 Android의 캘린더 API에 접근하는 방법을 알아본다. 이 과정을 마치면 JavaScript에서 CalendarModule.createCalendarEvent('Dinner Party', 'My House');를 호출해 Java/Kotlin 메서드를 실행하고 캘린더 이벤트를 생성할 수 있다.

설정하기

시작하려면 React Native 애플리케이션 내의 Android 프로젝트를 Android Studio에서 열어야 한다. React Native 앱에서 Android 프로젝트는 다음과 같은 위치에서 찾을 수 있다:

React Native 앱 내에서 Android 프로젝트를 열어보는 이미지
Android 프로젝트를 찾을 수 있는 위치 이미지

네이티브 코드를 작성할 때는 Android Studio를 사용하는 것을 권장한다. Android Studio는 Android 개발을 위해 만들어진 IDE로, 이를 사용하면 코드 문법 오류와 같은 사소한 문제를 빠르게 해결할 수 있다.

또한 Java/Kotlin 코드를 반복적으로 수정하면서 빌드 속도를 높이기 위해 Gradle Daemon을 활성화하는 것을 추천한다.

커스텀 네이티브 모듈 파일 생성하기

첫 번째 단계는 android/app/src/main/java/com/your-app-name/ 폴더 내에 (CalendarModule.java 또는 CalendarModule.kt) Java/Kotlin 파일을 생성하는 것이다. (Kotlin과 Java 모두 동일한 폴더 구조를 사용한다.) 이 Java/Kotlin 파일은 네이티브 모듈 Java/Kotlin 클래스를 포함하게 된다.

Android Studio에서 CalendarModule.java 클래스를 추가하는 방법을 보여주는 이미지
CalendarModuleClass 추가 방법 이미지

그런 다음 다음 내용을 추가한다:

java
package com.your-apps-package-name; // your-apps-package-name을 앱의 패키지 이름으로 변경
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;

public class CalendarModule extends ReactContextBaseJavaModule {
CalendarModule(ReactApplicationContext context) {
super(context);
}
}

보는 바와 같이, CalendarModule 클래스는 ReactContextBaseJavaModule 클래스를 상속한다. 안드로이드에서 Java/Kotlin 네이티브 모듈은 ReactContextBaseJavaModule을 상속하는 클래스로 작성되며, JavaScript가 요구하는 기능을 구현한다.

기술적으로 Java/Kotlin 클래스는 BaseJavaModule 클래스를 상속하거나 NativeModule 인터페이스를 구현하기만 하면 React Native에서 네이티브 모듈로 인정된다는 점을 알아두자.

그러나 위에서 보여준 것처럼 ReactContextBaseJavaModule을 사용하는 것을 권장한다. ReactContextBaseJavaModuleReactApplicationContext (RAC)에 접근할 수 있게 해주는데, 이는 액티비티 생명주기 메서드와 연동해야 하는 네이티브 모듈에 유용하다. 또한 ReactContextBaseJavaModule을 사용하면 향후 네이티브 모듈의 타입 안전성을 더 쉽게 보장할 수 있다. 미래 릴리스에서 도입될 네이티브 모듈 타입 안전성을 위해, React Native는 각 네이티브 모듈의 JavaScript 스펙을 확인하고 ReactContextBaseJavaModule을 상속하는 추상 기본 클래스를 생성한다.

모듈 이름

안드로이드에서 모든 Java/Kotlin 네이티브 모듈은 getName() 메서드를 구현해야 한다. 이 메서드는 네이티브 모듈의 이름을 나타내는 문자열을 반환한다. 이후 JavaScript에서 이 이름을 사용해 네이티브 모듈에 접근할 수 있다. 예를 들어, 아래 코드 스니펫에서 getName()"CalendarModule"을 반환한다.

java
// CalendarModule.java에 추가
@Override
public String getName() {
return "CalendarModule";
}

이렇게 하면 JavaScript에서 다음과 같이 네이티브 모듈에 접근할 수 있다:

tsx
const {CalendarModule} = ReactNative.NativeModules;

자바스크립트에서 네이티브 메서드 내보내기

다음으로, 캘린더 이벤트를 생성하고 자바스크립트에서 호출할 수 있는 네이티브 모듈 메서드를 추가해야 한다. 자바스크립트에서 호출하려는 모든 네이티브 모듈 메서드는 @ReactMethod로 주석 처리해야 한다.

CalendarModulecreateCalendarEvent() 메서드를 설정하고, 자바스크립트에서 CalendarModule.createCalendarEvent()로 호출할 수 있도록 한다. 현재는 이름과 위치를 문자열로 받는 메서드로 구성한다. 인자 타입 옵션은 곧 다룰 예정이다.

java
@ReactMethod
public void createCalendarEvent(String name, String location) {
}

메서드가 애플리케이션에서 호출되었는지 확인하기 위해 디버그 로그를 추가한다. 아래는 Android 유틸 패키지에서 Log 클래스를 임포트하고 사용하는 예제이다.

java
import android.util.Log;

@ReactMethod
public void createCalendarEvent(String name, String location) {
Log.d("CalendarModule", "Create event called with name: " + name
+ " and location: " + location);
}

네이티브 모듈 구현을 마치고 자바스크립트에 연결한 후, 이 단계를 따라 앱에서 로그를 확인할 수 있다.

동기식 메서드

네이티브 메서드를 동기식 메서드로 표시하려면 isBlockingSynchronousMethod = true를 전달한다.

java
@ReactMethod(isBlockingSynchronousMethod = true)

현재로서는 동기식 메서드 호출이 성능에 큰 영향을 미칠 수 있고, 네이티브 모듈에 스레드 관련 버그를 유발할 가능성이 있기 때문에 이를 권장하지 않는다. 또한, isBlockingSynchronousMethod를 활성화하면 앱에서 Google Chrome 디버거를 더 이상 사용할 수 없다. 동기식 메서드는 JS VM이 앱과 메모리를 공유해야 하기 때문이다. Google Chrome 디버거의 경우, React Native는 Google Chrome 내부의 JS VM에서 실행되며 WebSockets를 통해 모바일 장치와 비동기적으로 통신한다.

모듈 등록 (Android 전용)

네이티브 모듈을 작성한 후에는 React Native에 등록해야 한다. 이를 위해 네이티브 모듈을 ReactPackage에 추가하고, ReactPackage를 React Native에 등록한다. 초기화 과정에서 React Native는 모든 패키지를 순회하며, 각 ReactPackage 내의 네이티브 모듈을 등록한다.

React Native는 등록할 네이티브 모듈 목록을 얻기 위해 ReactPackagecreateNativeModules() 메서드를 호출한다. Android의 경우, createNativeModules에서 모듈이 인스턴스화되어 반환되지 않으면 JavaScript에서 사용할 수 없다.

네이티브 모듈을 ReactPackage에 추가하려면, 먼저 android/app/src/main/java/com/your-app-name/ 폴더 내에 ReactPackage를 구현하는 새로운 Java/Kotlin 클래스(MyAppPackage.java 또는 MyAppPackage.kt)를 생성한다.

그런 다음 다음 내용을 추가한다:

java
package com.your-app-name; // your-app-name을 앱 이름으로 변경
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class MyAppPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new CalendarModule(reactContext));

return modules;
}

}

이 파일은 생성한 네이티브 모듈인 CalendarModule을 불러온다. 그리고 createNativeModules() 함수 내에서 CalendarModule을 인스턴스화하고, 등록할 NativeModules 목록으로 반환한다. 나중에 더 많은 네이티브 모듈을 추가하면 여기서 반환하는 목록에 인스턴스화하여 추가할 수 있다.

이 방식으로 네이티브 모듈을 등록하면 애플리케이션이 시작될 때 모든 네이티브 모듈이 즉시 초기화되어 애플리케이션의 시작 시간이 늘어난다는 점에 유의해야 한다. 대안으로 TurboReactPackage를 사용할 수 있다. TurboReactPackage는 인스턴스화된 네이티브 모듈 객체 목록을 반환하는 createNativeModules 대신, 필요할 때 네이티브 모듈 객체를 생성하는 getModule(String name, ReactApplicationContext rac) 메서드를 구현한다. 현재로서는 TurboReactPackage를 구현하는 것이 조금 더 복잡하다. getModule() 메서드를 구현하는 것 외에도, 패키지가 인스턴스화할 수 있는 모든 네이티브 모듈 목록과 이를 인스턴스화하는 함수를 반환하는 getReactModuleInfoProvider() 메서드를 구현해야 한다. 예제는 여기에서 확인할 수 있다. TurboReactPackage를 사용하면 애플리케이션의 시작 시간을 단축할 수 있지만, 현재로서는 작성이 다소 번거롭다. 따라서 TurboReactPackage를 사용하려면 주의를 기울여야 한다.

CalendarModule 패키지를 등록하려면 MyAppPackage를 ReactNativeHost의 getPackages() 메서드에서 반환하는 패키지 목록에 추가해야 한다. MainApplication.java 또는 MainApplication.kt 파일을 열어 android/app/src/main/java/com/your-app-name/ 경로에서 찾을 수 있다.

ReactNativeHost의 getPackages() 메서드를 찾아 패키지 목록에 MyAppPackage를 추가한다:

java
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// 아직 자동 링크가 되지 않은 패키지는 여기에 수동으로 추가할 수 있다. 예:
// packages.add(new MyReactNativePackage());
packages.add(new MyAppPackage());
return packages;
}

이제 Android용 네이티브 모듈을 성공적으로 등록했다!

구축한 기능 테스트하기

이제 안드로이드 네이티브 모듈의 기본 구조를 완성했다. 자바스크립트에서 네이티브 모듈에 접근하고 내보낸 메서드를 호출해 테스트할 차례다.

앱에서 네이티브 모듈의 createCalendarEvent() 메서드를 호출할 적절한 위치를 찾는다. 아래는 앱에 추가할 수 있는 NewModuleButton 컴포넌트 예제다. NewModuleButtononPress() 함수 내부에서 네이티브 모듈을 호출할 수 있다.

tsx
import React from 'react';
import {NativeModules, Button} from 'react-native';

const NewModuleButton = () => {
const onPress = () => {
console.log('We will invoke the native module here!');
};

return (
<Button
title="Click to invoke your native module!"
color="#841584"
onPress={onPress}
/>
);
};

export default NewModuleButton;

자바스크립트에서 네이티브 모듈에 접근하려면 먼저 React Native에서 NativeModules를 임포트해야 한다.

tsx
import {NativeModules} from 'react-native';

그런 다음 NativeModules에서 CalendarModule 네이티브 모듈에 접근할 수 있다.

tsx
const {CalendarModule} = NativeModules;

이제 CalendarModule 네이티브 모듈을 사용할 수 있으므로, 네이티브 메서드 createCalendarEvent()를 호출할 수 있다. 아래는 NewModuleButtononPress() 메서드에 추가한 예제다.

tsx
const onPress = () => {
CalendarModule.createCalendarEvent('testName', 'testLocation');
};

마지막 단계는 React Native 앱을 다시 빌드해 최신 네이티브 코드(새로운 네이티브 모듈 포함)를 사용할 수 있도록 하는 것이다. React Native 애플리케이션이 위치한 디렉토리에서 다음 커맨드를 실행한다.

shell
npm run android

반복 작업 중 빌드하기

이 가이드를 따라 네이티브 모듈을 반복적으로 개선하다 보면, JavaScript에서 최신 변경 사항을 적용하려면 애플리케이션을 네이티브로 다시 빌드해야 한다. 작성 중인 코드가 애플리케이션의 네이티브 부분에 포함되기 때문이다. React Native의 Metro 번들러는 JavaScript의 변경 사항을 감지하고 자동으로 다시 빌드할 수 있지만, 네이티브 코드에 대해서는 그렇지 않다. 따라서 최신 네이티브 변경 사항을 테스트하려면 위 명령어를 사용해 다시 빌드해야 한다.

복습✨

이제 앱에서 네이티브 모듈의 createCalendarEvent() 메서드를 호출할 수 있어야 한다. 예제에서는 NewModuleButton을 눌러 이 작업을 수행한다. createCalendarEvent() 메서드에 설정한 로그를 확인하면 이를 확인할 수 있다. 이 단계를 따라 앱에서 ADB 로그를 볼 수 있다. 그런 다음 Log.d 메시지(예제에서는 "Create event called with name: testName and location: testLocation")를 검색하고, 네이티브 모듈 메서드를 호출할 때마다 로그가 기록되는지 확인할 수 있다.

로그 이미지
Android Studio의 ADB 로그 이미지

이 시점에서 여러분은 Android 네이티브 모듈을 생성하고 React Native 애플리케이션에서 JavaScript를 통해 네이티브 메서드를 호출했다. 이제 네이티브 모듈 메서드에서 사용할 수 있는 인자 타입과 콜백 및 Promise 설정 방법에 대해 더 알아볼 수 있다.

캘린더 네이티브 모듈을 넘어서

더 나은 네이티브 모듈 내보내기

위에서와 같이 NativeModules에서 네이티브 모듈을 가져오는 방식은 다소 번거롭다.

이를 매번 반복하지 않도록, 네이티브 모듈을 사용하는 개발자들을 위해 자바스크립트 래퍼를 만들 수 있다. CalendarModule.js라는 새로운 자바스크립트 파일을 생성하고 다음과 같은 내용을 추가한다.

tsx
/**
* 이 코드는 네이티브 CalendarModule 모듈을 자바스크립트 모듈로 노출한다. 여기에는 다음 매개변수를 받는 'createCalendarEvent' 함수가 포함되어 있다:

* 1. String name: 이벤트 이름을 나타내는 문자열
* 2. String location: 이벤트 장소를 나타내는 문자열
*/
import {NativeModules} from 'react-native';
const {CalendarModule} = NativeModules;
export default CalendarModule;

이 자바스크립트 파일은 자바스크립트 측 기능을 추가하기에도 적합한 위치가 된다. 예를 들어, TypeScript와 같은 타입 시스템을 사용한다면 여기에 네이티브 모듈에 대한 타입 주석을 추가할 수 있다. React Native가 아직 네이티브에서 자바스크립트로의 타입 안전성을 지원하지는 않지만, 모든 자바스크립트 코드는 타입 안전성을 갖게 된다. 이렇게 하면 나중에 타입 안전한 네이티브 모듈로 전환하기도 더 쉬워진다. 아래는 CalendarModule에 타입 안전성을 추가한 예제다.

tsx
/**
* 이 코드는 네이티브 CalendarModule 모듈을 자바스크립트 모듈로 노출한다. 여기에는 다음 매개변수를 받는 'createCalendarEvent' 함수가 포함되어 있다:
*
* 1. String name: 이벤트 이름을 나타내는 문자열
* 2. String location: 이벤트 장소를 나타내는 문자열
*/
import {NativeModules} from 'react-native';
const {CalendarModule} = NativeModules;
interface CalendarInterface {
createCalendarEvent(name: string, location: string): void;
}
export default CalendarModule as CalendarInterface;

다른 자바스크립트 파일에서 이 네이티브 모듈에 접근하고 메서드를 호출하려면 다음과 같이 할 수 있다.

tsx
import CalendarModule from './CalendarModule';
CalendarModule.createCalendarEvent('foo', 'bar');

이 예제는 CalendarModule을 가져오는 위치가 CalendarModule.js와 동일한 계층 구조에 있다고 가정한다. 필요에 따라 상대 경로를 업데이트해야 한다.

인자 타입

자바스크립트에서 네이티브 모듈 메서드를 호출할 때, React Native는 인자를 JS 객체에서 해당하는 Java/Kotlin 객체로 변환한다. 예를 들어, Java 네이티브 모듈 메서드가 double을 받는다면, JS에서는 숫자로 메서드를 호출해야 한다. React Native가 자동으로 변환을 처리해 준다. 아래는 네이티브 모듈 메서드에서 지원하는 인자 타입과 그에 해당하는 자바스크립트 타입을 정리한 표다.

JavaKotlinJavaScript
BooleanBoolean?boolean
booleanboolean
DoubleDouble?number
doublenumber
StringStringstring
CallbackCallbackFunction
PromisePromisePromise
ReadableMapReadableMapObject
ReadableArrayReadableArrayArray

아래 타입들은 현재 지원되지만 TurboModules에서는 지원되지 않을 예정이다. 사용을 피하는 것이 좋다:

  • Integer Java/Kotlin -> ?number
  • Float Java/Kotlin -> ?number
  • int Java -> number
  • float Java -> number

위에 나열되지 않은 타입의 경우, 직접 변환을 처리해야 한다. 예를 들어, Android에서는 Date 변환을 기본적으로 지원하지 않는다. 네이티브 메서드 내에서 Date 타입으로의 변환을 다음과 같이 직접 처리할 수 있다.

java
    String dateFormat = "yyyy-MM-dd";
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
Calendar eStartDate = Calendar.getInstance();
try {
eStartDate.setTime(sdf.parse(startDate));
}

상수 내보내기

네이티브 모듈은 getConstants() 메서드를 구현해 상수를 내보낼 수 있다. 이 메서드는 JS에서 사용 가능하며, 아래 예제에서는 getConstants()를 구현하고 DEFAULT_EVENT_NAME 상수를 포함한 Map을 반환한다. 이 상수는 자바스크립트에서 접근할 수 있다.

java
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("DEFAULT_EVENT_NAME", "New Event");
return constants;
}

이 상수는 JS에서 네이티브 모듈의 getConstants를 호출해 접근할 수 있다:

tsx
const {DEFAULT_EVENT_NAME} = CalendarModule.getConstants();
console.log(DEFAULT_EVENT_NAME);

기술적으로는 getConstants()에서 내보낸 상수를 네이티브 모듈 객체에서 직접 접근할 수도 있다. 하지만 TurboModules에서는 이 방식이 더 이상 지원되지 않으므로, 커뮤니티에서는 위의 접근 방식을 사용해 불필요한 마이그레이션을 피할 것을 권장한다.

현재 상수는 초기화 시점에만 내보내지므로, 런타임에 getConstants의 값을 변경해도 자바스크립트 환경에는 영향을 미치지 않는다. TurboModules에서는 이 동작이 변경된다. TurboModules에서는 getConstants()가 일반적인 네이티브 모듈 메서드가 되며, 각 호출 시 네이티브 측에 도달한다.

콜백

네이티브 모듈은 콜백이라는 특별한 인자 타입을 지원한다. 콜백은 비동기 메서드에서 Java/Kotlin에서 JavaScript로 데이터를 전달하는 데 사용된다. 또한 네이티브 측에서 JavaScript를 비동기적으로 실행하는 데에도 활용할 수 있다.

콜백을 사용하는 네이티브 모듈 메서드를 만들려면 먼저 Callback 인터페이스를 임포트한 후, 네이티브 모듈 메서드에 Callback 타입의 새로운 파라미터를 추가한다. TurboModules가 도입되면서 콜백 인자와 관련된 몇 가지 제약 사항이 사라질 예정이다. 현재는 함수 인자로 두 개의 콜백만 사용할 수 있다. 하나는 성공 콜백(successCallback)이고, 다른 하나는 실패 콜백(failureCallback)이다. 또한 네이티브 모듈 메서드 호출의 마지막 인자가 함수일 경우 성공 콜백으로 간주되며, 마지막에서 두 번째 인자가 함수일 경우 실패 콜백으로 간주된다.

java
import com.facebook.react.bridge.Callback;

@ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {
}

Java/Kotlin 메서드 내에서 콜백을 호출해 JavaScript로 전달할 데이터를 제공할 수 있다. 네이티브 코드에서 JavaScript로 직렬화 가능한 데이터만 전달할 수 있다는 점에 유의한다. 네이티브 객체를 전달해야 한다면 WriteableMaps를 사용하고, 컬렉션을 전달해야 한다면 WritableArrays를 사용한다. 또한 콜백은 네이티브 함수가 완료된 직후에 호출되지 않는다. 이전 호출에서 생성된 이벤트의 ID를 콜백으로 전달하는 예제를 살펴본다.

java
  @ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {
Integer eventId = ...
callBack.invoke(eventId);
}

이 메서드는 JavaScript에서 다음과 같이 접근할 수 있다:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent(
'Party',
'My House',
eventId => {
console.log(`Created a new event with id ${eventId}`);
},
);
};

또한 중요한 점은 네이티브 모듈 메서드는 하나의 콜백을 한 번만 호출할 수 있다는 것이다. 즉, 성공 콜백이나 실패 콜백 중 하나만 호출할 수 있으며, 각 콜백은 최대 한 번만 호출할 수 있다. 그러나 네이티브 모듈은 콜백을 저장해 나중에 호출할 수 있다.

콜백을 사용한 오류 처리에는 두 가지 방법이 있다. 첫 번째는 Node의 관례를 따라 콜백에 전달되는 첫 번째 인자를 오류 객체로 간주하는 것이다.

java
  @ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {
Integer eventId = ...
callBack.invoke(null, eventId);
}

JavaScript에서는 첫 번째 인자를 확인해 오류가 전달되었는지 확인할 수 있다:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent(
'testName',
'testLocation',
(error, eventId) => {
if (error) {
console.error(`Error found! ${error}`);
}
console.log(`event id ${eventId} returned`);
},
);
};

두 번째 방법은 onSuccess와 onFailure 콜백을 사용하는 것이다:

java
@ReactMethod
public void createCalendarEvent(String name, String location, Callback myFailureCallback, Callback mySuccessCallback) {
}

그런 다음 JavaScript에서 오류와 성공 응답을 위한 별도의 콜백을 추가할 수 있다:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent(
'testName',
'testLocation',
error => {
console.error(`Error found! ${error}`);
},
eventId => {
console.log(`event id ${eventId} returned`);
},
);
};

Promise 활용하기

네이티브 모듈은 Promise를 처리할 수 있다. 이 기능은 특히 ES2016의 async/await 문법을 사용할 때 자바스크립트 코드를 단순화하는 데 도움이 된다. 네이티브 모듈의 Java/Kotlin 메서드에서 마지막 매개변수가 Promise인 경우, 해당 JS 메서드는 JS Promise 객체를 반환한다.

콜백 대신 Promise를 사용하도록 위 코드를 리팩토링하면 다음과 같다:

java
import com.facebook.react.bridge.Promise;

@ReactMethod
public void createCalendarEvent(String name, String location, Promise promise) {
try {
Integer eventId = ...
promise.resolve(eventId);
} catch(Exception e) {
promise.reject("Create Event Error", e);
}
}

콜백과 마찬가지로, 네이티브 모듈 메서드는 Promise를 resolve하거나 reject할 수 있다(둘 다는 아님). 또한 각각은 최대 한 번만 호출할 수 있다. 즉, 성공 콜백이나 실패 콜백 중 하나만 호출할 수 있으며, 각 콜백은 최대 한 번만 호출할 수 있다. 하지만 네이티브 모듈은 콜백을 저장했다가 나중에 호출할 수 있다.

이 메서드의 자바스크립트 부분은 Promise를 반환한다. 따라서 await 키워드를 사용해 비동기 함수 내에서 이 메서드를 호출하고 결과를 기다릴 수 있다:

tsx
const onSubmit = async () => {
try {
const eventId = await CalendarModule.createCalendarEvent(
'Party',
'My House',
);
console.log(`Created a new event with id ${eventId}`);
} catch (e) {
console.error(e);
}
};

reject 메서드는 다음과 같은 다양한 조합의 인자를 받을 수 있다:

java
String code, String message, WritableMap userInfo, Throwable throwable

더 자세한 내용은 Promise.java 인터페이스를 여기에서 확인할 수 있다. userInfo가 제공되지 않으면 React Native는 이를 null로 설정한다. 나머지 매개변수는 React Native가 기본값을 사용한다. message 인자는 에러 호출 스택 상단에 표시되는 에러 message를 제공한다. 아래는 Java/Kotlin에서 reject를 호출했을 때 React Native 앱에서 표시되는 에러 메시지의 예시이다.

Java/Kotlin에서 reject 호출:

java
promise.reject("Create Event error", "Error parsing date", e);

Promise가 reject될 때 React Native 앱에서 표시되는 에러 메시지:

React Native 앱에서 표시되는 에러 메시지 이미지
에러 메시지 이미지

JavaScript로 이벤트 전송하기

네이티브 모듈은 직접 호출되지 않아도 JavaScript로 이벤트를 전송할 수 있다. 예를 들어, 네이티브 Android 캘린더 앱의 캘린더 이벤트가 곧 발생한다는 알림을 JavaScript에 전달하고 싶을 수 있다. 이를 가장 쉽게 구현하는 방법은 ReactContext에서 얻을 수 있는 RCTDeviceEventEmitter를 사용하는 것이다. 아래 코드 스니펫에서 이를 확인할 수 있다.

java
...
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
...
private void sendEvent(ReactContext reactContext,
String eventName,
@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}

private int listenerCount = 0;

@ReactMethod
public void addListener(String eventName) {
if (listenerCount == 0) {
// 필요한 경우 업스트림 리스너나 백그라운드 작업을 설정
}

listenerCount += 1;
}

@ReactMethod
public void removeListeners(Integer count) {
listenerCount -= count;
if (listenerCount == 0) {
// 업스트림 리스너 제거, 불필요한 백그라운드 작업 중지
}
}
...
WritableMap params = Arguments.createMap();
params.putString("eventProperty", "someValue");
...
sendEvent(reactContext, "EventReminder", params);

JavaScript 모듈은 NativeEventEmitter 클래스의 addListener를 통해 이벤트를 수신하도록 등록할 수 있다.

tsx
import {NativeEventEmitter, NativeModules} from 'react-native';
...
useEffect(() => {
const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample);
let eventListener = eventEmitter.addListener('EventReminder', event => {
console.log(event.eventProperty) // "someValue"
});

// 컴포넌트가 언마운트되면 리스너 제거
return () => {
eventListener.remove();
};
}, []);

startActivityForResult로부터 액티비티 결과 얻기

startActivityForResult로 시작한 액티비티의 결과를 얻으려면 onActivityResult를 리스닝해야 한다. 이를 위해 BaseActivityEventListener를 확장하거나 ActivityEventListener를 구현한다. API 변경에 더 강한 BaseActivityEventListener를 사용하는 것이 좋다. 그리고 모듈의 생성자에서 리스너를 등록한다.

java
reactContext.addActivityEventListener(mActivityResultListener);

이제 다음 메서드를 구현해 onActivityResult를 리스닝할 수 있다.

java
@Override
public void onActivityResult(
final Activity activity,
final int requestCode,
final int resultCode,
final Intent intent) {
// 여기에 로직을 추가한다
}

이를 보여주기 위해 기본 이미지 선택기를 구현해보자. 이미지 선택기는 JavaScript에 pickImage 메서드를 노출하고, 호출 시 이미지 경로를 반환한다.

kotlin
public class ImagePickerModule extends ReactContextBaseJavaModule {

private static final int IMAGE_PICKER_REQUEST = 1;
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
private static final String E_PICKER_CANCELLED = "E_PICKER_CANCELLED";
private static final String E_FAILED_TO_SHOW_PICKER = "E_FAILED_TO_SHOW_PICKER";
private static final String E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND";

private Promise mPickerPromise;

private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
if (requestCode == IMAGE_PICKER_REQUEST) {
if (mPickerPromise != null) {
if (resultCode == Activity.RESULT_CANCELED) {
mPickerPromise.reject(E_PICKER_CANCELLED, "Image picker was cancelled");
} else if (resultCode == Activity.RESULT_OK) {
Uri uri = intent.getData();

if (uri == null) {
mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "No image data found");
} else {
mPickerPromise.resolve(uri.toString());
}
}

mPickerPromise = null;
}
}
}
};

ImagePickerModule(ReactApplicationContext reactContext) {
super(reactContext);

// `onActivityResult` 리스너 추가
reactContext.addActivityEventListener(mActivityEventListener);
}

@Override
public String getName() {
return "ImagePickerModule";
}

@ReactMethod
public void pickImage(final Promise promise) {
Activity currentActivity = getCurrentActivity();

if (currentActivity == null) {
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist");
return;
}

// 선택기에서 데이터를 반환할 때 resolve/reject할 Promise 저장
mPickerPromise = promise;

try {
final Intent galleryIntent = new Intent(Intent.ACTION_PICK);

galleryIntent.setType("image/*");

final Intent chooserIntent = Intent.createChooser(galleryIntent, "Pick an image");

currentActivity.startActivityForResult(chooserIntent, IMAGE_PICKER_REQUEST);
} catch (Exception e) {
mPickerPromise.reject(E_FAILED_TO_SHOW_PICKER, e);
mPickerPromise = null;
}
}
}

라이프사이클 이벤트 수신하기

액티비티의 onResume, onPause와 같은 라이프사이클 이벤트를 수신하는 방법은 ActivityEventListener를 구현하는 방식과 매우 유사하다. 모듈은 LifecycleEventListener를 구현해야 한다. 그런 다음 모듈의 생성자에서 다음과 같이 리스너를 등록한다.

java
reactContext.addLifecycleEventListener(this);

이제 다음 메서드를 구현하여 액티비티의 라이프사이클 이벤트를 수신할 수 있다.

java
@Override
public void onHostResume() {
// Activity `onResume`
}
@Override
public void onHostPause() {
// Activity `onPause`
}
@Override
public void onHostDestroy() {
// Activity `onDestroy`
}

스레딩

현재 안드로이드에서는 모든 네이티브 모듈의 비동기 메서드가 하나의 스레드에서 실행된다. 네이티브 모듈은 자신이 어떤 스레드에서 호출되는지에 대해 어떤 가정도 해서는 안 된다. 현재 할당된 스레드는 향후 변경될 수 있기 때문이다. 블로킹 호출이 필요한 경우, 무거운 작업은 내부적으로 관리되는 워커 스레드로 전달해야 한다. 그리고 콜백은 해당 스레드에서 분배해야 한다.