Decisions
무엇을 고르고, 무엇을 버렸나
고른 것만 적어두면 나중에 왜 그랬는지 되짚을 수가 없다. 그래서 버린 선택지와 그걸 버린 이유, 그리고 그 결정이 남긴 대가까지 같이 적는다.
매장 내 실시간 통신은 MQTT 브로커로
상황
한 매장에 손님 태블릿 여러 대와 점원 태블릿이 붙는다. 주문이 들어오면 점원 화면에서 벨이 울려야 하고, 점원이 메뉴를 품절 처리하면 모든 손님 태블릿에서 그 메뉴가 잠겨야 한다. 단말은 꺼지고 켜지고, 자리를 옮기고, 교체된다.
선택지
- 버림폴링
체감 지연과 서버 부하를 맞바꾸는 구조. 단말 수만큼 선형으로 늘어난다.
- 버림WebSocket 직결
서버가 '이 매장에 지금 어떤 단말이 붙어 있는지'를 직접 들고 있어야 한다. 재접속·중복 세션 정리를 전부 직접 짜는 일.
- 채택MQTT 브로커
발행자는 토픽에만 던진다. 누가 받는지는 브로커가 안다. 재접속과 구독 복원이 프로토콜에 이미 들어 있다.
그래서 무엇이 남았나
서버 코드에서 수신자 목록이 사라진 대신, 토픽 문자열이 곧 권한 경계가 됐다. 토픽은 매장 식별자를 반드시 포함하는 다섯 갈래로 고정했다 — 매장 전체 / 손님 단말 전체 / 점원 단말 / 테이블 단위 / 개별 단말. 페이로드는 이벤트 이름 한 줄이고 데이터는 싣지 않는다. 받은 쪽이 다시 조회하게 해서, 메시지 유실이 곧 데이터 불일치가 되지 않도록.
앱 업데이트 파이프라인을 직접 만든다
상황
태블릿은 매장에 설치되어 있고, 화면에 버그가 하나 나와도 고치러 갈 방법이 사실상 없다. 스토어 심사를 거치는 배포 주기로는 대응이 안 되고, 애초에 일반 사용자가 설치하는 앱도 아니다.
선택지
- 버림스토어 배포
심사 대기. 그리고 매장 단말은 스토어를 통해 설치되지 않는다.
- 버림상용 OTA 서비스
배포 경로를 외부 서비스에 묶고 싶지 않았고, 매장·단말 단위 제어 같은 요구를 맞추기 어렵다.
- 채택자체 업데이트 서버 + CI 발행
JS 변경은 번들 교체로 2분 안에, 네이티브 변경은 APK 갱신으로. 릴리스 비활성화가 곧 롤백이다.
그래서 무엇이 남았나
서버는 번들 파일을 직접 다루지 않는다. 실제 파일은 정적 서버가 서빙하고, API는 요청받은 런타임 버전에 맞는 매니페스트만 조립해서 내려준다. 맞는 릴리스가 없으면 빈 응답을 준다. 덕분에 배포 서버가 파일 스토리지 역할까지 겸하는 결합이 생기지 않았다. 대신 '고쳤는데 단말에 안 내려간다'가 새로운 장애 유형이 됐고, 그게 이 프로젝트에서 가장 길었던 디버깅으로 이어졌다.
APK 빌드와 번들 발행 워크플로를 분리
상황
처음에는 하나의 파이프라인이 푸시마다 APK를 빌드하고 그 끝에 번들 발행을 붙이는 구조였다. 화면 문구 하나 고쳐도 30분을 기다려야 했다.
선택지
- 버림단일 워크플로 유지
설정은 단순하지만, JS만 바꾼 대부분의 커밋이 네이티브 빌드 시간을 그대로 물어야 한다.
- 채택변경 경로로 두 워크플로를 갈라놓기
네이티브 빌드 결과에 영향을 주는 경로 목록을 하나 정의하고, 한쪽은 그 경로에서만 돌고 다른 쪽은 정확히 그 반대에서만 돈다.
그래서 무엇이 남았나
두 워크플로가 상호 배타라 같은 커밋에서 겹쳐 돌지 않는다. JS만 고친 커밋은 2분 만에 단말까지 도달한다. 대신 '어떤 변경이 네이티브 빌드에 영향을 주는가'라는 목록을 사람이 관리해야 하고, 여기서 빠뜨리면 빌드 지문이 어긋난다.
코드값의 주인은 언제나 서버
상황
클라이언트가 매장에 설치된 태블릿이라는 건, 서버는 오늘 바뀌어도 앱은 몇 주 전 버전이 돌고 있을 수 있다는 뜻이다. 목록형 코드값을 클라이언트 열거형으로 닫으면 서버가 항목 하나를 추가하는 순간 구버전 앱이 깨진다.
선택지
- 버림클라이언트 열거형 / 유니온 타입
타입은 안전해 보이지만, 실제로는 서버 변경이 곧 구버전 앱 장애가 되는 결합이다.
- 채택서버 SSOT + 클라이언트 fallback
목록은 서버가 소유하고, 클라이언트는 모르는 값을 만나면 안전한 기본 동작으로 흘려보낸다.
그래서 무엇이 남았나
언어를 추가하거나 매장별 기능을 켜고 끄는 데 앱 배포가 필요 없어졌다. 매장별 기능 플래그는 서버 테이블 한 곳에 모아 두고, 앱은 받은 값을 그대로 소비한다. 대신 클라이언트 곳곳에 '모르는 값일 때' 분기가 생기므로, 이걸 화면마다 흩뿌리지 않고 공용 렌더 레이어에 모았다.
번역을 컬럼이 아니라 행으로 저장한다
상황
처음 구조는 메뉴 테이블에 이름 슬롯을 몇 개 두고, 매장별로 어떤 언어를 켰는지도 언어마다 불리언 컬럼을 하나씩 갖는 형태였다. 언어를 하나 더 지원하려면 스키마를 고치고, 슬롯 수가 지원 언어 수의 상한이 됐다.
선택지
- 버림언어별 컬럼 추가
언어 추가마다 마이그레이션. 슬롯이 곧 한계이고, 번역 대상이 늘 때마다 같은 구조를 또 만든다.
- 채택대상·필드·언어를 키로 갖는 범용 번역 테이블
무엇의(대상 종류·식별자), 어느 항목을(이름/설명), 어느 언어로. 이 네 값이 하나의 행을 이룬다.
그래서 무엇이 남았나
지금은 메뉴·메뉴그룹·옵션·옵션그룹·호출항목·매장명이 전부 같은 테이블을 쓴다. 새 번역 대상이 생겨도 테이블을 더 만들지 않는다. 전환은 한 번에 끊지 않고 새 구조를 먼저 세우고, 데이터를 옮기고, 마지막에 옛 컬럼을 지우는 세 단계로 나눴다 — 중간 어느 시점에 배포가 멈춰도 앱이 죽지 않도록.
디자인은 앱에 박지 않고 서버에서 내린다
상황
매장마다 원하는 분위기가 다르다. 이걸 매장 이름으로 분기하기 시작하면 매장이 늘어날수록 손댈 수 없는 코드가 된다.
선택지
- 버림매장별 분기 코드
첫 매장은 빠르지만 두 번째부터 조건문이 번식한다.
- 채택스타일 × 프리셋 × 토큰 3계층
레이아웃 골격을 스타일이, 색 조합을 프리셋이, 개별 값을 토큰이 담당한다. 아래 계층이 위를 덮어쓴다.
그래서 무엇이 남았나
관리 웹에서 고른 값이 저장되고 앱이 그대로 렌더한다. 웹은 CSS로, 앱은 그릴 수 없는 표현을 벡터로 다시 그리되 원본 값 자체는 공유한다. 변경 이력을 스냅샷으로 남겨서 되돌릴 수 있게 했다. 다만 관리 웹의 리졸버와 앱의 소비 코드가 손으로 맞춘 거울 관계라, 한쪽만 고치면 어긋난다 — 이건 아직 구조적으로 풀지 못한 부분이다.