[Windows, VSCODE] Electron(With CRA)에 Node C++ Add-on 추가하기

Acccdang·2021년 7월 6일
2
post-thumbnail

사내 프로젝트로 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 가 되시겠다.

1. 의존성 설치 및 환경 세팅

의존성 설치

앞서 말한 것 처럼 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++ 개발환경 세팅

의존성 설치가 완료되면 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의 경로를 읽을 수 없다고 뜨니, 꼭 해주자.)

2. 코드 작성

자 이제 얼추 환경 세팅은 끝났으니, 코딩만 하면 된다..!

다음과 같이 루트 폴더에 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"
  }
  ...
}

3. 빌드

빌드하자 !

yarn build


위 사진처럼 쭉쭉쭉 gyp가 빌드하면서, 최종적으로 build 디렉토리가 생성된다.
build 디렉토리를 살펴보면 Visual Studio solution 파일 sln, 프로젝트 파일 vcxproj 등 VS와 관련된 파일들이 생성되고, Release 디렉토리가 생성된다.
열어보면 앞서 binding.gyp 에서 설정했던 target_name 으로 각종 링커 파일들이 생성되는 것을 알 수 있는데, 그 중 우리는 node.js에서 사용할 .node 파일을 쓰면 된다.
(뭐 이런식으로 만들어진다)

4. Electron에 import 해보자.

자 이제 내가 진짜 원하던, 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 build 시에는?

나는 electron-builder를 사용하고 있고, 관련해서 build 설정들을 yml 파일로 분리해서 관리하고 있다.
어려울 것 없이 filesout 디렉토리를 추가해주면 된다.

...
files:
  - "out/**/*"
...

이후 electron-builder 를 이용해 빌드를 진행해주고, 확인해보자.
(빌드 기다리는 중...)

빌드 완료 후, .exe 파일이 있는 곳으로 간 후 터미널을 열어 터미널에서 실행시켜 보았다.

(깔-끔)

이제 C++ Native Module을 개발할 수 있는 환경은 갖춰졌으니, 열심히 만들어주기만 하면 된다..!!


참고자료

profile
개발이 취미가 되고픈 개발자

0개의 댓글