From native to JavaScript in Electron
Electron에서 C++나 Objective-C로 작성된 기능이 어떻게 JavaScript로 전달되어 최종 사용자에게 제공되는지 알아보자.
Electron은 내부적으로 Node.js와 Chromium을 사용한다. Node.js는 C++로 작성된 코어 모듈을 JavaScript로 노출시키는 메커니즘을 제공한다. 이 과정에서 N-API(Node-API) 또는 Native Addons라는 기술을 활용한다.
-
Native Addons 생성: C++로 작성된 코드를 Node.js가 이해할 수 있는 형태로 컴파일한다. 이를 위해 node-gyp라는 빌드 도구를 사용한다. 이 도구는 C++ 코드를 Node.js 바이너리와 호환되는 네이티브 모듈로 변환한다.
-
JavaScript 바인딩: 컴파일된 네이티브 모듈을 JavaScript에서 사용할 수 있도록 바인딩한다. 이를 통해 C++ 함수를 JavaScript 함수처럼 호출할 수 있다.
-
Electron 통합: 이렇게 생성된 네이티브 모듈을 Electron 애플리케이션에 통합한다. Electron은 Node.js 런타임을 포함하고 있기 때문에, 네이티브 모듈을 JavaScript 코드에서 직접 사용할 수 있다.
-
최종 사용자에게 노출: JavaScript로 래핑된 기능은 Electron 애플리케이션의 API로 제공된다. 이를 통해 최종 사용자는 JavaScript를 통해 C++ 또는 Objective-C로 구현된 기능을 사용할 수 있다.
이 과정을 통해 Electron은 하위 수준의 기능을 JavaScript로 쉽게 노출시킬 수 있다. 이를 통해 개발자는 복잡한 네이티브 코드를 직접 다루지 않고도 강력한 기능을 구현할 수 있다.
배경
Electron은 플랫폼별 구현에 신경 쓰지 않고도 강력한 데스크톱 앱을 개발할 수 있도록 도와주는 JavaScript 플랫폼이다. 그러나 내부적으로 Electron은 여전히 플랫폼별 기능을 시스템 언어로 작성해야 한다.
실제로 Electron은 네이티브 코드를 직접 처리해 주기 때문에 개발자는 단일 JavaScript API에만 집중할 수 있다.
그렇다면 이 과정은 어떻게 작동할까? C++이나 Objective-C로 작성된 Electron의 기능이 어떻게 JavaScript로 전달되어 최종 사용자에게 제공될까?
이 경로를 추적하기 위해 app
모듈부터 시작해 보자.
lib/
디렉토리 안에 있는 app.ts
파일을 열면 상단에 다음과 같은 코드를 찾을 수 있다:
const binding = process.electronBinding('app');
이 코드는 Electron이 C++/Objective-C 모듈을 JavaScript에 바인딩해 개발자가 사용할 수 있도록 하는 메커니즘을 직접 가리킨다. 이 함수는 ElectronBindings
클래스의 헤더와 구현 파일에 의해 생성된다.
process.electronBinding
이 파일들은 Node.js의 process.binding
과 유사하게 동작하는 process.electronBinding
함수를 추가한다. process.binding
은 Node.js의 require()
메서드의 저수준 구현체로, JS로 작성된 다른 코드 대신 네이티브 코드를 require
할 수 있게 해준다. 이 커스텀 process.electronBinding
함수는 Electron에서 네이티브 코드를 로드할 수 있는 기능을 제공한다.
최상위 자바스크립트 모듈(예: app
)이 이 네이티브 코드를 요청할 때, 해당 네이티브 코드의 상태는 어떻게 결정되고 설정되는가? 메서드와 프로퍼티는 어떻게 자바스크립트에 노출되는가?
native_mate
현재 이 질문에 대한 답은 native_mate
에서 찾을 수 있다. native_mate
는 Chromium의 gin
라이브러리를 포크한 것으로, C++과 JavaScript 간의 타입 변환을 더 쉽게 만들어준다.
native_mate/native_mate
디렉토리 안에는 object_template_builder
에 대한 헤더 파일과 구현 파일이 있다. 이 파일들은 네이티브 코드에서 JavaScript 개발자가 기대하는 형태로 모듈을 구성할 수 있게 해준다.
mate::ObjectTemplateBuilder
모든 Electron 모듈을 object
로 본다면, object_template_builder
를 사용해 이를 구성하려는 이유를 더 쉽게 이해할 수 있다. 이 클래스는 V8 위에 구축되어 있다. V8은 Google이 C++로 개발한 오픈소스 고성능 JavaScript 및 WebAssembly 엔진이다. V8은 JavaScript(ECMAScript) 사양을 구현하므로, 네이티브 기능 구현을 JavaScript 구현과 직접적으로 연결할 수 있다. 예를 들어, v8::ObjectTemplate
은 전용 생성자 함수와 프로토타입 없이 JavaScript 객체를 제공한다. 이는 Object[.prototype]
을 사용하며, JavaScript에서는 Object.create()
와 동등하다.
이를 실제로 확인하려면 app 모듈의 구현 파일인 atom_api_app.cc
를 살펴보자. 파일 하단에는 다음과 같은 코드가 있다.
mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("getGPUInfo", &App::GetGPUInfo)
위 코드에서 .SetMethod
는 mate::ObjectTemplateBuilder
인스턴스에 호출된다. .SetMethod
는 ObjectTemplateBuilder
클래스의 모든 인스턴스에서 호출할 수 있으며, JavaScript의 Object 프로토타입에 메서드를 설정한다. 사용법은 다음과 같다.
.SetMethod("method_name", &function_to_bind)
이는 JavaScript에서 다음과 동등하다.
function App{}
App.prototype.getGPUInfo = function () {
// implementation here
}
이 클래스는 또한 모듈에 속성을 설정하는 함수를 포함한다.
.SetProperty("property_name", &getter_function_to_bind)
또는
.SetProperty("property_name", &getter_function_to_bind, &setter_function_to_bind)
이것은 JavaScript의 Object.defineProperty 구현과 동등하다.
function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
})
그리고
function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
set(newPropertyValue) {
_myProperty = newPropertyValue
}
})
개발자가 기대하는 대로 프로토타입과 속성으로 구성된 JavaScript 객체를 생성할 수 있으며, 이 낮은 시스템 수준에서 구현된 함수와 속성에 대해 더 명확히 이해할 수 있다!
주어진 모듈 메서드를 어디에 구현할지 결정하는 것은 복잡하고 종종 비결정적인 문제이다. 이에 대해서는 추후 게시글에서 다룰 예정이다.