상용 배리어프리 키오스크 어플리케이션 개발

Category
Main Project

상용 배리어프리 키오스크 시스템

기술의 한계를 넘어 소통과 안정성을 설계하다
Electron & React 기반의 상용 배리어프리 키오스크 시스템을 처음부터 끝까지 설계하고 개발하여 복잡한 기술 해결과 실제 비즈니스 요구사항을 함께 고려하고 구현해 낸 프로젝트입니다.
 

1. 프로젝트 요약

  • Project Overview: 6개월간 호텔 및 상업 시설을 위한 배리어프리 키오스크 시스템의 상용화 버전을 개발했습니다. Electron과 React를 기반으로, 정보 소외 계층을 포함한 모든 사용자가 쉽게 이용할 수 있는 무인 자동화 솔루션을 구축하는 것을 목표로 했습니다.
  • My Role: 클라이언트 개발 (기여도 95% 이상). 아키텍처 설계, UI/UX 개발, 다중 하드웨어 연동, 빌드/배포 자동화 등 전 과정을 주도적으로 담당했습니다.
    • 단순한 FE 개발을 넘어, 사내 디자이너, 타사 백엔드, 외부 하드웨어 엔지니어 등 복잡한 이해관계자 간의 '소통 허브' 역할을 수행하며 기술과 기획, 내/외부 시스템을 잇는 최종 통합 지점을 책임졌습니다.
  • Key Outcomes: 복잡한 다중 하드웨어(결제, 스캐너, 발급기 등 4종 이상) 연동과 기능을 gRPC와 C++ 네이티브 모듈로 구현하였습니다. 또한, 모듈식 아키텍처 설계를 통해 향후 확장과 유지보수를 용이하게 할 수 있도록 구현하였습니다.
 
본 프로젝트는 국내 대형 PG사와의 보안 계약으로 인해 실제 서비스 링크 및 소스 코드 공개가 어렵습니다. 면접 과정에서 아키텍처 및 핵심 코드에 대한 상세한 설명이 가능합니다.
 

2. 기술 스택 및 선정 이유

  • Desktop Framework: Electron, Electron Builder
    • 선정 이유: 웹 기술(React)을 그대로 활용하면서도, 파일 시스템, 하드웨어 제어 등 OS 레벨의 기능이 필요했습니다. 크로스플랫폼을 지원하고 방대한 커뮤니티를 가진 Electron이 가장 적합하다고 판단했습니다.
  • Frontend: React, TypeScript, Recoil, Emotion.js
    • 선정 이유: (Recoil) Redux의 보일러플레이트 없이 atom 기반으로 여러 독립적인 상태를 직관적으로 관리할 수 있어 선택했습니다. 덕분에 상태 관리 코드의 복잡도를 낮추고 개발 속도를 높일 수 있었습니다.
  • Communication: gRPC-web, AWS IoT (MQTT)
    • 선정 이유: (gRPC) 하드웨어 제어는 실시간 양방향 스트리밍 통신이 필수적이었습니다. REST API의 요청/응답 모델로는 장비의 상태를 지속적으로 감지하기 어렵다고 판단, Protocol Buffer 기반의 고성능, 저지연 통신이 가능한 gRPC를 채택했습니다.
  • Database: SQLite (better-sqlite3)
    • 선정 이유: 각 키오스크가 독립적으로 거래 로그나 설정값을 저장해야 했기에, 별도의 서버가 필요 없는 경량 임베디드 데이터베이스인 SQLite를 선택하였습니다.
  • Native Module: C++ Addon (N-API), node-gyp
    • 선정 이유: 웹 기술만으로는 사용자의 헤드폰 연결 여부를 감지할 수 없는 명확한 한계를 극복하기 위해, OS 레벨의 오디오 장치 상태를 직접 조회할 수 있는 C++ 네이티브 모듈을 연동했습니다.
    •  

3. 아키텍처

notion image

핵심 설계 원칙

  • 프로세스 분리 및 보안: UI(Renderer)와 핵심 로직(Main) 프로세스를 명확히 분리했습니다. preload.jscontextBridge를 사용해 렌더러 프로세스가 Node.js API에 직접 접근하는 것을 막고, 허용된 IPC 채널로만 통신하도록 설계하여 보안을 강화했습니다.
  • 모듈화와 확장성: 모든 IPC 통신과 하드웨어 제어를 IpcHandler.ts와 각 기능 모듈(databaseModule, mqttModule 등)로 분리했습니다. 이를 통해 향후 새로운 하드웨어가 추가되더라도 해당 모듈만 구현하면 되도록 확장성을 확보했습니다.
 
 

4. 주요 기능

💳 다중 하드웨어 제어 (gRPC)

  • 4종 이상의 물리 장치(지폐 입출금기, 신분증 스캐너, 카드 발급기, 영수증 프린터)를 gRPC 통신으로 연동했습니다.
  • .proto 파일로 정의된 명세를 분석하여, 각 하드웨어의 비동기 이벤트 스트림을 처리하는 TypeScript 클라이언트 모듈을 직접 구현했습니다.
 

배리어프리 UX/UI (C++ Addon)

  • 기술 소외 계층의 접근성을 보장하기 위해 저시력자용 고대비 모드, 장애인용 낮은 화면 모드, 4개 국어(i18next)를 지원했습니다.
  • 특히 웹 기술의 한계(헤드폰 연결 감지 불가)를 극복하기 위해, C++로 작성된 네이티브 애드온(N-API) 모듈을 Electron 환경에 직접 연동했습니다. node-gyp을 이용한 빌드 과정을 거쳐, JavaScript 코드에서 운영체제(OS) 레벨의 오디오 장치 상태를 직접 조회하는 데 성공했습니다.
  • 이를 통해 사용자가 헤드폰을 연결하면 자동으로 개인화된 음성 안내(TTS)가 나오도록 구현하여 기술적 한계를 극복하고 사용자 경험을 극대화했습니다.
 

🔐 보안 IPC 아키텍처 설계

  • Electron의 Main 프로세스와 Renderer 프로세스를 명확히 분리하고, 보안 강화를 위해 Context IsolationcontextBridge를 활용했습니다.
  • DB 접근, 하드웨어 제어 등 모든 민감한 로직은 Main 프로세스에서만 실행되도록 격리했습니다.
  • Renderer(React)의 직접적인 시스템 접근을 원천 차단하고, 50개 이상의 Type-Safe한 IPC 채널을 설계하여 안정성과 보안성을 확보했습니다.
 

📊 데이터 관리 및 원격 통신

  • better-sqlite3를 사용하여 키오스크에서 발생하는 결제, 이용 로그 등 모든 트랜잭션을 로컬 DB에 저장하는 DAO(Data Access Object) 패턴의 모듈을 구현했습니다.
  • aws-iot-device-sdk-v2를 활용한 MQTT 클라이언트를 구현하여, 중앙 관제 시스템의 요청을 실시간으로 키오스크에게 전송하고 제어하는 기능을 개발했습니다.
 

🖥️ 무인 운영 및 유지보수 (Logging & Auto-Update)

  • 안정적 로깅 시스템: 무인 환경의 핵심인 원격 유지보수를 위해 electron-log를 도입했습니다. 모든 에러, 하드웨어 이벤트, 사용자 인터랙션을 파일로 기록하도록 설계하여, 현장 방문 없이도 신속한 원격 진단과 대응이 가능하도록 했습니다
  • 원격 자동 업데이트: electron-updaterGitHub Releases와 연동하여 원격 자동 업데이트 기능을 구현했습니다. 이를 통해 전국에 배포된 다수의 키오스크에 물리적 방문 없이도 신규 기능을 배포하고 치명적인 버그를 패치할 수 있는 운영 파이프라인을 확보했습니다.
 

4. 핵심 경험 및 문제 해결 과정

1. Stackflow 라이브러리 성능 저하 및 HOC를 통한 최적화

  • 문제 상황 모바일 앱처럼 자연스러운 화면 전환 UX를 위해 오픈소스 라이브러리 Stackflow를 도입했습니다. 하지만 사용자가 여러 페이지를 오가며 스택(Stack)이 5~6개 이상 쌓이자, 화면 전환이 눈에 띄게 버벅이고 UI 반응성이 저하되는 성능 문제가 발생했습니다. 특히 저사양 키오스크 하드웨어에서 문제가 심각했으며, 비활성 화면의 음성 안내(TTS)가 중복으로 재생되는 치명적인 버그도 함께 발견되었습니다.
  • 원인 분석: 라이브러리 내부 동작을 분석한 결과, Stackflow가 이전 Activity(화면)를 메모리에서 제거(unmount)하는 것이 아니라, DOM에서 숨기고 상태를 그대로 유지하는 방식임을 확인했습니다. 이로 인해 비활성 상태인 여러 화면이 백그라운드에서 불필요한 로직(useEffectsetInterval, TTS API 호출)을 계속 실행하며 리소스를 점유하고 있었습니다.
  • 해결 과정
    • 설계: 화면이 isActive: false 상태일 때는 렌더링 자체를 중단시켜 리소스 점유를 막는 것이 가장 효율적이라 판단했습니다. 이 로직을 재사용 가능하게 구현하기 위해 고차 컴포넌트(HOC)인 withActiveGuard를 직접 설계했습니다.
    • 구현: withActiveGuard HOC는 Stackflow의 useActivity 훅을 사용하여 현재 화면의 활성 상태(isActive)를 실시간으로 체크합니다. isActivefalse이고 transitionStateexit-active(사라지는 애니메이션 중)가 아니면 null을 반환하여 렌더링을 중단시킵니다.
    • 적용: 프로젝트의 모든 Activity(페이지 컴포넌트)를 withActiveGuard로 감싸도록 stackflow.tsactivities 객체를 수정하여 일괄 적용했습니다.
       
      import React from 'react'; import { ActivityComponentType, useActivity } from '@stackflow/react'; /** * - 전달된 Activity 컴포넌트를 감싸서, * - Stackflow 상에서 isActive=false(가려진 상태)면 null을 리턴(렌더링X)하여 * - 중복 음성 안내/로직 실행을 막음 */ export function withActiveGuard<T extends object>(WrappedActivity: ActivityComponentType<T>): ActivityComponentType<T> { const HOC: ActivityComponentType<T> = (props) => { // (1) Activity 정보 획득 const activity = useActivity(); // (2) 비활성이면(null 리턴) // exit-active 상태일 때도 컴포넌트 렌더링하여 애니메이션이 보이도록 if (!activity.isActive && activity.transitionState !== 'exit-active') { return null; } // (3) 활성 상태면 원본 컴포넌트 렌더 return <WrappedActivity {...props} />; }; // 디버깅용 displayName HOC.displayName = `withActiveGuard(${WrappedActivity.displayName || WrappedActivity.name || 'AnonymousActivity'})`; return HOC; }
 
  • 결과 비활성 화면의 불필요한 렌더링과 로직 실행을 원천 차단하여, 화면 스택이 10개 이상 쌓여도 일정한 UI 반응 속도를 확보했습니다. 가장 치명적이었던 백그라운드 음성 안내 중복 재생 버그를 근본적으로 해결했습니다. 오픈소스 라이브러리를 도입할 때, 해당 라이브러리의 성능 특성(특히 메모리 관리 방식)을 면밀히 검토하고 프로젝트 환경에 맞게 최적화하는 아키텍처 설계의 중요성을 깨달았습니다.
 

2. 공유 자원(프린터) 동시 접근으로 인한 시스템 충돌

  • 문제 상황 키오스크 운영 중, 관리자 페이지의 '프린터 상태 확인' 요청과 사용자의 '영수증 출력' 요청이 동시에 영수증 프린터라는 단일 하드웨어 자원에 접근을 시도할 때가 있었습니다. 이 경우, 명령어가 꼬이거나(e.g., 상태 확인 중 인쇄 명령 전송) 응답을 처리하던 중 연결이 끊겨 시스템이 멈추는 문제가 발생했습니다.
  • 해결 과정
    • 설계: 단 하나의 작업만 프린터에 접근할 수 있도록 보장하는 Mutex(상호 배제) 개념을 도입했습니다. printerLock.ts라는 락(Lock) 관리 모듈을 구현하여, 프린터 사용이 필요한 모든 로직(인쇄, 상태 확인 등)이 이 락을 통하도록 설계했습니다.
    • 구현
        1. acquire() (획득) 함수: 작업이 락을 요청할 때, 프린터가 이미 사용 중(isLocked = true)이면 Promise를 반환하고 대기 큐(queue)에 작업을 등록합니다. 사용 중이 아니면 즉시 락을 획득하고 작업을 실행합니다.
        1. release() (해제) 함수: 작업이 완료되면 락을 해제합니다. 이때, 대기 큐에 다음 작업이 있다면 큐에서 작업을 꺼내어 실행시키고, 락은 계속 유지합니다. 큐가 비어있다면 isLockedfalse로 변경하여 다른 작업이 락을 획득할 수 있도록 합니다.
      적용: 프린터를 사용하는 모든 IPC 핸들러 로직을 try...finally 구문으로 감싸고, await printerLock.acquire()로 락을 획득한 뒤 finally 블록에서 printerLock.release()가 반드시 호출되도록 하여 안정성을 확보했습니다.
      /** * 프린터 잠금 기능 제공하는 클래스 * - 프린터가 현재 사용 중인지(isLocked) 상태 관리 * - 다른 작업(인쇄, 버퍼 테스트 등)이 동시에 접근하지 못하도록 큐에 대기 */ class PrinterLock { private isLocked: boolean; // 프린터가 사용 중인지 아닌지 나타내는 변수 private queue: Array<() => void>; // 대기 중인 작업들 저장할 배열 constructor() { this.isLocked = false; this.queue = []; } /** * 프린터 잠금 */ public async acquire(): Promise<void> { // 프린터가 사용 중이 아닐 때만 if (!this.isLocked) { this.isLocked = true; // 프린터 잠금 (문을 잠근 것처럼) return Promise.resolve(); // 즉시 작업을 실행 } // 프린터가 이미 사용 중이라면 // 새 Promise를 만들어, 나중에 프린트 사용가능해지면 resolve return new Promise((resolve) => { this.queue.push(resolve); // 대기 목록에 추가 }); } /** * 프린터 잠금 해제 (일을 마치면 호출) * - 큐에 대기 중인 작업이 있으면, 그 작업 꺼내어(resolve) 실행 * - 더 이상 대기 중인 작업이 없으면 isLocked=false로 풀기 */ public release(): void { if (this.queue.length > 0) { const next = this.queue.shift(); // 대기열 첫 번째 작업 실행 if (next) next(); } else { this.isLocked = false; // 대기 중인 작업이 없으면 잠금 해제 } } } /** * 싱글톤(Singleton)으로 생성하여 애플리케이션 전역에서 단 하나의 락 인스턴스만 사용 */ export const printerLock = new PrinterLock();
 
  • 결과 공유 자원(프린터)에 대한 동시 접근을 원천 차단하여, 어떤 상황에서도 명령어 충돌이나 시스템 멈춤 현상 없이 안정적으로 프린터가 동작하도록 보장할 수 있었습니다. 이 경험을 통해 데이터 무결성을 확보하는 등, 시스템 전반의 안정성을 높이는 아키텍처 패턴을 경험할 수 있었습니다
 

3. 불안정한 네트워크 환경에서 AWS IoT(MQTT) 연결 유실 및 '좀비 상태' 해결

  • 문제 상황 키오스크가 설치된 환경은 24시간 계속 돌아가서 Wi-Fi 불안정, 오류등으로 MQTT 연결이 종종 중단되었습니다. SDK의 자동 재연결(reconnect) 기능으로 연결은 복구되었으나, 이후 중앙 관제 서버의 원격 명령(e.g., factory-reset, force-key-eject)을 전혀 수신하지 못하는 좀비 상태가 되는 치명적인 문제가 발생했습니다.
 
  • 해결 과정
  • 현상 너머의 근본 원인 분석: 원인 파악을 위해, 구축해둔 로깅 시스템(electron-log)으로 데이터를 분석한 결과, 네트워크 단절 후 '자동 재연결'은 성공했으나 정작 필수 토픽 '구독(Subscribe)'이 누락됨을 발견했습니다.
    • 더 나아가, MQTT 통신을 담당하는 메인 프로세스 코드의, connect 메서드의 연결 설정 분석을 진행했습니다. 핵심은 .with_client_id(${thingName}-${crypto.randomUUID()})를 통해 연결 시마다 고의적으로 새로운 랜덤 Client ID를 생성하도록 설계한 부분에 있었습니다.
    • 설계 의도: 만약 모든 키오스크가 동일한 Client ID(e.g., 'kiosk-001')를 사용하도록 설정하면, 한 호텔에서 다른 호텔로 장비가 이동/설치될 때 ID가 충돌하여 서로의 연결을 강제로 끊는 심각한 운영 장애가 발생할 수 있습니다. 랜덤 UUID를 사용함으로써 이 Client ID 충돌 가능성을 원천적으로 차단하고, 모든 연결이 항상 고유함을 보장하도록 한 것이었습니다.
    • 하지만 이 설계는 MQTT의 '영구 세션' 기능을 의도적으로 포기하는 트레이드오프를 가졌습니다. '영구 세션'(브로커가 구독 상태를 기억하는 기능)은 반드시 이전에 연결했던 것과 동일한 Client ID로 재연결해야만 동작하기 때문입니다.
    • 결론: '좀비 상태'는 버그가 아니라, 랜덤 ID 아키텍처 하에서 필연적으로 발생하는 문제였습니다. 재연결 시마다 키오스크는 '완전히 새로운 클라이언트'로 인식되었기에, SDK의 자동 재연결 기능만으로는 구독 상태를 복구할 수 없었습니다.
 
  • 자가 복구 아키텍처 구현
    • '영구 세션'의 근본적 한계: 설령 고정 ID로 설계를 변경하더라도, 24시간 이상 장기 오프라인(e.g., 정전) 시 브로커의 세션이 만료되면 동일한 버그가 재발할 수 있었습니다. 브로커의 불확실한 '기억력'에 의존하는 것은 상용 장비에 부적합하다고 판단했습니다. 이에 "클라이언트가 100% 자신의 상태를 책임진다"는 더 견고한 원칙을 세우고, 다음과 같이 '자가 치유' 로직을 구현했습니다.
    •  
    • 구현
      • 현 아키텍처에 맞게 .with_clean_session(true) 옵션을 명시적으로 설정했습니다.
      • SDK의 핵심 이벤트인 connection.on('resume', ...) 리스너를 구현했습니다.
      • resume 이벤트(재연결 성공 시 발생)의 sessionPresent 플래그가 false(복원할 세션 없음)일 때, 직접 구현한 await this.resubscribeAllTopics() 함수를 즉시 호출하도록 했습니다. 이 함수는 모든 필수 토픽을 처음 연결처럼 다시 구독(Subscribe)합니다.
  • 결과 이를 통해 네트워크가 일시적으로 중단되었다가 복구될 때마다, 키오스크가 능동적으로 모든 구독 상태를 스스로 복구하는 '자가 치유' MQTT 클라이언트를 구현할 수 있었습니다. '좀비 상태'의 발생 가능성을 원천적으로 제거하여, 상용 무인 장비에 필수적인 원격 제어 신뢰도와 시스템 안정성을 확보했습니다. MQTT 프로토콜 스펙과 SDK의 동작을 이해하고, Client ID 충돌 방지를 위한 초기 아키텍처(랜덤 ID)의 제약 사항 속에서, 이를 해결하기 위한 솔루션을 직접 설계하는 경험을 할 수 있었습니다
 

5. 프로젝트 성과

  • 정량적 성과
    • 개발 생산성 향상: 관리자 페이지 내 하드웨어 테스트 기능을 구현하여, QA 과정에서 이슈 원인 파악 시간을 40% 단축하고 통합 테스트 효율을 향상시켰습니다.
    • 핵심 기능 모듈 개발: 결제, 하드웨어 연동, 다국어 처리 등 10개 이상의 핵심 모듈을 독립적으로 개발 완료했습니다.
    • 안정적인 통신 설계: 50개 이상의 IPC 엔드포인트를 설계하고 구현하여 안정적인 데이터 통신 기반을 마련했습니다.
  • 정성적 성과
    • 사회적 가치 실현: 배리어프리 기능(음성안내, 저시력자 모드 등) 구현을 통해 정보 소외 계층의 서비스 접근성을 보장하고, 기업의 사회적 책임 강화에 기여했습니다.
    • 비즈니스 경쟁력 확보: 키오스크 자동화 기능 완성을 통해 호텔 프론트 데스크의 운영 비용을 절감하고 인력 효율화를 가능하게 하여, 공공기관 및 대형 호텔 납품 시 경쟁 우위를 확보하는 데 기여했습니다.
    •  

6. 회고 및 배운 점

  • 기술적 성장
    • 이번 프로젝트를 통해 단순히 눈에 보이는 UI를 만드는 개발자를 넘어, 눈에 보이지 않는 영역의 안정성, 보안, 성능까지 책임지는 시스템 아키텍처를 설계하는 엔지니어의 관점을 갖게 되었습니다. 웹 기술의 한계를 C++ 네이티브 모듈 연동으로 스스로 극복하며, 낯선 기술적 과제를 마주했을 때 집요하게 파고들어 결국 해결해내는 엔지니어로서의 끈기와 태도를 배울 수 있었습니다.
  • 협업과 책임감
    • 인턴 신분이었지만 프로젝트의 성공을 제 일처럼 여기고, 기술과 기획을 잇는 '허브'로서 책임을 다했습니다. 디자이너의 비전을 기술로 뒷받침하고, 타사와의 소통을 주도하며 공동의 목표를 달성하는 과정에서 가장 큰 보람을 느꼈습니다. 좋은 팀워크란 단순히 내 일을 잘하는 것을 넘어, 동료의 언어를 이해하고 공동의 가치를 위해 기술의 경계를 넘나들며 적극적으로 소통하는 태도에서 비롯됨을 배웠습니다.
  • 아쉬운 점 및 개선 방향
    • 프로젝트 초기, 빠른 프로토타이핑을 위해 단위 테스트 코드 작성을 미루었던 점이 아쉬움으로 남습니다. 이로 인해 후반부 기능 추가 시, 사이드 이펙트를 검증하기 위한 수동 테스트에 많은 시간을 소모했습니다. 만약 다시 프로젝트를 진행한다면, 개발 초기부터 Jest와 같은 테스트 프레임워크를 도입하여 테스트 커버리지를 확보하고, 더욱 견고하고 안정적인 시스템을 만들고 싶습니다.