기록의 습관화
article thumbnail

이번 포스트에서는 React Native에서 Native 모듈을 만드는 법에 대해서 다뤄보려고 합니다.

사이드 프로젝트를 하는데 공유하기 기능인 Share Intent의 경우 지원하는 라이브러리들이 모두 2년 전 라이브러리들이고

제대로 업데이트가 되지 않아서 작동을 제대로 안 하더라고요 😂

Android와 IOS 중에서 Android는 그나마 쉬운 편이라 일단 IOS부터 다뤄보도록 하겠습니다.

 

일단 들어가기 앞서서 Native 모듈을 사용할 수 있는 환경은 다음과 같습니다.

 

1. Expo prebuild 환경(android, ios 폴더 존재)

2. Pure React Native 환경

 

그리고 공유하기에 이용한 데이터는 웹 페이지에서 URL을 공유하는 기능을 만들어볼 것입니다.

다른 사진 공유나 영상 공유도 이와 비슷한 방법이니 조금씩만 바꿔나가시면 될 거예요.

 

React Native의 Share 기능을 이용할 수는 없는 건가요?

네 아쉽게도 요번에 저희가 이용할 기능은 다른 앱에서 내가 만든 앱으로 데이터를 공유하는 것입니다.

Share 기능은 내가 만든 앱에서 -> 다른 앱으로 데이터를 전달하기 때문에 이 기능은 따로 만들어줘야 합니다.

 


IOS Native 모듈 만들기

시작하기에 앞서서 주의하실 점은 React Native IOS에서의 공유하기 기능은 공유할 때 나의 앱을 열 수 없습니다.

정책상 앱이 꺼져 있는 경우를 제외하고는 Background나 Foreground에 있는 앱을 DeepLink를 이용해서 열 수 없다고 합니다.

그래서 저희가 흔히 사용하는 카카오톡, Notion 등의 앱들도 공유하기 기능을 통해 앱을 열어보면
앱 자체가 아닌 다른 창들이 열리는 것을 확인하실 수 있을 겁니다.

그렇기에 만약 공유하기 기능을 통해서 무언가를 하고 싶으시다면 IOS 화면을 따로 만들어 주시고 기능을 개발해야 합니다.

0. 시작해 보기

> IOS

https://reactnative.dev/docs/native-modules-ios

 

iOS Native Modules · React Native

Welcome to Native Modules for iOS. Please start by reading the Native Modules Intro for an intro to what native modules are.

reactnative.dev

이제 시작해 볼까요?

 

처음은 native 기능인 Share Intent 모듈을 만드는 것부터 시작합니다.

IOS에서는 공유하기 기능을 커스터마이징 하기 위해서는 IOS 자체적으로 지원해 주는 Share Extension 기능으로부터 시작하여야 합니다.

 

공유하기 기능

 

1. Share Extension Target 만들기

xcode를 열고 새로운 Share Extension Target을 만들어 줍니다.

 

| File > New > Target > Share Extension

ShareIntent 생성

IOS 에서는 Objective_C와 Swift 중 선택해서 개발이 가능한데 

굳이 Objective_C를 이용해서 개발을 진행할 필요는 없으니 Swift를 이용해서 개발을 진행하도록 하겠습니다.

 

그럼 다음과 같이 ShareVidewController가 생기게 됩니다.

이제 여기서 didSelectPost 메서드를 통해서 IOS 앱에서 공유하기를 선택해서 받은 데이터를 이용해 보도록 할 겁니다.

ShareIntent 모듈

2. didSelectPost 메서드 수정하기

//
//  ShareViewController.swift
//  ShareIntent
//
//  Created by 장동현 on 2023/08/31.
//

import UIKit
import Social
import MobileCoreServices

class ShareViewController: SLComposeServiceViewController {

    override func isContentValid() -> Bool {
        // Do validation of contentText and/or NSExtensionContext attachments here
        return true
    }
  
    override func didSelectPost() {
      let urlTypeIdentifier = kUTTypeURL as String
      
      guard let itemProvider = (self.extensionContext?.inputItems.first as? NSExtensionItem)?.attachments?.first as? NSItemProvider else { return }
      
      if itemProvider.hasItemConformingToTypeIdentifier(urlTypeIdentifier) {
        itemProvider.loadItem(forTypeIdentifier: urlTypeIdentifier, options: nil) { (item, error) in
          if let url = item as? URL {
            // 1. URL이 잘 공유 되었는지 확인
            print("Successfully retrieved URL item: \(url)")
            // 2. 공유하기 동작을 종료
            DispatchQueue.main.async {
              let alert = UIAlertController(title: "알림", message: "공유가 완료 되었습니다.", preferredStyle: .alert)
              alert.addAction(UIAlertAction(title: "확인", style: .default, handler: { _ in
                  self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
              }))
              self.present(alert, animated: true, completion: nil)
            }
          } else {
            print("Error while retrieving URL item: \(error?.localizedDescription ?? "Unknown error")")
          }
        }
      }
    }


    override func configurationItems() -> [Any]! {
        // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
        return []
    }

}

위 코드와 같이 didSelectPost 내용을 수정하면 이제 IOS 공유하기로부터 받은 데이터를 확인해 볼 수 있습니다.

현재는 URL에 대한 데이터만 받기로 했으므로 URL인지 검증하는 코드가 들어있습니다.

공유가 완료되었으면 메인 스레드에서 alert을 보여주어 공유가 완료되었다는 alert 기능까지 수행하게 됩니다.

 

더보기

스크린샷 보기

 

공유하기

 

공유가 완료됨

3. Info.pList 수정하기

공유하기 기능에서 어떤 데이터만 전달하고 싶은지에 대한 정보를 설정할 수 있습니다.

만약 URL만 공유하고 싶다! 하시면 다음과 같이 설정하시면 됩니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>NSExtension</key>
	<dict>
		<key>NSExtensionAttributes</key>
		<dict>
			<key>NSExtensionActivationRule</key>
			<string>SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url").@count == 1).@count == 1</string>
			<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
			<integer>1</integer>
		</dict>
		<key>NSExtensionMainStoryboard</key>
		<string>MainInterface</string>
		<key>NSExtensionPointIdentifier</key>
		<string>com.apple.share-services</string>
	</dict>
</dict>
</plist>

다른 데이터도 공유하고 싶으시다면 이 부분을 참고하시면 될 것 같습니다.

https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/AppExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW10

 

App Extension Keys

App Extension Keys App extensions enable you to provide features to other apps in iOS and macOS. Each app extension type defines keys that let you declare to the system information about an app extension's capabilities and intents. Keys for app extensions

developer.apple.com

developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/AppExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW10

4. React Native에 데이터 전달하기

이제 URL이 정상적으로 공유되었으니 이 데이터를 React Native에 전달도 할 수 있어야겠죠?

다만 주의할 점은 React Native로 데이터를 공유하는 것은

React Native가 Foreground로 동작하기 전까지 React Native에 전달되지 않습니다.

저희가 만든 Share Intent 기능은 Native 쪽에서 다르게 관리가 되기 때문에 앱이 실행이 되기 전 까지는 전달이 되지 못하는 거죠.

저는 이 부분에 대해서 ViewController를 따로 만들어서 공유된 데이터를 바탕으로 서버에 데이터를 올리고

공유가 완료됨에 대한 내용만 React Native에 전달하도록 하였는데 여기에서는 URL 자체를 전달해 보도록 하겠습니다.

 

4.1 데이터 공유하기

IOS에서는 모듈끼리 데이터를 공유하기 위해서 
App Groups를 사용하게 됩니다. 이는 Share Extension 기능과 메인 앱에 데이터 공유 공간을 둬서 이들이 서로 통신할 수 있게 합니다. FE에서 전역 상태 라이브러리를 사용해서 전역으로 데이터를 공유할 수 있다고 보면 되겠죠?

1. App Groups 추가하기

| Singing & Capabillities > + Capabillities > App Groups 검색 

App Groups 추가

2. 새로 생긴 App Groups에 group 추가하기

group의 경우 group.[name].[name] 이렇게 설정해 주시면 됩니다.

3. 똑같이 ShareIntent Extension 에도 동일하게 App group 추가하기

확장 기능에도 동일하게 추가

4.2 RCTBridge 만들기

이제 직접적으로 데이터를 전달하기 위해서 Bridge 모듈을 만들어 줘야 합니다.

공식문서 내용에도 이 부분에 대해서 잘 설명이 되어 있습니다.

 

RCTShareModule 이란 이름으로 모듈과 헤더 파일을 만들어 줍니다.

여기서는 Objective-C로 만들도록 하겠습니다. Swfit 모듈로 파일을 만들려면 다른 설정을 또 해줘야 하는데 굳이 데이터만 넘기면 되는 시점에서 파일을 하나 더 생성할 필요는 없을 것 같으니 만약 Swift로 파일을 만들고 싶으신 분들은 공식문서를 참고하시면 될 것 같습니다.

모듈 추가
헤더 추가

5. 코드 작성

이제 RCTShareModule.m과 헤더 파일을 작성해 줄 차례입니다.

그전에 아까 선언했던 didSelectPost 메서드를 조금 수정해줘야 하는데 

app groups에 받은 url 데이터를 전달하기 위함입니다.

 

아래 내용만 메서드에 추가시켜 주시면 됩니다.

 

// 2. app group에 공유
let sharedDefaults = UserDefaults(suiteName: "group.ww8007.share")
print("Before saving: \(sharedDefaults?.string(forKey: "sharedData") ?? "No Data")")
sharedDefaults?.set(url.absoluteString, forKey: "sharedData")
sharedDefaults?.synchronize()
print("After saving: \(sharedDefaults?.string(forKey: "sharedData") ?? "No Data")")

| RCTShareModule.m

#import "RCTShareModule.h"
#import <React/RCTLinkingManager.h>

@interface RCTShareModule()

@property (nonatomic, strong) NSString *sharedData;

@end

@implementation RCTShareModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(receiveSharedData:(RCTResponseSenderBlock)callback) {
    NSLog(@"receiveSharedData called");
    
    NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.ww8007.share"];
    NSString *sharedData = [sharedDefaults objectForKey:@"sharedData"];
    
    if (sharedData) {
        NSLog(@"receiveSharedData called@");
        callback(@[[NSNull null], sharedData]);
        // 초기화
        [sharedDefaults removeObjectForKey:@"sharedData"]; // 데이터 초기화
        [sharedDefaults synchronize];
        
    } else {
        NSLog(@"No shared data available");
        callback(@[@"No data available", [NSNull null]]);
    }
}


@end

| RCTShareModule.h

#import <React/RCTBridgeModule.h>

@interface RCTShareModule : NSObject <RCTBridgeModule>

@end

6. React Native 쪽에서 데이터 받아주기

이제 React Native 쪽에서 데이터를 받아주기만 하면 됩니다.

간단하게 custom hook을 이용해서 이를 받아주도록 하겠습니다.

 

앱이 Foreground 상태로 돌아왔을 때 감지할 수 있도록 다음과 같이 appState에 따라 native에게 url이 있는지 물어보도록 하겠습니다.

 

/* eslint-disable @typescript-eslint/no-unused-vars */
import {useCallback, useEffect, useRef, useState} from 'react';
import {Alert, AppState, AppStateStatus, NativeModules} from 'react-native';

const useSharedData = () => {
  const {ShareModule, CustomShareModule} = NativeModules;
  const appState = useRef(AppState.currentState);
  const [activeAppState, setActiveAppState] = useState(appState.current);
  const [sharedUrl, setSharedUrl] = useState<string>('');

  const checkSharedData = useCallback(() => {
    if (ShareModule && ShareModule.receiveSharedData) {
      ShareModule?.receiveSharedData((error: string, data: string) => {
        if (data) {
          Alert.alert('Shared Data', data);
          setSharedUrl(data);
        } else {
          console.log(error);
        }
      });
    }
  }, [ShareModule]);

  const handleAppStateChange = useCallback(
    (nextAppState: AppStateStatus) => {
      if (
        appState.current.match(/inactive|background/) &&
        nextAppState === 'active'
      ) {
        console.log('App has come to the foreground!');
        checkSharedData();
      }
      appState.current = nextAppState;
      setActiveAppState(appState.current);
    },
    [checkSharedData],
  );

  useEffect(() => {
    const subscription = AppState.addEventListener(
      'change',
      handleAppStateChange,
    );

    checkSharedData();

    return () => {
      subscription.remove();
    };
  }, [checkSharedData, handleAppStateChange]);

  const clearSharedText = () => {
    setSharedUrl('');
    if (CustomShareModule && CustomShareModule.clearSharedText) {
      CustomShareModule.clearSharedText();
    }
  };

  return {
    sharedUrl,
    clearSharedText,
  };
};

export default useSharedData;

7. 완성

이제 IOS 쪽은 공유하기 기능이 완성되었습니다 🥳
다음 같이 공유된 URL을 받아서 React Native 쪽에서 활용할 수 있게 되었네요

IOS 공유하기


Android 모듈 만들기

이제 안드로이드 모듈을 만들어볼 차례입니다.

다행히도 안드로이드에서는 사용자의 앱을 공유하기 기능 자체에서 열 수 있습니다.

IOS처럼 따로 View를 만들어서 제공할 필요가 없게 되는 거죠.

 

그럼 시작해 볼까요?

 

> Android

https://reactnative.dev/docs/native-modules-android

 

Android Native Modules · React Native

Welcome to Native Modules for Android. Please start by reading the Native Modules Intro for an intro to what native modules are.

reactnative.dev

 

1. CustomShareModule 및 Package 추가하기

CustomShareModule 추가

이제 CustomShareModule과 CustomShareModulePackage를 추가시켜 주면 됩니다.

ShareModule이라는 모듈 네이밍은 React Native 측에서 이미 사용하고 있어서 만약 사용하면 오류가 나니 이 부분은 참고하시길 바랍니다.

 

주의하실 점은 

1. ShareModule이라는 네이밍은 이미 React Native 측에서 사용하고 있으니 다른 네임으로 선언할 것

2. package 네임은 자신의 프로젝트 네이밍을 따라갈 것

이 있습니다.

 

코드를 간략하게 설명하자면 안드로이드의 자체의 공유하기 기능에서

변경점을 받아서 Promise로 return 해주게 됩니다.

| CustomShareModule.java

package com.share_intent;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;

public class CustomShareModule extends ReactContextBaseJavaModule {

    public CustomShareModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @NonNull
    @Override
    public String getName() {
        return "CustomShareModule";
    }

    @ReactMethod
    public void getSharedText(Promise promise) {
        Activity currentActivity = getCurrentActivity();

        if (currentActivity == null) {
            promise.reject("NO_ACTIVITY", "No activity found");
            return;
        }

        Intent intent = currentActivity.getIntent();
        String action = intent.getAction();
        String type = intent.getType();

        Uri data = intent.getData();
        if (data != null) {
            Log.d("CustomShareModule", "Received URL: " + data.toString());
        }

        if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
            String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
            if (sharedText != null) {
                promise.resolve(sharedText);
            } else {
                promise.reject("SHARED_TEXT_NULL", "Shared text is null");
            }
        } else {
            promise.reject("NO_VALID_ACTION", "No valid action");
        }
    }

    @ReactMethod
    public void clearSharedText() {
        Activity currentActivity = getCurrentActivity();
        if (currentActivity != null) {
            Intent intent = currentActivity.getIntent();
            intent.removeExtra(Intent.EXTRA_TEXT);
        }
    }

}

| CustomShareModulePackage.java

이 파일에서는 React Native 측에서 모듈에 추가할 수 있도록

createNativeModules

를 만들어서 native 모듈 측에서 사용할 수 있게 해 줍니다.

package com.share_intent;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class CustomShareModulePackage implements ReactPackage {

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @NonNull
    @Override
    public List<NativeModule> createNativeModules(
            @NonNull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();

        modules.add(new CustomShareModule(reactContext));

        return modules;
    }
}

 

2. MainApplication에 CustomShareModule 추가하기

이제 Android에게 저희가 만든 CustomShareModule을 등록해 줄 차례입니다.

MainApplication.java에 다음과 같이 내용을 추가해 주시면 됩니다.

@Override
protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // 새로 만든 커스텀 패키지 추가
  packages.add(new CustomShareModulePackage());
  return packages;
}

3. MainActivity에서 Intent 기능 감시하기

이 부분은 안 해주면 동작을 안 하더라고요...

기본적인 onNewIntent 기능에 Intent를 등록할 수 있게 메서드를 수정해 줍니다.

  @Override
  public void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
  }

4. AndroidManifest 수정하기

아까 IOS에서 Info.PList에 어떤 데이터를 공유할지 설정했던 것처럼 Android도 동일하게 이를 설정해 줄 수 있습니다.

저는 URL만 필요하기 때문에 다음과 같이 설정하였습니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <!-- 다음과 같이 추가 -->
        <intent-filter>
          <action android:name="android.intent.action.SEND" />
          <category android:name="android.intent.category.DEFAULT" />
          <data android:mimeType="text/plain" />
        </intent-filter>
        <!-- 여기까지 -->
      </activity>
    </application>
</manifest>

5. React Native에서 데이터 받아주기

아까 Promise로 설정을 하였기 때문에. then을 이용해서 데이터를 받고 똑같이 확인을 위해서 Alert을 시켜 줍시다.

> useSharedDatat.ts

  const checkSharedText = useCallback(() => {
    if (CustomShareModule && CustomShareModule.getSharedText) {
      CustomShareModule.getSharedText()
        .then((sharedText: string) => {
          Alert.alert('Shared Text', sharedText);
          setSharedUrl(sharedText);
        })
        .catch((error: string) => {
          console.log(error);
          setSharedUrl('');
        });
    }
  }, [CustomShareModule]);

6. 완성

IOS와는 다르게 앱 자체가 열리고 이제 공유받은 데이터를 바로 사용할 수 있게 되었습니다.

만약 이 데이터를 받았을 때 어느 화면을 열어야 한다고 하면

데이터를 받았을 때 그 화면으로 이동시키고 데이터를 전달해 주면 되겠죠?

 

안드로이드 공유하기

마치면서

이렇게 Android와 IOS 모두 공유하기 기능을 사용해 보았습니다.

지금까지 되게 어렵다고 생각해서 누군가 만들어놓은 React Native 모듈만 가져다 쓸 줄 알았지

이렇게 직접 native 코드를 이용해서 기능을 구현해 본 적은 처음이네요.

생각보다 구현해 보니 별거 아니었네? 하는 생각이 드는 거 같습니다.

 

블로그의 코드만 으로는 어떻게 코드들이 구현되어 있는지 감이 안 잡히시는 분들을 위해서 

깃허브 링크도 첨부하도록 하겠습니다.

https://github.com/ww8007/react-native-share-intent

 

GitHub - ww8007/react-native-share-intent: This is example code of share intent feature in android and ios

This is example code of share intent feature in android and ios - GitHub - ww8007/react-native-share-intent: This is example code of share intent feature in android and ios

github.com

 

 

끝으로 이 공유하기 기능을 넣게 된 제가 만든 피클리 서비스를 약간? 홍보하면서 글을 마치도록 하겠습니다 😃

 

간단한 북마크 URL 공유 서비스인데 앞으로 크롬 익스텐션으로도 북마크를 추가할 수 있도록 기능을 지원할 예정입니다.

많이 많이 사용해 주세요 😄

 

https://play.google.com/store/apps/details?id=com.ww8007.pickly

 

pickly - Google Play 앱

북마크 관리하기 어려우셨죠? 이제 피클리와 함께 북마크를 관리해 보세요!

play.google.com

https://apps.apple.com/kr/app/pickly/id6450514861

 

‎pickly

‎하루하루를 보람차게 마무리하기 위해 관심 있는 아티클들을 모아봅시다 흥미로워 보이는 각종 아티클들을 미루지 않고, 꾸준히 읽어볼 수 있도록 피클리가 도와드려요! ㅁ 인터넷 서핑 중

apps.apple.com

 

profile

기록의 습관화

@ww8007

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!