Skip to main content

Android 네이티브 모듈

info

네이티브 모듈(Native Module)과 네이티브 컴포넌트(Native Components)는 기존 아키텍처에서 사용하던 안정적인 기술이다. 새로운 아키텍처가 안정화되면 이 기술들은 점차 사용되지 않을 예정이다. 새로운 아키텍처는 터보 네이티브 모듈패브릭 네이티브 컴포넌트를 사용해 유사한 결과를 달성한다.

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

캘린더 네이티브 모듈 만들기

이 가이드에서는 Android의 캘린더 API를 JavaScript에서 사용할 수 있도록 해주는 CalendarModule이라는 네이티브 모듈을 만드는 방법을 설명한다. 이 가이드를 마치면 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 파일을 생성하는 것이다. 이 파일은 네이티브 모듈 Java/Kotlin 클래스를 포함할 것이다.

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

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

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 클래스를 확장한다. Android의 경우, 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;

네이티브 메서드를 JavaScript로 내보내기

다음으로 캘린더 이벤트를 생성하고 JavaScript에서 호출할 수 있는 네이티브 모듈에 메서드를 추가해야 한다. JavaScript에서 호출할 모든 네이티브 모듈 메서드는 @ReactMethod로 어노테이션을 추가해야 한다.

CalendarModulecreateCalendarEvent() 메서드를 설정한다. 이 메서드는 JavaScript에서 CalendarModule.createCalendarEvent()로 호출할 수 있다. 현재는 문자열 형태의 이름과 위치를 인자로 받는다. 인자 타입 옵션은 곧 다룰 예정이다.

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

애플리케이션에서 이 메서드를 호출할 때 정상적으로 실행되는지 확인하기 위해 디버그 로그를 추가한다. 아래는 Android util 패키지의 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);
}

네이티브 모듈 구현을 마치고 JavaScript와 연결한 후, 이 단계를 따라 앱의 로그를 확인할 수 있다.

동기식 메서드

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

java
@ReactMethod(isBlockingSynchronousMethod = true)

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

모듈 등록하기 (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/ 폴더 안에 MyAppPackage.java 또는 MyAppPackage.kt라는 이름의 새로운 Java/Kotlin 클래스를 생성하고 ReactPackage를 구현한다.

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

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를 사용할 수 있다. createNativeModules 대신 getModule(String name, ReactApplicationContext rac) 메서드를 구현해 필요한 시점에 네이티브 모듈 객체를 생성한다. TurboReactPackage는 현재 구현이 조금 더 복잡하다. getModule() 메서드 외에도 getReactModuleInfoProvider() 메서드를 구현해야 한다. 이 메서드는 패키지가 인스턴스화할 수 있는 모든 네이티브 모듈 목록과 그들을 인스턴스화하는 함수를 반환한다. 예시는 여기에서 확인할 수 있다. TurboReactPackage를 사용하면 애플리케이션의 시작 시간을 단축할 수 있지만, 현재는 작성이 다소 번거롭다. 따라서 TurboReactPackage를 사용할 때는 주의해야 한다.

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

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

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

작성한 코드 테스트하기

이 시점에서 여러분은 Android 네이티브 모듈의 기본 구조를 설정했다. 이제 네이티브 모듈에 접근하고 JavaScript에서 내보낸 메서드를 호출해 테스트할 수 있다.

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

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

const NewModuleButton = () => {
const onPress = () => {
console.log('네이티브 모듈을 호출합니다!');
};

return (
<Button
title="네이티브 모듈 호출하기!"
color="#841584"
onPress={onPress}
/>
);
};

export default NewModuleButton;

JavaScript에서 네이티브 모듈에 접근하려면 먼저 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));
}

상수 내보내기

네이티브 모듈은 JS에서 사용할 수 있는 getConstants() 메서드를 구현해 상수를 내보낼 수 있다. 아래에서는 getConstants()를 구현하고, JavaScript에서 접근할 수 있는 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 값을 변경해도 JavaScript 환경에는 영향을 미치지 않는다. 이는 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를 반환한다. 즉, async 함수 내에서 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 인자는 에러 호출 스택의 상단에 표시되는 에러 메시지를 제공한다. 아래는 Java/Kotlin에서 reject 호출 시 자바스크립트에서 표시되는 에러 메시지의 예시이다.

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로부터 Activity 결과 받기

startActivityForResult로 시작한 Activity의 결과를 받으려면 onActivityResult를 리스닝해야 한다. 이를 위해 BaseActivityEventListener를 확장하거나 ActivityEventListener를 구현한다. 전자가 API 변경에 더 강력하므로 권장한다. 그런 다음 모듈의 생성자에서 다음과 같이 리스너를 등록한다:

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`
}

스레딩

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