Native Module의 활용

박은정·2022년 7월 20일
1

리액트네이티브

목록 보기
2/24
post-thumbnail

Beyond a Calendar Native Module

Better Native Module Export

지난번에 알아본 것 처럼 NativeModules에서 네이티브 모듈을 가져와서 사용하는 것은 다소 번거롭습니다.

네이티브 모듈의 사용자가 네이티브 모듈을 활용할 때마다 해당 모듈을 사용할 필요가 없도록 모듈에 대한 자바스크립트 Wrapper를 작성할 수 있습니다.

// CalendarModule.js
import { NativeModules } from 'react-native';
const { CalendarModule } = NativeModules;
export default CalendarModule;

또한 이러한 자바스크립트 파일은 자바스크립트 사이드 기능을 추가하기 편해집니다.
예를 들어 타입스크립트와 같은 타입시스템을 사용하는 경우, 네이티브 모듈에 대한 유형 주석을 여기에 추가할 수 있습니다.
리액트 네이티브는 아직 네이티브에서 자바스크립트 형식으로 지원하지 않지만, 모든 자바스크립트 코드는 타입세이프합니다. 이렇게 하면 나중에 타입세이프한 네이티브 모듈로 전환하기 쉬워집니다.

// 타입스크립트 추가
import { NativeModules } from 'react-native';
const { CalendarModule } = NativeModules;
interface CalendarInterface {
   createCalendarEvent(name: string, location: string): void;
}
export default CalendarModule as CalendarInterface;

다른 자바스크립트에서 위처럼 불러온 네이티브 모듈을 불러와 메서드를 호출할 수 있습니다.

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

Argument Types

자바스크립트에서 네이티브 모듈 메서드가 호출되면, 리액트 네이티브는 자바스크립트 객체의 arguments를 그들의 자바/코틀린 객체와 유사하게 변환합니다.
만약 자바 네이티브 모듈 메서드가 Double(자바에서 8바이트~64바이트의 숫자를 나타내는 자료형)을 허용하면, 자바스크립트에서 숫자를 사용해서 메서드를 호출해야 합니다. 리액트 네이티브가 대신 변환해줍니다.

아래 타입들은 현재 지원되지만 터보 모듈에서는 지원되지 않기 때문에 사용하지 않는 걸 권장합니다.

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

일부 타입을 제외하고 지원하지 않는 타입의 경우, 직접 변환해줘야 합니다.
예를 들어 안드로이드에서는 즉시 날짜 변환이 지원되지 않아 아래처럼 직접 변환해줘야 합니다.

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

Exporting Constants: 상수 내보내기

네이티브 모듈은 자바스크립트에서 사용할 수 있는 네이티브 메서드 getConstants()를 구현해서 상수를 내보낼 수 있습니다.

// getConstant()를 호출하고 자바스크립트에서 접근할 수 있는 DEFAULT_EVENT_NAME 상수가 포함된 Map을 return합니다.
@Override
public Map<String, Object> getConstants() {
   final Map<String, Object> constants = new HashMap<>();
   constants.put("DEFAULT_EVENT_NAME", "New Event");
   return constants;
}

그런 다음 자바스크립트의 네이티브 모듈에서 getConstant()를 호출해서 상수에 접근할 수 있습니다.

import CalendarModule from './CalendarModule';
const { DEFAULT_EVENT_NAME } = CalendarModule.getConstants();
console.log(DEFAULT_EVENT_NAME);

기술적으로 네이티브 모듈 객체에서 getConstant()에서 내보낸 상수에 직접 접근할 수 있습니다만 이 기능은 터보모듈에서는 더 이상 지원되지 않기 때문에 이후에 필연적인 운영체제 변경(necessary migration) 이 발생하지 않도록 커뮤니티에서는 위와 같은 방식으로 수정하기를 권장합니다.

현재 상수는 초기화할 때만 내보내기 때문에 런타임에서 getConstant()값을 변경해도 자바스크립트 환경에 영향을 주지 않지만 터보모듈에서 변경될 것입니다.
터보모듈의 경우 getConstant()는 일반적인 네이티브 모듈 메서드가 되며, 각각의 호출은 네이티브에도 도달됩니다.

Callbacks

네이티브 모듈은 유니크한 종류의 argument인 콜백또한 지원합니다. 콜백은 자바/코틀린에서 자바스크립트로 비동기방식으로 데이터를 전달하기위해 사용됩니다. 네이티브 측에서 자바스크립트를 비동기적으로 실행될때도 사용될 수도 있습니다.

콜백이 있는 네이티브 모듈 메서드를 만들기 위해서는 먼저 콜백 인터페이스를 가져온 다음, 콜백 유형의 네이티브 모듈 메서드에 새로운 파라미터(매개변수)를 추가하면 됩니다.

터보모듈을 통해 제거될 콜백 arguments와 관련되어 일부 수정될 것입니다.
먼저 함수 arguments(인수)에는 successCallback과 failureCallback 두 가지의 콜백함수만 사용할 수 있습니다. 또한 네이티브 모듈 메서드 호출에 대한 마지막 인수가 함수인 경우, successCallback으로 처리되고 네이티브 모듈 메서드 호출에 대한 두번째 인수가 함수일 때 failureCallback으로 처리됩니다.

import com.facebook.react.bridge.Callback;

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

자바/코틀린 메서드에서 콜백을 호출해서 자바스크립트에 전달할 데이터를 제공할 수 있습니다.
네이티브 코드에서 자바스크립트로 직렬화 가능한 데이터 만 전달할 수있습니다. 네이티브 객체를 전달해야 하는 경우 WriteableMaps를 사용할 수 있고, 컬렉션을 사용해야 하는 경우 WriteableArrays를 사용할 수 있습니다.
또한 네이티브 함수가 성공한 이후 콜백이 즉시 호출되지 않음을 강조해야 합니다.

데이터 직렬화

데이터의 메모리 구조는 스택에 메모리가 쌓여서 직접 접근이 가능한 Value Type인 데이터와, Object이나 포인터 변수들같이 힙에 메모리 할당이 되고 스택에서는 이러한 힙 메모리를 참조하는 구조인 Reference Type인 데이터가 있는데
디스크에 저장하거나 통신할 때에는 Value Type의 데이터만 사용할 수 있기 때문에 메모리를 디스크에 저장하거나 네트워크 통신에 사용하기 위한 형식으로 변환하는 것을 말합니다. 이전 호출에서 생성된 이벤트ID는 콜백내부로 전달됩니다. (Below the ID of an event created in an earlier call is passed to the callback.)

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

자바에서 생성된 메서드는 아래와 같이 자바스크립트에서 접근할 수 있습니다.

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

네이티브 모듈 메서드는 콜백을 한 번만 호출할 수 있습니다.
즉, successCallback이나 failureCallback을 호출할 수는 있지만 둘 다 호출할 수는 없으며 각 콜백은 최대 한번만 호출할 수 있습니다. 하지만 네이티브 모듈은 콜백을 저장하고 나중에 다시 호출할 수 있습니다.

콜백을 사용한 오류처리를 하는 방법은 두 가지가 있는데
먼저 Node의 규칙을 따르고 콜백에 전달된 첫번째 인자를 오류 객체로 처리할 수 있습니다.

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

자바스크립트에서 첫번째 인자를 확인해서 오류가 전달되었는지 확인할 수 있습니다.

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

Promises

네이티브모듈은 특히 ES6의 async/await 구문을 사용할 때 자바스크립트를 단순화시킬 수있는 프로미스에서도 사용할 수 있습니다. 네이티브 모듈 자바/코틀린 메서드의 마지막 매개변수가 프로미스인 경우 해당 자바스크립트 메서드는 자바스크립트 프로미스 객체를 반환합니다.

콜백함수 대신 프로미스를 사용하기 위해서 아래와 같이 자바코드를 수정할 수 있습니다.

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);
    }
}

콜백파트에서 말한 것처럼, 콜백과 유사하게 네이티브 모듈 메서드는 프로미스를 reject나 resolve할 수 있으며(둘 다 아닐 수도 있음) 최대 한번만 할 수 있습니다. 네이티브모듈에서는 콜백을 한번만 호출할 수 있기 때문에
즉, successCallback 혹은 failureCallback을 호출할 수 있지만, 둘 다 호출할 수는 없으며 각 콜백은 최대 한 번만 호출할 수 있습니다.
하지만 네이티브 모듈은 콜백을 저장하고 나중에 호출할 수 있습니다.

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 메서드는 다음 인수들을 서로 다른 조합으로 사용합니다.

String code, String message, WritableMap userInfo, Throwable throwable

자세한 내용은 여기에서 Promise.java를 찾을 수 있습니다. userInfo가 제공되지 않으면 리액트 네이티브는 null로 설정합니다. 나머지 파라미터에 대해 리액트 네이티브는 기본값을 사용합니다.
message인자는 에러 콜스택의 맨 상단에 표시된 에러메시지를 제공합니다.

아래는 자바/코틀린에서 reject를 호출한 다음 자바스크립트에 표시되는 오류메시지의 예시입니다.

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

Sending Events to 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);
}
@ReactMethod
public void addListener(String eventName) {
  // Set up any upstream listeners or background tasks as necessary
}

@ReactMethod
public void removeListeners(Integer count) {
  // Remove upstream listeners, stop unnecessary background tasks
}
...
WritableMap params = Arguments.createMap();
params.putString("eventProperty", "someValue");
...
sendEvent(reactContext, "EventReminder", params);

그러면 자바스크립트 모듈은 NativeEventEmitter 클래스의 addListener를 통해 이벤트를 들을 수 있도록 등록할 수 있습니다.

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

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

 componentWillUnmount() {
   this.eventListener.remove(); //Removes the listener
 }

Getting Activity Result from startActivityForResult

startActivityForResult에서 시작한 활동에서 결과를 얻으려면 onActivityResult를 들어야 합니다. 이렇게 하면 BaseActivityEventListener를 확장하거나 ActivityEventListner를 구현해야합니다. BaseActivityEventListener를 확장하는 것은 API 변화에 더 탄력적이기 때문에 선호됩니다. 그리고 나서 다음과 같은 리스너를 모듈생성자에 등록해야 합니다.

// java
reactContext.addActivityEventListener(mActivityResultListener);

이제, 다음 메서드를 구현해서 onActivityResult를 들을 수 있습니다.

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

예시로 기본 이미지 선택기를 구현해보겠습니다. 이미지 선택기에서 이미지 선택 메서드가 자바스크립트에 노출되며, 이 메서드는 호출될 때 이미지의 경로를 변환합니다.

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);

    // 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;
    }
  }
}

Listening to Lifecycle Events

onResume, onPause 등과 같은 Activity LifeCycle 이벤트를 듣는 것은 ActivityEventListener가 구현된 방법과 매우 유사합니다. 모듈은 LifeCycleEventListener를 구현해야 합니다.
그런 다음 다음과 같이 모듈 생성자에수신기를 등록해야 합니다.

reactContext.addLifecycleEventListener(this);

이제 다음 메서드를 구현해서 Activity LifeCycle 이벤트를 들을 수 있습니다.

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

Activity LifeCycle

https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko

Threading

현재까지 안드로이드에서는 모든 네이티브 모듈의 비동기 메서드가 하나의 스레드에서 실행됩니다.
네이티브 모듈은 현재 할당된 것이 미래에 변경될 수도 있기 때문에 호출되는 스레드에 대해 어떠한 가정도 갖지 않아야 합니다.
blocking call이 필요한 경우, 무거운 작업을 안에서 관리되는 작업자 스레드로 보내고 거기에서 배포되는 모든 콜백으로 전송해야 합니다.

profile
새로운 것을 도전하고 노력한다

0개의 댓글