React Native iOS 네이티브 모듈

사담

최근 회사 업무에 여유가 생기면서, 이 시간을 어떻게 활용할지 고민했다.

그러다 그동안 중요하다고 생각하면서도 계속 미뤄왔던 React Native 네이티브 모듈 개발을 시작해보기로 했다.

그동안 미뤄왔던 이유를 생각해보면, 오픈소스 라이브러리를 활용하면 대부분의 기능을 구현할 수 있어서 굳이 직접 개발할 필요성을 느끼지 못했던 점이 크다.

게다가 쉽게 따라할 수 있는 정리된 튜토리얼도 부족해 시작이 막막했었던것 같다.

하지만 최근 ChatGPT 같은 AI LLM 이 크게 발전하면서, 내가 잘 모르는 네이티브 문법이나 구현 방식에 대해서 도움을 받을 수 있게 되었다. 그래서 늦기 전에 일단 도전해보려고 한다! 🐢

 

 

iOS Native Module (from. Docs)

React Native (New Archtecture) cli 로 Native Module 가이드를 따라가는 과정입니다.

 

프로젝트 생성

 

아래 커맨드로 cli 프로젝트를 새로 생성한다.

Shell
$ npx @react-native-community/cli@latest init <ProjectName>

 

현재 기준으로 최신버전의 React Native 버전으로 프로젝트가 생성된다.

나는 0.81.0 버전을 사용했다.

 

 

패키지 설치

기존 pod 설치 커맨드 pod install를 사용하니 앞으로 대체될 것이라는 deprecated notice 가 있었다. 이런 최신 변경사항들은 염두해두면 좋을것 같다.

 

자세한 내용의 블로그 글:

 

pod install 대신 아래 커맨드를 사용

Shell
# npm 패키지 설치 $ yarn # iOS 프로젝트 실행 $ yarn ios

 

만약 수동 설치가 필요하다면 아래 커맨드를 사용

Shell
$ cd ios $ bundle install $ bundle exec pod install

 

 

모듈 스펙(사양) 정의

이 단계에서는 Codegen 사용을 위해 Typescript로 네이티브 모듈에 대한 스펙 파일을 정의한다.

Codegen이란, Typescript(혹은 Flow)로 작성된 인터페이스 스펙을 기반으로 자동으로 네이티브 코드를 생성해주는 도구이며 이는 New Architecture(Fabric + TurboModules)의 핵심 구성 요소 중 하나이다.

 

루트 경로에 specs 폴더를 생성하고, 폴더 내부에 NativeLocalStorag.tsx 파일을 추가한다.

해당 모듈은 iOS UserDefault에 key-value 형식으로 데이터를 저장, 불러오기, 삭제 하는 간단한 모듈이다.

 

Javascript
import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { setItem(value: string, key: string): void; getItem(key: string): string | null; removeItem(key: string): void; clear(): void; } // NativeLocalStorage 라는 (터보)네이티브 모듈을 가져오되 없으면 에러를 throw 한다. export default TurboModuleRegistry.getEnforcing<Spec>('NativeLocalStorage');
specs/NativeLocalStorage.ts

 

그리고 package.json에 codegenConfig 관련한 내용을 추가한다.

Json
"codegenConfig": { "name": "NativeLocalStorageSpec", "type": "modules", "jsSrcsDir": "specs", "ios": { "modulesProvider": { "NativeLocalStorage": "RCTNativeLocalStorage" } } }
package.json

 

이렇게 스펙 파일 정의 및 package.json 에 관련 설정을 포함하여 모두 작성한 뒤에,

pod 수동설치를 진행하거나 앱을 빌드하면 자동으로 네이티브 코드를 생성한다고 한다.

 

어떤 경로에 추가되는지 확인:

 

네이티브 코드 구현

이제 실제 네이티브 단에서 필요한 로직을 구현해야한다.

나는 Docs 에 나와있는 RCTNativeLocalStorage.mm 파일의 Object-C 구현부는 래퍼 역할로만 사용하도록 하고 Swift 파일을 추가해서 연결해주었다.

전적으로 GPT 의 도움을 받아 적용한 코드라 틀린 부분이 있을 수 있으니 주의해야한다.

여담이지만 이 단계에서 Object-C 코드상에 에러가 나고 안나고를 계속 반복해서.. 문제를 제대로 파악하기가 힘들었다 (..)

 

순서

  1. 1

    ios 경로에 NativeLocalStorage 그룹 생성

  1. 2

    그룹 내부에 파일 생성

    1. a

      New file from template → Cocoa Touch Class → Subclass of: NSObject, Language: Object-C 로 NativeLocalStorage 파일 생성 → h, m 파일 자동 생성 됨

  1. 3

    m 파일은 확장자를 mm 으로 변경

  1. 4

    RCTNativeLocalStorageImpl.swift 파일 추가

    1. a

      이때 Swift 파일 추가시 브리징 헤더 추가 여부를 묻는데 추가 할 것. 파일명은 프로젝트명-Bridging-Header.h 가 되어야 한다.

  1. 5

    Codegen으로 생성된 스펙 사양에 맞게 네이티브 로직 작성

 

Objective-c
#import "RCTAppDelegate.h"
Playground-Bridging-Header.h

 

Objective-c
#import <Foundation/Foundation.h> #import <NativeLocalStorageSpec/NativeLocalStorageSpec.h> NS_ASSUME_NONNULL_BEGIN @interface RCTNativeLocalStorage : NSObject <NativeLocalStorageSpec> @end NS_ASSUME_NONNULL_END
RCTNativeLocalStorage.h

 

C++
#import "RCTNativeLocalStorage.h" #import "Playground-Swift.h" // 클래스의 구현부를 시작한다는 뜻. 구현부를 마치면 @end로 끝낸다 @implementation RCTNativeLocalStorage { RCTNativeLocalStorageImpl *_impl; } // 이 클래스를 React Native에 모듈로 등록 RCT_EXPORT_MODULE() // 생성자 init - (instancetype) init { if (self = [super init]) { // _impl에 Swift 구현체 인스턴스를 생성 _impl = [RCTNativeLocalStorageImpl new]; // @implementation의 _impl 타입과 일치시켜야함 } return self; } - (std::shared_ptr<facebook::react::TurboModule>) getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared<facebook::react::NativeLocalStorageSpecJSI>(params); } - (NSString * _Nullable) getItem:(NSString *)key { return [_impl getItem:key]; } - (void) setItem:(NSString *)value key:(NSString *)key { [_impl setItem:value key:key]; } - (void) removeItem:(NSString *)key { [_impl removeItem:key]; } - (void) clear { [_impl clear]; } @end
RCTNativeLocalStorage.mm

 

Swift
import Foundation @objcMembers final class RCTNativeLocalStorageImpl: NSObject { private let storage: UserDefaults override init() { self.storage = UserDefaults(suiteName: "local-storage") ?? UserDefaults.standard super.init() } func getItem(_ key: String) -> String? { return storage.string(forKey: key) } func setItem(_ value: String, key: String) { storage.set(value, forKey: key) } func removeItem(_ key: String) { storage.removeObject(forKey: key) } func clear() { for (key, _) in storage.dictionaryRepresentation() { storage.removeObject(forKey: key) } } }
RCTNativeLocalStorageImpl.swift

 

 

앱에서 사용하기

지금까지 구현한 기능을 React Native 앱에서 사용하는 단계가 남았다.

초반부에 specs 폴더에 추가했던 NativeLocalStorage.ts 파일로부터 import 하면 되어 간단하다.

 

Javascript
import React from 'react'; import { SafeAreaView, StyleSheet, Text, TextInput, Button, } from 'react-native'; import NativeLocalStorage from './specs/NativeLocalStorage'; function App(): React.JSX.Element { const [value, setValue] = React.useState<string | null>(null); const [editingValue, setEditingValue] = React.useState<string | null>(null); React.useEffect(() => { const storedValue = NativeLocalStorage?.getItem('myKey'); setValue(storedValue ?? ''); }, []); function saveValue() { NativeLocalStorage?.setItem(editingValue ?? '', 'myKey'); setValue(editingValue); } function clearAll() { NativeLocalStorage?.clear(); setValue(''); } function deleteValue() { NativeLocalStorage?.removeItem('myKey'); setValue(''); } return ( <SafeAreaView style={{ flex: 1 }}> <Text style={styles.text}> Current stored value is: {value ?? 'No Value'} </Text> <TextInput placeholder="Enter the text you want to store" style={styles.textInput} onChangeText={setEditingValue} /> <Button title="Save" onPress={saveValue} /> <Button title="Delete" onPress={deleteValue} /> <Button title="Clear" onPress={clearAll} /> </SafeAreaView> ); } const styles = StyleSheet.create({ text: { margin: 10, fontSize: 20, }, textInput: { margin: 10, height: 40, borderColor: 'black', borderWidth: 1, paddingLeft: 5, paddingRight: 5, borderRadius: 5, }, }); export default App;
App.tsx

 

빌드나 테스트상에 문제가 없다면 성공적으로 iOS 네이티브 모듈을 구현한 것이다.

안드로이드도 문서를 차근차근히 따라간다면 문제는 없을것이다.

 

앞으로 다른 기능들을 하나씩 추가해보면서 블로그에 기록을 남겨볼까 한다.

네이티브 모듈 개발에 자신감을 붙힐 수 있기를 바라며..

 

 


 

그 외 트러블 슈팅

 

Codegen 으로 네이티브 코드 생성 이후 pod 패키지 설치할때 아래 오류 발생하는경우

Shell
bundler: failed to load command: pod (/playground-rn/vendor/bundle/ruby/2.6.0/bin/pod)

 

new architecture 활성화 커맨드로 재설치 시도

Shell
$ RCT_NEW_ARCH_ENABLED=1 bundle exec pod install