본문 바로가기
개발/ReactNative

ReactNative_ IOS 커스텀 RCTEmitter 생성 및 적용

by JunsC 2024. 3. 5.
728x90

알람앱을 만들면서 ReactNative의 DeviceEventEmitter 를 직접적으로 커스터마이징이 필요한 상황이 오게 되었다.

IOS 에서 어떻게 RCTEmitter 를 커스터마이징하여 적용할지 찾아보았다.

흔들기 기능을 추가하려면 흔들기 감지하고 그것을 실시간 보내주어야 하는데, 그부분을 모듈화하여 ReactNative 안에서 실행하려고 했다.

 

🚀 React Native iOS 커스텀 RCTEmitter 생성하기

React Native에서 iOS용 커스텀 이벤트 모듈을 생성하려면 RCTEventEmitter를 활용하면 돼.
iOS 네이티브 코드(Objective-C 또는 Swift)에서 이벤트를 발생시키고, React Native(JavaScript)에서 이를 **구독(listen)**할 수 있어!

 

 

 

🟢 1. RCTEventEmitter란?

RCTEventEmitter는 iOS 네이티브 코드에서 JS로 이벤트를 전달하는 인터페이스야.
iOS의 네이티브 기능(예: 센서 값, 네트워크 상태 변화, Bluetooth 이벤트 등)을 JavaScript에서 감지할 때 사용해.

사용 예시:

  • 네이티브 센서 값 변화 → React Native에서 감지
  • 네이티브 네트워크 상태 변경 → React Native에서 감지
  • 커스텀 네이티브 이벤트 실행 후 JS로 결과 전달

🔵 2. iOS 네이티브 모듈(RCTEventEmitter) 생성하기

1) 네이티브 모듈 생성 (MyCustomEventEmitter.m)

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

@interface RCT_EXTERN_MODULE(MyCustomEventEmitter, RCTEventEmitter)
RCT_EXTERN_METHOD(supportedEvents)
RCT_EXTERN_METHOD(triggerEvent:(NSString *)message)
@end
 
import Foundation
import React

@objc(MyCustomEventEmitter)
class MyCustomEventEmitter: RCTEventEmitter {

  // 지원하는 이벤트 목록 정의
  override func supportedEvents() -> [String]! {
    return ["CustomEvent"]
  }

  // iOS에서 이벤트를 발생시키는 함수
  @objc func triggerEvent(_ message: String) {
    sendEvent(withName: "CustomEvent", body: ["message": message])
  }

  // iOS에서는 기본적으로 구독자가 있을 때만 이벤트 전송이 가능
  override func startObserving() {
    print("✅ 이벤트 리스너 시작됨")
  }

  override func stopObserving() {
    print("🛑 이벤트 리스너 중지됨")
  }
}

📌 설명

✔ supportedEvents() → JS에서 구독할 이벤트 이름 정의
✔ sendEvent(withName:body:) → JS로 이벤트를 전송하는 함수
✔ triggerEvent(_ message: String) → 외부에서 이벤트를 실행하는 함수
✔ startObserving() / stopObserving() → JS에서 이벤트 리스너 등록/해제 시 실행됨

 

 

 

2) React Native에 모듈 등록 (MyCustomEventEmitter.h)

 

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

@interface MyCustomEventEmitter : RCTEventEmitter <RCTBridgeModule>
@end
 

 

#import "MyCustomEventEmitter.h"

@implementation MyCustomEventEmitter

RCT_EXPORT_MODULE();

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

@end

 

📌 설명

✔ RCT_EXPORT_MODULE(); → React Native에서 이 모듈을 사용할 수 있도록 등록
✔ supportedEvents → CustomEvent를 React Native에서 감지 가능하도록 설정

 

 

 

🔴 3. JavaScript에서 네이티브 이벤트 사용하기

1) 이벤트 구독 (React Native)

이제 JS에서 네이티브 이벤트를 받을 준비를 해야 해!

📌 App.js 또는 이벤트를 받을 컴포넌트에 추가

 

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

const { MyCustomEventEmitter } = NativeModules;
const eventEmitter = new NativeEventEmitter(MyCustomEventEmitter);

const App = () => {
  useEffect(() => {
    // 1️⃣ 네이티브 이벤트 리스너 등록
    const eventListener = eventEmitter.addListener("CustomEvent", (event) => {
      console.log("📢 네이티브 이벤트 수신:", event.message);
    });

    // 2️⃣ 네이티브 이벤트 직접 호출 (테스트용)
    MyCustomEventEmitter.triggerEvent("안녕하세요, iOS에서 왔어요!");

    // 3️⃣ 언마운트 시 리스너 제거
    return () => {
      eventListener.remove();
    };
  }, []);

  return null;
};

export default App;
 

📌 설명

✔ NativeEventEmitter(MyCustomEventEmitter) → 네이티브 모듈을 JS에서 이벤트 리스너로 변환
✔ eventEmitter.addListener("CustomEvent", callback) → iOS에서 발생한 이벤트를 감지
✔ MyCustomEventEmitter.triggerEvent("메시지") → JS에서 직접 네이티브 이벤트 실행
✔ useEffect() 내부에서 이벤트 리스너 등록 & 제거 (컴포넌트가 언마운트되면 리스너도 정리)

 

 

 

우선 , .Swift  와 .h 을 만들어야한다 . 그리고 흔들기 기능관련된 부분을 추가해준다.

 

 

import Foundation

import UIKit

import AVFoundation

import React

import CoreMotion



@objc(ShakeModule)

class ShakeModule : RCTEventEmitter {

  

  var motionManager: CMMotionManager!

  

  override init() {

    super.init()

    self.motionManager = CMMotionManager()

    self.motionManager.accelerometerUpdateInterval = 0.2    

  }

  

  override class func requiresMainQueueSetup() -> Bool {

    return true

  }

  

  override func supportedEvents() -> [String] {

    return ["ShakeEvent"]

  }

  

  @objc

  func startAccelerometerUpdates() {

    self.motionManager.startAccelerometerUpdates(to: .main) { [weak self] data, error in

      guard let x = data?.acceleration.x,

            let y = data?.acceleration.y,

            let z = data?.acceleration.z else {

        return

      }

      

      let speed = sqrt(pow(x, 2)) + sqrt(pow(y, 2)) + sqrt(pow(z, 2))

      if speed > 2.0 {

        self?.sendEvent(withName: "ShakeEvent", body: ["isShake": true])

      } else {

        self?.sendEvent(withName: "ShakeEvent", body: ["isShake": false])

      }

    }

  }

  

  @objc

  func stopAccelerometerUpdates() {

    self.motionManager.stopAccelerometerUpdates()

  }    

}

 

 

#import <Foundation/Foundation.h>

#import <React/RCTBridgeModule.h>

#import <React/RCTEventEmitter.h>

#import <React/RCTEventDispatcher.h>





@interface RCT_EXTERN_REMAP_MODULE(RNShake ,ShakeModule, RCTEventEmitter)



_RCT_EXTERN_REMAP_METHOD(startShakeInit, startAccelerometerUpdates, false)

_RCT_EXTERN_REMAP_METHOD(stopShakeInit, stopAccelerometerUpdates, false)



@end

 

그리고 React Native에서 

 

위와 같이 EventEmitter라고 선언해주고 그 값들을 형변환하여 가져온다.

 

 

그리고 위와 같이 사용하면 커스터마이징 된 Emitter를 사용할 수 있게 된다 !!

끝!!

 

 

참고 *

ReactNative IOS 에서 UIViewController 도 역시 커스터마이징 가능하다

 

NSDictionary *initProps = [self prepareInitialProps];

  UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"alarm", initProps);

  rootView.backgroundColor = [UIColor whiteColor];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

  MainViewController *rootViewController = [MainViewController new];

  rootViewController.view = rootView;

  self.myAlarm = [[Alarm alloc] init];

  [self.myAlarm initBridgeWithBridge:bridge];

  [application setApplicationSupportsShakeToEdit:YES];

  [application registerForRemoteNotifications];

  [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

  center.delegate = self;

  AVAudioSession * session = [AVAudioSession sharedInstance];

  [session setCategory: AVAudioSessionCategoryPlayback error: nil];

  [FIRApp configure];

  if (@available(iOS 13.0, *)) {

    [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.alarm.backgroundTasking" usingQueue:nil launchHandler:^(BGTask *task) {

      [self.myAlarm appTerminated];

    }];

    [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.alarm.backgroundAppRefresh" usingQueue:nil launchHandler:^(BGTask *task) {

      

    }];

  } else {

    UIApplication* app = [UIApplication sharedApplication];

    __block UIBackgroundTaskIdentifier backgroundTask;

    backgroundTask = [app beginBackgroundTaskWithExpirationHandler:^{

      [app endBackgroundTask:backgroundTask];

    }];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

      [app endBackgroundTask:backgroundTask];

    });

  }

 

 

 

 

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."