Skip to main content

native-modules-ios

id: native-modules-ios title: iOS 네이티브 모듈

info

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

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

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

이 가이드에서는 JavaScript에서 Apple의 캘린더 API에 접근할 수 있는 CalendarModule이라는 네이티브 모듈을 만드는 방법을 설명한다. 이 가이드를 마치면 JavaScript에서 CalendarModule.createCalendarEvent('Dinner Party', 'My House');를 호출해 캘린더 이벤트를 생성하는 네이티브 메서드를 실행할 수 있다.

시작하기

먼저 React Native 애플리케이션 내의 iOS 프로젝트를 Xcode에서 열어야 한다. React Native 앱에서 iOS 프로젝트는 다음과 같은 위치에서 찾을 수 있다:

React Native 앱 내에서 iOS 프로젝트를 Xcode에서 여는 이미지
iOS 프로젝트를 찾을 수 있는 위치

네이티브 코드를 작성할 때는 Xcode를 사용하는 것을 추천한다. Xcode는 iOS 개발을 위해 특화되어 있으며, 이를 사용하면 코드 구문과 같은 작은 오류를 빠르게 해결할 수 있다.

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

첫 번째 단계는 커스텀 네이티브 모듈의 헤더와 구현 파일을 만드는 것이다. RCTCalendarModule.h라는 새 파일을 생성한다.

RCTCalendarModule.h 클래스를 생성하는 이미지
AppDelegate와 동일한 폴더 내에 커스텀 네이티브 모듈 파일을 생성하는 과정

그리고 다음 내용을 추가한다:

objectivec
//  RCTCalendarModule.h
#import <React/RCTBridgeModule.h>
@interface RCTCalendarModule : NSObject <RCTBridgeModule>
@end

여러분이 만들고 있는 네이티브 모듈에 맞는 이름을 사용할 수 있다. 이 예제에서는 캘린더 네이티브 모듈을 만들기 때문에 클래스 이름을 RCTCalendarModule로 지정한다. ObjectiveC는 Java나 C++처럼 언어 수준에서 네임스페이스를 지원하지 않기 때문에 클래스 이름 앞에 접두사를 붙이는 것이 관례다. 이 접두사는 애플리케이션 이름이나 인프라 이름의 약어가 될 수 있다. 이 예제에서 RCT는 React를 의미한다.

아래에서 볼 수 있듯이, CalendarModule 클래스는 RCTBridgeModule 프로토콜을 구현한다. 네이티브 모듈은 RCTBridgeModule 프로토콜을 구현한 ObjectiveC 클래스다.

다음으로, 네이티브 모듈을 구현해 보자. Xcode에서 Cocoa Touch 클래스를 사용해 동일한 폴더에 RCTCalendarModule.m 파일을 생성하고 다음 내용을 포함시킨다:

objectivec
// RCTCalendarModule.m
#import "RCTCalendarModule.h"

@implementation RCTCalendarModule

// RCTCalendarModule이라는 이름으로 모듈을 내보내기
RCT_EXPORT_MODULE();

@end

모듈 이름

현재 RCTCalendarModule.m 네이티브 모듈은 RCT_EXPORT_MODULE 매크로만 포함하고 있다. 이 매크로는 네이티브 모듈 클래스를 React Native에 등록하고 내보내는 역할을 한다. RCT_EXPORT_MODULE 매크로는 선택적 인자를 받을 수 있으며, 이 인자는 JavaScript 코드에서 모듈에 접근할 때 사용할 이름을 지정한다.

이 인자는 문자열 리터럴이 아니다. 아래 예제에서 RCT_EXPORT_MODULE(CalendarModuleFoo)를 전달하지만, RCT_EXPORT_MODULE("CalendarModuleFoo")는 전달하지 않는다.

objectivec
// CalendarModuleFoo라는 이름으로 모듈을 내보낸다
RCT_EXPORT_MODULE(CalendarModuleFoo);

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

tsx
const {CalendarModuleFoo} = ReactNative.NativeModules;

이름을 지정하지 않으면 JavaScript 모듈 이름은 Objective-C 클래스 이름과 동일하게 되며, "RCT" 또는 "RK" 접두사는 제거된다.

아래 예제를 따라 RCT_EXPORT_MODULE를 인자 없이 호출해 보자. 결과적으로 모듈은 CalendarModule이라는 이름으로 React Native에 노출된다. 이는 Objective-C 클래스 이름에서 RCT를 제거한 이름이다.

objectivec
// 이름을 전달하지 않으면 Objective-C 클래스 이름에서 "RCT"를 제거한 이름으로 모듈을 내보낸다
RCT_EXPORT_MODULE();

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

tsx
const {CalendarModule} = ReactNative.NativeModules;

네이티브 메서드를 JavaScript로 노출하기

React Native는 명시적으로 지정하지 않으면 네이티브 모듈의 메서드를 JavaScript에 노출하지 않는다. 이를 위해 RCT_EXPORT_METHOD 매크로를 사용할 수 있다. RCT_EXPORT_METHOD 매크로로 작성된 메서드는 비동기 방식으로 동작하며, 반환 타입은 항상 void이다. RCT_EXPORT_METHOD 메서드의 결과를 JavaScript로 전달하려면 콜백을 사용하거나 이벤트를 발생시킬 수 있다(이에 대해서는 아래에서 다룬다). 이제 RCT_EXPORT_METHOD 매크로를 사용해 CalendarModule 네이티브 모듈에 createCalendarEvent()라는 메서드를 설정해 보자. 현재는 name과 location을 문자열 인자로 받도록 한다. 인자 타입 옵션에 대해서는 곧 다룰 것이다.

objectivec
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
}

TurboModules를 사용할 경우, 메서드가 RCT 인자 변환에 의존하지 않는 한 RCT_EXPORT_METHOD 매크로가 필요하지 않다(아래 인자 타입 참조). 결국 React Native는 RCT_EXPORT_MACRO를 제거할 예정이므로, RCTConvert 사용을 권장하지 않는다. 대신 메서드 본문 내에서 인자 변환을 수행할 수 있다.

createCalendarEvent() 메서드의 기능을 구현하기 전에, React Native 애플리케이션에서 JavaScript로부터 호출되었는지 확인할 수 있도록 메서드에 콘솔 로그를 추가하자. React의 RCTLog API를 사용한다. 파일 상단에 해당 헤더를 임포트한 후 로그 호출을 추가한다.

objectivec
#import <React/RCTLog.h>
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)name location:(NSString *)location)
{
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

동기식 메서드

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD를 사용해 동기식 네이티브 메서드를 만들 수 있다.

objectivec
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getName)
{
return [[UIDevice currentDevice] name];
}

이 메서드의 반환 타입은 객체 타입(id)이어야 하며, JSON으로 직렬화 가능해야 한다. 따라서 이 훅은 nil 또는 JSON 값(예: NSNumber, NSString, NSArray, NSDictionary)만 반환할 수 있다.

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

구현 내용 테스트하기

이제 iOS에서 네이티브 모듈의 기본 구조를 설정했다. JavaScript에서 네이티브 모듈에 접근하고, 내보낸 메서드를 호출해 테스트해보자.

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

tsx
import React from 'react';
import {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 ios

반복 작업하며 빌드하기

이 가이드를 따라 네이티브 모듈을 반복적으로 수정하면서 작업할 때, JavaScript에서 최신 변경 사항을 반영하려면 애플리케이션을 네이티브로 다시 빌드해야 한다. 이는 작성한 코드가 애플리케이션의 네이티브 부분에 위치하기 때문이다. React Native의 메트로 번들러는 JavaScript의 변경 사항을 감지하고 JS 번들을 즉시 다시 빌드할 수 있지만, 네이티브 코드에 대해서는 동일한 작업을 수행하지 않는다. 따라서 최신 네이티브 변경 사항을 테스트하려면 위에서 언급한 명령어를 사용해 다시 빌드해야 한다.

요약✨

이제 여러분은 자바스크립트에서 네이티브 모듈의 createCalendarEvent() 메서드를 호출할 수 있다. 함수 내에서 RCTLog를 사용하고 있으므로, 앱에서 디버그 모드를 활성화하고 Chrome의 JS 콘솔이나 모바일 앱 디버거 Flipper를 통해 네이티브 메서드가 호출되는지 확인할 수 있다. 네이티브 모듈 메서드를 호출할 때마다 RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); 메시지가 표시되는 것을 확인할 수 있다.

로그 이미지
Flipper에서 확인한 iOS 로그 이미지

여러분은 이제 iOS 네이티브 모듈을 생성하고 React Native 애플리케이션의 자바스크립트에서 이 메서드를 호출했다. 네이티브 모듈 메서드가 어떤 인자 타입을 받는지, 그리고 네이티브 모듈 내에서 콜백과 Promise를 설정하는 방법에 대해 더 알아보려면 계속 읽어보자.

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

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

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

네이티브 모듈을 사용할 때마다 이런 작업을 반복하지 않도록, JavaScript 래퍼를 만들어 활용한다. NativeCalendarModule.js라는 새로운 JavaScript 파일을 생성하고 다음 내용을 추가한다:

tsx
/**
* 이 코드는 네이티브 CalendarModule 모듈을 JS 모듈로 노출한다.
* 'createCalendarEvent' 함수는 다음과 같은 매개변수를 받는다:

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

이 JavaScript 파일은 JavaScript 측 기능을 추가하기에도 적합한 위치가 된다. 예를 들어, TypeScript와 같은 타입 시스템을 사용한다면 여기에 네이티브 모듈에 대한 타입 주석을 추가할 수 있다. React Native가 아직 Native to JS 타입 안전성을 지원하지 않지만, 이러한 타입 주석을 통해 모든 JS 코드가 타입 안전성을 갖추게 된다. 또한, 이러한 주석은 나중에 타입 안전 네이티브 모듈로 전환하는 데도 도움이 된다. 아래는 Calendar Module에 타입 안전성을 추가한 예시이다:

tsx
/**
* 이 코드는 네이티브 CalendarModule 모듈을 JS 모듈로 노출한다.
* '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;

다른 JavaScript 파일에서 네이티브 모듈에 접근하고 메서드를 호출하려면 다음과 같이 작성한다:

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

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

인자 타입 매핑

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

Objective-CJavaScript
NSStringstring, ?string
BOOLboolean
doublenumber
NSNumber?number
NSArrayArray, ?Array
NSDictionaryObject, ?Object
RCTResponseSenderBlockFunction (success)
RCTResponseSenderBlock, RCTResponseErrorBlockFunction (failure)
RCTPromiseResolveBlock, RCTPromiseRejectBlockPromise

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

  • Function (failure) -> RCTResponseErrorBlock
  • Number -> NSInteger
  • Number -> CGFloat
  • Number -> float

iOS의 경우, RCTConvert 클래스에서 지원하는 모든 인자 타입을 사용해 네이티브 모듈 메서드를 작성할 수 있다. (지원되는 타입에 대한 자세한 내용은 RCTConvert 참조) RCTConvert 헬퍼 함수들은 모두 JSON 값을 입력으로 받아 네이티브 Objective-C 타입이나 클래스로 매핑한다.

상수 내보내기

네이티브 모듈은 constantsToExport() 메서드를 재정의하여 상수를 내보낼 수 있다. 아래 예제에서 constantsToExport()를 재정의하고, 자바스크립트에서 접근할 수 있는 기본 이벤트 이름 프로퍼티를 포함한 딕셔너리를 반환한다:

objectivec
- (NSDictionary *)constantsToExport
{
return @{ @"DEFAULT_EVENT_NAME": @"New Event" };
}

이 상수는 자바스크립트에서 네이티브 모듈의 getConstants()를 호출하여 접근할 수 있다:

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

기술적으로는 constantsToExport()로 내보낸 상수를 NativeModule 객체에서 직접 접근할 수도 있다. 하지만 이 방식은 TurboModules에서는 더 이상 지원되지 않으므로, 앞으로의 마이그레이션을 고려해 위의 방식을 사용하는 것을 권장한다.

상수는 초기화 시점에만 내보내지므로, 런타임에 constantsToExport()의 값을 변경해도 자바스크립트 환경에는 영향을 미치지 않는다.

iOS의 경우, constantsToExport()를 재정의했다면 + requiresMainQueueSetup도 구현해야 한다. 이 메서드는 자바스크립트 코드가 실행되기 전에 모듈이 메인 스레드에서 초기화되어야 하는지 React Native에게 알려준다. 이 메서드를 구현하지 않으면, 모듈이 백그라운드 스레드에서 초기화될 수 있다는 경고가 표시된다. 만약 모듈이 UIKit에 접근할 필요가 없다면, + requiresMainQueueSetup에 NO를 반환하면 된다.

콜백

네이티브 모듈은 특별한 종류의 인자를 지원한다. 바로 콜백이다. 콜백은 비동기 메서드에서 Objective-C에서 JavaScript로 데이터를 전달하는 데 사용된다. 또한 네이티브 측에서 JavaScript를 비동기적으로 실행하는 데도 활용할 수 있다.

iOS에서는 콜백이 RCTResponseSenderBlock 타입으로 구현된다. 아래 예제에서 myCallback이라는 콜백 파라미터를 createCalendarEventMethod()에 추가했다:

objectivec
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)title
location:(NSString *)location
myCallback:(RCTResponseSenderBlock)callback)

그런 다음 네이티브 함수 내에서 콜백을 호출해 JavaScript로 전달할 결과를 배열 형태로 제공할 수 있다. RCTResponseSenderBlock은 단 하나의 인자만 받는다. 바로 JavaScript 콜백에 전달할 파라미터의 배열이다. 아래 예제에서는 이전 호출에서 생성된 이벤트의 ID를 반환한다.

중요한 점은 콜백이 네이티브 함수가 완료된 직후에 호출되지 않는다는 것이다. 통신이 비동기적으로 이루어지기 때문이다.

objectivec
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)title location:(NSString *)location callback: (RCTResponseSenderBlock)callback)
{
NSInteger eventId = ...
callback(@[@(eventId)]);

RCTLogInfo(@"Pretending to create an event %@ at %@", title, location);
}

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

tsx
const onSubmit = () => {
CalendarModule.createCalendarEvent(
'Party',
'04-12-2020',
eventId => {
console.log(`Created a new event with id ${eventId}`);
},
);
};

네이티브 모듈은 콜백을 한 번만 호출해야 한다. 하지만 콜백을 저장했다가 나중에 호출할 수도 있다. 이 패턴은 델리게이트가 필요한 iOS API를 래핑할 때 자주 사용된다. 예를 들어 RCTAlertManager를 참고하면 된다. 콜백이 호출되지 않으면 메모리 누수가 발생할 수 있다.

콜백을 사용한 에러 처리에는 두 가지 접근 방식이 있다. 첫 번째는 Node의 관례를 따라 콜백 배열의 첫 번째 인자를 에러 객체로 처리하는 것이다.

objectivec
RCT_EXPORT_METHOD(createCalendarEventCallback:(NSString *)title location:(NSString *)location callback: (RCTResponseSenderBlock)callback)
{
NSNumber *eventId = [NSNumber numberWithInt:123];
callback(@[[NSNull null], eventId]);
}

JavaScript에서는 첫 번째 인자를 확인해 에러가 전달되었는지 여부를 판단할 수 있다:

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

다른 옵션은 두 개의 콜백을 사용하는 것이다. 하나는 실패 시(onFailure), 다른 하나는 성공 시(onSuccess) 호출된다.

objectivec
RCT_EXPORT_METHOD(createCalendarEventCallback:(NSString *)title
location:(NSString *)location
errorCallback: (RCTResponseSenderBlock)errorCallback
successCallback: (RCTResponseSenderBlock)successCallback)
{
@try {
NSNumber *eventId = [NSNumber numberWithInt:123];
successCallback(@[eventId]);
}

@catch ( NSException *e ) {
errorCallback(@[e]);
}
}

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

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

JavaScript에 에러와 유사한 객체를 전달하려면 RCTUtils.h.RCTMakeError를 사용한다. 현재는 JavaScript에 Error 형태의 딕셔너리만 전달하지만, React Native는 앞으로 실제 JavaScript Error 객체를 자동으로 생성하는 것을 목표로 하고 있다. 또한 RCTResponseErrorBlock 인자를 제공할 수도 있다. 이 인자는 에러 콜백에 사용되며 NSError \* 객체를 받는다. 하지만 이 인자 타입은 TurboModules에서는 지원되지 않는다.

Promise 활용하기

네이티브 모듈은 Promise를 처리할 수 있다. 이를 통해 JavaScript 코드를 단순화할 수 있으며, 특히 ES2016의 async/await 문법을 사용할 때 유용하다. 네이티브 모듈 메서드의 마지막 매개변수가 RCTPromiseResolveBlockRCTPromiseRejectBlock일 경우, 해당 JS 메서드는 JS Promise 객체를 반환한다.

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

objectivec
RCT_EXPORT_METHOD(createCalendarEvent:(NSString *)title
location:(NSString *)location
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSInteger eventId = createCalendarEvent();
if (eventId) {
resolve(@(eventId));
} else {
reject(@"event_failure", @"no event id returned", nil);
}
}

이 메서드의 JavaScript 부분은 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);
}
};

JavaScript로 이벤트 전송하기

네이티브 모듈은 직접 호출되지 않아도 JavaScript로 이벤트를 보낼 수 있다. 예를 들어, 네이티브 iOS 캘린더 앱의 일정이 곧 시작된다는 알림을 JavaScript로 전달하고 싶을 수 있다. 이를 구현하는 가장 좋은 방법은 RCTEventEmitter를 상속받고 supportedEvents를 구현한 뒤 sendEventWithName을 호출하는 것이다.

헤더 파일을 업데이트하여 RCTEventEmitter를 임포트하고 RCTEventEmitter를 상속받도록 한다:

objectivec
//  CalendarModule.h

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

@interface CalendarModule : RCTEventEmitter <RCTBridgeModule>
@end

JavaScript 코드는 모듈 주변에 새로운 NativeEventEmitter 인스턴스를 생성해 이 이벤트를 구독할 수 있다.

리스너가 없을 때 이벤트를 발생시키면 불필요한 리소스를 소모한다는 경고를 받게 된다. 이를 방지하고 모듈의 작업 부하를 최적화하려면(예: 업스트림 알림 구독 취소 또는 백그라운드 작업 일시 중지) RCTEventEmitter 서브클래스에서 startObservingstopObserving을 재정의한다.

objectivec
@implementation CalendarModule
{
bool hasListeners;
}

// 이 모듈의 첫 번째 리스너가 추가될 때 호출된다.
-(void)startObserving {
hasListeners = YES;
// 필요한 경우 업스트림 리스너 설정 또는 백그라운드 작업 시작
}

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

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
NSString *eventName = notification.userInfo[@"name"];
if (hasListeners) {// 리스너가 있을 때만 이벤트 전송
[self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
}
}

스레딩

네이티브 모듈이 자체 메서드 큐를 제공하지 않는다면, 어떤 스레드에서 호출되는지에 대해 가정하지 말아야 한다. 현재 네이티브 모듈이 메서드 큐를 제공하지 않으면 React Native가 별도의 GCD 큐를 생성하고 그곳에서 메서드를 호출한다. 이는 구현 세부사항일 뿐이며 변경될 수 있다는 점을 명심해야 한다. 네이티브 모듈에 명시적으로 메서드 큐를 제공하려면, 네이티브 모듈에서 (dispatch_queue_t) methodQueue 메서드를 재정의해야 한다. 예를 들어, 메인 스레드 전용 iOS API를 사용해야 한다면 다음과 같이 지정할 수 있다:

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

마찬가지로, 특정 작업이 완료되기까지 오랜 시간이 걸릴 수 있다면, 네이티브 모듈은 자체 큐를 지정해 작업을 실행할 수 있다. 다시 말하지만, 현재 React Native는 네이티브 모듈에 별도의 메서드 큐를 제공하지만, 이는 의존해서는 안 되는 구현 세부사항이다. 자체 메서드 큐를 제공하지 않으면, 향후 네이티브 모듈의 장기 실행 작업이 다른 관련 없는 네이티브 모듈에서 실행되는 비동기 호출을 블로킹할 수 있다. 예를 들어, RCTAsyncLocalStorage 모듈은 자체 큐를 생성하여 React 큐가 잠재적으로 느린 디스크 접근을 기다리며 블로킹되지 않도록 한다.

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

지정된 methodQueue는 모듈 내 모든 메서드에서 공유된다. 특정 메서드만 장기 실행되거나(또는 다른 이유로 다른 큐에서 실행되어야 한다면), 해당 메서드 내에서 dispatch_async를 사용해 특정 메서드의 코드를 다른 큐에서 실행할 수 있다. 이는 다른 메서드에 영향을 미치지 않는다:

objectivec
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 백그라운드 스레드에서 장기 실행 코드 호출
...
// 콜백은 어떤 스레드/큐에서든 호출 가능
callback(@[...]);
});
}

모듈 간 디스패치 큐 공유

methodQueue 메서드는 모듈이 초기화될 때 한 번 호출되며, React Native에 의해 유지된다. 따라서 모듈 내에서 사용하려는 경우가 아니라면 큐에 대한 참조를 유지할 필요가 없다. 그러나 여러 모듈 간에 동일한 큐를 공유하려면 각 모듈에 대해 동일한 큐 인스턴스를 유지하고 반환해야 한다.

의존성 주입

React Native는 등록된 네이티브 모듈을 자동으로 생성하고 초기화한다. 하지만 특정 의존성을 주입하기 위해 직접 모듈 인스턴스를 생성하고 초기화할 수도 있다.

이를 위해 RCTBridgeDelegate 프로토콜을 구현하는 클래스를 만들고, 해당 델리게이트를 인자로 RCTBridge를 초기화한 뒤, 초기화된 브릿지를 사용해 RCTRootView를 생성한다.

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

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

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

Swift 모듈 내보내기

Swift는 매크로를 지원하지 않기 때문에 React Native 내부에서 JavaScript에 네이티브 모듈과 메서드를 노출하려면 약간의 추가 설정이 필요하다. 하지만 기본적인 방식은 비슷하다. 예를 들어 동일한 CalendarModule을 Swift 클래스로 구현한다고 가정해 보자.

swift
// CalendarModule.swift

@objc(CalendarModule)
class CalendarModule: NSObject {

@objc(addEvent:location:date:)
func addEvent(_ name: String, location: String, date: NSNumber) -> Void {
// Date를 사용할 준비가 되었다!
}

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

}

클래스와 함수가 Objective-C 런타임에 올바르게 노출되도록 @objc 수정자를 사용하는 것이 중요하다.

그런 다음 React Native에 필요한 정보를 등록할 private 구현 파일을 만든다.

objectivec
// CalendarModuleBridge.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(CalendarModule, NSObject)

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

@end

Swift와 Objective-C를 처음 접하는 사람들을 위해, iOS 프로젝트에서 두 언어를 혼합할 때는 Swift 파일에 Objective-C 파일을 노출하기 위해 추가적인 브릿징 파일인 브릿징 헤더가 필요하다. Xcode에서 Swift 파일을 File>New File 메뉴 옵션을 통해 앱에 추가하면 Xcode가 이 헤더 파일을 생성하도록 제안한다. 이 헤더 파일에 RCTBridgeModule.h를 임포트해야 한다.

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

또한 RCT_EXTERN_REMAP_MODULERCT_EXTERN_REMAP_METHOD를 사용해 내보내는 모듈이나 메서드의 JavaScript 이름을 변경할 수 있다. 더 자세한 정보는 RCTBridgeModule을 참고하자.

서드파티 모듈을 만들 때 중요한 점: Swift를 사용한 정적 라이브러리는 Xcode 9 이상에서만 지원된다. 모듈에 포함된 iOS 정적 라이브러리에서 Swift를 사용할 때 Xcode 프로젝트가 빌드되려면, 메인 앱 프로젝트에 Swift 코드와 브릿징 헤더가 포함되어 있어야 한다. 앱 프로젝트에 Swift 코드가 없다면, 빈 .swift 파일과 빈 브릿징 헤더를 추가하는 방법으로 해결할 수 있다.

예약된 메서드 이름

invalidate()

iOS에서 네이티브 모듈은 invalidate() 메서드를 구현해 RCTInvalidating 프로토콜을 준수할 수 있다. 이 메서드는 네이티브 브릿지가 무효화될 때(예: 개발 모드 리로드 시) 호출될 수 있다. 네이티브 모듈에서 필요한 정리 작업을 수행하려면 이 메커니즘을 적절히 활용한다.