Skip to main content

Electron Internals: Using Node as a Library

· 9 min read

이 글은 Electron의 내부 구조를 설명하는 시리즈의 두 번째 포스트다. 아직 보지 않았다면 이벤트 루프 통합에 관한 [첫 번째 포스트][event-loop]를 먼저 확인해 보자.

대부분의 사람들은 Node를 서버 측 애플리케이션에 사용한다. 하지만 Node의 풍부한 API와 활발한 커뮤니티 덕분에, Node는 내장 라이브러리로도 적합하다. 이 글에서는 Electron에서 Node가 어떻게 라이브러리로 사용되는지 설명한다.


빌드 시스템

Node와 Electron 모두 빌드 시스템으로 GYP를 사용한다. 앱 내부에 Node를 포함시키려면, 여러분도 GYP를 빌드 시스템으로 사용해야 한다.

GYP가 처음이라면, 이 글을 계속 읽기 전에 [이 가이드][gyp-docs]를 먼저 읽어보는 것이 좋다.

Node의 빌드 플래그

Node 소스 코드 디렉토리에 있는 node.gyp 파일은 Node가 어떻게 빌드되는지 설명한다. 이 파일에는 Node의 어떤 부분을 활성화할지, 특정 설정을 열어둘지 여부를 제어하는 여러 GYP 변수들이 포함되어 있다.

빌드 플래그를 변경하려면 프로젝트의 .gypi 파일에서 변수를 설정해야 한다. Node의 configure 스크립트는 일반적인 설정을 자동으로 생성해 준다. 예를 들어, ./configure --shared를 실행하면 Node를 공유 라이브러리로 빌드하도록 지시하는 변수들이 포함된 config.gypi 파일이 생성된다.

Electron은 자체 빌드 스크립트를 사용하기 때문에 configure 스크립트를 사용하지 않는다. 대신, Electron의 루트 소스 코드 디렉토리에 있는 common.gypi 파일에서 Node의 설정을 정의한다.

Electron에서 Node.js 연결하기

Electron에서는 GYP 변수인 node_sharedtrue로 설정하여 Node.js를 공유 라이브러리로 연결한다. 이로 인해 Node.js의 빌드 타입은 실행 파일(executable)에서 공유 라이브러리(shared_library)로 변경되며, Node.js의 main 진입점을 포함한 소스 코드는 컴파일되지 않는다.

Electron은 Chromium과 함께 제공되는 V8 라이브러리를 사용하기 때문에, Node.js 소스 코드에 포함된 V8 라이브러리는 사용되지 않는다. 이를 위해 node_use_v8_platformnode_use_bundled_v8 변수를 모두 false로 설정한다.

공유 라이브러리와 정적 라이브러리

Node와 링크할 때는 두 가지 옵션이 있다. Node를 정적 라이브러리로 빌드해 최종 실행 파일에 포함시키거나, 공유 라이브러리로 빌드해 최종 실행 파일과 함께 배포하는 방법이다.

Electron에서는 오랫동안 Node를 정적 라이브러리로 빌드했다. 이 방식은 빌드 과정을 단순화하고 최적의 컴파일러 최적화를 가능하게 하며, 추가적인 node.dll 파일 없이 Electron을 배포할 수 있게 했다.

하지만 Chrome이 [BoringSSL][boringssl]을 사용하도록 변경된 후 이 상황이 바뀌었다. BoringSSL은 [OpenSSL][openssl]의 포크 버전으로, 사용되지 않는 여러 API를 제거하고 기존 인터페이스를 많이 변경했다. Node는 여전히 OpenSSL을 사용하기 때문에, 두 라이브러리를 함께 링크하면 충돌하는 심볼로 인해 컴파일러가 수많은 링크 오류를 발생시켰다.

Electron은 Node에서 BoringSSL을 사용하거나 Chromium에서 OpenSSL을 사용할 수 없었기 때문에, 유일한 선택지는 Node를 공유 라이브러리로 빌드하고 각 컴포넌트에서 [BoringSSL과 OpenSSL 심볼을 숨기는][openssl-hide] 것이었다.

이 변경은 Electron에 몇 가지 긍정적인 부작용을 가져왔다. 이전에는 네이티브 모듈을 사용하는 경우 Windows에서 Electron 실행 파일의 이름을 변경할 수 없었다. 실행 파일 이름이 임포트 라이브러리에 하드 코딩되어 있었기 때문이다. Node가 공유 라이브러리로 빌드된 후에는 모든 네이티브 모듈이 node.dll에 링크되었고, 이 파일의 이름을 변경할 필요가 없어졌기 때문에 이러한 제약이 사라졌다.

네이티브 모듈 지원하기

Node의 네이티브 모듈은 Node가 로드할 수 있는 진입 함수를 정의한 다음, Node에서 V8과 libuv의 심볼을 검색하는 방식으로 동작한다. 하지만 이 방식은 임베더에게 다소 번거로운 문제를 일으킬 수 있다. 기본적으로 Node를 라이브러리로 빌드할 때 V8과 libuv의 심볼이 숨겨져 있기 때문에, 네이티브 모듈이 심볼을 찾지 못해 로드에 실패할 수 있다.

따라서 네이티브 모듈이 정상적으로 동작하도록 하기 위해, Electron에서는 V8과 libuv의 심볼을 노출시켰다. V8의 경우 Chromium의 설정 파일에서 모든 심볼을 강제로 노출하는 방식을 사용했다. libuv의 경우에는 BUILDING_UV_SHARED=1 정의를 설정하여 이를 달성했다.

앱에서 Node 시작하기

Node를 빌드하고 연결하는 모든 작업을 마친 후, 마지막 단계는 앱에서 Node를 실행하는 것이다.

Node는 다른 앱에 자신을 포함시키기 위한 공개 API를 많이 제공하지 않는다. 일반적으로 [node::Startnode::Init][node-start]을 호출해 Node의 새 인스턴스를 시작할 수 있다. 하지만 Node를 기반으로 복잡한 앱을 구축하는 경우, node::CreateEnvironment와 같은 API를 사용해 각 단계를 정밀하게 제어해야 한다.

Electron에서는 Node가 두 가지 모드로 시작된다. 하나는 메인 프로세스에서 실행되는 독립 실행 모드로, 공식 Node 바이너리와 유사하다. 다른 하나는 Node API를 웹 페이지에 삽입하는 임베디드 모드이다. 이에 대한 자세한 내용은 추후 포스트에서 설명할 예정이다.