프로젝트 목록

에이치티비욘드

POS / 주문 키오스크 시스템

아파트 단지 내 입주민 대상 카페·매점에서 사용하는 클라우드 POS·주문 키오스크. 두 대의 독립 크롬 윈도우 간 실시간 동기화를 Broadcast Channel + localStorage 이중 구조로 해결

기간: 2023.03 - 2023.04역할: 프론트엔드 개발
ReactTypeScriptReact QueryRecoilTailwind CSSBroadcast Channel APIlocalStorage

주요 성과

  • 아파트 단지 내 입주민 대상 카페·매점에서 사용하는 클라우드 기반 POS·주문 키오스크 시스템 신규 개발
  • React Query·Recoil로 서버 상태와 UI 상태를 분리하여 컴포넌트 간 의존성 감소
  • AbortController로 결제 API를 요청 단위 취소 가능하게 구성. 사용자가 결제 도중 화면을 이탈해도 잔여 요청이 후속 화면에 영향 주지 않도록 처리
  • POS(가로 1024×768) · KIOSK(세로 1024×1920) 두 가지 화면 방향·비율을 Tailwind 토큰으로 추상화하여 동일 컴포넌트로 양 환경 대응
  • 트러블슈팅: 한 매장에 메인(직원)+서브(고객) 두 대의 독립 크롬 윈도우가 동시에 떠 결제 상태·주문 내역·세션을 실시간 공유해야 하는 요구. 팀 내 윈도우 간 통신 선례가 없는 상황에서 WebSocket 서버 신설 없이 동일 origin 내에서 해결 가능한 Web API Broadcast Channel을 1차 채널로 채택하고, 메시지 유실에 대비해 localStorage event를 백업 채널로 두는 이중 구조 설계. 수신 측에서 메시지를 받아 React Query invalidate를 호출하는 방식으로 두 화면 동기화 통합

트러블슈팅 자세히 보기

독립된 두 크롬 윈도우 사이의 실시간 동기화를 어떻게 풀었는지 — 검토한 옵션 4가지의 비교, 채택 근거, 통신 구조 다이어그램과 코드, 그리고 안정성 보강을 위해 추가한 백업 채널까지.

1. 요구사항

한 매장에 메인(직원용) + 서브(고객용) 두 대의 모니터가 동시에 떠 있고, 결제 진행 상태·주문 내역·세션을 실시간으로 공유해야 함. 두 모니터는 각각 독립된 크롬 윈도우로 실행되며, 별도 WebSocket 인프라는 신설하지 않는다는 제약.

2. 검토한 옵션 비교

localStorage 직접 사용

기각

setItem/getItem으로 데이터를 공유하고 storage event로 변경 알림

+별도 API 불필요, 구현 단순

−값 설정/조회 시 동기 작동 → 렌더링 블락 가능. 데이터 매개체로는 부적합

BroadcastChannel API

채택

탭·윈도우·iframe·Web Worker 간 통신을 위한 단일 API

+비동기 메시지 전달, 채널 단위 라우팅 분리, 다양한 컨텍스트를 단일 API로 통합

−구형 브라우저 미지원 (POS·KIOSK는 모던 크롬 고정 환경이라 무관)

Service Worker postMessage

기각

Service Worker를 매개로 윈도우 간 메시지 전달

+오프라인·푸시 등 SW의 부가 기능과 통합 가능

−러닝 커브 + SW 등록/관리 부담. 단순 윈도우 동기화에는 과도한 도구

Window postMessage

기각

window.open()으로 연 윈도우 간 메시지 전달

+iframe·팝업 통신의 표준

−window.open()/document.open()으로 직접 연 윈도우에만 사용 가능. 독립 실행된 두 모니터에는 부적합

3. 채택: BroadcastChannel + 도메인별 채널 분리

비동기 메시지 전달 + 동일 origin 내 다중 컨텍스트 통신을 단일 API로 해결할 수 있는 BroadcastChannel을 채택. 채널을 도메인별(주문/결제/세션)로 분리하여 메시지 라우팅 로직 단순화.

메인 모니터직원용 (가로 1024×768)서브 모니터고객용 (세로 1024×1920)① BroadcastChannel② localStorage event (백업)React Queryinvalidate → refetch → 화면 자동 갱신결제 완료 → 메시지 발송메시지 수신 → invalidate 호출

sync.ts — 핵심 동기화 로직 (요약)

// 1. 도메인별 BroadcastChannel 분리 (메시지 라우팅 단순화)
const paymentChannel = new BroadcastChannel("payment");
const orderChannel = new BroadcastChannel("order");

// 2. 메인 모니터: 결제 완료 시 메시지 발송
function onPaymentCompleted(orderId: string) {
  const message = { type: "payment-completed", orderId, ts: Date.now() };

  // 1차: BroadcastChannel
  paymentChannel.postMessage(message);

  // 2차(백업): localStorage event 트리거
  // (BroadcastChannel 메시지 유실 가능성 대비)
  localStorage.setItem("__sync__", JSON.stringify(message));
}

// 3. 서브 모니터: 두 채널 모두 수신 → React Query invalidate
paymentChannel.onmessage = (e) => handle(e.data);

window.addEventListener("storage", (e) => {
  if (e.key !== "__sync__" || !e.newValue) return;
  handle(JSON.parse(e.newValue));
});

function handle(msg: SyncMessage) {
  if (msg.type === "payment-completed") {
    queryClient.invalidateQueries({ queryKey: ["order", msg.orderId] });
  }
}

4. 안정성 보강: 백업 채널 추가

개발 단계에서 BroadcastChannel만으로는 메시지 안정성에 부족함을 확인 — 브라우저 정책·탭 비활성화 등으로 메시지가 유실될 수 있음. 1차 검토에서 ‘데이터 매개체’ 용도로 기각했던 localStorage를 ‘메시지 알림 트리거’로 용도를 바꿔 백업 채널로 추가. 수신 측은 어느 채널이든 메시지가 도착하면 React Query invalidate를 호출하여 결국 화면이 갱신되는 단일 흐름으로 통합.

→ 동일 API라도 ‘무엇을 위해 쓰는가’의 정의가 바뀌면 재검토 가치가 있음을 확인.