리액트 네이티브 공식 문서Guide(Android)의 Native Modules의 번역 자료입니다. 번역에 문제가 있다면 댓글 달아주시구요. 원문을 보시기를 추천드립니다.

때로는 앱에 React Native에 해당 모듈이없는 플랫폼 API에 액세스 해야하는 경우가 있습니다. JavaScript로 다시 구현하지 않고도 기존 Java 코드를 재사용하거나 이미지 처리, 데이터베이스 또는 여러 가지 고급 확장과 같은 고성능 다중 스레드 코드를 작성할 수 있습니다.

실제 네이티브 코드를 작성하고 플랫폼의 모든 기능에 액세스 할 수 있도록 React Native를 설계했습니다. 이것은 고급 기능이므로 일반적인 개발 프로세스에 포함되지는 않지만 반드시 있어야 합니다. React Native가 필요한 기본 기능을 지원하지 않으면 직접 빌드 할 수 있어야합니다.

 

네이티브 모듈 설정

기본 모듈은 일반적으로 Android 라이브러리 프로젝트를 포함 할 일반적인 Javascript 파일 및 리소스와 별도의 npm 패키지로 배포됩니다. 이 프로젝트는 다른 미디어 에셋과 유사한 NPM의 관점에서 볼 때 특별한 것이 없음을 의미합니다. 기본 지식(scaffolding)을 얻으려면 먼저 기본 모듈 설정 안내서를 읽으십시오.

 

Gradle 사용

Java 코드를 변경하려는 경우 Gradle Daemon을 활성화하여 빌드 속도를 높이는 것이 좋습니다.

 

토스트 모듈

이 안내서는 토스트(Toast) 예제를 사용합니다. JavaScript에서 토스트 메시지를 만들 수 있다고 가정 해 봅시다.

기본 모듈을 만드는 것으로 시작합니다. 기본 모듈은 일반적으로 ReactContextBaseJavaModule 클래스를 확장하고 JavaScript에 필요한 기능을 구현하는 Java 클래스입니다. 우리의 목표는 ToastExample.show('Awesome', ToastExample.SHORT); JavaScript를 사용하여 화면에 짧은 토스트를 표시하십시오.

android/app/src/main/java/com/your-app-name/ 폴더에 ToastModule.java라는 새 Java 클래스를 아래 컨텐츠로 작성하십시오.

// ToastModule.java

package com.your-app-name;

import android.widget.Toast;

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 ToastModule extends ReactContextBaseJavaModule {
  private static ReactApplicationContext reactContext;

  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  ToastModule(ReactApplicationContext context) {
    super(context);
    reactContext = context;
  }
}

ReactContextBaseJavaModule을 사용하려면 getName이라는 메소드가 구현되어 있어야합니다. 이 메소드의 목적은 JavaScript에서 이 클래스를 나타내는 NativeModule의 문자열 이름을 리턴하는 것입니다. 여기서는 JavaScript에서 React.NativeModules.ToastExample을 통해 액세스 할 수 있도록 ToastExample을 호출합니다.

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

getConstants라는 선택적 메소드는 JavaScript에 노출 된 상수 값을 리턴합니다. 구현은 필수는 아니지만 JavaScript에서 Java로 동기화 되어야하는 미리 정의 된(pre-defined) 값에 매우 유용합니다.

  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }

메소드를 JavaScript에 노출하려면 @ReactMethod를 사용하여 Java 메소드에 어노테이션(annotated)을 달아야합니다. 브릿지 메소드의 리턴 유형은 항상 void입니다. React Native 브리지는 비동기식이므로 결과를 JavaScript로 전달하는 유일한 방법은 콜백 또는 이벤트 생성 (아래 참조)을 사용하는 것입니다.

  @ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }

 

Argument Types

다음 인수 유형은 @ReactMethod로 어노테이션이 달린 메소드에 대해 지원되며 JavaScript에 해당하는 항목에 직접 맵핑됩니다.

Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array

ReadableMapReadableArray에 대해서 더 읽어보세요.

 

모듈 등록

Java의 마지막 단계는 모듈을 등록하는 것입니다. 이것은 앱 패키지의 createNativeModules에서 발생합니다. 모듈이 등록되지 않으면 JavaScript에서 사용할 수 없습니다.

android/app/src/main/java/com/your-app-name/ 폴더에 CustomToastPackage.java라는 새 Java 클래스를 아래 내용으로 만듭니다.

// CustomToastPackage.java

package com.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 CustomToastPackage 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 ToastModule(reactContext));

    return modules;
  }

}

패키지는 MainApplication.java 파일의 getPackages 메소드에 제공되어야합니다. 이 파일은 react-native 응용 프로그램 디렉토리의 android 폴더에 있습니다. 이 파일의 경로는 android/app/src/main/java/com/your-app-name/MainApplication.java입니다.

// MainApplication.java

...
import com.your-app-name.CustomToastPackage; // <-- 패키지 이름으로 이 줄을 추가하십시오.
...

protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // 아직 자동 링크 할 수 없는 패키지는 여기에 수동으로 추가 할 수 있습니다. 
  // 예 : packages.add(new MyReactNativePackage ());
  packages.add(new CustomToastPackage()); // <-- 패키지 이름으로 이 줄을 추가하십시오.
  return packages;
}

JavaScript에서 새 기능에 액세스하려면 기본 모듈을 JavaScript 모듈로 랩핑(wrap)하는 것이 일반적입니다. 이것은 반드시 필요한 것은 아니지만 라이브러리 소비자가 매번 NativeModule에서 이를 제거해야 할 필요성을 줄여줍니다. 이 JavaScript 파일은 JavaScript 측 기능을 추가하기에 좋은 위치가됩니다.

아래 내용으로 ToastExample.js라는 새 JavaScript 파일을 만듭니다.

/**
 * 네이티브 ToastExample 모듈이 JS 모듈로 노출됩니다. This has a
 * 여기에는 다음 매개 변수를 사용하는 'show'함수가 있습니다.
 *
 * 1. String message: 토스트 텍스트가 있는 문자열
 * 2. int duration: 토스트의 지속 시간. ToastExample.SHORT 또는 ToastExample.LONG 일 수 있습니다.
 */
import {NativeModules} from 'react-native';
module.exports = NativeModules.ToastExample;

이제 다른 JavaScript 파일에서 다음과 같이 메소드를 호출 할 수 있습니다.

import ToastExample from './ToastExample';

ToastExample.show('Awesome', ToastExample.SHORT);

이 JavaScript가 ToastExample.js와 동일한 계층 구조인지 확인하십시오.

 

토스트 너머 (Beyond Toasts)

콜백 (Callbacks)

네이티브 모듈은 또한 고유 한 인수 인 콜백을 지원합니다. 대부분의 경우 JavaScript에 함수 호출 결과를 제공하는 데 사용됩니다.

import com.facebook.react.bridge.Callback;

public class UIManagerModule extends ReactContextBaseJavaModule {

...

  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Callback errorCallback,
      Callback successCallback) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);
      float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
      float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
      float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
      float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
      successCallback.invoke(relativeX, relativeY, width, height);
    } catch (IllegalViewOperationException e) {
      errorCallback.invoke(e.getMessage());
    }
  }

...

이 방법은 다음을 사용하여 JavaScript로 액세스합니다.

UIManager.measureLayout(
  100,
  100,
  (msg) => {
    console.log(msg);
  },
  (x, y, width, height) => {
    console.log(x + ':' + y + ':' + width + ':' + height);
  },
);

네이티브 모듈은 콜백을 한 번만 호출해야합니다. 그러나 콜백을 저장하고 나중에 호출 할 수 있습니다.

기본 함수가 완료된 직후에 콜백이 호출되지 않는다는 점을 강조한 것이 매우 중요합니다. 브리지 통신은 비동기식이며 실행 루프와도 연결되어 있습니다.

 

프라미스 (Promises)

기본 모듈은 프라미스를 이행 할 수 있으며, 특히 ES2016의 async/await 구문을 사용할 때 JavaScript를 단순화 할 수 있습니다. 브리지 된 기본 메소드의 마지막 매개 변수가 Promise 인 경우 해당 JS 메소드는 JS Promise 오브젝트를 리턴합니다.

콜백 대신 약속을 사용하기 위해 위의 코드를 리팩토링하면 다음과 같습니다.

import com.facebook.react.bridge.Promise;

public class UIManagerModule extends ReactContextBaseJavaModule {

...
  private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR";
  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Promise promise) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);

      WritableMap map = Arguments.createMap();

      map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0]));
      map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1]));
      map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2]));
      map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3]));

      promise.resolve(map);
    } catch (IllegalViewOperationException e) {
      promise.reject(E_LAYOUT_ERROR, e);
    }
  }

...

이 메소드의 JavaScript는 Promise를 리턴합니다. 이것은 async 키워드를 async 함수 내에서 사용하여 호출하고 결과를 기다릴 수 있음을 의미합니다.

async function measureLayout() {
  try {
    var {relativeX, relativeY, width, height} = await UIManager.measureLayout(
      100,
      100,
    );

    console.log(relativeX + ':' + relativeY + ':' + width + ':' + height);
  } catch (e) {
    console.error(e);
  }
}

measureLayout();

 

Threading

네이티브 모듈은 현재 할당이 미래에 변경 될 수 있으므로 어떤 스레드를 호출하는지에 대한 가정이 없어야합니다. 차단 호출(blocking call)이 필요하다면, 무거운 작업(heavy work)은 내부적으로 관리되는 워커(worker) 스레드로 전달되고 콜백은 분산(distributed)됩니다.

 

JavaScript로 이벤트 보내기

기본 모듈은 직접 호출하지 않고도 JavaScript로 이벤트를 신호 할 수 있습니다. 가장 쉬운 방법은 아래 코드 조각과 같이 ReactContext에서 얻을 수있는 RCTDeviceEventEmitter를 사용하는 것입니다.

...
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);
}
...
WritableMap params = Arguments.createMap();
params.putString("eventProperty", "someValue");
...
sendEvent(reactContext, "EventReminder", params);

그런 다음 JavaScript 모듈은 NativeEventEmitter 클래스에서 addListener로 이벤트를 수신하도록 등록 할 수 있습니다.

import { NativeEventEmitter, NativeModules } from 'react-native';
...

  componentDidMount() {
    ...
    const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample);
    eventEmitter.addListener('EventReminder', (event) => {
       console.log(event.eventProperty) // "someValue"
    }
    ...
  }

 

startActivityForResult에서 활동 결과 가져 오기

startActivityForResult로 시작한 활동에서 결과를 얻으려면 onActivityResult를 청취(listen)해야 합니다. 이를 수행하려면 BaseActivityEventListener를 확장하거나 ActivityEventListener를 구현해야합니다. 전자는 API 변경에 대해 더 탄력적이므로 선호됩니다. 그런 다음 모듈의 생성자에 리스너를 등록해야합니다.

reactContext.addActivityEventListener(mActivityResultListener);

이제 다음 메소드를 구현하여 onActivityResult를 청취(listen) 할 수 있습니다.

@Override
public void onActivityResult(
  final Activity activity,
  final int requestCode,
  final int resultCode,
  final Intent intent) {
  // Your logic here
}

이를 설명하기 위해 기본 이미지 선택기를 구현할 것입니다. 이미지 선택기는 pickImage 메소드를 JavaScript에 노출하며, 호출 될 때 이미지의 경로를 반환합니다.

public class ImagePickerModule extends ReactContextBaseJavaModule {

  private static final int IMAGE_PICKER_REQUEST = 467081;
  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);

    // Add the listener for `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;
    }

    // Store the promise to resolve/reject when picker returns data
    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;
    }
  }
}

 

LifeCycle 이벤트 리스닝

onResume, onPause 등과 같은 활동의 LifeCycle 이벤트를 듣는 것은 ActivityEventListener를 구현 한 방법과 매우 유사합니다. 모듈은 LifecycleEventListener를 구현해야합니다. 그런 다음 모듈의 생성자에 리스너를 등록해야합니다.

reactContext.addLifecycleEventListener(this);

이제 다음 방법을 구현하여 활동의 LifeCycle 이벤트를들을 수 있습니다.

@Override
public void onHostResume() {
    // Activity `onResume`
}

@Override
public void onHostPause() {
    // Activity `onPause`
}

@Override
public void onHostDestroy() {
    // Activity `onDestroy`
}