React Native New Architecture와 iOS 외부 앱 이미지 공유 시스템 구현기(Claude)

zion·2025년 9월 12일

Native

목록 보기
2/2

프로젝트 개요

이전에 Android로 구현한 React Native 앱에서 외부 앱(사진, 카메라, 갤러리 등)에서 공유되는 이미지를 받아서 처리하는 기능을 iOS으로 개발하는 작업이었습니다.

🔄 기술 스택 선택의 여정

Swift에서 Objective-C로의 전환

처음에는 Swift로 개발을 시작했습니다. 최신 언어이고 문법이 깔끔하며, iOS 개발에서 애플이 권장하는 언어이기 때문이었죠.

하지만 Native Module 개발에서는 Objective-C를 강력히 권장하고 있었습니다:

React Native가 Objective-C를 권장하는 이유

  1. TurboModule 지원: React Native의 새로운 아키텍처인 TurboModule은 Objective-C 기반으로 설계
  2. 코드 생성: react-native-codegen이 Objective-C 헤더 파일을 자동 생성
  3. 브릿지 안정성: JavaScript와 Native 간의 브릿지가 Objective-C에서 더 안정적
  4. 타입 안정성: C++ 백엔드와의 연동이 Objective-C에서 더 매끄러움

CodeGen 지원

React Native 0.68+ 버전의 New Architecture를 지원하려면 CodeGen을 사용해야 했는데, 이 도구가 생성하는 인터페이스가 순수 Objective-C였습니다. Swift에서 이를 구현하려면 복잡한 브릿징 코드가 필요했지만, Objective-C에서는 아래 코드로 바로 구현 가능했습니다.

  #import "LPShareModuleSpec.h"

  @interface LPShareModule : NativeLPShareModuleSpecBase 
  <NativeLPShareModuleSpec>
  @end

  @implementation LPShareModule
  @end

🔗 Cross-Platform 인터페이스 통일

기존 Android 모듈 활용

가장 중요했던 것은 기존 Android용으로 만들어진 TypeScript 인터페이스를 그대로 활용하는 것이었습니다. 이미 Android 개발 시 만들어진 NativeLPShareModule.ts가 있었고, iOS에서도 동일한 인터페이스로 동작하도록 구현했습니다.

기존 TypeScript 인터페이스

// 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 코드에서는 플랫폼을 신경 쓰지 않고 동일하게 사용할 수 있었습니다:

통합의 이점

  1. 코드 재사용성: 상위 레벨 React Native 코드는 플랫폼 구분 없이 재사용
  2. 유지보수성: 인터페이스 변경 시 TypeScript 파일 하나만 수정
  3. 타입 안정성: TypeScript가 두 플랫폼 모두의 타입 검사
  4. 개발 효율성: 새로운 기능 추가 시 스펙 정의 후 각 플랫폼에서 구현만 하면 됨

🏗️ 아키텍처 설계

iOS에서 외부 앱 공유를 처리하는 방법은 Share Extension을 사용하는 것입니다. 전체 구조는 다음과 같습니다:

외부 앱 → Share Extension → App Group → Main App → React Native

주요 구성요소

  1. Share Extension: 외부 앱에서 공유된 이미지를 받는 진입점
  2. App Group: Share Extension과 Main App 간의 데이터 공유
  3. Native Module: React Native Bridge를 통한 데이터 전달

🔧 구현 과정

1. Share Extension 생성

먼저 Xcode에서 새로운 Share Extension Target을 생성했습니다.

Info.plist 설정

  <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>

2. ShareViewController 구현

핵심 로직을 담은 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];
          }];
      }
  }

3. App Group을 통한 데이터 공유

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"];
      // ... 동일한 로직
  }

4. Native Module 구현

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]});
      }
  }

🎯 설정 및 권한

Entitlements 설정

각 Target별로 App Groups 권한 설정:

  LifePuzzleShareRelease.entitlements:
  <key>com.apple.security.application-groups</key>
  <array>
     <string>group.io.itmca.lifepuzzle</string>
  </array>

💡 배운 점들

1. 기술 선택의 중요성

처음엔 "최신 = 최선"이라고 생각했지만, 프레임워크의 권장사항을 따르는 것이 더 중요하다는 것을 깨달았습니다.

2. React Native의 철학 이해

React Native가 왜 Objective-C를 권장하는지 이해하게 되었습니다:

  • 안정성 > 모던함
  • 생태계 호환성 > 개인 선호도

3. 인터페이스 통일의 중요성

Android에서 이미 만들어진 TypeScript 인터페이스를 그대로 사용함으로써:

  • 개발 시간 단축
  • 버그 위험 감소
  • 코드 일관성 확보
  • 유지보수성 향상

🎉 결과

ios (카카오톡 이미지 공유, iPhone)

이미지1이미지1이미지1이미지2
  • ✅ 외부 앱에서 단일/다중 이미지 공유 지원
  • ✅ iOS 13+ 호환성 확보
  • ✅ 메모리 누수 방지 및 자동 정리
  • ✅ 안정적인 데이터 전달 메커니즘
  • ✅ TypeScript 타입 안정성
  • ✅ React Native New Architecture 완벽 지원
  • ✅ Android와 동일한 인터페이스로 Cross-Platform 코드 재사용

이제 iOS 사용자들도 갤러리, 카메라, 다른 앱에서 이미지를 LifePuzzle 앱으로
직접 공유할 수 있게 되었습니다! 🎊

profile
be_zion

0개의 댓글