이전 포스팅에서는 라이브러리를 이해하기에 앞서 라이브러리 프로젝트를 생성하는 시간을 가졌습니다. 🥳
이번 포스팅에서는 라이브러리 프로젝트의 폴더 및 파일 구조를 살펴보며, 모듈을 정의하는 코드를 살펴보겠습니다! +_+
먼저 프로젝트의 폴더 및 파일 구조부터 살펴보겠습니다.
크게 보면 android, example, ios, src 이렇게 폴더가 구성된 걸 볼 수 있습니다.
/test-library
├── ...
├── android
├── example
├── ios
├── src
└── ...
하나하나 어떻게 구성되어있는지 살펴보겠습니다!! 🤩 🤩
먼저 example 폴더부터 살펴보겠습니다.
example 폴더는 라이브러리가 쓰일 프로젝트의 예시를 드러낸 폴더입니다.
App.tsx는 react native 가장 최상단인 index.js에서 선언하는 파일입니다.
위 코드는 react-native-test-library
라이브러리에서 multipy
라는 메서드를 꺼내 result를 반환하는 코드임을 알 수 있습니다.
multiply
는 라이브러리 프로젝트에서 기본으로 제공해주는 메서드인데 어디에 정의 되어있을까요?
바로바로 여깁니다~^^
src 폴더 내부에는 라이브러리 모듈 인터페이스를 정의해놓는 파일이 있습니다.
/test-library/src/
├── NativeTestLibrary.ts
└── index.tsx
src 안에는 NativeTestLibrary.ts, index.tsx 파일 두개가 있습니다.
각 파일에 대해 알아봅시다. 😎 😎
가장 먼저 src 폴더의 진입점인 index 파일부터 보겠습니다.
import TestLibrary from './NativeTestLibrary';
export function multiply(a: number, b: number): number {
return TestLibrary.multiply(a, b);
}
잘은 모르겠지만 multiply 메서드는 NativeTestLibrary
폴더에서 multiply라는 메서드를 리턴하는걸 볼 수 있습니다.
NativeTestLibrary
가 어떻게 되어있는지 바로 구경해봅시다! +_+
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
multiply(a: number, b: number): number;
}
export default TurboModuleRegistry.getEnforcing<Spec>('TestLibrary');
전문은 이렇고 하나하나 살펴보겠습니다!
export interface Spec extends TurboModule {
multiply(a: number, b: number): number;
}
저는 앞에서 라이브러리 프로젝트를 생성할 때, 모듈 옵션으로 Turbo 모듈 옵션을 선택하였기 때문에 TurboModule
을 기반으로한 인터페이스가 생성되었습니다. TurboModule
은 네이티브 모듈과 JavaScript 간의 브릿지를 제공하여 네이티브 기능을 JavaScript에서 사용할 수 있게 합니다.
export default TurboModuleRegistry.getEnforcing<Spec>('TestLibrary');
여기서 TurboModuleRegistry
는 React Native에서 TurboModule
을 등록하고 가져오는 데 사용되는 레지스트리입니다. TurboModuleRegistry
를 통해 특정 TurboModule
을 가져오거나 등록할 수 있습니다.
TurboModuleRegistry
에서 'TestLibrary'라는 이름의 TurboModule
을 가져오고, 이를 Spec 타입으로 강제합니다. 이 모듈을 기본 내보내기로 설정합니다.
src 폴더는 그럼 메서드 인터페이스가 정의되어있다는 것은 알았는데...
그렇다면 메서드 정의는 어디에 되어있을까요~? 🤔 🤔
지금 하고 있는 건 크로스플랫폼 라이브러리 구현이니까, 메서드는 각 OS 별로 다르겠죠? (*OS마다 호출하는 함수가 다를 거니까!)
메서드가 어떻게 정의 되어있는지 네이티브 폴더로 가보겠습니다!
먼저 android 부터 보겠습니다.
저는 라이브러리 이름을 test-library로 만들었더니 각 파일 이름이 라이브러리 이름을 따라 파스칼 형식으로 저장된 걸 볼 수 있습니다!
/test-library/android/src/main/java/com/testlibrary/
├── TestLibraryModule.kt
└── TestLibraryPackage.kt
각 파일에는 코드가 어떻게 기본으로 세팅되어있는지 확인해봅시다.
먼저 TestLibraryPackage
부터 보겠습니다!
TestLibraryPackage
는 네이티브 모듈을 React Native 애플리케이션에 등록하는 역할을 합니다.
React Native 애플리케이션이 시작될 때, TestLibraryPackage
가 로드되고, 이 패키지에서 제공하는 네이티브 모듈들이 등록됩니다.
코드 전문을 보겠습니다. 😌 😌
class TestLibraryPackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == TestLibraryModule.NAME) {
TestLibraryModule(reactContext)
} else {
null
}
}
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
moduleInfos[TestLibraryModule.NAME] = ReactModuleInfo(
TestLibraryModule.NAME,
TestLibraryModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
moduleInfos
}
}
}
이제 getModule 메서드부터 살펴봅시다!
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == TestLibraryModule.NAME) {
TestLibraryModule(reactContext)
} else {
null
}
}
이 getModule 메서드는 React Native 애플리케이션에서 네이티브 모듈을 호출할 때 호출되어, 모듈의 이름을 기반으로 TestLibraryModule
인스턴스를 생성하고 반환합니다.
다음은 getReactModuleInfoProvider 메서드 입니다.
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
moduleInfos[TestLibraryModule.NAME] = ReactModuleInfo(
TestLibraryModule.NAME, // 모듈 이름
TestLibraryModule.NAME, // 모듈 이름
false, // canOverrideExistingModule (기존 모듈 덮어쓰기 가능 여부)
false, // needsEagerInit (애플리케이션 시작 시 초기화 필요 여부)
true, // hasConstants (상수 포함 여부)
false, // isCxxModule (C++ 모듈 여부)
true // isTurboModule (TurboModule 여부)
)
moduleInfos
}
}
getReactModuleInfoProvider 메서드는 ReactModuleInfoProvider
를 반환하여 네이티브 모듈의 정보를 React Native에 등록하는 데 사용됩니다.
ReactModuleInfoProvider
는 네이티브 모듈의 이름, 초기화 방식, 상수 제공 여부, TurboModule
여부 등의 정보를 포함하는 ReactModuleInfo
객체를 제공합니다.
('C++ 모듈 여부'는 라이브러리를 생성할 때 언어 선택 부분에서 공유 C++ 라이브러리(Android 및 iOS용 C++)를 사용할지 선택하면 기본 true로 설정되어있습니다.)
TestLibraryPackage
에서 React Native에 모듈을 등록했으니...
등록할 모듈을 만들어보겠습니다!! 😎 😎
먼저 전문을 살펴볼까요? 드디어 라이브러리 모듈 공개...! (두둥 +_+)
@ReactModule(name = TestLibraryModule.NAME)
class TestLibraryModule(reactContext: ReactApplicationContext) :
NativeTestLibrarySpec(reactContext) {
override fun getName(): String {
return NAME
}
// Example method
// See https://reactnative.dev/docs/native-modules-android
override fun multiply(a: Double, b: Double): Double {
return a * b
}
companion object {
const val NAME = "TestLibrary"
}
}
TestLibraryModule 클래스는 JavaScript와 네이티브 코드 간의 상호 작용을 가능하게 하는 브릿지의 중요한 부분입니다. JavaScript 코드에서 TestLibrary 모듈을 호출하면, React Native의 TurboModule
과 JSI
를 통해 이 클래스의 메서드가 실행됩니다.
(저는 앞에서 라이브러리 프로젝트를 생성할 때, 모듈 옵션으로 Turbo 모듈 옵션을 선택하였기 때문이죵!)
기본으로 multiply라는 메서드가 제공되는데,
override fun multiply(a: Double, b: Double): Double {
return a * b
}
매개변수로 Double
타입의 a, b의 수를 받아 곱하는 메서드입니다.
여기까지가! 안드로이드에서 네이티브 모듈을 등록하는 코드입니다.
그래서 모듈과의 통신을 어떻게 하는지는 ios까지 코드 확인해보고 뒤에서 살펴보겠습니다. >,<
android까지 훑어봤으니 iOS도 금방 볼 수 있을 겁니다!!!
(그렇다고 해주세요. 초롱초롱 +_+)
딱봐도! iOS는 android보다 덜 복잡한거 보이시죠?!! 폴더도 더 없고 파일 두개가 끝! 입니다.
라이브러리 이름을 test-library로 만들었더니 iOS도 android처럼 각 파일 이름이 라이브러리 이름을 따라 파스칼 형식으로 저장된 걸 볼 수 있습니다!
/test-library/ios/
├── TestLibrary.h
└── TestLibrary.mm
각 파일에는 코드가 어떻게 기본으로 세팅되어있는지 확인해봅시다.
TestLibrary
헤더 파일은 모듈이 정의 될 TestLibrary
클래스의 인터페이스를 정의합니다.
코드 전문을 보겠습니다!
#import "generated/RNTestLibrarySpec/RNTestLibrarySpec.h"
@interface TestLibrary : NSObject <NativeTestLibrarySpec>
@end
짜잔! 3줄이 다입니다. 🫢 🫢
이 파일은 TestLibrary
클래스의 인터페이스를 정의하는 코드입니다. 이 클래스는 NSObject
(Objective-C에서 가장 기본이 되는 클래스)를 상속받고, NativeTestLibrarySpec
프로토콜을 준수합니다.
(NativeTestLibrarySpec
은 RNTestLibrarySpec 헤더 파일에 있으며, RNTestLibrarySpec 헤더 파일은 빌드하면 자동으로 생성되는 파일입니다.)
인터페이스를 정의해놓는 것이 test-library/src/NativeTestLibrary.ts
의 Spec과 동일한 역할을 합니다.
자~ 그렇다면 TestLibrary 모듈은 어떻게 구성되어있는지 볼까요?
android에서는 TestLibraryPackage
에서 네이티브 모듈을 React Native 애플리케이션에 등록하는 역할을 했죠? iOS에서는 TestLibrary.mm 파일의 TestLibrary
클래스가 동일한 역할을 합니다.
코드 전문부터 보겠습니다!
#import "TestLibrary.h"
@implementation TestLibrary
RCT_EXPORT_MODULE()
- (NSNumber *)multiply:(double)a b:(double)b {
NSNumber *result = @(a * b);
return result;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeTestLibrarySpecJSI>(params);
}
@end
(어웅 복잡해~~~😵😵 objective-c가 익숙치 않은 저같은 개발자들은 위 코드가 다소 낯설 수도 있습니다. 킹치만...! 차근차근 보면 됩니다...! )
바로 한줄한줄 해석해보겠습니다~!
#import "TestLibrary.h"
위에서 설명드린 TestLibrary.h 헤더 파일을 포함합니다.
RCT_EXPORT_MODULE()
이 부분이 ✌️포인트✌️인데, 이 매크로는 TestLibrary
모듈을 React Native에 등록하는 부분입니다. 이 매크로를 사용하면 JavaScript 코드에서 이 모듈을 사용할 수 있게 됩니다. android에서 getReactModuleInfoProvider 메서드와 같은 역할입니다!!
- (NSNumber *)multiply:(double)a b:(double)b {
NSNumber *result = @(a * b);
return result;
}
위 코드는 android와 마찬가지로 기본으로 제공하는 multiply 메서드 입니다. 매개변수로 double 타입의 a, b의 수를 받아 곱해 NSNumber 객체로 반환하는 메서드입니다.
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeTestLibrarySpecJSI>(params);
}
getTurboModule 메서드는 TurboModule
을 초기화하고 반환합니다. 이 메서드는 TurboModule
을 사용하여 JavaScript
와 네이티브 코드 간의 상호 작용을 가능하게 합니다. NativeTestLibrarySpecJSI
클래스는 TurboModule
의 구현을 담당합니다.
어웅... 넘 복잡스럽고 어려웠습니다... 😭 😭
but...!!
다음 포스팅에서 모듈 통신을 이해하는 과정을 위 코드와 함께 차근차근 다시 살펴볼 것입니다! +_+
그럼 다음 포스팅에서 만나요!! 👋👋