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

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

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

네이티브 모듈을 작성하는 방법을 안내하는 고급 안내서입니다. 독자가 Objective-C 또는 Swift 및 핵심 라이브러리 (Foundation, UIKit)를 알고 있다고 가정합니다.

 

네이티브 모듈 설정

네이티브 모듈은 일반적으로 npm 패키지로 배포됩니다. 단, 네이티브 모듈인 경우 Xcode 라이브러리 프로젝트가 포함됩니다. 기본 지식(scaffolding)을 얻으려면 먼저 네이티브 모듈 설정 안내서를 읽으십시오.

 

iOS Calendar 모듈 예

이 가이드는 iOS 캘린더 API 예제를 사용합니다. JavaScript에서 iOS 캘린더에 액세스하고 싶다고 가정해 봅시다.

네이티브 모듈은 RCTBridgeModule 프로토콜을 구현하는 Objective-C 클래스입니다. RCT는 ReaCT의 약자입니다.

// CalendarManager.h
#import <React/RCTBridgeModule.h>

@interface CalendarManager : NSObject <RCTBridgeModule>
@end

RCTBridgeModule 프로토콜을 구현하는 것 외에도 클래스에는 RCT_EXPORT_MODULE() 매크로가 포함되어야 합니다. 여기에는 JavaScript 코드에서와 같이 모듈에 액세스 할 수있는 이름을 지정하는 선택적 인수가 사용됩니다 (나중에 자세히 설명). 이름을 지정하지 않으면 JavaScript 모듈 이름이 Objective-C 클래스 이름과 매치됩니다. Objective-C 클래스 이름이 RCT로 시작하면 JavaScript 모듈 이름은 RCT 접두사를 제외합니다.

// CalendarManager.m
#import "CalendarManager.h"

@implementation CalendarManager

// CalendarManager라는 모듈을 내보내려면
RCT_EXPORT_MODULE();

// 대신 모듈 이름을 AwesomeCalendarManager로 지정합니다
// RCT_EXPORT_MODULE(AwesomeCalendarManager);

@end

React Native는 명시적으로 지시되지 않는 한 CalendarManager의 메소드를 JavaScript에 노출 하지 않습니다. 이는 RCT_EXPORT_METHOD() 매크로를 사용하여 수행됩니다.

#import "CalendarManager.h"
#import <React/RCTLog.h>

@implementation CalendarManager

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

@end

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

import {NativeModules} from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');

참고 : JavaScript 메소드 이름

JavaScript로 내 보낸 메소드 이름은 첫 번째 콜론까지의 고유 메소드 이름입니다. React Native는 RCT_REMAP_METHOD()라는 매크로를 정의하여 JavaScript 메서드의 이름을 지정합니다. 여러 네이티브 메소드가 첫 번째 콜론까지 동일하고 JavaScript 이름이 충돌 할 때 유용합니다.

CalendarManager 모듈은 [CalendarManager new] 호출을 사용하여 Objective-C 측에서 인스턴스화됩니다. 브릿지 메소드의 리턴 유형은 항상 void입니다. React Native 브리지는 비동기식이므로 결과를 JavaScript로 전달하는 유일한 방법은 콜백 또는 이벤트 생성 (아래 참조)을 사용하는 것입니다.

 

Argument Types

RCT_EXPORT_METHOD 는 다음과 같은 모든 표준 JSON 객체 유형을 지원합니다.

  • string (NSString)
  • number (NSInteger, float, double, CGFloat, NSNumber)
  • boolean (BOOL, NSNumber)
  • array (NSArray) 이 목록에서 모든 유형의
  • object (NSDictionary) 이 목록의 문자열 키와 모든 유형의 값
  • function (RCTResponseSenderBlock)

RCTConvert 클래스에서 지원하는 모든 유형에서도 작동합니다 (자세한 내용은 RCTConvert 참조). RCTConvert 헬퍼 함수는 모두 JSON 값을 입력으로 승인하여 네이티브 Objective-C 유형 또는 클래스에 맵핑합니다.

CalendarManager 예제에서 이벤트 날짜를 네이티브 메소드에 전달해야합니다. 브리지를 통해 JavaScript Date 객체를 보낼 수 없으므로 날짜를 문자열 또는 숫자로 변환해야합니다. 다음과 같이 네이티브 함수를 작성할 수 있습니다.

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
  NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}

또는 이와 같이 :

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
  NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}

그러나 자동 유형 변환 기능을 사용하면 수동 변환 단계를 완전히 건너 뛰고 다음을 작성할 수 있습니다.

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
  // 날짜를 사용할 준비가되었습니다!
}

그런 다음 다음 중 하나를 사용하여 JavaScript에서 이를 호출합니다.

CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.getTime(),
); // 유닉스 시간(epoch) 이후 날짜를 밀리 초 단위로 전달

또는

CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.toISOString(),
); // ISO-8601 문자열로 날짜 전달

그리고 두 값 모두 네이티브 NSDate로 올바르게 변환됩니다. Array와 같은 잘못된 값은 (도움이 되는) “RedBox” 오류 메시지를 생성합니다.

CalendarManager.addEvent 메소드가 점점 복잡해짐에 따라 인수 수가 증가합니다. 그들 중 일부는 선택 사항(optional) 일 수 있습니다. 이 경우 다음과 같이 이벤트 속성 사전을 승인하기 위해 API를 약간 변경하는 것이 좋습니다.

#import <React/RCTConvert.h>

RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
{
  NSString *location = [RCTConvert NSString:details[@"location"]];
  NSDate *time = [RCTConvert NSDate:details[@"time"]];
  ...
}

JavaScript에서 호출하십시오.

CalendarManager.addEvent('Birthday Party', {
  location: '4 Privet Drive, Surrey',
  time: date.getTime(),
  description: '...',
});

참고 : 배열(array)과 맵(map)에 관해

Objective-C는 이러한 구조의 값 유형에 대한 보증(guarantees)을 제공하지 않습니다. 네이티브 모듈은 문자열 배열을 기대할 수 있지만 JavaScript가 숫자와 문자열을 포함하는 배열로 메소드를 호출하면 NSNumberNSString이 혼합 된 NSArray를 얻게됩니다. 배열의 경우 RCTConvertNSStringArray 또는 UIColorArray와 같은 메서드 선언에 사용할 수있는 형식화 된 컬렉션을 제공합니다. 맵의 경우 RCTConvert 헬퍼 메소드를 수동으로 호출하여 값 유형을 개별적으로 확인하는 것은 개발자의 책임입니다.

 

Callbacks

경고

이 섹션은 아직 콜백에 대한 확실한 모범 사례가 없기 때문에 다른 섹션보다 실험적입니다.

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

RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
  NSArray *events = ...
  callback(@[[NSNull null], events]);
}

RCTResponseSenderBlock은 JavaScript 콜백에 전달할 매개 변수 배열 인 하나의 인수만 허용합니다. 이 경우 노드의 규칙을 사용하여 첫 번째 매개 변수는 error object (일반적으로 오류가없는 경우 null)이고 나머지 매개 변수는 함수의 결과입니다.

CalendarManager.findEvents((error, events) => {
  if (error) {
    console.error(error);
  } else {
    this.setState({events: events});
  }
});

네이티브 모듈은 콜백을 정확히 한 번만 호출해야 합니다. 콜백을 저장하고 나중에 호출해도 됩니다. 이 패턴은 종종 델리게이트(delegates)가 필요한 iOS API를 랩핑(wrap)하는 데 사용됩니다. 예제는 RCTAlertManager를 참조하세요. 콜백이 호출되지 않으면 일부 메모리가 누출됩니다. onSuccessonFail콜백이 모두 전달되면 그 중 하나만 호출해야 합니다.

error-like 오브젝트를 JavaScript로 전달하려면 RCTUtils.h의 RCTMakeError를 사용하십시오. 지금은 Error-shaped dictionary 만 JavaScript로 전달하지만 앞으로는 실제 JavaScript Error 오브젝트를 자동으로 생성하려고합니다.

 

Promises

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

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

RCT_REMAP_METHOD(findEvents,
                 findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  NSArray *events = ...
  if (events) {
    resolve(events);
  } else {
    NSError *error = ...
    reject(@"no_events", @"There were no events", error);
  }
}

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

async function updateEvents() {
  try {
    var events = await CalendarManager.findEvents();

    this.setState({events});
  } catch (e) {
    console.error(e);
  }
}

updateEvents();

 

Threading

네이티브 모듈에는 어떤 스레드가 호출되는지에 대한 가정이 없어야 합니다. React Native는 별도의 직렬 GCD 큐(serial GCD queue)에서 네이티브 모듈 메소드를 호출하지만 이는 구현 세부 사항이며 변경 될 수 있습니다. - (dispatch_queue_t) methodQueue 메소드를 사용하면 네이티브 모듈이 메소드를 실행할 큐를 지정할 수 있습니다. 예를 들어 메인 스레드 전용 iOS API를 사용해야하는 경우 다음을 통해 지정해야합니다.

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

마찬가지로 작업을 완료하는 데 시간이 오래 걸릴 수 있는 경우 네이티브 모듈은 작업을 실행하기 위해 자체 대기열을 차단하고 지정할 수 없습니다. 예를 들어 RCTAsyncLocalStorage 모듈은 자체 대기열을 생성하므로 잠재적으로 느린 디스크 액세스를 기다리는 동안 React 대기열이 차단되지 않습니다.

- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

지정된 methodQueue는 모듈의 모든 메소드에서 공유합니다. 메소드 중 하나만 오래 실행되는 경우 (또는 어떤 이유로 다른 큐와 다른 큐에서 실행해야하는 경우) 메소드 내에서 dispatch_async를 사용하여 다른 큐에 영향을주지 않고 다른 큐에서 특정 메소드의 코드를 수행 할 수 있습니다.

RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 백그라운드 스레드에서 long-running 코드 호출
    ...
    // 모든 thread/queue에서 콜백을 호출(invoke) 할 수 있습니다
    callback(@[...]);
  });
}

참고 : 모듈간에 디스패치 큐(dispatch queues) 공유

methodQueue 메소드는 모듈이 초기화 될 때 한 번 호출 된 후 브릿지에서 보유되므로 모듈 내에서 큐를 사용하지 않는 한 큐를 직접 보유 할 필요가 없습니다. 그러나 여러 모듈간에 동일한 대기열을 공유하려면 각 모듈에 대해 동일한 대기열 인스턴스를 유지하고 반환해야 합니다. 각각 동일한 이름의 대기열을 반환하면 작동하지 않습니다.

 

의존성 주입 (Dependency Injection)

브리지는 등록 된 RCTBridgeModule을 자동으로 초기화하지만 고유 한 모듈 인스턴스를 인스턴스화 할 수 있습니다 (예 : 종속성을 주입 할 수 있음).

RCTBridgeDelegate 프로토콜을 구현하는 클래스를 작성하고 대리자(delegate)를 인수로 RCTBridge를 초기화하고 초기화 된 브릿지로 RCTRootView를 초기화하여 이를 수행 할 수 있습니다.

id<RCTBridgeDelegate> moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];

RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil];

RCTRootView *rootView = [[RCTRootView alloc]
                        initWithBridge:bridge
                            moduleName:kModuleName
                     initialProperties:nil];

 

상수 내보내기 (Exporting Constants)

네이티브 모듈은 런타임시 JavaScript에 바로 사용 가능한 상수를 내보낼 수 있습니다. 이는 브리지를 통한 왕복이 필요한 정적 데이터 통신에 유용합니다.

- (NSDictionary *)constantsToExport
{
  return @{ @"firstDayOfTheWeek": @"Monday" };
}

JavaScript는 이 값을 동기적으로 즉시 사용할 수 있습니다.

console.log(CalendarManager.firstDayOfTheWeek);

상수는 초기화시에만 내보내지므로 런타임에 constantsToExport 값을 변경하면 JavaScript 환경에 영향을 미치지 않습니다.

 

구현 + requiresMainQueueSetup

- constantsToExport를 재정의하는 경우 모듈이 메인 스레드에서 초기화되어야하는지 여부를 React Native에 알리려면 + requireMainQueueSetup도 구현해야합니다. 그렇지 않으면 + requireMainQueueSetup으로 명시적으로 거부(opt out)하지 않는 한 나중에 모듈이 백그라운드 스레드에서 초기화 될 수 있다는 경고가 표시됩니다.

+ (BOOL)requiresMainQueueSetup
{
  return YES;  // 모듈 초기화가 UIKit 호출에 의존하는 경우에만이 작업을 수행하십시오!
}

모듈이 UIKit에 액세스 할 필요가 없으면 NO를 사용하여 + needsMainQueueSetup에 응답해야합니다.

 

열거형 상수 (Enum Constants)

NS_ENUM을 통해 정의 된 열거 형은 먼저 RCTConvert를 확장하지 않으면 메소드 인수로 사용할 수 없습니다.

다음 NS_ENUM 정의를 내보내려면 다음을 수행하십시오.

@implementation RCTConvert (StatusBarAnimation)
  RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
                                               @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
                                               @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
                      UIStatusBarAnimationNone, integerValue)
@end

그런 다음 메소드를 정의하고 다음과 같이 열거 형 상수를 내보낼 수 있습니다.

- (NSDictionary *)constantsToExport
{
  return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
            @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
            @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) };
};

RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
                                completion:(RCTResponseSenderBlock)callback)

그런 다음 내 보낸 메소드로 전달되기 전에 제공된 선택기 (위 예제의 integerValue)를 사용하여 열거 형이 자동으로 래핑 해제됩니다.

 

JavaScript로 이벤트 보내기

네이티브 모듈은 직접 호출하지 않고 JavaScript로 이벤트를 신호 할 수 있습니다. 이를 수행하는 선호되는 방법은 RCTEventEmitter를 서브 클래스 화하고 supportedEvents를 구현하고 self sendEventWithName을 호출하는 것입니다.

// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>

@end
// CalendarManager.m
#import "CalendarManager.h"

@implementation CalendarManager

RCT_EXPORT_MODULE();

- (NSArray<NSString *> *)supportedEvents
{
  return @[@"EventReminder"];
}

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
}

@end

JavaScript 코드는 모듈 주위에 새 NativeEventEmitter 인스턴스를 작성하여 이러한 이벤트를 구독 할 수 있습니다.

import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;

const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);

const subscription = calendarManagerEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
...
// 일반적으로 componentWillUnmount에서 구독 취소하는 것을 잊지 마십시오.
subscription.remove();

JavaScript로 이벤트를 보내는 예제는 RCTLocationObserver를 참조하십시오.

 

제로 리스너 최적화 (Optimizing for zero listeners)

리스너가 없는 상태에서 이벤트를 생성하여 불필요하게 자원을 소비하면 경고가 표시됩니다. 이를 피하고 (예 : 업스트림 알림을 구독 취소하거나 백그라운드 작업을 일시 중지하여) 모듈의 작업 부하를 최적화하기 위해 RCTEventEmitter 하위 클래스에서 startObservingstopObserving을 재정의 할 수 있습니다.

@implementation CalendarManager
{
  bool hasListeners;
}

// 이 모듈의 첫 번째 리스너가 추가되면 호출됩니다.
-(void)startObserving {
    hasListeners = YES;
    // 필요에 따라 업스트림 리스너(upstream listeners) 또는 백그라운드 작업(background tasks) 설정
}

// 이 모듈의 마지막 리스너가 제거되거나 해제(dealloc) 될 때 호출됩니다.
-(void)stopObserving {
    hasListeners = NO;
    // 업스트림 리스너(upstream listeners) 제거, 불필요한 백그라운드 작업 중지
}

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  if (hasListeners) { // 누군가 듣고있는 경우에만 이벤트 보내기
    [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
  }
}

 

스위프트 내보내기 (Exporting Swift)

Swift는 매크로를 지원하지 않으므로 React Native에 노출하려면 약간 더 설정해야하지만 상대적으로 동일하게 작동합니다. 동일한 CalendarManager를 가지고 있지만 Swift 클래스와 같다고 가정 해보겠습니다.

// CalendarManager.swift

@objc(CalendarManager)
class CalendarManager: NSObject {

  @objc(addEvent:location:date:)
  func addEvent(name: String, location: String, date: NSNumber) -> Void {
    // Date is ready to use!
  }

  @objc
  func constantsToExport() -> [String: Any]! {
    return ["someKey": "someValue"]
  }

}

참고 : 클래스와 함수가 Objective-C 런타임으로 올바르게 내보내려면 @objc 수정자(modifiers)를 사용해야합니다.

그런 다음 React Native 브릿지에 필요한 정보를 등록 할 프라이빗 구현 파일(private implementation file)을 작성하십시오.

// CalendarManagerBridge.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)

RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)

@end

Swift 및 Objective-C를 처음 사용하는 사용자에게는 iOS 프로젝트에서 두 언어를 혼합 할 때마다 Objective-C 파일을 Swift에 노출시키기 위해 브리징 헤더(bridging header)라고하는 추가 브리징 파일이 필요합니다. Xcode는 Xcode File>New File 메뉴 옵션을 통해 Swift 파일을 앱에 추가하면이 헤더 파일을 생성하도록 제안합니다. 이 헤더 파일에서 RCTBridgeModule.h를 가져와야합니다.

// CalendarManager-Bridging-Header.h
#import <React/RCTBridgeModule.h>

RCT_EXTERN_REMAP_MODULE_RCT_EXTERN_REMAP_METHOD를 사용하여 내보내는 모듈 또는 메소드의 JavaScript 이름을 변경할 수도 있습니다. 자세한 정보는 RCTBridgeModule을 참조하십시오.

서드 파티 모듈 제작시 중요 : Swift가 포함 된 정적 라이브러리는 Xcode 9 이상에서만 지원됩니다. 모듈에 포함 된 iOS 정적 라이브러리에서 Swift를 사용할 때 Xcode 프로젝트를 빌드하려면 메인 앱 프로젝트에 Swift 코드와 브리징 헤더(bridging header)가 포함되어야합니다. 앱 프로젝트에 Swift 코드가 포함되어 있지 않은 경우 해결 방법은 하나의 빈 .swift 파일과 빈 브리징 헤더 일 수 있습니다.