개요
이번 글에서는 터보 모듈(TurboModule)에 대해 살펴보려고 한다.
공식 문서에서 소개하는 것처럼, TurboModule은 터보 네이티브 모듈이라는 용어로 표기하려고 한다.
- TurboModule: 터보 네이티브 모듈
- NativeModules: 레거시 네이티브 모듈
참고로 이번 글에서는 Bridgeless 모드를 기준으로 설명하며, 터보 네이티브 모듈의 하위 호환(Interop)에 대한 내용은 다루지 않으니 참고 바란다.
TurboModule 이란?
React Native 0.68 버전에서 새로운 Fabric과 함께 추가된 New Architecture의 네이티브 모듈 구현체이다. 공식 문서에서는 터보 네이티브 모듈(Turbo Native Module)이라고 소개하고 있으며, 레거시 아키텍처(Bridge)에서 사용하던 비동기 메시지 큐 기반 대신, JSI(JavaScript Interface)를 통해 C++ 레이어에서 직접 상호작용하는 메커니즘으로 동작한다. 안드로이드는 플랫폼 특성상 JNI(Java Native Interface) 레이어를 추가로 거쳐 상호작용 한다.
터보 네이티브 모듈 뿐만 아니라 코드젠(Codegen) 기능이 함께 추가되어, 보다 안정적인 타입 안정성을 제공하고, 복잡한 네이티브 모듈 스캐폴딩 과정을 간소화할 수 있다.
터보 네이티브 모듈의 경우 공식 문서에 잘 설명되어있어서 쉽게 만들어볼 수 있다.
TypeScript 혹은 Flow 환경에서 TurboModule
인터페이스를 확장하여 원하는 모듈의 사양(Spec)을 정의하면 기본 준비는 끝난다.
이렇게 정의한 모듈 사양은 Android의 경우 Gradle 빌드 과정에서, iOS의 경우 Pod 설치 시점에 Codegen에 의해 필요한 네이티브 구현체들이 자동으로 생성된다. 우리는 생성된 네이티브 코드 인터페이스에 맞게 기능을 구현하기만 하면 된다.
터보 네이티브 모듈은 TurboModuleRegistry
의 get
(모듈이 없을 경우 null
반환) 혹은 getEnforcing
(모듈이 없을 경우 Throw) 메서드를 통해 가져올 수 있다.
네이티브 구현체를 코드 한 줄만으로 가져오는 것이 신기하지 않은가? TurboModuleRegistry.js 파일을 살펴보면, 어떻게 동작하는지 엿볼 수 있다.
구현체는 생각보다 간단한데, global.__turboModuleProxy
혹은 NativeModules
객체에서 모듈을 참조해보고, 모듈이 존재하지 않을 경우 get
, getEnforcing
함수 구현에 따라 각각 처리한다.
참고로 NativeModules
는 global.nativeModuleProxy이다. 이에 대한 자세한 내용은 차차 살펴볼 예정이다.
미리 언급하자면, 터보 네이티브 모듈은 NativeModules
(global.nativeModuleProxy
)를 통해 참조하고, Bridgeless
(New Architecture 기본) 모드가 활성화되어있는 경우 터모 네이티브 모듈 호환(Interop) 플래그도 함께 활성화된다. 이는 기존 레거시 네이티브 모듈과의 호환성을 위함이다.
다시 본 주제로 돌아와서, 터보 네이티브 모듈은 결국 전역에 노출된 nativeModuleProxy
객체를 통해 참조한다는 것을 알 수 있다. Codegen 된 네이티브 구현체들이 어떤 과정을 거쳐 JavaScript 런타임 환경에 객체로 노출되는지 하나씩 살펴보려고 한다.
TurboModule
Android, iOS 터보 네이티브 모듈을 살펴보기에 앞서, 기반이 되는 TurboModule
구현체를 간단히 살펴보려고 한다.
TurboModule
은 JSI를 통해 JavaScript 런타임과 상호작용 할 수 있도록 jsi::HostObject
클래스를 상속하고 있다.
덕분에 레거시 네이티브 모듈의 기반이 되던 비동기 메시지 큐(Message Queue) 없이, 즉시 런타임과 상호작용 할 수 있다.
이러한 구조적인 특성 덕분에 과거 브릿지(Bridge) 시절의 레거시 네이티브 모듈보다 월등히 좋은 퍼포먼스를 자랑한다.
구현체는 다소 간단하다. 주요한 부분만 간단히 짚고 넘어가자면 다음과 같다.
name_
: 네이티브 모듈의 이름methodMap_
: 네이티브 모듈에 구현된 메서드 Map 컬렉션eventEmitterMap_
: 네이티브 모듈에 구현된 Event Emitter Map 컬렉션jsInvoker_
: CallInvoker 구현체의 공유 참조 포인터jsRepresentation_
: JavaScript에 노출될 객체(natievModuleProxy
)의 단일 참조 포인터
jsInvoker_
는 CallbackInvoker
구현체의 참조 포인터로, JavaScript 스레드에서 Native -> JavaScript 방향으로 함수를 호출하는 것을 보장해 준다.
jsRepresentation_
은 JavaScript 런타임에서 접근하는 네이티브 모듈의 실체(객체)이며, 모듈 속성에 대한 캐싱을 지원하기 위해 사용된다.
methodMap_
은 이름에서 유추할 수 있듯이, 네이티브 모듈에 구현된 함수들이 포함된다. MethodMetadata
구조체 타입을 값으로 들고 있으며, 이는 메서드가 받을 수 있는 인자의 수, 실제로 호출하게 될 함수 포인터로 구성되어 있는 것을 확인할 수 있다.
methodMap_
그리고 eventEmitterMap_
은 지난 글에서 살펴본 코드젠(Codegen)과 밀접하게 연관되어 있다. 앞서 살펴보았던 Baisc 모듈 사양을 기준으로 조금 더 살펴보도록 하자.
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
numericMethod(arg: number): number;
booleanMethod(arg: boolean): boolean;
stringMethod(arg: string): string;
}
export default TurboModuleRegistry.getEnforcing<Spec>('Basic');
JavaTurboModule (Android)
Android 환경에서는 다음과 같은 코드들이 생성된다.
android/app/build/generated/source/codegen/jni/BasicSpec-generated.cpp
#include "BasicSpec.h"
namespace facebook::react {
static facebook::jsi::Value __hostFunction_NativeBasicSpecJSI_numericMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
static jmethodID cachedMethodId = nullptr;
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, NumberKind, "numericMethod", "(D)D", args, count, cachedMethodId);
}
static facebook::jsi::Value __hostFunction_NativeBasicSpecJSI_booleanMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
static jmethodID cachedMethodId = nullptr;
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, BooleanKind, "booleanMethod", "(Z)Z", args, count, cachedMethodId);
}
static facebook::jsi::Value __hostFunction_NativeBasicSpecJSI_stringMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
static jmethodID cachedMethodId = nullptr;
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, StringKind, "stringMethod", "(Ljava/lang/String;)Ljava/lang/String;", args, count, cachedMethodId);
}
NativeBasicSpecJSI::NativeBasicSpecJSI(const JavaTurboModule::InitParams ¶ms)
: JavaTurboModule(params) {
methodMap_["numericMethod"] = MethodMetadata {1, __hostFunction_NativeBasicSpecJSI_numericMethod};
methodMap_["booleanMethod"] = MethodMetadata {1, __hostFunction_NativeBasicSpecJSI_booleanMethod};
methodMap_["stringMethod"] = MethodMetadata {1, __hostFunction_NativeBasicSpecJSI_stringMethod};
}
std::shared_ptr<TurboModule> BasicSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) {
if (moduleName == "Basic") {
return std::make_shared<NativeBasicSpecJSI>(params);
}
return nullptr;
}
} // namespace facebook::react
android/build/generated/source/codegen/jni/BasicSpec.h
#pragma once
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
namespace facebook::react {
/**
* JNI C++ class for module 'NativeBasic'
*/
class JSI_EXPORT NativeBasicSpecJSI : public JavaTurboModule {
public:
NativeBasicSpecJSI(const JavaTurboModule::InitParams ¶ms);
};
JSI_EXPORT
std::shared_ptr<TurboModule> BasicSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms);
} // namespace facebook::react
안드로이드 터보 네이티브 모듈은 JavaTurboModule을 상속한 NativeBasicSpecJSI
클래스가 정의된 것을 볼 수 있다. 여기서 상속하는 JavaTurboModule
은 내부적으로 TurboModule
을 상속하고 있다.
생성된 코드를 조금 더 자세히 들여다보자.
methodMap_
에 정의한 메서드 이름과 MethodMetadata
를 Key-Value 쌍으로 할당하는 모습을 볼 수 있다. 이 부분이 바로 앞서 살펴본 TurboModule
의 인터페이스이다.
즉, JavaTurboModule
은 Java(JNI) 환경에서 터보 네이티브 모듈을 구현하기 위한 TurboModule
의 래퍼이다.
이렇게 생성된 C++ 코드들은 React Native 런타임(JSI)과 Android 플랫폼(JNI) 사이에서 서로 상호작용할 수 있도록 중간 다리 역할을 수행한다.
실제 네이티브 모듈 기능을 구현하는 것은 우리의 몫인데, 코드젠을 통해 정해진 메서드들을 구현할 수 있도록 모듈 사양을 포함하는 추상 클래스(Abstract class)가 생성된 것을 볼 수 있다.
android/build/ganerated/source/codegen/java/<package>/NativeBasicSpec.java
실제 모듈 구현체 코드를 살펴보자.
미리 만들어진 추상 클래스를 상속하도록 함으로써, 타입 불일치, 메서드 구현 누락 등의 문제를 사전에 미리 방지한다.
이렇게 생성된 터보 네이티브 모듈의 구현체는 Gradle 빌드 과정에서 수행되는 Auto Linking 시점에 일괄적으로 등록된다.
먼저 터보 네이티브 모듈(Kotlin) 구현체는 GeneratePackageListTask.kt 태스크를 통해 PackageList.getPackages
메서드 반환 값에 자동으로 추가된다.
android/app/build/generated/autolinking/src/main/java/com/facebook/react/PackageList.java
이렇게 만들어진 PackageList
는, MainApplication에서 참조한다.
getPackages
반환 값에 패키지를 수동으로 추가하여 모듈을 등록할 수도 있는데, 이것이 바로 Manual Linking(Non-Auto)이다.
추가로 reactHost
가 오버라이드 된 것을 볼 수 있는데, Bridgeless 모드에서는 ReactNativeHost
를 그냥 사용하지 않고 ReactHost
로 변환하여 인스턴스를 생성하기에 이를 위한 멤버가 존재한다.
이 부분을 언급하는 이유는 등록된 PackageList
를 통해 터보 네이티브 모듈에 접근하기 때문이다.
Getter를 통해 ReactHost에 접근하는 경우 DefaultReactHost.getDefaultReactHost를 통해 ReactHost
인스턴스를 생성한다. 여기에서 MainApplication
에 정의된 DefaultReactNativeHost.getPackages()를 통해 등록된 모듈들을 가져온다.
ReactHost 생성 시점을 확인해보면, DefaultTurboModuleManagerDelegate.Builder를 TurboModuleManagerDelegate
빌더로 사용하고, DefaultReactHostDelegate를 ReactHostDelegate
로 사용하는 것을 확인할 수 있다.
이들은 추후 ReactInstance
가 생성될 때, 참조되며 가져온 패키지 목록을 TurboModuleManagerDelegate(DefaultTurboModuleManagerDelegate)에 전달하게 된다.
DefaultTurboModuleManagerDelegate
는 ReactPackageTurboModuleManagerDelegate를 상속하고 있으며, 실제 기능은 부모 클래스에 구현되어있다.
구현체를 간단히 살펴보면 패키지 목록을 순회하며, 각 패키지별로 모듈 제공자(Provider) 람다 함수를 만들어 할당하는 것을 볼 수 있다.
이후 살펴볼 내용이지만, getModule
, getLegacyModule
을 통해 모듈을 가져오게 되는데, 이는 package.getModule
을 의미한다. 패키지에서 제공하는 모듈은 우리가 구현하는 Java(Kotlin) 모듈로, TurboModule
클래스를 상속하는 모듈이다.
지금까지 Java(Kotlin)단에서 어떻게 등록되는지 간단히 살펴보았는데, 터보 네이티브 모듈은 C++ 구현체이다. 이 부분은 어떻게 처리되는지 이어서 살펴보자.
동일하게 Gradle 빌드 시점에 GenerateAutolinkingNewArchitecturesFileTask.kt 태스크를 통해 C++ 빌드 구성(CMake)과 Auto Linking될 터보 네이티브 모듈 코드들이 생성된다.
React Native 프로젝트 루트 기준으로 아래 경로를 찾아가보면, CMake 파일이 하나 생성된 것을 확인할 수 있다.
android/app/build/generated/autolinking/src/main/jni/Android-autolinking.cmake
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
set(REACTNATIVE_MERGED_SO true)
add_subdirectory("/path/to/android/build/generated/source/codegen/jni/" BasicSpec_autolinked_build)
set(AUTOLINKED_LIBRARIES
react_codegen_BasicSpec
)
내용을 살펴보면, 사용하고 있는 터보 네이티브 모듈이 빌드 대상에 포함되는 것을 볼 수 있다.
같은 디렉터리에 있는 autolinking.cpp
파일을 확인해보면, 앞서 살펴본 Basic 모듈의 C++ Provider가 통합되어있는 것을 확인할 수 있다.
React Native 코어에서는 autolinking_cxxModuleProvider
, autolinking_ModuleProvider
를 통해 Auto Linking 된 터보 네이티브 모듈에 접근하게 된다.
React Native 구현체를 살펴보면, JNI를 통해 모듈을 참조할 수 있도록 구현되어있고, 로드 될 때 DefaultTurboModuleManagerDelegate
의 모듈 제공자(Provider)로, facebook::react::cxxModuleProvider
, facebook::react::javaModuleProvider
의 함수 포인터를 할당하는 것을 확인할 수 있다.
DefaultTurboModuleManagerDelegate
는 이후 알아볼 TurboModuleManagerDelegate
의 기본 구현체로, 여기에 정의된 getTurboModule
메서드를 통해 터보 네이티브 모듈에 접근하게 된다는 점만 알아두자.
추가로 CMakeLists.txt를 확인해보면, appmodules
프로젝트에서 ReactNative-application.cmake 파일을 참조하는데, default-app-setup/*.cpp
과 Auto Linking 과정에서 생성된 cpp 파일들을 빌드 대상으로 포함시키도록 구성되어있다.
이렇게 빌드되는 C++ 구현체는 각 아키텍처에 맞는 Shared Library(.so) 파일로 생성되고, 이는 DefaultSoLoader를 통해 로드된다.
실제 터보 네이티브 모듈 구현체는 Java(Kotlin) 기반인데, C++ 구현체와 상호작용 하는 부분은 아직 보이지 않는 것이 정상이다. 이 부분에 대해서는 이후 알아볼 예정인데, 내부적으로는 JNI를 통해 Java(Kotlin) <-> C++간 상호작용 한다는 것만 알고 있자.
지금까지 살펴본 과정을 정리해보면 Codegen 과정을 통해 생성된 코드, 그리고 Auto Linking 을 통해 생성된 코드들은 공유 라이브러리 형태로 빌드되고, React Native 인스턴스가 생성되고 초기화되는 시점에 SoLoader를 통해 로드된다. 로드된 라이브러리는 JNI를 통해 Android 플랫폼 환경과 상호작용할 수 있도록 초기화 된다.
ObjCTurboModule (iOS)
iOS 환경에서 생성되는 네이티브 코드는 다음과 같다.
ios/build/generated/ios/BasicSpec/BasicSpec-generated.mm
#import "BasicSpec.h"
@implementation NativeBasicSpecBase
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper
{
_eventEmitterCallback = std::move(eventEmitterCallbackWrapper->_eventEmitterCallback);
}
@end
namespace facebook::react {
static facebook::jsi::Value __hostFunction_NativeBasicSpecJSI_numericMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, NumberKind, "numericMethod", @selector(numericMethod:), args, count);
}
static facebook::jsi::Value __hostFunction_NativeBasicSpecJSI_booleanMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, BooleanKind, "booleanMethod", @selector(booleanMethod:), args, count);
}
static facebook::jsi::Value __hostFunction_NativeBasicSpecJSI_stringMethod(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, StringKind, "stringMethod", @selector(stringMethod:), args, count);
}
NativeBasicSpecJSI::NativeBasicSpecJSI(const ObjCTurboModule::InitParams ¶ms)
: ObjCTurboModule(params) {
methodMap_["numericMethod"] = MethodMetadata {1, __hostFunction_NativeBasicSpecJSI_numericMethod};
methodMap_["booleanMethod"] = MethodMetadata {1, __hostFunction_NativeBasicSpecJSI_booleanMethod};
methodMap_["stringMethod"] = MethodMetadata {1, __hostFunction_NativeBasicSpecJSI_stringMethod};
}
} // namespace facebook::react
<appRoot>/ios/build/generated/ios/BasicSpec/BasicSpec.h
#import <Foundation/Foundation.h>
#import <RCTRequired/RCTRequired.h>
#import <RCTTypeSafety/RCTConvertHelpers.h>
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTCxxConvert.h>
#import <React/RCTManagedPointer.h>
#import <ReactCommon/RCTTurboModule.h>
#import <optional>
#import <vector>
NS_ASSUME_NONNULL_BEGIN
@protocol NativeBasicSpec <RCTBridgeModule, RCTTurboModule>
- (NSNumber *)numericMethod:(double)arg;
- (NSNumber *)booleanMethod:(BOOL)arg;
- (NSString *)stringMethod:(NSString *)arg;
@end
@interface NativeBasicSpecBase : NSObject {
@protected
facebook::react::EventEmitterCallback _eventEmitterCallback;
}
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper;
@end
namespace facebook::react {
class JSI_EXPORT NativeBasicSpecJSI : public ObjCTurboModule {
public:
NativeBasicSpecJSI(const ObjCTurboModule::InitParams ¶ms);
};
} // namespace facebook::react
NS_ASSUME_NONNULL_END
#endif
코드를 살펴보면 ObjCTurboModule을 상속한 NativeBasicSpecJSI
클래스가 정의된 것을 볼 수 있다. 여기서 상속하는 ObjCTurboModule
은 내부적으로 TurboModule
을 상속하고 있다.
생성된 코드를 조금 더 자세히 들여다보자.
안드로이드와 동일하게,methodMap_
에 정의한 메서드 이름과 MethodMetadata
를 Key-Value 쌍으로 할당하는 모습을 볼 수 있다. 이 부분이 바로 앞서 살펴본 TurboModule
의 구현체이다.
즉, ObjCTurboModule
은 Objective-C 환경에서 터보 네이티브 모듈을 구현하기 위한 TurboModule
의 래퍼이다.
이렇게 생성된 코드를 통해 JSI와 Objective-C가 서로 상호작용 하게 된다.
실제 네이티브 모듈 기능을 구현하는 것은 우리의 몫인데, 코드젠을 통해 정해진 메서드들을 구현할 수 있도록 모듈 사양을 포함하는 프로토콜(Protocol)이 생성된 것을 볼 수 있다.
실제 터보 네이티브 모듈 구현체 부분을 살펴보면 아래와 같다.
미리 만들어진 프로토콜을 구현하도록 함으로써, 타입 불일치, 메서드 구현 누락 등의 문제를 사전에 미리 방지한다.
실제 구현체 코드를 한 번 살펴보자.
여기에서도 Codegen을 통해 생성된 BasicSpec.h
헤더 파일을 통해, 정의된 facebook::react::NativeBasicSpecJSI
을 참조하고 있는 것을 확인할 수 있다.
생성된 C++ 코드와 Objective-C 구현체 코드는 Codegen을 통해 함께 생성된 podspec 파일을 통해 하나로 묶여 프로젝트에 포함된다.
ios/build/generated/ios/ReactCodegen.podspec
Pod::Spec.new do |s|
s.name = "ReactCodegen"
s.version = version
s.summary = 'Temp pod for generated files for React Native'
s.homepage = 'https://facebook.com/'
s.license = 'Unlicense'
s.authors = 'Facebook'
s.compiler_flags = "#{folly_compiler_flags} #{boost_compiler_flags} -Wno-nullability-completeness -std=c++20"
s.source = { :git => '' }
s.header_mappings_dir = './'
s.platforms = min_supported_versions
s.source_files = "**/*.{h,mm,cpp}"
s.exclude_files = "RCTAppDependencyProvider.{h,mm}" # these files are generated in the same codegen path but needs to belong to a different pod
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"FRAMEWORK_SEARCH_PATHS" => framework_search_paths,
"OTHER_CPLUSPLUSFLAGS" => "$(inherited) #{folly_compiler_flags} #{boost_compiler_flags}"
}
s.dependency "React-jsiexecutor"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "React-Core"
s.dependency "React-jsi"
s.dependency "ReactCommon/turbomodule/bridging"
s.dependency "ReactCommon/turbomodule/core"
s.dependency "React-NativeModulesApple"
s.dependency 'React-graphics'
s.dependency 'React-rendererdebug'
s.dependency 'React-Fabric'
s.dependency 'React-FabricImage'
s.dependency 'React-debug'
s.dependency 'React-utils'
s.dependency 'React-featureflags'
s.dependency 'React-RCTAppDelegate'
depend_on_js_engine(s)
add_rn_third_party_dependencies(s)
s.script_phases = {
# ...
}
end
파일을 살펴보면, 소스 파일(source_files
)로 Codegen 디렉터리 내의 모든 .cpp
, .mm
, .h
파일들을 포함하는 것을 볼 수 있다.
여기서 중요한 부분이 하나 더 있는데, 그것은 바로 RCT_EXPORT_MODULE 매크로이다. 이전 레거시 네이티브 모듈에서도 사용되던 매크로인데, 매크로 구현체를 살펴보면, RCTBridge.mm 에 구현된 RCTRegisterModule
를 호출하여 등록하는 것을 볼 수 있다.
등록 과정에서 등록할 클래스가 RCTBridgeModule
프로토콜을 따르는지 검사하는 과정을 볼 수 있는데, 이 부분이 다소 의아할 수 있다. RCTBridge
는 이름에서 유추할 수 있듯이, 레거시 아키텍처에서 사용되던 브릿지의 역할을 수행하는데, 왜 여기서도 사용되는 것일까?
애초부터 RCT_EXPORT_MODULE
은 레거시 아키텍처에서부터 존재하던 매크로이다. 레거시 아키텍처와 새로운 아키텍처 환경에서 호환성을 최대한 보장하기 위해 지금과 같은 구조가 된 것인데, RCTBridge.mm 구현체만 봐도, 레거시 구현체와 새로운 아키텍처 구현체가 공존해 있는 것을 볼 수 있다.
RCTRegisterModule
은 [RCTModuleClasses addObject]
를 통해 모듈 클래스가 등록되는 것을 볼 수 있다. RCTModuleClasses
는 등록된 모듈 클래스들은 RCTGetModuleClasses를 통해 참조되는데, 어디에서 이를 요구하는지 좀 더 살펴보자.
코드를 찾다보면, RCTTurboModuleManager에서 RCTGetModuleClasses
를 호출하는 것을 확인할 수 있다. 호출부가 두 군데 있지만, 터보 네이티브 모듈과 관련된 부분만 한 번 살펴보자.
호출부를 찾아보면 _getModuleClassFromName 에서 호출하고 있다. 모듈 이름을 받아, 해당 이름으로 등록된 모듈 클래스를 찾도록 구현되어있다. 그러면 이 메서드는 어디에서 호출하고 있을까?
_provideObjCModule에서 호출하고 있고, 이는 오버로드된 또 다른 _provideObjCModule에서 호출하고 있다. 호출부를 추적해보면 _moduleProviderForName 에서 호출하고 있으며 이는 provideTurboModule 메서드가 호출하는 것을 확인할 수 있다.
provideTurboModule
구현체를 살펴보면, _moduleProviderForName
를 통해 RCT_EXPORT_MODULE
에 의해 등록된 모듈 클래스를 가져온 뒤 터보 네이티브 모듈의 getTurboModule
을 호출하여 C++ 기반의 터보 모듈을 인스턴스화 하는 과정을 거친다.
이처럼 provideTurboModule
메서드는 터보 네이티브 모듈을 제공하는 역할을 수행하는데, 이는 안드로이드에서 확인했던 cxxModuleProvider
, javaModuleProvider
의 역할과 동일하다.
지금까지 살펴본 내용을 정리하면 Codegen 과정을 통해 생성된 코드, 그리고 Auto Linking 을 통해 생성된 코드들은 하나의 podspec으로 묶여 프로젝트에 포함되어 빌드되고, 안드로이드와 달리 Objective-C 특성상 JNI와 같은 별도 레이어 없이 직접 상호작용한다.
TurboModuleManager
React Native 런타임에서 터보 네이티브 모듈에 어떻게 접근하는지 이해하기 위해서는 등록된 모듈들을 관리하는 TurboModuleManager
에 대해 이해해야 한다.
TurboModuleManager
는 ReactInstance
가 초기화 되는 시점에 같이 생성되는데, 애플리케이션이 실행되었을 때 어떤 흐름을 거쳐 TurboModuleManager
가 생성되는지 살펴보자.
Android
코드를 하나 하나 살펴보기에는 분량이 많아질 것 같아 정리한 그림으로 대체한다. 주요한 흐름에 맞게 번호를 붙여두었는데 자세히 살펴보고 싶다면 아래에 정리해둔 내용과 소스코드를 참고하자.
흐름은 MainApplication
에서 시작한다.
- 애플리케이션이 실행되면
MainApplication
의 생명주기 중 onCreate 가 호출되고, 이어서loadReactNative
메서드가 호출된다.loadReactNative
메서드는 코드 상에서 찾아볼 수 없는데, 이는 빌드 시점에 Gradle 플러그인(GenerateEntryPointTask.kt)을 통해 동적으로 생성되는 메서드이다.
loadReactNative
에 의해 DefaultNewArchitectureEntryPoint.load 메서드가 호출되고, React Native의 기본 네이티브 구현체와 앞서 살펴보았던 Auto Linking을 통해 생성된 appmodule 정적 라이브러리를 로드한다.MainActivity
가 생성되어,onCreate
생명주기가 호출된다.- ReactActivity에 onCreate가 오버라이드 되어있고,
ReactActivityDelegate
의 onCreate를 호출한다.ReactActivity
인스턴스가 생성될 때,createReactActivityDelegate
를 호출하여ReactActivityDelegate
인스턴스를 생성하는데, 이는 MainActivity에 오버라이드 되어 구현된 메서드이다.
onCreate
내에서 ReactDelegate 인스턴스를 생성하고, 이어서 loadApp 메서드를 호출한다.ReactDelegate
인스턴스를 생성할 때,ReactHost
인스턴스를 넘겨주는데, 이는MainApplication
에 정의된reactHost
Getter를 통해 가져온다.
ReactDelegate
의 loadApp 메서드가 호출되면 Fabric 아키텍처에서 관리하는 화면 단위인 Surface를 생성하고,surface.start()
메서드를 호출한다.- createSurface 내에서는 Surface 구현체(ReactSurfaceImpl)와 View(ReactSurfaceView)를 생성하고
ReactHost
를 Attach 하는 과정을 포함한다.
- createSurface 내에서는 Surface 구현체(ReactSurfaceImpl)와 View(ReactSurfaceView)를 생성하고
Surface.start
메서드에서는ReactHostImpl
의 startSurface 메서드를 호출한다.ReactInstance
를 생성하기 위한 태스크(Task)를 만들어 트리거 한다.callAfterGetOrCreateReactInstance
->getOrCreateReactInstance
->getOrCreateReactInstanceTask
순서로 진행된다.- 태스크 실행이 성공적으로 완료되는 경우, ReactInstance를 생성한다.
ReactInstance
초기화 시점에 TurboModuleManagerDelegate와 TurboModuleManager 인스턴스를 생성한다.TurboModuleManager
에서는 installJSIBindings을 호출한다- 실제로는 JNI를 통해 바인딩된 C++ 구현체(TurboModuleManager::installJSIBindings)를 호출한다.
- 인자로 모듈을 제공하는 Provider들을 넘기는 것을 확인할 수 있다.
- TurboModuleBinding::install을 통해 터보 네이티브 모듈 프록시를 JavaScript 런타임에 노출시킨다. (
global.nativeModuleProxy
)
여기서 주요한 부분만 추려서 정리해보면, Auto Linking을 통해 생성된 C++의 빌드 결과물인 appmodules
정적 라이브러리를 로드하는 것을 확인할 수 있다.
이어서 ReactInstance
가 생성되면서 TurboModuleManager
도 함께 생성되고, TurboModuleBinding
을 통해 JavaScript 런타임에 터보 네이티브 모듈 프록시 객체가 노출된다.
조금 더 자세히 살펴볼 부분은 모듈 제공자(Provider)인데, TurboModuleManager
가 초기화 될 때 다음과 같은 코드를 확인할 수 있다.
구현체를 보면 모듈 이름을 받아 위임자(TurboModuleManagerDelegate
)의 getModule
(레거시인 경우 getLegacyModule
) 메서드를 호출하여 반환하는 것이 전부이다.
이는 앞서 살펴본 DefaultTurboModuleManagerDelegate
(= ReactPackageTurboModuleManagerDelegate
)의 getModule, getLegacyModule 메서드를 호출하는 것이다.
이렇게 만들어진 모듈 제공자는 getOrCreateModule에서 참조되고 있으며, 이는 C++ 구현체에서 호출하는 getTurboJavaModule, getTurboLegacyCxxModule 등의 메서드를 통해 getModule을 거쳐 접근한다.
C++ 구현체를 살펴보면, 위 메서드들은 결국 TurboModuleManager::getTurboModule을 통해 호출되는 것을 확인할 수 있다.
getTurboModule
은 TurboModuleManager::createTurboModuleProvider에서 호출하는데, 이를 통해 TurboModuleBinding::install 메서드에 전달되는 모듈 제공자가 된다.
JavaScript 런타임에서 접근하는 global.nativeModuleProxy
는 결국 TurboModuleManagerDelegate
의 getModule
(레거시인 경우 getLegacyModule
)의 반환값이라는 것이다.
TurboModuleManagerDelegate.kt 코드를 보면 비어있는데, 실제 로직은 다음 코드들에 구현되어있다.
지금까지 살펴본 내용 중 기억할 부분은 javaModuleProvider
, cxxModuleProvider
를 통해 모듈에 접근하게 되며, 이들은 TurboModuleBinding::install
의 인자로 전달된다는 부분이다.
iOS
iOS 역시 모든 코드를 살펴보지 않고 정리된 그림으로 대체한다. 자세한 내용은 아래에 정리해둔 내용과 소스코드를 참고하자.
iOS의 경우 많은 로직이 위임자(Delegate) 패턴으로 구현되어있어 순서대로 나열하면 헷갈릴 수 있으니, 인스턴스 생성과 할당과 같은 부분을 먼저 훑어본 뒤 그림에 표기한 순서를 설명하고자 한다.
- 애플리케이션 시작 지점인 AppDelegate.swift를 살펴보면, RCTDefaultReactNativeFactoryDelegate를 상속한
ReactNativeDelegate
클래스를 확인할 수 있다.- 이 클래스를 인스턴스화 한 뒤 RCTReactNativeFactory 생성자에게 전달한다.
- self.createRCTRootViewFactory를 호출하는데 구현체를 살펴보면, 기본 구성(
configuration
)에 위임자(delegate
)의 동작을 할당하는 것을 확인할 수 있다. - 여기서 의미하는 위임자는 앞서 생성한
ReactNativeDelegate
의 인스턴스이다. - 마지막으로 RCTRootViewFactory를 생성하며, 위임자로
self
(=RCTReactNativeFactory
)를 전달한다.
RCTRootViewFactory
를 생성한 뒤에는 스스로를 반환한다. 다시AppDelegate
로 돌아가서 구현체를 살펴보자.- 위임자(
delegate
)의dependencyProvider
에RCTAppDependencyProvider
를 할당한다. 이는 이전 글에서 살펴보았던 것처럼 코드젠에 의해 생성되는 모듈 제공자이다. - 생성한 팩토리의
startReactNative
메서드를 호출하여 React Native 애플리케이션을 실행한다.
- 위임자(
여기까지가 React Native 애플리케이션을 실행되기 위해 기본적으로 준비하는 과정이다.
이어서 순서를 살펴보기에 앞서, 그림에 나타낸 것 처럼 RCTReactNativeFactory
는 RCTHostDelegate 그리고 RCTTurboModuleManagerDelegate를 구현하고 있다는 점을 잘 기억하고 있자. (언급하진 않았지만, RCTComponentViewFactoryComponentProvider, RCTJSRuntimeConfiguratorProtocol 도 포함된다)
factory.startReactNative
메서드가 호출되면RCTReactNativeFactory
의 startReactNativeWithModuleName메서드가 호출된다.- 앞서 생성해둔
RCTRootViewFactory
의 viewWithModuleName 메서드를 통해 루트 뷰를 생성한다. - viewWithModuleName 메서드를 살펴보면
- initializeReactHostWithLaunchOptions 메서드를 호출한다.
- createReactHostIfNeeded 메서드를 호출한다.
- 초기에는 생성된
self.reactHost
가 없으니 createReactHost 메서드를 호출하여 RCTHost 인스턴스를 생성한다.- 이때
turboModuleManagerDelegate
를 전달하는데, 이는RCTReactNativeFactory
이다.
- 이때
- 생성한 RCTHost의 start 메서드를 호출한다.
RCTHost
에서는 RCTInstance 인스턴스를 생성한다.- 이때
turboModuleManagerDelegate
를 전달하는데, 이는RCTReactNativeFactory
이다.
- 이때
- 전달받은 여러 위임자와 구성을 할당한 뒤 _start 메서드를 호출한다.
- 구현체를 살펴보면, JavaScript 런타임 스레드에서 코드를 실행시키도록 도와주는
jsInvoker
와, 레거시 아키텍처에서 사용하던RCTBridge
의 호환 레이어인RCTBridgeProxy
를 생성하고 있는 것을 볼 수 있다. - `RCTTurboModuleManager` 인스턴스도 생성하고 있는 것을 확인할 수 있다.
- 이때 위임자를 전달하는데,
self
(=RCTInstance
)이다. RCTInstnace
에 구현한RCTTurboModuleManagerDelegate
프로토콜 구현체를 살펴보면, 실제로는_appTMMDelegate
의 메서드를 호출하고 있다. 이는 결국RCTReactNativeFactory
를 의미한다.
- 이때 위임자를 전달하는데,
- 구현체를 살펴보면, JavaScript 런타임 스레드에서 코드를 실행시키도록 도와주는
- 필요한 구성 요소들을 생성한 뒤, React Native 런타임을 초기화 한다.
- 런타임 초기화 직후
TurboModuleManager
의 installJSIBindings 메서드를 호출한다.
- 런타임 초기화 직후
- installJSBindings 메서드 구현체를 살펴보면, 모듈을 제공하기 람다 함수를 생성하고, 이를 TurboModuleBinding::install 메서드 인자로 전달하여 JavaScript 런타임에
global.nativeModuleProxy
를 정의하는 것을 확인할 수 있다.
안드로이드와 달리 JNI 같은 레이어 없이 즉시 상호작용 하기 때문에 때문에 상대적으로 구조는 간단하다.
조금 더 자세히 살펴볼 부분은 모듈 제공자(Provider)인데, installJSBindings
메서드 내에서 다음과 같은 코드를 확인할 수 있다.
모듈을 제공하기 위한 람다(Lambda) 함수를 선언하고, 이에 대한 참조를 TurboModuleBinding::install
의 인자로 전달한다.
람다 함수 내에서는 앞서 살펴본 provideTurboModule
를 통해 모듈을 가져오는 것을 확인할 수 있는데, 이는 곧 RCT_EXPORT_MODULE
매크로를 통해 등록된 모듈 클래스를 가져온 뒤, 터보 네이티브 모듈의 getTurboModule
을 호출하여 C++ 기반의 터보 모듈을 인스턴스화 하는 과정을 거치는 것이다.
지금까지 살펴본 내용 중 기억할 부분은 provideTurboModule
을 통해 모듈에 접근하게 되며, 이들은 TurboModuleBinding::install
의 인자로 전달된다는 부분이다.
TurboModuleBinding
Android, iOS 환경에서 React Native가 초기화 되면서 어떻게 TurboModuleManager
가 구성되는지 살펴보았다.
TurboModuleBinding:install
호출부까지 살펴보고 끝났는데, 이번 섹션에서 조금 더 자세히 알아보려고 한다.
React Native에서는 JavaScript 런타임에서 네이티브 모듈에 접근할 수 있도록, React Native 인스턴스가 생성될 때 JSI를 통해 프록시 객체를 런타임에 노출시킨다.
여기서 TurboModuleBinding이 '프록시 객체를 런타임에 노출'시키는 역할을 수행한다. 구체적으로 런타임 환경에 모듈을 어떻게 노출 시키는지 한 번 살펴보자.
클래스 정의는 다음과 같으며, 정적 메서드 install
그리고, 네이티브 모듈을 가져오기 위한 getModule
메서드가 정의되어있는 것을 확인할 수 있다.
install
메서드는 네이티브 모듈들을 JavaScript 런타임 환경에서 접근할 수 있도록 처리하는 역할을 수행하는데, 실제 구현체를 살펴보도록 하자.
구현체를 살펴보면, 인자로 JSI 런타임 컨텍스트와 모듈 제공자 두 개를 받는 것을 확인할 수 있다.
하나는 터보 네이티브 모듈 제공자이며, 또 다른 하나는 레거시 모듈 제공자이다.
터보 네이티브 모듈 호환성 플래그(Turbo module interop) 상태에 따라 레거시 모듈 제공자는 존재하지 않을 수도 있다. 두 가지 제공자를 모두 받도록 구현되어있는 이유는, 새로운 아키텍쳐에서의 네이티브 모듈 체계가 하위 호환성을 어느정도 보장하기 때문이다.
이어서 메서드 구현체를 살펴보면, Bridgeless 모드가 비활성화 되었을 때, 전역 컨텍스트에 __turboModuleProxy
라는 이름의 함수를 생성하고, 아닌 경우에는 레거시(브릿지) 아키텍쳐와 동일하게 nativeModuleProxy
라는 이름의 프록시 객체(jsi::Object
)를 생성한다.
이번 글에서는 Bridgeless 환경(두 번째 케이스)인 경우를 살펴보고자 한다.
구현체를 살펴보면, nativeModuleProxy
에 정의되는 객체는std::make_shared<BridgelessNativeModuleProxy>(...)
를 통해 동적으로 BridgelessNativeModuleProxy
인스턴스를 생성하고, 이에 대한 참조 포인터를 넘긴다.
같은 TurboModuleBinding.cpp
코드를 살펴보면, 여기에 BridgelessNativeModuleProxy 클래스가 구현되어있는 것을 확인할 수 있다.
이는 jsi::HostObject
를 상속하고 있어, JSI를 통해 JavaScript 런타임에 바인딩 될 수 있는 구조이다.
또한, BridgelessNativeModuleProxy
는 앞서 살펴본 것과 같이 두 개의 모듈 제공자(Provider)를 참조를 받고, 이를 TurboModuleBinding 인스턴스로 만드는 것을 확인할 수 있다.
내부 구현체를 살펴보면, jsi::HostObject
의 get
메서드를 오버라이드 하고 있는데, 이 부분을 통해 JavaScript 객체에서 속성 접근을 시도할 때, 네이티브 모듈을 가져오게 된다.
객체에 접근하는 프로퍼티 값을 모듈 이름으로 간주하여 TurboModuleBinding
에서 네이티브 모듈을 찾아오는 매커니즘이다.
예를 들어 JavaScript에서 다음과 같이 코드를 작성하는 경우,
global.nativeModuleProxy['SomeModule'];
React Native 코어에서는 turboBinding_.getModule(runtime, "SomeModule")
이 호출되는 셈이다.
만약 터모 네이티브 모듈 바인딩에서 모듈을 찾을 수 없다면, 레거시 모듈 바인딩에서 모듈을 찾는다.
실제 모듈을 가져오는 TurboModuleBinding::getModule 을 조금 더 살펴보자.
getModule
메서드는 JSI 컨텍스트에 접근하기 위한 런타임 참조와, 가져올 네이티브 모듈의 이름 두 개의 인자를 받아 jsi::Value
값을 반환한다.
초반에 TurboModule
구현체가 jsi::HostObject
기반이었다는 점을 기억하는가? getModule
메서드에서 반환되는 값은 TurboModule
혹은 모듈이 존재하지 않을 때 사용되는 null
값 (jsi::Value(nullptr)
)이다.
조금 더 자세히 이야기 하자면, 안드로이드 환경에서는 JavaTurboModule
인스턴스가, iOS 환경에서는 ObjCTurboModule
인스턴스를 가져온다.
순차적으로 살펴보자.
Step 1
지정된 네이티브 모듈 제공자로부터 TurboModule
인스턴스를 가져온다. 만약 모듈이 존재하지 않는 경우 else
블럭으로 이동하여 jsi::Value(nullptr)
값을 반환한다.
Step 2
TurboModuleWithJSIBindings 클래스의 installJSIBindings
메서드를 호출한다.
TurboModule
에는 두 가지 유형이 있는데, 첫 번째는 일반적인 TurboModule
이고, 두 번째는 언급한 TurboModuleWithJSIBindings
이다.
둘의 차이는 크지 않은데, TurboModuleWithJSIBindings
는 TurboModule
을 상속하는 클래스로, 정적 메서드 installJSIBindings
와, 내부용 installJSIBindingsWithRuntime
메서드가 포함된다.
구현체를 확인하면 알 수 있듯이, 인자로 받은 TurboModule
인스턴스를 대상으로 다이나믹 캐스팅(Dynamic Cast)을 시도한 뒤 TurboModuleWithJSIBindings
인터페이스를 충족하는 경우, 모듈 인스턴스의 installJSIBindingsWithRuntime
메서드를 호출한다.
참고로 NitroModule iOS 구현체가 이를 활용하여 JSI에 C++ 구현체를 직접 주입하고 있다. (안드로이드에는 조금 다른 방식으로 구현했다)
Step 3_jsRepresentation
값은 앞서 살펴본 TurboModule
클래스에 포함된 멤버 변수(포인터 변수)이다. TurboModule
인스턴스가 처음 생성되면 해당 값은 빈 참조를 갖고 있기에, getModule
이 처음 호출되었을 때에는 조건이 충족되지 않아 다음 코드로 이어지게 된다.
Step 4
반대로 이 과정부터는 모듈 최초 접근 시 반드시 거쳐가는 과정이라고 볼 수 있다. 먼저 빈 JavaScript 객체를 생성하고, 이를 weakJsRepresentation
(= _jsRepresentation
)에 단일 참조 포인터로 할당한다.
Step 5TurboModule
인스턴스가 바인딩된 jsi::Object
인스턴스를 새로 만든다. TurboModule
클래스는 jsi::HostObject
클래스를 상속하기에 가능하다는 점은 앞서 살펴본 내용이다.
이후 4번 과정에서 생성한 빈 객체의 프로토타입(__proto__
)에 할당하고 해당 객체를 반환한다.
왜 이러한 과정을 거치는지 궁금할 것이다. 이유는 바로 TurboModule
속성에 대한 캐싱을 구현하기 위함이다.
5번 과정까지 거친 뒤 가져온 객체는 JavaScript 런타임에서 확인해보면 빈 객체로 보일 것이다.
const mod = TurboModuleRegistry.getEnforcing('MyModule');
console.log(mod); // {}
여기에서 속성 접근을 시도할 경우 JavaScript의 프로토타입 체이닝으로 인해 빈 객체인 mod
를 지나 프로토타입(__proto__
) 즉, 앞서 할당했던 hostObject
인 TurboModule
의 실체에 접근할 수 있다.
// => __proto__['someMethod']
mod['someMethod'];
여기까지만 두고 보면 이게 캐싱과 무슨 연관이 있을까? 의문이 들 것이다.
TurboModule 구현체를 보면 정답이 숨겨져있다.
TurboModule
은 jsi::HostObject
의 get
메서드를 오버라이딩하고 있는데, 객체 속성에 접근하는 경우 내부에 구현된 로직을 통해 값을 생성 및 반환하도록 구현되어있다.
먼저 속성에 접근하는 경우에는 create
메서드가 호출되는데, 여기에서는 속성 이름이 _methodMap
내에 존재하는지, _eventEmitterMap
에 존재하는지 확인한다.
메서드 부분을 살펴보면, 속성 이름에 해당하는 메서드가 존재할 경우 jsi::Function
타입의 함수 인스턴스를 생성한 뒤 반환한다. 이 함수는 추후 JavaScript 런타임에서 호출하게 되는 네이티브 모듈의 메서드가 된다.
함수 구현체를 보면, 이전에 살펴본 MethodMetadata
의 invoker
를 호출하는 것을 확인할 수 있다.
이렇게 반환된 값은 jsReresentation_
객체 속성에 추가되며, 이후 동일한 속성으로 접근할 경우에는 값을 새로 만들지 않고 기존에 생성된 jsi::Value
값을 반환하게 된다.
지금까지 살펴본 내용을 간단히 정리하면 다음과 같다.
TurboModuleBinding::install
메서드가 호출되면 BridgelessNativeModuleProxy
인스턴스가 생성되고, 이에 대한 레퍼런스가 jsi::HostObject
에 바인딩되어 JavaScript 런타임에 주입된다.
JavaScript에서는 TurboModuleRegistry
를 통해 모듈을 가져올 경우, 주입된 HostObject
(BridgelessNativeModuleProxy
)를 거쳐 모듈 제공자(Provider)로부터 등록된 모듈을 찾아온다.
이것이 바로 JavaScript 런타임에서 터보 네이티브 모듈을 참조하는 매커니즘이다.
마무리하며
지금까지 터보 네이티브 모듈이 어떻게 등록되고, JavaScript 런타임에서 참조되어 동작하는지 간단히 살펴보았다. 이제 터보 네이티브 모듈은 기본 사양이 되었고, 이를 기반으로 하는 다양한 프로젝트들도 많이 탄생하고 있다. (대표적으로 Nitro)
이번 글에서 다루진 않았지만 플랫폼 종속적인 터보 네이티브 모듈(JavaTurboModule
, ObjCTurboModule
) 뿐만 아니라 C++ 구현체만 사용하여 모듈을 구현할 수도 있다. 물론 터보 네이티브 모듈이 아닌 순수 C++ 모듈을 구현할 수도 있다.
필자 개인적으로는 내부 구현을 살펴보면서, 새로운 아키텍처 환경에서 시도해볼 수 있는 것들이 정말 많을 것이라는 기대로 가득 차게 되었다. 앞으로는 단순히 퍼포먼스가 뛰어난 C++ 기반 모듈 뿐만 아니라, React Native 환경에서의 Node-API, Rust 통합 등 다양한 기술들이 탄생할 것이라고 믿는다.
ps 1. 정리하다보니 내용이 너무 많아 세부적으로 다루지 못하고 건너뛴 부분들이 많이 있다. 글은 지속적으로 업데이트 할 예정이나, 보충, 수정이 필요한 부분이 보인다면 댓글에 남겨주길 바란다.
ps 2. Surface와 같이 Fabric과 연관된 내용이 글에서 몇 번 언급이 되었으나, Fabric은 필자의 관심사가 아니기에 후속 글이 작성될지는 모른다