이전에 Android로 구현한 React Native 앱에서 외부 앱(사진, 카메라, 갤러리 등)에서 공유되는 이미지를 받아서 처리하는 기능을 iOS으로 개발하는 작업이었습니다.
처음에는 Swift로 개발을 시작했습니다. 최신 언어이고 문법이 깔끔하며, iOS 개발에서 애플이 권장하는 언어이기 때문이었죠.
하지만 Native Module 개발에서는 Objective-C를 강력히 권장하고 있었습니다:
React Native 0.68+ 버전의 New Architecture를 지원하려면 CodeGen을 사용해야 했는데, 이 도구가 생성하는 인터페이스가 순수 Objective-C였습니다. Swift에서 이를 구현하려면 복잡한 브릿징 코드가 필요했지만, Objective-C에서는 아래 코드로 바로 구현 가능했습니다.
#import "LPShareModuleSpec.h"
@interface LPShareModule : NativeLPShareModuleSpecBase
<NativeLPShareModuleSpec>
@end
@implementation LPShareModule
@end
가장 중요했던 것은 기존 Android용으로 만들어진 TypeScript 인터페이스를 그대로 활용하는 것이었습니다. 이미 Android 개발 시 만들어진 NativeLPShareModule.ts가 있었고, iOS에서도 동일한 인터페이스로 동작하도록 구현했습니다.
// src/NativeLPShareModule.ts - 이미 Android용으로 구현됨
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import {TurboModuleRegistry} from 'react-native';
export interface SharedData {
readonly type: string | null;
readonly uri?: string;
readonly uriList?: ReadonlyArray<string>;
}
export interface Spec extends TurboModule {
sendSharedData(eventName: string, data: string): void;
testMethod(): Promise<string>;
getSharedData(): Promise<SharedData>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('LPShareModule');
이렇게 구현한 덕분에 React Native 코드에서는 플랫폼을 신경 쓰지 않고 동일하게 사용할 수 있었습니다:
iOS에서 외부 앱 공유를 처리하는 방법은 Share Extension을 사용하는 것입니다. 전체 구조는 다음과 같습니다:
외부 앱 → Share Extension → App Group → Main App → React Native
주요 구성요소
먼저 Xcode에서 새로운 Share Extension Target을 생성했습니다.
<key>NSExtensionActivationRule</key>
<dict>
<!-- 최대 10개 이미지 지원 -->
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<!-- iOS 14+ 방식 -->
<key>NSExtensionActivationSupportsImage</key>
<true/>
<!-- iOS 13 이하 호환성 -->
<key>NSExtensionActivationSupportsAttachmentsWithMatchingUTIs</key>
<array>
<string>public.image</string>
</array>
</dict>
핵심 로직을 담은 ShareViewController:
- (void)handleSharedContent {
NSExtensionContext *extensionContext = self.extensionContext;
NSArray<NSExtensionItem *> *inputItems = extensionContext.inputItems;
// 이미지 타입 첨부파일만 필터링
NSMutableArray<NSItemProvider *> *imageAttachments = [[NSMutableArray
alloc] init];
for (NSExtensionItem *item in inputItems) {
for (NSItemProvider *attachment in item.attachments) {
if ([attachment
hasItemConformingToTypeIdentifier:UTTypeImage.identifier]) {
[imageAttachments addObject:attachment];
}
}
}
if ([imageAttachments count] == 1) {
// 단일 이미지 처리
[self processImageAttachment:imageAttachments[0] completion:^(NSString
*uri) {
[self saveSharedDataToUserDefaults:@"single" uri:uri uriList:nil];
[self openMainApp];
}];
} else {
// 다중 이미지 처리
[self processMultipleImageAttachments:imageAttachments
completion:^(NSArray<NSString *> *uris) {
[self saveSharedDataToUserDefaults:@"multiple"
uri:uris.firstObject uriList:uris];
[self openMainApp];
}];
}
}
Share Extension과 Main App 간의 데이터 공유를 위해 App Group을 설정:
- (void)saveSharedDataToUserDefaults:(NSString *)type uri:(NSString *)uri
uriList:(NSArray<NSString *> *)uriList {
NSUserDefaults *groupUserDefaults = [[NSUserDefaults alloc]
initWithSuiteName:@"group.io.itmca.lifepuzzle"];
NSUserDefaults *standardUserDefaults = [NSUserDefaults
standardUserDefaults];
// Group UserDefaults에 우선 저장
if (groupUserDefaults) {
[groupUserDefaults setObject:type forKey:@"SharedDataType"];
if (uri) [groupUserDefaults setObject:uri forKey:@"SharedDataURI"];
if (uriList) [groupUserDefaults setObject:uriList
forKey:@"SharedDataURIList"];
[groupUserDefaults synchronize];
}
// Standard UserDefaults에 백업 저장
[standardUserDefaults setObject:type forKey:@"SharedDataType"];
// ... 동일한 로직
}
React Native와 iOS 간의 브릿지 역할을 하는 Native Module:
- (void)getSharedData:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
NSUserDefaults *groupUserDefaults = [[NSUserDefaults alloc]
initWithSuiteName:@"group.io.itmca.lifepuzzle"];
NSString *sharedType = [groupUserDefaults stringForKey:@"SharedDataType"];
NSString *sharedURI = [groupUserDefaults stringForKey:@"SharedDataURI"];
NSArray<NSString *> *sharedURIList = [groupUserDefaults
arrayForKey:@"SharedDataURIList"];
if (sharedType) {
NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
[result setObject:sharedType forKey:@"type"];
if ([sharedType isEqualToString:@"single"] && sharedURI) {
[result setObject:sharedURI forKey:@"uri"];
} else if ([sharedType isEqualToString:@"multiple"] && sharedURIList)
{
[result setObject:sharedURIList forKey:@"uriList"];
}
// 사용 후 데이터 삭제
[groupUserDefaults removeObjectForKey:@"SharedDataType"];
[groupUserDefaults removeObjectForKey:@"SharedDataURI"];
[groupUserDefaults removeObjectForKey:@"SharedDataURIList"];
[groupUserDefaults synchronize];
resolve(result);
} else {
resolve(@{@"type": [NSNull null]});
}
}
각 Target별로 App Groups 권한 설정:
LifePuzzleShareRelease.entitlements:
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.itmca.lifepuzzle</string>
</array>
처음엔 "최신 = 최선"이라고 생각했지만, 프레임워크의 권장사항을 따르는 것이 더 중요하다는 것을 깨달았습니다.
React Native가 왜 Objective-C를 권장하는지 이해하게 되었습니다:
Android에서 이미 만들어진 TypeScript 인터페이스를 그대로 사용함으로써:
이제 iOS 사용자들도 갤러리, 카메라, 다른 앱에서 이미지를 LifePuzzle 앱으로
직접 공유할 수 있게 되었습니다! 🎊