Notice
Recent Posts
Recent Comments
반응형
관리 메뉴

HM소프트

Electron 키오스크 모드와 웹 데모 배포 — 현장 키오스크 개발기 (4) 본문

외주 개발일지

Electron 키오스크 모드와 웹 데모 배포 — 현장 키오스크 개발기 (4)

HM소프트 2026. 6. 11. 17:00
반응형

Electron 키오스크 모드(전체화면·close 이벤트 차단·Ctrl+Shift+Q 탈출구·setLoginItemSettings 자동실행)와, 같은 코드를 mock electronAPI로 브라우저 웹 데모로 배포한 기록. isDev 분기, SQL 문자열 흉내 mock, transform scale 반응형, Vite base 경로, .nojekyll 함정까지 다룬다.

같은 코드를 데스크톱 앱과 브라우저, 두 곳에서 돌리기

시리즈의 마지막 편이다. 앞선 세 편에서 렌더러와 메인 사이의 IPC 경계, Konva 기반 디자인 편집기, 그리고 실제 하드웨어를 흉내 내는 스텁을 차례로 다뤘다. 이번 편의 주제는 두 가지다. 하나는 현장에 놓인 기기를 "앱에서 빠져나갈 수 없는 상태"로 가두는 키오스크 모드, 다른 하나는 똑같은 코드베이스를 Electron 없이 브라우저에서 도는 웹 데모로 배포한 이야기다.

이 두 작업은 겉보기에 정반대다. 키오스크 모드는 기기를 닫아거는 일이고, 웹 데모는 아무 브라우저에서나 열리게 푸는 일이다. 그런데 막상 구현해 놓고 보니 둘은 같은 뿌리에서 나온 결정이었다. "플랫폼에 따라 달라지는 것을 한 군데로 몰아 둔다"는 원칙이다. 키오스크 모드는 그 차이를 메인 프로세스의 창 설정 한 곳에, 웹 데모는 렌더러가 바라보는 API 객체 한 겹에 몰아넣었다. 이 글은 그 "한 곳"이 어디였고 왜 거기여야 했는지에 대한 기록이다.

커밋 로그를 보면 이 두 작업이 정확히 두 발자국으로 찍혀 있다. feat: 키오스크 모드 (전체화면, 키 차단, 자동시작), 그리고 feat: GitHub Pages 웹 데모 배포. 그 뒤에 fix: .nojekyll 추가라는 작은 커밋 하나가 붙어 있는데, 이 한 줄짜리 수정이 데모를 살린 이야기는 뒤에서 따로 풀겠다.

개발이냐 현장이냐 — isDev 하나로 두 모드를 가르다

먼저 짚어 둘 게 있다. 키오스크 모드의 가장 큰 적은 의외로 "개발자 자신"이다. 창을 닫을 수 없고, 새로고침도 막히고, 부팅하면 자동으로 떠 버리는 모드를 개발 중에 그대로 켜 두면 코드를 짤 수가 없다. 그래서 모든 키오스크 동작은 개발 환경에서는 전부 꺼지고, 프로덕션 빌드에서만 켜지도록 단일 플래그로 묶었다.

frame: isDev라는 표현이 처음엔 거꾸로 보일 수 있다. 개발일 때만 true, 즉 테두리를 보여 준다는 뜻이다. 현장 기기에서는 frame: false가 되어 창의 닫기 버튼·최소화 버튼·제목 표시줄이 통째로 사라진다. 같은 논리로 fullscreenkiosk!isDev라 현장에서만 켜진다. Electron의 kiosk: true는 단순 전체화면을 넘어, OS의 작업 표시줄이나 다른 창이 위로 올라오지 못하게 화면을 점유하는 모드다.

여기서 한 가지 의식적으로 한 결정이 있다. 개발 모드는 loadURL로 Vite 개발 서버(http://localhost:5173)를 띄우고 개발자 도구까지 열지만, 현장 빌드는 loadFile빌드된 정적 HTML을 직접 로드한다. 즉 현장 기기는 어떤 네트워크 서버도 거치지 않고 디스크의 파일을 연다. 이게 중요한 이유는, 매장에 놓인 기기가 인터넷이 잠깐 끊겨도 화면 자체는 멀쩡히 떠야 하기 때문이다. 렌더러는 로컬 파일이고, 데이터는 뒤에서 SQLite가 들고 있으니, 외부 의존이 없다.

isDev 한 줄이 결국 "개발자가 쓰는 일반 데스크톱 앱"과 "현장 직원조차 빠져나갈 수 없는 키오스크"라는 전혀 다른 두 인격을 하나의 코드에 공존시킨다. 분기를 여러 곳에 흩뿌리지 않고 창 생성 시점에 몰아 둔 게 핵심이다. 나중에 "전체화면만 켜고 키오스크 점유는 빼자" 같은 미세 조정이 필요해도, 손댈 곳이 이 객체 리터럴 하나뿐이다.

기기를 가두는 세 가지 장치

현장 키오스크에서 "갇힌다"는 건 구체적으로 세 가지를 막는다는 뜻이다. 창을 닫지 못하게, 앱이 종료돼도 다시 떠오르게, 그리고 관리자만 정상적으로 빠져나갈 수 있게.

1) 닫기 자체를 거부한다

테두리를 없앴으니 닫기 버튼은 사라졌다. 하지만 Alt+F4나 OS 차원의 종료 신호는 여전히 창을 닫으려 든다. 그래서 창의 close 이벤트 자체를 가로채 막았다.

이 코드의 골자는 두 줄의 긴장 관계다. close 이벤트에서 e.preventDefault()를 호출하면 그 어떤 경로로 들어온 닫기 요청도 전부 무시된다. 사용자가 어떻게든 종료 신호를 보내도 창은 살아남는다. 그런데 이렇게 완전히 닫아거는 순간, 정작 관리자가 기기를 정상 종료할 방법도 함께 사라진다. 전원 코드를 뽑는 것 말고는.

그래서 destroy()로 빠져나가는 비상구를 딱 하나 뚫어 뒀다. Ctrl+Shift+Q는 일반 손님이 우연히 누를 일이 거의 없는 조합이고, 관리자만 알고 있으면 된다. 여기서 mainWindow.close()가 아니라 mainWindow.destroy()를 쓴 게 포인트다. close()는 위에서 막아 둔 close 이벤트를 다시 발생시켜 또 preventDefault에 걸린다 — 즉 자기가 친 그물에 자기가 걸린다. destroy()는 그 이벤트를 거치지 않고 창을 강제로 파괴한다. 닫기를 막아 두면 정상 종료 경로까지 막힌다는 이 함정은, 직접 밟아 보기 전엔 잘 안 보인다.

globalShortcut을 쓴 것도 의도가 있다. 렌더러 안에서 키 이벤트를 듣는 방식은 포커스가 어디 가 있느냐에 따라 안 먹을 수 있는데, globalShortcut은 앱이 떠 있는 한 OS 레벨에서 단축키를 가로채므로 화면 어느 상태에서든 동작한다.

2) 꺼져도 다시 떠오른다

매장 기기는 정전이 나거나, 누군가 실수로 전원을 건드리거나, 야간 점검으로 재부팅되는 일이 흔하다. 그때마다 사람이 가서 앱을 다시 켜 줄 수는 없다. 그래서 OS가 부팅돼 로그인되면 앱이 자동으로 실행되도록 로그인 항목에 자기 자신을 등록했다.

app.whenReady().then(() => {
  createWindow();

  // 현장 빌드에서만 OS 로그인 시 자동 실행 등록
  if (!isDev) {
    app.setLoginItemSettings({
      openAtLogin: true,
      path: app.getPath('exe'),
    });
  }
});

setLoginItemSettings는 Electron이 OS의 자동 실행 설정을 추상화해 주는 API다. openAtLogin: true로 등록해 두면, 다음 부팅부터 사용자가 아무것도 하지 않아도 키오스크 앱이 알아서 전면에 떠오른다. path로 실행 파일 경로(app.getPath('exe'))를 명시한 건, 설치 위치가 달라져도 정확한 실행 파일을 가리키게 하기 위해서다.

이걸 app.whenReady() 안에서, 그것도 !isDev 조건으로 감싼 게 중요하다. 개발 중에 이게 켜지면 내 노트북이 부팅될 때마다 키오스크 앱이 전체화면으로 떠 버린다. 한 번 그렇게 당해 본 뒤로는 이 가드를 절대 빼지 않는다.

3) 빌드 설정까지 한 모드로

소프트웨어 쪽에서 테두리를 없애고 자동 실행을 걸었으니, 배포 패키지 설정도 같은 결을 맞춰야 한다. 설치 파일을 만드는 빌드 설정은 별도 파일로 분리해 뒀다.

appId: com.example.fieldkiosk.app
productName: 현장키오스크
directories:
  output: release
  buildResources: resources
files:
  - dist/**/*
  - node_modules/**/*
win:
  target: nsis
nsis:
  oneClick: false
  allowToChangeInstallationDirectory: true

win.target: nsis로 Windows 설치 관리자를 만들고, oneClick: falseallowToChangeInstallationDirectory: true설치 경로를 고를 수 있는 일반 설치 마법사 형태를 택했다. 키오스크라고 해서 원클릭 자동 설치가 좋은 건 아니다. 현장 PC마다 디스크 구성이 다르고, 설치 위치를 지정해 두면 나중에 원격으로 점검할 때 경로를 예측할 수 있어 운영이 편하다.

작아 보이지만 이 세 장치 — 닫힘 차단, 자동 실행, 설치 패키지 — 가 모여야 "어떻게든 바탕화면이 보이면 안 된다"는 현장의 요구가 비로소 충족된다. 키오스크 완성도는 화려한 기능이 아니라 이런 빈틈을 메우는 데서 갈린다.

웹 데모의 출발점 — 경계를 일찍 그어 둔 보답

여기서부터가 이번 편에서 가장 흥미로웠던 부분이다. 이 앱은 Electron과 하드웨어, 그리고 로컬 SQLite에 단단히 묶여 있다. 그런데 영업·소개 자리에서는 "노트북에 설치 파일을 깔고 프린터를 연결"하는 게 현실적이지 않다. 필요한 건 링크 하나만 누르면 아무 브라우저에서나 바로 도는 데모였다.

문제는 명확했다. 브라우저에는 Electron이 없다. window.electronAPI가 없고, IPC도 없고, better-sqlite3도 돌지 않는다. 화면을 그리는 React 코드는 전부 이 API에 의존하고 있는데, 그 토대가 통째로 사라진 환경에서 어떻게 같은 화면을 띄울 것인가.

답은 1편에서 그어 둔 경계에 이미 있었다. 렌더러는 데이터베이스든 하드웨어든 결제든, 메인 프로세스가 하는 모든 일을 오직 window.electronAPI라는 한 겹을 통해서만 호출하도록 설계돼 있었다. 화면 코드 어디에도 ipcRendererbetter-sqlite3가 직접 등장하지 않는다. 이 단일 통로가 preload의 contextBridge로 노출된다.

렌더러 입장에서 electronAPI는 그저 "약속된 모양의 객체"다. 어떤 함수들이 있고, 각각 무엇을 받아 무엇을 돌려주는지가 타입으로 못 박혀 있다. 그 약속만 지키면, 안쪽이 진짜 IPC든 아니든 화면은 알 바가 아니다. 즉 브라우저용으로는 이 한 겹만 통째로 갈아끼우면 된다. 경계를 일찍 그어 둔 게, 마지막 편에 와서 이렇게 보답했다.

window.electronAPI가 없으면 가짜를 끼운다

전환의 스위치는 단 한 군데, 앱이 부팅되는 진입점에 뒀다. 렌더러가 마운트되기 직전에 window.electronAPI가 있는지 보고, 없으면 — 즉 Electron이 아니면 — mock 구현을 동적으로 불러와 그 자리에 꽂아 넣는다.

이 작은 분기에 몇 가지 결정이 압축돼 있다. 첫째, 판별 기준을 window.electronAPI의 존재 여부로 삼았다. 실제 환경에서는 preload가 렌더러보다 먼저 실행돼 이 객체를 심어 두므로, 화면이 뜰 때 이미 존재한다. 브라우저에는 preload가 없으니 당연히 비어 있다. 별도의 "지금 Electron인가" 플래그를 만들 필요 없이, 통로의 존재 자체가 곧 환경 판별이 된다.

둘째, mock을 정적 import가 아니라 동적 import() 불러왔다. mock 구현은 브라우저에서만 필요한 코드다. 실제 키오스크 빌드에 이 가짜 데이터와 SQL 흉내 로직이 섞여 들어가는 건 군더더기다. 동적 import로 두면 번들러가 이걸 별도 청크로 떼어 내고, mock이 실제로 필요한 브라우저 환경에서만 네트워크로 받아 온다. 키오스크 빌드는 이 청크를 아예 부르지 않는다.

셋째, mock 주입이 끝난 뒤에야 renderApp()을 호출한다. 동적 import는 비동기다. 만약 import가 끝나기 전에 React를 렌더링하면, 화면이 마운트되는 순간 window.electronAPI가 아직 비어 있어 첫 데이터 호출이 터진다. 그래서 양쪽 분기 모두 "API가 확실히 준비된 다음"에만 렌더 함수를 부르도록 순서를 묶었다. 동기 경로(Electron)와 비동기 경로(브라우저)가 결국 같은 renderApp 한 점에서 만난다.

렌더러가 API를 꺼내 쓰는 쪽은 더 단순하다. 그냥 window.electronAPI를 돌려주는 한 줄짜리 훅이 전부다.

export function useIpc() {
  const api = window.electronAPI;
  return api;
}

화면 컴포넌트는 useIpc().dbQuery(...)를 호출할 뿐, 그게 IPC를 타고 SQLite로 가는지 메모리 객체를 뒤지는지 전혀 모른다. 3편에서 하드웨어를 스텁으로 가린 발상이, 이번엔 앱 경계 전체로 확장된 셈이다.

가짜 데이터베이스 — SQL 문자열을 흉내 내다

mock을 만들 때 가장 고민한 지점은 "어느 수준까지 진짜처럼 만들 것인가"였다. 가장 쉬운 길은 함수마다 그냥 그럴듯한 값을 반환하게 하는 것이다. 하지만 그러면 실제 코드와 mock의 인터페이스가 어긋나기 쉽다. 실제 dbQuerySQL 문자열과 파라미터 배열을 받는데, mock이 다른 모양을 받으면 화면 코드가 환경에 따라 갈라져야 한다. 그건 경계를 한 겹으로 유지한 의미를 깨뜨린다.

그래서 mock의 dbQuery/dbExecute실제와 똑같이 SQL 문자열을 받게 하고, 그 문자열을 대충 파싱해 메모리 객체로 응답하도록 만들었다. 진짜 SQL 파서는 아니고, 문장에 어떤 키워드가 들어 있는지를 보고 분기하는 식이다.

이 방식의 묘미는, 화면이 실제로 던지는 SQL을 그대로 받아서 처리한다는 점이다. 관리자 대시보드가 매출 통계를 위해 SUM(amount)가 들어간 쿼리를 날리면, mock은 그 키워드를 보고 메모리의 주문 배열을 집계해 똑같은 모양({ sales, cnt })으로 돌려준다. 주문을 등록하면 lastInsertRowid까지 흉내 내, 실제 SQLite가 돌려주는 응답 형태와 맞춘다. 덕분에 화면 코드는 단 한 줄도 분기 없이, 브라우저에서도 "주문이 쌓이고 매출 숫자가 오르는" 동작을 그대로 보여 준다.

물론 한계도 분명하다. 이건 진짜 SQL 엔진이 아니라 키워드 매칭이라, 화면이 새로운 형태의 쿼리를 던지면 mock에 분기를 한 줄 더 추가해 줘야 한다. 하지만 데모의 목적은 "실제 업무 흐름을 브라우저에서 보여 주는 것"이지 임의의 SQL을 처리하는 게 아니다. 화면이 실제로 쓰는 쿼리 패턴은 손에 꼽고, 그만큼만 흉내 내면 충분하다. 미래의 모든 쿼리를 미리 지원하려 들지 않는 게 오히려 이 mock을 가볍게 유지한다.

하드웨어 쪽 mock은 한술 더 떠서 시간 지연까지 흉내 냈다. 결제나 인쇄는 실제로는 몇 초가 걸리는 동작이다. 데모에서 이게 즉시 끝나 버리면 "결제 중…" 같은 진행 화면이 한순간에 지나가, 정작 보여 주고 싶은 UI를 못 보여 준다.

requestPayment: async (_amount: number) => {
  await new Promise(r => setTimeout(r, 3000));   // 결제 단말 대기 흉내
  return {
    success: true,
    transactionId: `TX${Date.now()}`,
    approvalNo: `AP${Math.floor(Math.random() * 900000 + 100000)}`,
  };
},

printImage: async (_imageDataUrl: string) => {
  await new Promise(r => setTimeout(r, 3000));   // 인쇄 시간 흉내
  return { success: true };
},

setTimeoutPromise로 감싸 3초를 기다리게 한 건, 데모를 보는 사람이 진행 상태 화면과 완료 화면의 전환을 또렷이 보게 하기 위한 연출이다. 거래 번호와 승인 번호도 Date.now()와 난수로 매번 그럴듯하게 새로 만들어, 같은 데모를 반복해도 "방금 막 결제된 것 같은" 신선함을 준다. 가짜라는 걸 알면서도, 흐름의 리듬만큼은 실제와 똑같게 맞춘 것이다.

화면 크기가 제각각인 브라우저에 키오스크를 욱여넣기

또 하나의 현실적 문제는 화면 크기였다. 키오스크 UI는 현장 기기의 고정 해상도(세로형 대형 화면) 를 전제로 디자인됐다. 창 생성 코드에서 본 1920×1080이 그 기준이다. 그런데 데모를 보는 사람의 노트북·태블릿·휴대폰은 가로세로 비율도 크기도 천차만별이다. 고정 좌표로 짠 키오스크 화면을 작은 브라우저 창에 그대로 그리면 레이아웃이 깨지거나 일부가 잘려 나간다.

이걸 반응형 CSS로 일일이 다시 짜는 건 비현실적이다. 화면이 수십 개고, 각각이 절대 좌표 기반 캔버스를 품고 있다. 그래서 택한 방법은 정반대였다. 키오스크 캔버스를 원래 크기 그대로 그리되, 그 덩어리 전체를 비율을 유지하며 통째로 축소(또는 확대)하는 것이다. 내용은 손대지 않고, 바깥에서 줌만 거는 셈이다.

const KIOSK_WIDTH = 1080;
const KIOSK_HEIGHT = 1920;

function fitToViewport(stage: HTMLElement) {
  // 가로·세로 중 더 빡빡한 비율에 맞춰, 화면을 넘지 않게
  const scale = Math.min(
    window.innerWidth / KIOSK_WIDTH,
    window.innerHeight / KIOSK_HEIGHT,
  );
  stage.style.transform = `scale(${scale})`;
  stage.style.transformOrigin = 'top left';
}

핵심은 Math.min이다. 가로 비율(innerWidth / KIOSK_WIDTH)과 세로 비율 중 더 작은 값을 택한다. 더 큰 쪽을 택하면 한 축은 꽉 차지만 다른 축이 화면을 넘쳐 잘린다. 더 작은 쪽을 택해야 양쪽 모두 화면 안에 들어온다. 이렇게 하면 어떤 브라우저 크기에서도 키오스크가 "비율 그대로 줄어든 모형"처럼 보인다. 가로로 넓은 화면에서는 좌우에 여백이, 세로로 긴 화면에서는 위아래에 여백이 생기지만, 키오스크의 생김새 자체는 절대 일그러지지 않는다.

transform: scale()을 쓴 것도 의도가 있다. 이건 레이아웃을 다시 계산하지 않고 GPU가 합성 단계에서 처리하는 변형이라, 내부 수백 개 요소의 위치를 건드리지 않고도 전체를 한 번에 줄인다. 키오스크 내부 코드는 자기가 축소돼 보이는 줄도 모른다. 여기서도 똑같은 원칙이다 — 플랫폼 차이(화면 크기)를 안쪽으로 흘려보내지 않고, 가장 바깥 껍데기 한 곳에서 흡수한다.

빌드 경로의 두 얼굴 — 환경변수로 가르는 base

데모를 정적 호스팅에 올리려면 빌드 산출물이 가리키는 경로 문제를 먼저 풀어야 한다. Electron에서는 렌더러를 loadFile로 로컬 파일에서 여니까, 에셋 경로가 현재 파일 기준 상대 경로(./)면 된다. 그런데 정적 호스팅은 보통 도메인/저장소이름/ 같은 하위 경로에 사이트가 놓인다. 이때 에셋을 루트(/) 기준 절대 경로로 찾으면 전부 404가 난다.

Vite의 base 옵션이 이 경로의 접두사를 정한다. 그래서 빌드 환경에 따라 base를 갈랐다.

GITHUB_PAGES 환경변수가 켜진 채로 빌드하면 base가 저장소 하위 경로가 되어, 모든 에셋이 그 접두사를 달고 출력된다. 평소(Electron용) 빌드에서는 ./ 상대 경로라 로컬 파일에서도 잘 열린다. 한 빌드 설정으로 두 배포 대상을 모두 만족시키는 셈이다. 데모 정적 사이트는 별도 출력 폴더에 모아 정적 호스팅이 그 폴더를 그대로 서빙하게 했다.

한 글자짜리 빈 파일이 데모를 살린 사연

여기까지 해 놓고 정적 호스팅에 올렸는데, 사이트가 새하얗게 떴다. 콘솔을 열어 보니 빌드 산출물 일부가 404. 분명 빌드는 됐고 파일도 올라갔는데, 브라우저가 그 파일을 못 찾고 있었다.

원인은 호스팅 플랫폼의 기본 동작이었다. 많은 정적 호스팅(특히 GitHub Pages)은 올라온 파일을 그대로 서빙하는 게 아니라, 기본적으로 Jekyll이라는 정적 사이트 생성기로 한 번 처리한다. 그리고 Jekyll에는 "밑줄(_)로 시작하는 파일과 폴더는 내부용으로 간주해 무시한다"는 규칙이 있다. 하필 빌드 도구가 만든 에셋 폴더 이름이 밑줄로 시작했다. 빌드 산출물은 멀쩡히 올라갔지만, Jekyll이 그걸 "내부 파일"로 보고 빼 버려 외부에서 접근하면 404가 난 것이다.

해결은 허무할 만큼 간단했다. 루트에 .nojekyll이라는 빈 파일 하나를 놓는 것. 이 파일이 있으면 호스팅이 Jekyll 처리를 통째로 건너뛰고, 올라온 파일을 글자 그대로 서빙한다. 커밋 로그에 fix: .nojekyll 추가로 Jekyll 빌드 비활성화가 따로 한 줄 찍혀 있는 이유다. 내용이 한 글자도 없는 파일이지만, 이게 없으면 데모 전체가 백지로 뜬다.

이 버그가 남긴 교훈은 도구가 아니라 태도 쪽이다. 빌드가 성공하고 배포가 끝났다고 해서, 그게 서빙된다는 보장은 아니다. 내 손을 떠난 뒤 호스팅 플랫폼이 파일에 무슨 짓을 하는지까지가 배포의 범위였다. 정적 호스팅의 "기본 처리"라는 보이지 않는 단계가, 멀쩡한 빌드를 조용히 망가뜨릴 수 있다는 걸 이때 배웠다. 이런 함정은 한 번 밟아 봐야 다음부터 보인다.

같은 코드, 두 가지 모습

결과적으로 단 하나의 코드베이스가 정반대의 두 형태로 돈다.

  • 현장: Electron 데스크톱 앱. 테두리 없는 전체화면, 닫기 차단, 부팅 자동 실행, 실제 하드웨어와 로컬 SQLite.
  • 소개용: 브라우저 웹 데모. mock API가 SQLite와 하드웨어를 흉내 내고, 키오스크 캔버스를 비율 유지 스케일링으로 줄여 정적 호스팅에 올린다.

이 두 인격이 가능했던 건, 차이를 만드는 지점을 처음부터 딱 두 군데로 못 박아 뒀기 때문이다. 플랫폼 동작(전체화면·키오스크·자동 실행)의 차이는 메인 프로세스의 창 설정 한 곳에, 데이터·하드웨어의 차이는 렌더러가 바라보는 window.electronAPI 한 겹에. 화면을 그리는 수십 개의 컴포넌트는 이 차이를 단 한 줄도 알지 못한 채, 두 환경 모두에서 똑같이 동작했다.

시리즈를 마치며

네 편에 걸쳐 현장 키오스크 한 대를 처음부터 정리했다. 돌아보면 모든 편을 관통하는 교훈은 하나였다 — 경계를 일찍, 명확히 긋는 것.

  • 1편: 렌더러와 메인 사이의 IPC 경계
  • 2편: 상태(React)와 그리기(Konva) 사이의 경계
  • 3편: 화면과 하드웨어 사이의 인터페이스 경계
  • 4편: 그 경계 덕에 가능했던 플랫폼 전환 — 키오스크 모드와 mock 기반 웹 데모

경계가 분명하면, 그 뒤에 닥치는 거의 모든 변화 — 가짜를 진짜로 바꾸기, 장비 추가, 그리고 이번 편의 데스크톱↔브라우저 전환 — 가 경계 안쪽의 국소적인 일로 줄어든다. window.electronAPI라는 한 겹을 갈아끼우는 것만으로 앱 전체가 다른 플랫폼에서 살아난 게 그 증거다. 만약 화면 코드 곳곳에 ipcRenderer나 SQLite 호출이 박혀 있었다면, 웹 데모는 사실상 별도 앱을 새로 짜는 일이 됐을 것이다.

가장 만족스러운 건, 이 구조가 이 프로젝트 하나로 끝나지 않는다는 점이다. 다음에 또 다른 현장 키오스크를 의뢰받아도, 이 경계 설계를 골격으로 그대로 가져가면 된다. 하드웨어 종류가 달라지면 스텁만 바꾸고, 소개가 필요하면 mock을 끼워 브라우저에 올리고, 현장에 깔 때는 같은 isDev 분기로 가두면 된다. 한 번 잘 그어 둔 경계는, 그 프로젝트가 끝난 뒤에도 두고두고 일한다.

반응형