React Native iOS 네이티브 모듈
사담
최근 회사 업무에 여유가 생기면서, 이 시간을 어떻게 활용할지 고민했다.
그러다 그동안 중요하다고 생각하면서도 계속 미뤄왔던 React Native 네이티브 모듈 개발을 시작해보기로 했다.
그동안 미뤄왔던 이유를 생각해보면, 오픈소스 라이브러리를 활용하면 대부분의 기능을 구현할 수 있어서 굳이 직접 개발할 필요성을 느끼지 못했던 점이 크다.
게다가 쉽게 따라할 수 있는 정리된 튜토리얼도 부족해 시작이 막막했었던것 같다.
하지만 최근 ChatGPT 같은 AI LLM 이 크게 발전하면서, 내가 잘 모르는 네이티브 문법이나 구현 방식에 대해서 도움을 받을 수 있게 되었다. 그래서 늦기 전에 일단 도전해보려고 한다! 🐢
iOS Native Module (from. Docs)
React Native (New Archtecture) cli 로 Native Module 가이드를 따라가는 과정입니다.
프로젝트 생성
아래 커맨드로 cli 프로젝트를 새로 생성한다.
$ npx @react-native-community/cli@latest init <ProjectName>
현재 기준으로 최신버전의 React Native 버전으로 프로젝트가 생성된다.
나는 0.81.0 버전을 사용했다.
패키지 설치
기존 pod 설치 커맨드 pod install를 사용하니 앞으로 대체될 것이라는 deprecated notice 가 있었다. 이런 최신 변경사항들은 염두해두면 좋을것 같다.
자세한 내용의 블로그 글:
pod install 대신 아래 커맨드를 사용
# npm 패키지 설치
$ yarn
# iOS 프로젝트 실행
$ yarn ios
만약 수동 설치가 필요하다면 아래 커맨드를 사용
$ cd ios
$ bundle install
$ bundle exec pod install
모듈 스펙(사양) 정의
이 단계에서는 Codegen 사용을 위해 Typescript로 네이티브 모듈에 대한 스펙 파일을 정의한다.
루트 경로에 specs 폴더를 생성하고, 폴더 내부에 NativeLocalStorag.tsx 파일을 추가한다.
해당 모듈은 iOS UserDefault에 key-value 형식으로 데이터를 저장, 불러오기, 삭제 하는 간단한 모듈이다.
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');
그리고 package.json에 codegenConfig 관련한 내용을 추가한다.
"codegenConfig": {
"name": "NativeLocalStorageSpec",
"type": "modules",
"jsSrcsDir": "specs",
"ios": {
"modulesProvider": {
"NativeLocalStorage": "RCTNativeLocalStorage"
}
}
}
이렇게 스펙 파일 정의 및 package.json 에 관련 설정을 포함하여 모두 작성한 뒤에,
pod 수동설치를 진행하거나 앱을 빌드하면 자동으로 네이티브 코드를 생성한다고 한다.
어떤 경로에 추가되는지 확인:
네이티브 코드 구현
이제 실제 네이티브 단에서 필요한 로직을 구현해야한다.
나는 Docs 에 나와있는 RCTNativeLocalStorage.mm 파일의 Object-C 구현부는 래퍼 역할로만 사용하도록 하고 Swift 파일을 추가해서 연결해주었다.
전적으로 GPT 의 도움을 받아 적용한 코드라 틀린 부분이 있을 수 있으니 주의해야한다.
여담이지만 이 단계에서 Object-C 코드상에 에러가 나고 안나고를 계속 반복해서.. 문제를 제대로 파악하기가 힘들었다 (..)
순서
- 1
ios 경로에 NativeLocalStorage 그룹 생성
- 2
그룹 내부에 파일 생성
- a
New file from template → Cocoa Touch Class → Subclass of: NSObject, Language: Object-C 로 NativeLocalStorage 파일 생성 → h, m 파일 자동 생성 됨
- 3
m 파일은 확장자를 mm 으로 변경
- 4
RCTNativeLocalStorageImpl.swift 파일 추가
- a
이때 Swift 파일 추가시 브리징 헤더 추가 여부를 묻는데 추가 할 것. 파일명은 프로젝트명-Bridging-Header.h 가 되어야 한다.
- 5
Codegen으로 생성된 스펙 사양에 맞게 네이티브 로직 작성
#import "RCTAppDelegate.h"
#import <Foundation/Foundation.h>
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeLocalStorage : NSObject <NativeLocalStorageSpec>
@end
NS_ASSUME_NONNULL_END
#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
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)
}
}
}
앱에서 사용하기
지금까지 구현한 기능을 React Native 앱에서 사용하는 단계가 남았다.
초반부에 specs 폴더에 추가했던 NativeLocalStorage.ts 파일로부터 import 하면 되어 간단하다.
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;
빌드나 테스트상에 문제가 없다면 성공적으로 iOS 네이티브 모듈을 구현한 것이다.
안드로이드도 문서를 차근차근히 따라간다면 문제는 없을것이다.
앞으로 다른 기능들을 하나씩 추가해보면서 블로그에 기록을 남겨볼까 한다.
네이티브 모듈 개발에 자신감을 붙힐 수 있기를 바라며..
그 외 트러블 슈팅
Codegen 으로 네이티브 코드 생성 이후 pod 패키지 설치할때 아래 오류 발생하는경우
bundler: failed to load command: pod (/playground-rn/vendor/bundle/ruby/2.6.0/bin/pod)
new architecture 활성화 커맨드로 재설치 시도
$ RCT_NEW_ARCH_ENABLED=1 bundle exec pod install