사내 프로젝트로 Electron + React 환경에서 개발하고 있는데, 도중 Windows 환경에서 Alt+F4, Alt+Tab 등의 OS Native한 명령에 대해 hooking 처리를 해야 하는 일이 생겼다.
hooking하는 부분은 잡혀있는데, 문제는 이게 C++로 작성되어있다 보니 Electron에 어떻게 붙혀야 하는지 난감했다.
찾아보니 node_addon_api를 이용해 c++ native module을 가져다 붙힐 수 있는 것 같았다.
(그래서 도전)
개발 환경은 Windows 10
VSCode
에서 진행,
주요 라이브러리들은 electron
electron-builder
CRA(Create-React-App)
node-addon-api
가 되시겠다.
앞서 말한 것 처럼 OS에 따라 빌드시 필요한 툴이 다른데, 이 글은 Windows를 기준으로 설명한다. (추후 MacOS도 업데이트할 예정)
Windows의 경우 windows-build-tools
을 설치하면 된다....고 했는데.. 설치하려고 보니 계속 기다려도 설치가 완료되지 않는다..
(나같은 경우 Windows 환경을 우선적으로 개발하고 있는데, Visual Studio 2015, 2017, 2019가 다 있어서 그런건지, node.js와의 버전 충돌로 그런건지 찾아봐도 알아낼 수 없었다.. 혹시 왜 그런지 아시는 분이 계신다면 알려주세요..)
일단, 구글링을 좀 해보니 4.0.0(vs2015) 버전으로 맞춰 설치하면 잘 된다는 것을 확인하고 시도해보니 잘 된다.
(참고로, windows-build-tools 설치 시 관리자 권한이 필요하므로 터미널을 열어서 하자.)
yarn global add windows-build-tools@4.0.0
또는, 나는 이미 Visual Studio가 설치되어 있어 시도해보진 않았지만
[VSCODE] C++ 빌드 환경 만들기(Window 10) ('블루웨일' 님 블로그) 을 참고해서 build tools를 설치해도 될 것 같다. (오히려 이게 더 편할지도... 나중에 C++ 환경세팅 시에도 컴파일러를 설정해야 하니..)
이어서 node-gyp
를 글로벌로 설치한다.
yarn global add node-gyp
마지막으로 본인만의 addon project 생성 후 그 안에서 node-addon-api
, bindings
를 local dependency로 설치한다.
yarn init -y
yarn add node-addon-api bindings # bindings 는 모듈 연결을 쉽게 해주는 라이브러리다.
의존성 설치가 완료되면 C/C++ 개발환경을 설정하기 위해 Extension을 설치해주자.
vscode extension에서 c/c++로 검색한 후 아래 extension 설치 (c 만 쳐도 나오더라..)
그 후, Windows 기준 Ctrl+Shift+P
로 vscode command 창을 연 후 C/C++:Edit Configurations(UI) (한국어론 구성 편집(UI) 로 나올것임)
을 선택한다.
그러면 C/C++ Configurations 창이 나타나게 되는데, 여기서 구성 이름, 컴파일러 경로, 컴파일러 인수 등을 설정할 수 있다.
나 같은 경우는 Visual Studio를 설치했기 때문에 컴파일러 경로가 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\bin\Hostx64\x64\cl.exe
로 자동으로 MSVC(visual studio compiler임) 가 설정되어 있었다.
만약 나타나지 않는다면 위 windows build tools이 없는거니 설치하고 다시 확인해보자. (build tools로 설치하면 경로가 Community
가 아닌 BuildTools
로 나타난다.)
gcc compiler로 설정해서 한번 시도해 봤으나, gcc가 napi.h를 읽지 못하길래 그냥 msvc로 진행했다.
구글링해보니 node-gyp를 쓰지 않고 gcc로만 빌드하는 것도 보긴 했는데, 나중에 한번 알아봐야 될 것 같다.
설정을 완료하면 루트 경로에 .vscode
디렉토리가 생기고, 그 안에 c_cpp_properties.json
, settings.json
이 생성된다.
c_cpp_properties.json
을 연 후 includePath에 우리가 사용할 node-gyp header들의 경로를 설정해줘야 한다.
아래와 같이 includePath 아래에 C:/Users/{사용자 이름}/AppData/Local/node-gyp/Cache/{노드 버전}/include/node
경로를 추가해주자. (만약 이렇게 하지 않으면 c++ 코드 작성 시 napi.h 안의 node_api.h의 경로를 읽을 수 없다고 뜨니, 꼭 해주자.)
자 이제 얼추 환경 세팅은 끝났으니, 코딩만 하면 된다..!
다음과 같이 루트 폴더에 binding.gyp
를 생성한다.
해당 파일은 node-gyp를 통해 빌드를 할 때, 어떤 파일을 어떻게 binding 해서 빌드할 지를 설정하는 파일이다. (json 형식으로 작성해주면 된다.)
# binding.gyp
{
"target": [
{
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"target_name": "./hello_world",
# 여기서 타겟 소스파일을 지정한다.
"source": [ "hello_world.cc" ],
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
}
]
}
이제 다음으로 내가 모듈화할 C++ 코드를 작성한다.
이 글에서는 루트 경로에 hello_world.cc
로 작성한다. (역시 처음엔 헬로월드다.)
// napi.h 호출, 기본적으로 node-addon-api를 설치하면 그 안에 포함되어 있다.
// 만약 빨간줄이 뜨는 경우, 아래 기술된 내용으로 해결해보자.
#include <napi.h>
// Javascript의 String 객체를 반환하는 함수, 즉 우리가 export 시켜주고 싶은 함수를 만든다.
// 파라미터는 info[n] 형태로 얻어올 수 있다. (param이 2개라면 info[0], info[1] 이런 식으로)
Napi::String HelloWorld(const Napi::CallbackInfo& info) {
// info에는 현재 scope 정보(env)도 들어있다.
// js 객체를 생성하려면 반드시 env를 받아와야 함.
Napi::Env env = info.Env();
// scope info(env)와 std::string 객체를 사용해 문자열 리턴
return Napi::String::New(env, "Hello World !");
}
// Add-on Initializer.
// js object(exports)에 export 시킬 객체들(함수, 변수 등..)을 넣고 리턴시키면 된다.
Napi::Object init(Napi::Env env, Napi::object exports) {
exports.Set(Napi::String::New(env, "HelloWorld"), Napi::Function::New(env, HelloWorld));
// 더 추가할꺼라면 위와 같은 형식으로 세팅..
return exports;
}
// Add-on이 실제 호출될 때의 alias와 initializer를 인자로 받아 설정함.
NODE_API_MODULE(hello_world, init);
package.json
으로 넘어가 scripts를 만들어주자.
{
...
"scripts": {
"build": "node-gyp rebuild",
"clean": "node-gyp clean"
}
...
}
빌드하자 !
yarn build
위 사진처럼 쭉쭉쭉 gyp가 빌드하면서, 최종적으로 build
디렉토리가 생성된다.
build
디렉토리를 살펴보면 Visual Studio solution 파일 sln, 프로젝트 파일 vcxproj 등 VS와 관련된 파일들이 생성되고, Release
디렉토리가 생성된다.
열어보면 앞서 binding.gyp
에서 설정했던 target_name
으로 각종 링커 파일들이 생성되는 것을 알 수 있는데, 그 중 우리는 node.js에서 사용할 .node
파일을 쓰면 된다.
(뭐 이런식으로 만들어진다)
자 이제 내가 진짜 원하던, Electron에서 해당 native module을 사용해보자.
나는 Electron에 Renderer process로 React(CRA)
를 사용하고 있는데, react-scripts build
시에 만들어지는 디렉토리 이름이 build
라서, add-on 생성을 루트 디렉토리에서 바로 할 수가 없었다.
그래서 addon
디렉토리를 새로 만들고, 위의 과정들을 그 안에서 진행했다.
자 그럼 이제 electron root 파일인 electron.js
에서 해당 모듈을 호출해야 한다.
일단 무작정 불러와보자
const addon = require('bindings')('hello_world');
const str = addon.HelloWorld();
console.log(`test :: ${str}`);
그리고 electron.js를 실행해보자
(두둥)
bindings file을 못찾는단다.
자세히 보면 아래 ->
로 표시된 경로 중 하나로 해당 node를 설정해야 하는 듯 하다.
build
디렉토리는 react-script build 시 사용되는 디렉토리기 때문에, 나는 out/Release/
로 해당 node 파일을 복사해 아래와 같이 위치시켜 주기로 했다.
다시 실행해보면..
(짠!)
test :: Hello World ! 라고, 우리가 앞서 C++로 작성한 문자열이 제대로 잘 나오는 것을 알 수 있다.
나는 electron-builder
를 사용하고 있고, 관련해서 build 설정들을 yml 파일로 분리해서 관리하고 있다.
어려울 것 없이 files
에 out
디렉토리를 추가해주면 된다.
...
files:
- "out/**/*"
...
이후 electron-builder
를 이용해 빌드를 진행해주고, 확인해보자.
(빌드 기다리는 중...)
빌드 완료 후, .exe
파일이 있는 곳으로 간 후 터미널을 열어 터미널에서 실행시켜 보았다.
(깔-끔)
이제 C++ Native Module을 개발할 수 있는 환경은 갖춰졌으니, 열심히 만들어주기만 하면 된다..!!