최근 사내에서 진행하는 키오스크 프로젝트에서 일렉트론을 사용해 데스크탑 앱을 만들어보고 있다.
일렉트론이란 웹에서 사용하는 기술, HTML, CSS, JS 를 가지고 데스크탑 프로그램을 개발할 수 있는 프레임워크이다.
웹 프론트 기술을 활용할 수 있어서, 리액트 혹은 Next.js 와 함께 사용도 가능하다.
도입 계기
계기는 내부적으로 조금 촉박하게 키오스크 프로그램을 개발해야하는 상황이었다. 처음에는 Next.js 를 사용해서 웹과 키오스크 펌웨어간 통신을 계획했는데, 만약 기기가 여러대가 생긴다면... 서버의 부담도 생기고 관리 방식도 엄청 복잡해질 것이란 우려가 나와 지속가능성이 떨어진다고 판단했다. 그래서 Electron에서 로컬 서버를 띄우고 private IP를 활용해 http 통신 방식으로 펌웨어 - 일렉트론 - 웹 키오스크 UI 이런 방식으로 변경하게 되었다.
이 상황에서 만약 일렉트론으로 UI 와 펌웨어 통신을 모두 담당하게 된다면 두 개의 별도 프로젝트가 아닌 하나의 프로젝트, 하나의 프로그램으로 개발하는 것도 가능하다는 판단이 들어 내부적으로 계속 개발을 진행중이다.
앞서 말했듯 웹 프론트엔드 개발자가 가장 접근하기 쉬운 기술인 HTML,CSS,JS 를 기반으로 하기 때문에 러닝커브가 적었고, 그래서 기존에 웹으로 띄우던 UI 를 긁어와 하나의 데스크탑 프로그램으로 통합하는 작업을 진행하게 되었다.
찾아보니, Vscode, Slack, Discord 등 자주 사용하는 서비스가 일렉트론을 기반으로 개발이 되었기도 하고, 이 분야에서 현재 일렉트론만큼 넓은 점유율을 보유한 기술도 없는 것 같아 선택하게 되었다. 그래서 개발을 하다가 막히면, '그래.. 슬랙과 디스코드도 일렉트론으로 개발했는데 나라고 못할거 없지...' 란 생각이 들면서 위안이 된다...
겪은 어려움
일단 데스크탑 앱인 만큼, 브라우저에서 너무 당연하게 핸들링 해주던 많은 부분을 직접 핸들링 해야한다는 점이 어려웠다.
특히 보안에 많은 신경을 써야해서 - 유저의 파일 시스템 등에 직접 접근이 가능하기 때문에 - 유저와 직접 맞닿는 UI 부분은 Renderer 프로세스가, 이외에는 Main 프로세스가 담당한다. 이 둘 간의 통신 방식을 이해하는 것이 중요했는데, 처음엔 좀 어려웠음. 그리고 사실 아직도 완벽하게 이해를 한 것은 아니라 많은 공부가 필요하다. 만약 일렉트론을 계속 사용할거라면...
contextBridge를 사용해보아요~
두 종류의 프로세스로 구성된 일렉트론에서, 프로세스간 통신을 위해서 사용하는 것이 바로 IPC 모듈이다.
appWindow = new BrowserWindow({
width: 800,
height: 600,
backgroundColor: '#202020',
show: false,
autoHideMenuBar: true,
frame: false,
titleBarStyle: 'hidden',
icon: path.resolve('assets/images/appIcon.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false,
preload: APP_WINDOW_PRELOAD_WEBPACK_ENTRY,
sandbox: false,
},
});
위 코드는 메인 프로세스에서 프로그램 윈도우 인스턴스를 생성하는 코드이다. webPreferences 속성에 중 특히 봐야하는게 nodeIntergration 그리고 contextIsolation 이다. 위 두 옵션은 프로세스간 통신에서 보안을 강화하기 위한 옵션인데, 현재 설정과 같이 되어있다면 렌더러 프로세스에서 Node 모듈 접근을 할 수 없고, 두 프로세스간 격리된 컨텍스트를 생성하게 된다.
이렇게 보안이 중요한 이유는, 만약 외부의 콘텐츠를 렌더링하는 상황이 발생한다면, 두 컨텍스트는 격리되어 있기 때문에 노드 모듈에 접근하지 못하게 방지하는 역할을 하기 때문이다...
추가적으로 preload 경로도 있는데, preload 스크립트 파일의 위치를 의미하는 것이다.
preload 스크립트란 웹 콘텐츠를 렌더링 하기 전에, 렌더링 프로세스에서 실행되는 코드를 포함하는 스크립트 파일이다. 렌더러 프로세스 내에서 실행되지만, Node 모듈에 대한 접근 권한을 갖기 때문에, 만약 contextIsolation 옵션이 true 로 활성화 되어 있다면, 이 preload 스크립트에서 contextBridge 를 활용해 각 프로세스간 통신을 제어할 수 있다.
실제 코드를 보면서 다시 살펴보자...
// 메인 프로세스와 통신할 명령어
export const serialBridge = {
motorDown: () => {
ipcRenderer.send('motor-down', '모터 다운이다옹...');
},
};
// preload 파일 내에서 contextBridge를 통해 등록
contextBridge.exposeInMainWorld('serial', serialBridge);
// 실제 사용
window.serial.motorDown();
앞서 통신을 할 때 ipc 모듈을 활용한다고 했는데, 렌더러 프로세스에서는 ipcRenderer 모듈을, 메인 프로세스에서는 ipcMain 모듈을 활용해 메시지를 주고받게 된다.
아래는 렌더러 프로세스에 등록된 위 요청을 메인 프로세스에서 어떻게 처리하는지 보여주는 코드 조각이다.
// 메인 프로세스...
export function registerPortCommands() {
const port = initSerialPort();
ipcMain.on('motor-down', () => {
port.write(Buffer.from(KIOSK_CMD.motorDown), function (error) {
if (error) {
console.error(`Error on serial`, error.message);
}
`motor-down 입니다.`;
});
port.flush();
});
...
}
우리는 serialport 라이브러리를 사용해 기기 펌웨어와 통신을 하기 때문에, UI 에서 명령 요청 - 메인 프로세스에서 - 시리얼 포트에 접근해 핸들링 하는 방식이다.
이 외에도, 외부 api 로의 http 요청도 메인 프로세스에서 핸들링하도록 되어있다. contextBridge 를 쓰면 이에 대한 모든 명령을 preload 에 등록하고, 만약 타입스크립트를 사용한다면 이에 대한 타입도 선언을 해주어야 하는 귀찮음이 존재한다. 만약 복잡한 로직이라면 관리하기가 상당히 까다로울 것 같다는 생각도 들고... 어떤 구조로 가져가야 유지보수하기 편할지 고민도 된다...
현재는 s3를 사용한 update 기능까지 구현이 된 상태인데, 동작이 완벽하게 되는지는 지속적인 테스트가 필요하다. 사실 계속 일렉트론으로 키오스크 프로그램을 개발할지도 미지수이지만... 이렇게 시도해볼 수 있을 때 해보는 것도 경험이고 그게 스타트업의 장점이 아닌가 싶기도... 다음주부턴 기존 소스 개선 및 신규 개발건이 있어서 당분간 일렉트론을 많이 하진 않을 것 같다..
'Stacks' 카테고리의 다른 글
[Electron.js] electron 의 rebuild 란? (0) | 2024.02.18 |
---|---|
[Node.js] Node의 require module 은 어떻게 동작하는가? (0) | 2024.02.04 |
[Node.js] Streams 에 대해서 알아봐요. (0) | 2024.01.28 |
[Node.js] Node란 무엇이냐..이벤트 루프란..? (1) | 2024.01.14 |