
이 글은 최신 리액트 네이티브 문서에서 Lagacy Native Module이라고 부르는 브릿지를 통하는 기존의 네이티브 모듈 방식으로 작업한다.

나는 react-native-cli를 사용해 AwesomeProject 빈 프로젝트를 생성한 뒤, xcode로 프로젝트를 열어줬다.

그러면 브릿징 헤더파일을 생성할거냐는 질문창이 뜨는데 만들어준다.
이름 그대로 브릿지를 해주는 역할로, Swift에서 Object-C 사용을 위함이다.
그리고 아래와 같이 브릿지 모듈을 임포트하자
// AwesomeProject-Bridging-Header.h
#import "React/RCTBridgeModule.h"
참고로, 생성된 헤더파일 이름을 바꾸고 싶을 땐 아래 사진에 있는 Target - Build Settings 탭에서 Swift Compiler쪽에서 연결된 항목의 이름 변경도 확실히 확인해주도록 하자.

Counter.swift 파일 내 간단하게 카운터 클래스를 작성해본다.
이 때 달아주는 @objc 어노테이션은 Object-C에서 Swift를 사용하기 위함이다.
// Counter.swift
import Foundation
@objc(Counter)
class Counter: NSObject {
private var count = 0
@objc
func increment() {
count += 1
print(count)
}
@objc
func decrement() {
count -= 1
print(count)
}
}
위 카운터를 리액트 네이티브에서 호출하기 위해서 같은 이름의 Object-C 파일을 만들어준다.
그리고 아래와 같이 코드를 작성해주었다.
// Counter.m
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(Counter, NSObject)
RCT_EXTERN_METHOD(increment)
RCT_EXTERN_METHOD(decrement)
@end
RCT_EXTERN_MODULE은 외부에서 모듈을 참조할 수 있도록 하는 기능을 하고
RCT_EXTERN_METHOD는 외부에서 메소드를 참조할 수 있도록 한다.
즉, Counter 모듈과 increment, decrement 메소드를 리액트 네이티브에서 사용할 수 있게끔 해주는 곳이라 보면 되겠다.
import React from 'react';
import { StyleSheet, Text, View, NativeModules, Button } from 'react-native';
const App: React.FC = () => {
return (
<View style={styles.layout}>
<Text style={styles.count}>0</Text>
<Button
title='INCREMENT'
onPress={() => NativeModules.Counter.increment()}
/>
<Button
title='DECREMENT'
onPress={() => NativeModules.Counter.decrement()}
/>
</View>
);
};
가벼운 마음으로 버튼을 눌러보면 갑자기 경고 메세지가 보인다.
Module Counter requires main queue setup since it overrides
initbut doesn't implementrequiresMainQueueSetup. In a future release React Native will default to initializing all native modules on a background thread unless explicitly opted-out of.
네이티브 모듈이 메인 스레드에서 초기화되어야 하는지 여부를 알려주도록 requiresMainQueueSetup 메소드를 구현하도록 하자. 메인 스레드에서 초기화와 UI 업데이트등의 작업이 필요하니 true를 반환해주도록 했다. 추가로 추후 카운트 초기값인 0도 참조할 수 있개 constantsToExport 메소드도 작성해주자.
// Counter.swift
...
@objc
override func constantsToExport() -> [AnyHashable : Any]! {
return ["initialCount": 0]
}
@objc
static func requiresMainQueueSetup() -> Bool {
return true;
}
그리고 다시 빌드해 실행해보면 경고창도 사라지고 increment를 열 번 눌러보니 잘 호출되는 것을 xcode 콘솔창에서 확인할 수 있었다.

연결은 다 되었으니 해당 모듈을 구독해 리액트의 상태로 활용할 수 있도록 완성시켜보자.
일단 초기에 만들었던 브릿지 헤더에서 React/RCTEventEmitter.h 파일을 임포트 해준다.
// AwesomeProject-Bridging-Header.h
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h" // <-- 이벤트 에미터 임포트
requiresMainQueueSetup 메소드가 정의되어 있다는 오류가 나니 override 해주도록 하자.supportedEvents 메소드도 오버라이드 해주어 이벤트를 정의해주자.sendEvent 를 통해 count 값을 알려준다. // Counter.swift
@objc(Counter)
class Counter: RCTEventEmitter {
private var count = 0
@objc
override static func requiresMainQueueSetup() -> Bool {
return true
}
@objc
override func supportedEvents() -> [String]! {
return ["onCountChanged"];
}
@objc
func increment() {
count += 1
sendEvent(withName: "onCountChanged", body: count)
}
@objc
func decrement() {
count -= 1
sendEvent(withName: "onCountChanged", body: count)
}
}
잊지 않고 Object-C 파일에서도 NSObject에서 RCTEventEmitter로 변경해준 뒤
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h> // 이벤트 에미터 임포트
@interface RCT_EXTERN_MODULE(Counter, RCTEventEmitter) // NSObject에서 변경
RCT_EXTERN_METHOD(increment)
RCT_EXTERN_METHOD(decrement)
@end
리액트 네이티브에서 리스너를 달아 버튼을 눌러보자.
const { Counter: CounterModule } = NativeModules;
const App: React.FC = () => {
useEffect(() => {
const counterEventEmitter = new NativeEventEmitter(CounterModule);
counterEventEmitter.addListener('onCountChanged', count => {
console.log(`COUNT CHANGED: ${count}`);
});
return () => {
counterEventEmitter.removeAllListeners('onCountChanged');
};
}, []);
return (
<View style={styles.layout}>
<Text style={styles.count}>{count}</Text>
<Button
title='INCREMENT'
onPress={() => CounterModule.increment()}
/>
<Button
title='DECREMENT'
onPress={() => CounterModule.decrement()}
/>
</View>
);
};
다시 버튼을 눌러보면 잘 작동하는 것을 JS 콘솔에서도 확인해볼 수 있다.
카운터 초기값 설정을 위해 위에서 constantsToExport 메소드를 통해 initialCount 라는 키로 0을 반환해주었기 때문에 Counter 모듈을 콘솔로 찍어보면 내부에 initialCount 값을 확인해 볼 수 있다.
useState를 통해 initialCount로 초기값 설정을 해준 뒤, 이벤트로 들어오는 count 값을 상태에 반영해주면 Swift - React Native 카운터가 완성된다. 리액트 네이티브단의 코드 전체는 아래와 같다.
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View, NativeModules, Button, NativeEventEmitter } from 'react-native';
const { Counter: CounterModule } = NativeModules;
const App: React.FC = () => {
const [ count, setCount ] = useState<number>(CounterModule.initialCount);
useEffect(() => {
const counterEventEmitter = new NativeEventEmitter(CounterModule);
counterEventEmitter.addListener('onCountChanged', setCount);
return () => {
counterEventEmitter.removeAllListeners('onCountChanged');
};
}, []);
return (
<View style={styles.layout}>
<Text style={styles.count}>{count}</Text>
<Button
title='INCREMENT'
onPress={() => CounterModule.increment()}
/>
<Button
title='DECREMENT'
onPress={() => CounterModule.decrement()}
/>
</View>
);
};
useState 한 줄이면 구현할 수 있는걸 굳이 Swift를 사용해 구현해보았다.
위 예제에서의 제한사항은 화면 전환시 같은 모듈을 가져와 사용한다 해도 initialCount는 항상 0이기 때문에 초기화할때의 문제가 있지만 RCTResponseSendBlock 같은 녀석들을 사용해 초기화를 따로 해주는 등의 방법이 있겠다.
플러터의 메소드 채널과 매우 유사하고 간단한 예제이기 때문에 크게 어려운 부분은 없지만, 네이티브 브릿징이 크로스 플랫폼 앱 개발이 가지고있는 가슴 뛰는 부분이아닐까 생각한다.