스크린골프장 5,000개를 하나의 코드베이스로 — O2O 무인화 SaaS 구축기

윤상민

윤상민

2026년 3월 6일4분 분량

스크린골프장 5,000개를 하나의 코드베이스로 — O2O 무인화 SaaS 구축기

“예약이 왜 없어요? 분명히 전화했는데요.”

스크린골프장 사장님들이 가장 난감해하는 상황 중 하나입니다. 고객은 분명히 전화로 예약했고, 사장님은 메모지에 적어뒀는데, 근무자가 바뀌는 사이 그 메모는 사라져 있습니다.

이게 예외적인 상황이 아니었습니다. 전화 예약 → 수기 등록 → 근무자 간 인수인계 로 이어지는 구조 자체가 문제였습니다.

문제발생 원인
예약 누락전화 내용을 수기로 받아 적다 빠뜨림
예약 밀림앞 손님이 늦어지면 뒷 예약이 연쇄적으로 밀림
인수인계 오류근무자 교대 시 예약 현황이 제대로 전달 안 됨
더블 부킹전화와 현장 예약이 동시에 들어올 때 충돌

우리가 만든 김캐디는 이 문제를 해결하기 위해 시작한 예약 플랫폼입니다. 고객이 앱에서 직접 예약하면 전화 한 통 없이도 예약이 확정됩니다.

그런데 플랫폼만 있어선 반쪽짜리였습니다. 김캐디의 예약 데이터가 매장 현장에 실시간으로 반영되지 않으면, 결국 사장님은 앱과 메모장을 번갈아 보며 운영해야 합니다. 문제의 형태만 바뀔 뿐입니다.

그래서 매장 운영 SaaS를 만들고, 김캐디 플랫폼과 실시간으로 연결했습니다. 예약이 생성되는 순간 매장 시스템에 즉시 반영되고, 키오스크·결제·룸 PC까지 하나의 흐름으로 이어지는 구조입니다.


시스템 전체 구조

문제를 레이어로 나눠보니 크게 세 영역으로 정리됐습니다.



시스템 전체 아키텍처

Online Layer는 예약이 만들어지는 곳입니다. 김캐디 플랫폼은 WebSocket으로 실시간 연동하고, API가 없는 플랫폼은 어댑터를 통합합니다.

Store SaaS Layer는 실제 매장 운영의 두뇌입니다. 모든 예약 소스를 단일 시스템으로 통합하고, 키오스크·메인PC·룸PC에 상태를 전파합니다.

Hardware Layer는 물리 세계와 닿는 지점입니다. 결제 단말기와 IoT 기기들이 여기에 연결됩니다.


핵심 설계 결정 다섯 가지

1. 예약하고 바로 달려와도 키오스크에 예약이 있습니다

처음부터 WebSocket을 선택했습니다. 예약이 생성되는 즉시 매장에 전달되어야 한다는 요구사항이 명확했기 때문입니다. REST 폴링은 고려 대상이 아니었습니다.

그런데 WebSocket만으로는 충분하지 않았습니다. 네트워크가 불안정하거나 재연결 타이밍이 엇갈리면, 이벤트가 유실될 수 있었습니다. 예약이 생성됐는데 매장 SaaS가 해당 이벤트를 받지 못한 상황 — 고객은 왔는데 키오스크에 예약이 없는 상황입니다.

이를 방어하기 위해 Reconciliation(리컨실레이션) 방식을 도입했습니다. WebSocket 이벤트는 실시간 전달을 담당하고, 주기적으로 서버에서 전체 예약 상태를 풀 싱크(full sync)해서 누락된 이벤트를 보정합니다. 실시간성과 데이터 정합성을 동시에 확보하는 구조입니다.

WebSocket + Reconciliation 시퀀스

2. 사장님이 쓰는 모든 예약 채널을 하나로

세상의 모든 예약 플랫폼이 API를 제공하지는 않습니다. 일부는 웹 어드민 화면만 있습니다. 사장님은 그 플랫폼도 쓰고 있고, 예약은 계속 들어오고 있습니다.

API 제공 여부와 관계없이 모든 소스를 통합하기 위해, 예약 소스를 인터페이스로 추상화했습니다.

typescript
interface ReservationSource {
  sourceId: string;
  fetch(): Promise<Reservation[]>;
  normalize(raw: unknown): Reservation;
}

// API 있는 플랫폼
class WebSocketSource implements ReservationSource { ... }

// API 없는 플랫폼
class ScrapingSource implements ReservationSource { ... }

어떤 소스에서 왔든 Reservation 타입으로 정규화되면, 이후 로직은 동일합니다. 통합 예약 관리 시스템은 소스를 신경 쓰지 않습니다.

3. 기존 단말기 그대로, 시스템만 바뀝니다

매장마다 VAN사가 다릅니다. 단말기도 제각각입니다. 처음엔 각 단말기 SDK를 직접 붙였는데, 금방 한계가 왔습니다.

typescript
interface PaymentAdapter {
  requestApproval(amount: number): Promise<ApprovalResult>;
  requestCancel(approvalNo: string): Promise<CancelResult>;
  getStatus(): Promise<TerminalStatus>;
}

이 인터페이스 하나만 맞추면 어떤 단말기든 붙을 수 있습니다. 신규 VAN사가 들어오면 어댑터만 하나 더 만들면 됩니다.


4. 개발자 없이 사장님이 직접 정책을 바꿉니다

“예약 단위가 30분인 매장도 있고 1시간인 매장도 있어요. 노쇼 패널티도 다 달라요. 심야 할증도요.”

초기에 이런 분기를 코드에 넣었습니다. 금방 읽을 수 없는 코드가 됐고, 새 매장이 들어올 때마다 배포가 필요했습니다.

핵심 원칙을 하나 잡았습니다.

코드는 “무엇을 할 수 있는가(Capability)”를 정의하고, Config는 “무엇을 할 것인가(Policy)”를 결정한다.

지금은 새 매장이 들어오면 Config만 추가합니다. 코드를 건드는 일이 없습니다.

5. 예약 시간에 맞춰 방이 알아서 제어됩니다.

예약 n분 전에 룸 PC가 자동으로 켜지고, 시뮬레이터가 실행되고, 조명이 들어와야 합니다. 퇴장하면 다 꺼져야 합니다.

매장에 따라 키고 싶은 기기도, 시간도, 방식도 다릅니다.

꺼져 있는 PC는 WOL(Wake-on-LAN)로 켭니다. 켜진 뒤엔 Windows API로 제어합니다.

WOL + Windows API 시퀀스


왜 이 구조가 필요했는가

단순히 기술적으로 깔끔해 보이기 때문이 아닙니다.

운영 현실 때문입니다.

1,000개 매장이 있으면 1,000개의 운영 정책이 있습니다. 단말기도 다 다르고, 키오스크 OS도 다 다르고, 어떤 예약 플랫폼을 쓰는지도 다 다릅니다.

이걸 하나하나 커스텀 개발로 대응하면 스케일이 없습니다. 반대로 너무 경직된 시스템을 강요하면 매장이 도입을 안 합니다.

각 레이어가 자기 역할만 하고, 변경이 필요한 지점을 명확히 격리하는 것 — 그게 이 구조의 목표였습니다.


시리즈 예고

이 글은 전체 구조를 조감도 수준으로 훑었습니다. 각 주제는 별도 파트에서 깊게 다룹니다.

파트주제
Part 2Config-Driven Architecture 심화 — Policy Engine 설계
Part 3WebSocket 실시간 동기화 — 연결 유지, 재연결, 메시지 유실 처리
Part 4Adapter Pattern으로 VAN사 N개 대응하기
Part 5WOL + Windows API로 룸 PC 완전 자동화

다음 글은 Config-Driven Architecture 심화입니다. 1,000개 매장의 운영 정책을 코드 한 줄 건드리지 않고 어떻게 처리하는지, Policy Engine 설계와 Config 스키마 전략을 깊게 다룹니다.


Series · 김캐디 프로덕트

1 / 1
스크린골프장 5,000개를 하나의 코드베이스로 — O2O 무인화 SaaS 구축기1

스크린골프장 5,000개를 하나의 코드베이스로 — O2O 무인화 SaaS 구축기

Mar 6
윤상민

윤상민

Frontend Engineer

김캐디

Series · 김캐디 프로덕트

스크린골프장 5,000개를 하나의 코드베이스로 — O2O 무인화 SaaS 구축기
1
스크린골프장 5,000개를 하나의 코드베이스로 — O2O 무인화 SaaS 구축기
Reading now