앱 하나로 두 매장 — 두 번째 매장이 코드 수정이 되면 진 것이다
6 분두 번째 매장이 열렸다. 첫 번째 매장의 앱을 그대로 쓰고 싶다.
여기서 대개 이렇게 된다.
if (shopName == 'Riverside') { ... } else { ... }그리고 세 번째 매장에서 또 한 번, 네 번째에서 또 한 번. 2년 뒤에는 이미 문 닫은 매장의 분기가 파일에 남아 있고 아무도 지우지 못한다.
앱은 매장을 몰라야 한다
이 샘플의 규칙은 하나다.
/// The tempting shape is a `shopName` switch somewhere. Then the second shop
/// is a code change, the third shop is a code change, and eventually the file
/// has a branch for a shop that closed two years ago.
///
/// So nothing here knows any shop's name. The config file is an argument.매장은 설정 파일이고, 설정 파일은 앱 밖에 있다.
shop.mbd/ ← 앱. 두 매장 공용
configs/
riverside.json ← 매장 1
hilltop.json ← 매장 2같은 파일, 다른 매장


shop.mbd — 3 files, sha256:f5f905958eac
[riverside.json] RIVERSIDE · 07:00 - 20:00 · 3 items on the menu
[hilltop.json] HILLTOP · 09:00 - 18:00 · 4 items on the menu
same screen file, two shops: RIVERSIDE due $8.10 vs HILLTOP due $10.45번들 전체를 해시로 찍어 뒀다. 두 매장 사이에 뭐가 다르든, 저 안에 있는 건 아니다.
검증은 grep 한 줄이다
이 편의 주장은 검사 한 개로 요약된다.
for NAME in Riverside Hilltop; do
if grep -RIiq "$NAME" shop.mbd shop_server/bin; then
echo " \"$NAME\" appears inside the app — the difference has leaked in"; exit 1
fi
done neither shop name appears in shop.mbd or shop_server/bin앱 안에 매장 이름이 나타나면 실패한다. 이 검사는 앞으로 누가 급해서 if (shop == 'Hilltop') 한 줄을 넣는 순간 빨간불이 된다. 그게 이 검사의 존재 이유다 — 그 한 줄은 언제나 급할 때 들어온다.
메뉴에 없는 걸 누르면
화면 파일이 같으니 당근 케이크 버튼은 두 매장 화면에 똑같이 있다.
리버사이드에는 당근 케이크가 없다.
[riverside.json] add CK -> "CK is not on this menu"
[hilltop.json] add CK -> "added Carrot cake"거절은 서버가 한다.
// A sku that is on one shop's menu and not the other's must be refused
// here, not hidden by the screen. The screen is the same file in both
// shops and cannot know.
if (item.isEmpty) {
return _state(notice: '$sku is not on this menu');
}이건 이건 누가 눌러도 되나와 같은 이야기의 다른 얼굴이다. 거기서는 권한이 화면 밖에 있었고, 여기서는 카탈로그가 화면 밖에 있다. 공통점은 화면이 같은 파일이라는 것이고, 같은 파일은 아무것도 결정할 수 없다.
세금 표기가 다르다
이게 실무에서 제일 자주 갈리는 지점이다. 한 매장은 가격에 세금이 포함이고, 다른 매장은 카운터에서 더한다.
[riverside.json] tax $0.74 · due $8.10 · 10% tax included in the prices above
[hilltop.json] tax $0.95 · due $10.45 · 10% tax added at the counter코드 경로를 두 개 만들면 반드시 갈라진다. 그래서 식 하나가 플래그를 읽는다.
// Two shops, two ways of quoting. The arithmetic is one expression that
// reads the flag — not two code paths that drift apart.
final tax = included
? (gross * pct / (100 + pct)).round()
: (gross * pct / 100).round();
final due = included ? gross : gross + tax;$8.10 에서 $0.74. 포함 세액은 8.10 × 10 / 110 이고, 8.10 × 0.1 = 0.81 이 아니다. 이걸 틀리는 코드가 실무에 정말 많다.
세 번째 매장
이 구조에서 세 번째 매장을 여는 일은 이렇다.
configs/seaside.json파일 하나. 배포도 없고, 빌드도 없고, 리뷰도 없다. 그리고 그게 이 구조를 고른 유일한 이유다.
이 샘플의 범위
설정을 누가 고치는지가 없다. 파일을 손으로 고친다. 실제로는 사장이 화면에서 가격을 바꿔야 하고, 그러면 설정을 편집하는 화면이 필요하다. 그건 이 샘플 밖이다.
설정 검증이 없다. taxPercent 에 문자열을 넣으면 서버가 그냥 죽는다. 매장이 열 곳이 되면 잘못된 설정 하나가 그 매장만 못 열게 만들어야 하는데, 안 했다.
두 매장이 한 대에서 돌았다. 각기 다른 매장에 있는 두 대의 기기가 각자 자기 설정을 받는 배포 이야기는 안 했다. 그건 폴더 하나가 앱이다가 다룬 배달 문제로 이어진다.
공통 메뉴의 동기화도 없다. 아메리카노 이름을 두 매장에서 같이 바꾸려면 파일 두 개를 고쳐야 한다. 공유 설정 + 매장별 덮어쓰기 구조가 필요하고, 안 만들었다.
프랜차이즈 도구가 비싼 이유
매장 관리 SaaS가 비싼 건 기능이 많아서가 아니다. 매장마다 다른 걸 다르게 유지하는 일이 원래 어렵기 때문이다.
이 샘플이 보여준 건 그 어려움이 어디에 두느냐의 문제라는 것이다. 앱 안에 두면 매장 수만큼 분기가 늘고, 밖에 두면 매장 수만큼 파일이 는다. 분기는 서로 얽히고 파일은 안 얽힌다.
그 차이가 두 번째 매장에서는 안 보이고, 다섯 번째 매장에서 보인다.
확인 과제
같은 예약을 연달아 두 번 보내 보세요. 두 번째가 예약을 하나 더 만들지 않는다는 것을 보이고, 어느 쪽이 그것을 보장했는지 설명하세요.