Electron.js를 통해 키오스크 개발을 진행하면서, 겪었던 다양한 경험을 공유합니다.
이번에 키오스크를 갑자기 만들게 된 “라이언” 이라고 합니다.
프로젝트 기한은 1주일 정도로 짧은 기한 이었지만,
개발 과정에서 겪었던 고충들을 공유해보려고 합니다.
시리얼 통신
저희는 안정적인 외부기기와의 연결을 위해서, “시리얼 통신” 을 진행하였습니다.
USB의 통한 연결이 최근에는 보편적이라고 생각할 수 있지만, USB를 통한 연결에는 “Driver” 라는 변수가 발생합니다.
Driver가 정상적으로 동작할 때는, 아무런 문제 없이 잘 돌아가지만, Driver가 중간에 오류를 발생시키면 연결성이 끊기고, 해결하기가 어려운 문제가 발생하기도 합니다.
필요할 때 만, Trigger 작업만 필요한 프린터에는 연결성을 가져갈 필요가 없어 USB로 연결하는 방식도 괜찮지만,
꾸준히 장시간의 연결성을 가져가는데 있어서는 COM포트를 통한 시리얼 통신을 이용하는 것이 훨씬 유리합니다.

키오스크 전용으로 나오는 하드웨어에는 대부분 이런 직렬포트 “COM“포트가 존재합니다.
언뜻 보면, RGB 포트와 착각하기 쉽습니다.

COM포트를 사용하게 되면, 다양한 장점을 누릴 수 있습니다.
- 장기간 연결성
- 회복성
- 외부 장치가 이탈해도, 다시 연결만 하면 유지됨
- 프로그램이 장치가 아닌 정해진 규약에 맞춘 포트에만 의존하기 때문임.
- 호환성
특히 COM포트의 연결은 시리얼통신으로 진행하므로, Driver가 필요없어,
COM포트를 정상적으로 지원한다? 하면 호환성이 매우 뛰어납니다.
뭐 OS에 따라서 Driver가 정상동작하지 않는다는 것을 신경쓰지 않아도 됩니다.
그런데, 저차원의 명령을 직접 보내야 하므로, 복잡한 명령이 들어가는 경우, 개발자가 힘들어진다는 단점도 지니고 있습니다.
또한, 장기간 연결성과 회복성 덕분에 반드시 키오스크에서 COM포트를 사용하는 이유가 됩니다.
COM포트는 분명히 Read/Write를 받는 연결성이 있으면서도 없습니다.

왜냐하면, COM포트는 포트 자체에 이미 통신규약을 정해놓았습니다.
COM포트는 5가지 정도의 규약을 지정하고 갑니다.
- 통신 속도 (Baud Rate)
- 데이터 비트
- 패리티 비트
- 정지 비트
- 흐름 관리
그 중에서도, 가장 중요한 것은 “통신 속도” 입니다.
이는 미리 상대편의 연결되는 장치가 이렇게 들어오고/받을 것이다를 미리 정해둔 것입니다.
이렇게 미리 연결 전부터 미리 협상이 되었기 때문에,
장치가 중간에 이탈해서, 다시 연결되어도 연결성은 항상 유지됩니다.
쉽게 생각하면, 프로그램은 장치에 연결하는게 아니고, 포트에 연결한다고 생각하면 됩니다.
포트는 어디 도망가지 않으니까요, 안정성이 좋을 수 밖에 없습니다.
일렉트론에서 사용하는 시리얼통신 라이브러리
감사하게도, 일렉트론에서도, 이런 시리얼통신을 수행가능하게 해주는 라이브러리가 존재합니다.
https://www.npmjs.com/package/serialport
바로 “serialport” 라는 라이브러리 입니다.
해당 라이브러리를 통해서, 우리는 COM포트를 통해서 시리얼 통신을 쉽게 연결할 수 있게 됩니다.
- serialport 설치하기
npm i serialport
시리얼 포트 목록 불러오기
const { SerialPort } = require('serialport');
/**
* 시리얼 장치 등록하기.
*/
SerialPort.list()
.then(serialportList => {
registerSerialportList(serialportList);
});
serialport에서는 list() 메서드를 통해서,
현재 컴퓨터에서 사용가능한 시리얼포트 목록을 조회할 수 있습니다.
비동기 함수 이므로, await을 사용하여 받거나,
위와 같이 Promise를 사용하여, 받으셔도 됩니다.
시리얼포트 연결하기
- 디바이스 선언하기
let serialDevice = null;
우선 시리얼 장치를 담을 변수를 하나 선언해야 합니다.
- 연결할 때는 기존의 연결 감지하기
/**
* 이미 장치가 연결되어 있는 경우. 무시하기.
*/
if (serialDevice !== null && serialDevice.isOpen) {
console.log("기존 시리얼 장치 연결이 종료되었습니다.");
serialDevice.close();
dialog.showMessageBox({
title: "알림",
message: "기존 시리얼 장치 연결을 해제했습니다. 다시 저장하기 버튼을 눌러주세요."
});
return;
};
이미 시리얼장치가 할당되어 있는경우, 다시 연결을 시도하려고 하면 오류가 발생합니다.
이를 막기 위해서, 이미 시리얼장치가 존재하고, isOpen을 통해 연결성을 가지고 있으면, close메서드를 수행하도록 합니다.
여기서 연결단계로 넘어가지 않는 이유는 close() 자체가 함수상은 동기함수로 동작하지만, 실제로는 비동기 형태로 동작하기 때문입니다.
이로 인해서, close() 이후 다시 open()처리를 수행하게 되면, 간헐적으로 오류가 발생하게 됩니다.
- 시리얼 포트 연결
try {
serialDevice = new SerialPort({
path: "COM1",
baudRate: 9600,
autoOpen: true
});
} catch {
dialog.showErrorBox("연결 실패", "현재 시리얼 포트가 연결되어 있지 않습니다. 다시 시도 해주세요.");
return;
}
path는 COM포트와 같은 이름을 의미합니다. Linux와 Mac이라면 dev/tty 이런 형태로 입력해야합니다.
보편적으로 키오스크는 Windows OS에 많이 탑재되므로, 주로 “COM{숫자}” 형태의 입력이 들어가게 됩니다.
baudRate도 지정합니다. 저희는 보편적으로 가장 많이 사용되는 9600으로 지정하였습니다.
autoOpen 명령을 통해서, 선언과 동시에, 프로그램과 포트를 연결시킬 수 있습니다.
- parser 처리
const parser = serialDevice.pipe(new ReadlineParser({delimiter: '\r\n'}));
parser.on('data', (qrData) => {
eventBus.emit('qr-read', qrData);
});
Parser를 통해서 구분자 Enter를 지정하여, 데이터를 분리하여 읽고,
내용을 전달 할 수 있습니다.
저희는 QR코드 리더기를 시리얼 통신으로 연결하였는데요,
해당 기기에서는 데이터를 전부 읽으면, 개행 값을 주어서, 마무리를 정확히 끊을 수 있도록 정보를 보내줍니다.
사실 연결까지만 보면, 이게 전부입니다.
예상치 못한 연결 이슈
테스트 할 때는 문제없이 되었는데, 역시 실제 사용하기 전에 테스트를 해보면,
문제가 언제든 나오는 것이 인지상정!
시리얼통신이 현장에서는, 간헐적으로 끊긴다는 의견을 듣고, 의문감을 가지게 되었습니다.
시리얼통신이 연결성을 안정적으로 가져가기 위해서 사용하는데, 이게 연결이 끊기 다니..
원인은 부팅이후 장치 로드 단계에 있었습니다.
키오스크 사양이 CPU 4코어에 RAM 4GB정도 되는, 요즘 시대에서는 그렇게 높지 않은 그럭저럭한 낮은 사양을 가지고 있었습니다.
저는 당연히 높은 사양의 컴퓨터에서 키오스크를 개발 했으므로, 당연히 해당 이슈는 재현이 거의 불가능 했습니다.
그 당시에는 사양 문제라고는 전혀 생각치도 못했습니다.
사양이 낮은 컴퓨터가 시리얼 장치를 로드하는데 시간이 꽤나 걸려서,
로드하기 전에 키오스크를 먼저 실행시키면, 시리얼 장치가 제대로 연결되지 않는 상황이 발생하였습니다.
저는 이를 위해서 2가지의 안정성 조치를 내렸습니다.
- 실사용 전에는, 컴퓨터 부팅 후 2분 정도 이후에 키오스크 프로그램을 실행 할 것.
- 그럼에도 시리얼통신이 오류가 발생할 가능성을 염두하여, 재시도 로직을 작성 할 것.
해당 조치는 매우 정확한 조치였습니다.
try {
serialDevice = new SerialPort({
path: serialPathName,
baudRate: Number(baudRate),
autoOpen: true
});
} catch {
serialErrorHandler();
return;
}
/**
* 스캐너 입력에 ERROR가 감지된 경우.
*/
serialDevice.on('error', async () => {
serialErrorHandler();
});
저는 포트 오픈 단계에서 ErrorHandler호출,
사용 중간에 ErrorHandler 호출을 구현하였습니다.
const serialErrorHandler = async () => {
console.log("COM PORT cannot found");
await sleep(2000);
if (serialDevice !== null && !serialDevice.isOpen && currentRetry < MAX_RETRY_COUNT) {
currentRetry += 1;
connectSerialDevice();
} else {
currentRetry = 0;
dialog.showErrorBox("포트 연결 실패", `포트를 ${MAX_RETRY_COUNT}회 연결을 시도하였으나, 정상적으로 연결되지 않았습니다. 프로그램을 재시작 해주세요.`);
}
};
ErrorHandler에서는 시리얼포트 연결에 오류가 발생하면, 지정한 MAX_RETRY_COUNT 번 만큼 백오프 타임 2초를 두고, 수시로 다시 연결을 시도합니다.
이는 실수로 프로그램을 빠르게 실행하였을 때, 초기 연결 이슈를 막아주기도 하고,
연결 이후에, 발생한 오류를 막아주는 역할도 진행할 수 있게 됩니다.
적어도 한 번 성공적으로 연결성을 가져갔다면, 해당 이후는 포트에 무슨일이 있지 않는 한,
연결성이 끊길 가능성은 낮으므로, 초기 연결에 대해 안정적인 연결이 진행 될 수 있도록 코드를 작성하였습니다.
물론 아무리해도 연결이 되지 않는다면, 최후의 선택으로 프로그램을 재시작 해달라는 오류 메세지를 띄우는 조치를 하였습니다.
이를 통해, 초기 시리얼통신 연결에 있어 꽤나 안정성 높은 연결을 유지할 수 있었습니다.
화면단 개발
저희 서비스는 화면단에 대한 수정사항이 매우 많이 발생하기 때문에,
화면단을 모두 네이티브에 담아서 구현하지 않고,
외부 웹뷰를 사용하여 개발하였습니다.
이는 일렉트론이 웹기반 기술로 화면을 띄워주기 때문에,
충분히 가능한 전략입니다.
const createWindow = async () => {
mainWindow = new BrowserWindow({
kiosk: true,
webPreferences: {
preload: path.join(__dirname, '../preload.js'),
},
});
const isOnline = await checkNetwork();
// 메인 페이지 열기. (온라인이면, 키오스크화면, 아닐 경우, 오프라인 페이지)
if (isOnline) {
mainWindow.loadURL(`https://test.picksco.com`);
} else {
mainWindow.loadFile(path.join(__dirname, '../view/offline.html'));
}
};
코드는 매우 간단합니다.
mainWindow에 키오스크 모드를 통해 띄워 줄 것이고,
미리 이벤트를 정의해둔 preload.js를 포함시켜 줍니다.
또한, 실제로 연결한 서버에 응답이 오는지 확인하여 온라인 여부를 판단하고,
온라인 상태인 경우에는, BrowserWindow객체에 있는 loadURL() 메서드를 사용하여, 해당 화면에 웹뷰를 띄워줍니다.
아니라면, 내부 오프라인 파일을 통해서, 수동 종료하거나 새로고침을 하도록 도와주는 페이지를 오픈합니다.
오프라인 인터페이스를 제공하지 않는다면, 키오스크를 종료하기 어려운 곤란한 광경에 처해집니다.
탈출 통로 개발하기
키오스크에는 탈출하는 통로가 존재해야 합니다.
그리고, 저는 반드시 고객이 키오스크를 이용하면서, 절대로 관리자페이지나 프로그램 종료를 할 수 있도록 허용하고 싶지 않았습니다.
일반적으로 키오스크 탈출 통로를 쉽게 구현하기 위해서,
특정 위치를 5번 누르거나, 터치를 어디에 동시에 해야하는지 관리자페이지가 열리거나,
고객이 접근 가능한 설정으로 만들어 두는 경우도 많습니다.
저는 키보드와 스캐너를 이용한 전략을 선택하였습니다.
키보드는 단축키를 미리 지정하여, 특정한 단축키가 감지되면, 프로그램을 종료하거나, 관리자페이지로 이동할 수 있게 합니다.
스캐너에도 특정 입력이 감지되면, 마찬가지의 행동을 수행할 수 있게 하였습니다.
- 단축키 관련 코드
/**
* 프로그램 종료 단축키
*/
globalShortcut.register("Control+Shift+Q", () => {
app.quit();
});
일렉트론에서는 globalShortcut 기능을 통해서, 프로그램에 단축키를 추가할 수 있게 제공합니다.
키오스크의 USB포트는 잠겨있으므로, 미리 사전에 무선 키보드를 연결하여, 긴급상황에 대비할 수 있습니다.
Windows 터치스크린에 대한 탈출 이슈
Windows에는 터치스크린의 지원하는 기기에 대해서
Edge Swipe 라는 기능을 제공합니다.
이는 좌측 엣지에서 우측으로 스와이프 했을 때,
뉴스가 뜬다던지,
우측 엣지에서 좌측으로 스와이프 했을 때,
알림센터가 뜬다던지,

이는 OS단에서 지원하는 기능이라서,
일렉트론의 키오스크 모드도 막을 수 없습니다.
대부분의 키오스크는 당연히 터치스크린을 사용하므로,
우리는 이를 반드시 막아야 악성 고객을 막을 수 있습니다.
- 윈도우 10
https://www.tenforums.com/tutorials/48507-enable-disable-edge-swipe-screen-windows-10-a.html
- 윈도우 11
https://www.elevenforum.com/t/enable-or-disable-screen-edge-swipe-in-windows-11.3717
윈도우 10은 설정이 꽤 복잡합니다.
윈도우 11부터는 쉽게 비활성화 할 수 있도록 제공하고 있습니다.
아무래도, 10버전에서 너무 불편하게 제공하니까, 개선사항이 꽤 있었나 봅니다.
하지만, 저희는 윈도우10을 사용하므로,
귀찮은 방법을 통해서, Edge Swipe를 비활성화 시켜야 했습니다.
리빌드
serialport 패키지를 사용하고,
빌드를 해보시면, 빌드가 실패하게 됩니다.
왜냐하면, serialport는 각 OS에 맞게 라이브러리의 리빌드가 필요하기 때문입니다.
또한, 이러한 OS특화 라이브러리를 인해서, 크로스 플랫폼 빌드가 안되고, 실제 사용하는 OS와 같은 환경에서 빌드가 진행되어야 합니다.
이를 위해서는 rebuild 개발의존성에 대한 설치가 필요합니다.
Windows환경에서 rebuild를 하기 위해서는
Python 3버전 이상
C++ buildTools 에 대한 설치가 필요합니다. (buildTools는 microsoft에서 도구를 제공합니다. 저는 2022버전을 설치하였습니다.)
.\node_modules\.bin\electron-rebuild.cmd
리빌드가 설치되면, Windows에는 해당 명령어를 통해 네이티브 라이브러리를 자동으로 탐지하고
리빌드를 수행합니다.
해당 작업 과정이 끝난 이후에, 프로그램 파일을 빌드하시면 오류가 발생하지 않게 됩니다.
마무리
그 외에도 프린터 장치도 같은 방식으로 연결하였습니다.
물론, 복잡한 내용이 출력되어야 하는 프린터는 USB통신을 통해서, Driver를 호출하는 식으로 작업하기는 하였습니다.
이는 드라이버에 대한 신뢰가 보장되어있고, 연결성이 필요없었기 때문에 가능한 선택이었고,
결정적으로는 해당 프린터가 USB만 지원하기 때문에 그런 것도 있었습니다.
이번 키오스크 프로젝트의 시간은 일주일정도 기한이 주어졌는데요,
짧은 시간동안에, 많은 경우를 경험했던 것 같아 재밌고 좋은 경험이었습니다.
답글 남기기