게임 50종을 허브 9개로 묶은 방법 — 게임 코드는 한 줄도 고치지 않고, 멀티 페이지 번들과 전역 플래그 하나로
개별 앱 50여 종을 퍼즐카페·뇌나이·오락실·한판승부·올림픽·행운연구소·하루상식·복불복·카드놀이 허브 9개로 묶었습니다. SPA로 합치지 않고 게임마다 페이지를 그대로 빌드하는 멀티 페이지 번들을 택한 이유, 기존 앱 52개에 영향을 주지 않으려고 런타임 전역 플래그 하나로 허브 모드를 켠 방식, 오늘의 미션·허브 포인트·영역 점수·주간 리그 같은 허브 전용 기능, 감사 도구와 에뮬레이터 점검, 그리고 두 번 반려된 아이콘 이야기를 적었습니다
Read in English How I Bundled 50 Games into 9 Hub Apps Without Touching a Line of Game Code — Multi-Page Bundles and One Global Flag
10월 17일에 허브 9종을 만들기 시작해 24일에 첫 허브가 Google Play에 나갔고, 26일 새벽에는 앱인토스에도 네 개가 올라갔습니다. 왜 허브로 가는지는 노출 개편 글에 적었으니, 이 글은 만드는 쪽 이야기입니다. 제약이 하나 있었습니다. 게임 50여 종의 코드를 고치지 않을 것. 개별 앱 52개가 두 스토어에 라이브 중이라, 허브 때문에 그 앱들이 바뀌면 안 됐습니다.
왜 SPA가 아니라 멀티 페이지인가
허브라고 하면 게임 50종을 한 화면 앱으로 합치는 그림이 먼저 떠오릅니다. 해 보면 바로 막힙니다. 게임들이 전부 모듈 최상위에서 실행됩니다. 파일을 불러오는 순간 캔버스를 잡고 이벤트를 걸고 저장소를 읽습니다. 이걸 SPA 안에서 켜고 끄려면 50종 전부를 “시작·정리” 함수로 감싸야 하고, 그건 곧 50종을 고치는 일입니다.
대신 번들 구조를 살펴봤습니다. 게임들은 모두 Vite로 빌드되고 자산 경로가 상대 경로입니다. 그러면 출력 폴더만 허브 밑으로 돌려도 게임은 그 하위 경로에서 그대로 돕니다. 허브 번들은 이렇게 생겼습니다.
1
2
3
4
5
6
dist-hub/puzzle/
index.html 허브 홈
hub-env.js 허브 설정(아래 설명)
g/sudoku/index.html 게임 페이지 — 개별 앱 빌드 결과 그대로
g/2048/index.html
...
허브 홈에서 게임으로 가는 건 평범한 링크입니다. 링크로 이동하면 브라우저 히스토리에 항목이 쌓이고, 토스의 뒤로가기는 히스토리를 하나 빼는 동작이라 게임 홈에서 뒤로가기를 누르면 자연히 허브 홈으로 돌아옵니다. 뒤로가기 처리 코드를 따로 쓰지 않았습니다.
빌드 도구는 게임마다 그 게임 폴더의 Vite를 불러 출력 폴더만 바꿔 돌리고, 마지막에 모든 페이지에 작은 스크립트 하나를 꽂습니다. 그게 허브 모드의 전부입니다.
전역 플래그 하나로 켜는 허브 모드
게임 소스를 고치지 않고 허브에서만 다르게 동작하게 하려면 분기점이 공용 패키지에 있어야 합니다. 다행히 게임들은 광고·리더보드·푸시·크로스프로모를 전부 공용 패키지로 씁니다. 그래서 공용 패키지가 런타임에 window.__HUB__ 전역 하나를 보게 했습니다. 이 값은 허브 번들의 게임 페이지에만 꽂히는 스크립트가 심어 주고, 개별 앱 번들 52개에는 그 스크립트가 없으니 undefined입니다. 빌드 플래그가 아니라 런타임 전역을 고른 이유도 같습니다. 빌드 설정을 건드리면 개별 앱 빌드가 바뀔 수 있기 때문입니다.
허브 모드에서 달라지는 건 다섯 가지뿐입니다.
| 기능 | 개별 앱 | 허브 모드 |
|---|---|---|
| 토스 리더보드 | 앱마다 제출 | 끔. 토스 리더보드는 앱당 하나라 허브 포인트가 대신합니다 |
| 한 판 끝남 신호 | 푸시 동의·별점 시점에 쓰던 신호 | 같은 신호로 허브의 “오늘의 미션 완료”를 기록 |
| 광고 지면 ID | 앱마다 자기 지면 | 허브 앱의 지면으로 덮어쓰기 |
| 크로스프로모·앱 공유 | 표시 | 숨김. 허브 홈 격자가 곧 목록이라서 |
| 1원 프로모션 | 게임마다 지급 | 게임별 지급은 끄고 허브 전체로 하루 한 번 |
“한 판 끝남” 신호에는 함정이 하나 있었습니다. 스도쿠·노노그램처럼 완주형 게임 8종은 그 신호를 완주했을 때와 중간에 나갈 때 두 곳에서 보냅니다. 그대로 두면 판을 열고 나가기만 해도 미션이 끝납니다. 게임 코드를 고치는 대신, 신호가 올 때 결과 화면이 떠 있을 때만 완료로 치게 했습니다.
같은 원리로 좋은 쪽 효과도 있었습니다. 알림 동의, 별점 요청, 광고 일일 상한 같은 저장 키는 원래 앱 단위라서, 허브 안에서는 자동으로 허브 전체 하나로 공유됩니다. 게임별 기록은 앱 접두어가 붙은 키라 서로 섞이지 않습니다.
허브 홈: 원격 요청 0, 한 번만 그리기
허브 홈은 별도 패키지지만 외부 의존성이 없습니다. 제목·탭·수록 게임·미션 풀은 빌드 때 설정 파일과 앱 레지스트리에서 읽어 스크립트에 내장합니다. 네트워크 요청이 하나도 없는 이유는 앱인토스의 첫 로딩 20초 반려와 첫 화면 안정 규칙 때문입니다. 홈은 세션에 한 번만 그리고, 게임에서 돌아왔을 때는 미션 표시와 숫자만 고칩니다. 이미지 태그도 없습니다. 게임 아이콘은 CSS 배경으로 그립니다.
탭 전환도 토스에서는 뒤로가기 대상입니다. 기본 탭이 아닌 탭으로 갈 때만 히스토리에 넣고, 뒤로가기 처리 안에서는 다시 넣지 않습니다. 이 규칙을 어긴 게 예전 비게임 앱 네 번째 반려의 원인이었습니다.
오늘의 미션은 날짜를 시드로 미션 풀에서 몇 개를 뽑습니다. 같은 날 모든 사용자에게 같은 미션이 걸리고, 미션 하나에 10점, 그날 전부 끝내면 20점을 더하고, 전부 끝낸 날이 이어지면 연속 일수가 올라갑니다. 정산은 홈을 그리기 전에 끝내서 숫자가 그려진 뒤 바뀌지 않게 했습니다.
허브마다 하나씩 넣은 전용 기능
미션과 포인트는 공통이고, 허브마다 성격에 맞는 기능을 하나씩 더 넣었습니다. 전부 기기 안 저장소만 쓰고 원격 요청은 없습니다.
- 퍼즐카페, 별. 끝내면 1별, 시간이나 점수가 기준을 넘으면 2~3별. 같은 날 다시 도전해 더 좋은 별이 나오면 차이만큼만 더합니다.
- 뇌나이, 영역 점수. 언어·기억·순발력·주의 네 영역에 게임을 배정하고 최근 3판을 0~100으로 환산해 영역 점수와 종합을 보여 줍니다. 나이로 환산하지 않고, 카드 아래에 “게임 점수 기준이고 의학적 진단이 아니다”를 항상 적습니다. 이름을 ‘뇌나이’로 한 건 ‘Brain Age’가 닌텐도 상표라 영어 이름을 Brain Check로 둔 것과 짝입니다.
- 오락실, 주간 리그. 7종 각각의 이번 주 최고점을 기준점 대비로 정규화해 합산하고 월요일에 리셋합니다. 등급은 새내기부터 챔피언까지. 토스 리더보드를 허브에서 껐기 때문에 허브 안의 유일한 순위 요소입니다. 다른 사람과의 비교는 원격 요청을 쓰지 않기로 해서 없습니다.
- 한판승부, 단수. 보드게임 16종에서 AI 단계별 첫 승리에만 점수를 주고(쉬움 1, 보통 3, 어려움 6) 입문부터 명인까지 등급을 올립니다. 패배로 내려가지 않습니다.
- 복불복, 공용 참가자 명단. 룰렛·사다리·제비뽑기·벌칙 룰렛이 참가자 이름을 한 번만 적으면 같이 씁니다. 신작 도구들과 저장 키를 맞춰 놓았습니다.
- 카드놀이, 공용 덱. 솔리테어·프리셀·스파이더·피라미드가 카드 그림 한 벌을 나눠 씁니다. 이건 따로 적을 이야기가 있어 다음 글로 미룹니다.
임계값은 전부 초기 추정치입니다. 실제 점수 분포를 보고 숫자만 조정하도록 한 파일에 모아 두었습니다.
검사 도구와 에뮬레이터
멀티 페이지 번들은 페이지가 열 개 넘게 들어가서 눈으로 다 못 봅니다. 감사 스크립트가 허브 번들마다 확인하는 것은 이렇습니다. 설정에 적힌 게임 페이지가 전부 있는가, 모든 페이지의 자산 경로가 상대 경로인가, 허브 플래그가 게임 페이지 전부에 꽂혔는가, 토스 브릿지 스크립트가 모든 페이지에 있는가, 허브 홈에 외부 URL과 이미지 태그가 없는가, 뒤로가기 처리 안에 히스토리 추가가 없는가. 브릿지 미주입은 예전에 한 앱이 세 번 연속 반려된 원인이라 항목으로 박아 두었습니다.
여기에 기존 앱에 쓰던 첫 로딩 게이트와 첫 화면 안정 게이트를 허브에도 돌리고, 안드로이드 에뮬레이터에서 허브 9종을 자동으로 열어 게임마다 “열기, 렌더, 뒤로가면 허브 복귀, 홈에서 뒤로가면 종료, 콘솔 오류 0, 외부 요청 0”을 확인하는 스크립트를 만들었습니다. 9종 전부 통과하기까지 고친 건 대부분 뒤로가기 쪽이었습니다.
두 번 반려된 아이콘
허브 아이콘과 토스 썸네일은 글자 없이 도형만으로 만듭니다. 처음 썸네일을 허브 아이콘 하나에 배경 패턴만 깐 고정형으로 만들었다가 “너무 성의 없다”고 반려됐고, 허브 아이콘 왼쪽에 수록 앱 아이콘 여섯 칸을 넣는 형태로 다시 만들었습니다. 아이콘 자체도 회사 로고 배지의 위치와 크기, 불투명도를 네 번 고쳤습니다. 뇌나이는 뇌 그림이 “징그럽다”는 의견으로 번개 필라멘트 전구가 됐습니다.
허브 아이콘 9종. 행운연구소는 네잎클로버, 뇌나이는 전구, 올림픽은 금메달입니다. 같은 배지 규격은 이후 기존 앱 60개에도 적용했습니다.
지금 상태와 남은 것
- Play에는 게임 허브 5종이 라이브이고, 비게임 허브 4종과 카드놀이는 다음 주 새 앱 생성 한도가 풀리면 올립니다.
- 앱인토스에는 비게임 허브 4종이 라이브, 게임 허브 5종은 앱 정보 검토 중입니다. 과정은 전환기 글에 있습니다.
- 허브 안 게임 페이지는 같은 앱 식별자 아래 같은 저장 키를 쓰므로, 개별 앱에서 쌓은 기록은 허브 안 그 게임에서 그대로 보입니다.
- 남은 것은 허브 홈 배너 광고, 결과 화면의 ‘다음 미션’ 버튼을 더 많은 게임에 붙이는 일, 그리고 임계값을 실측으로 조정하는 일입니다.
만든 앱들은 GAMES와 APPS에 있고, 일부는 브라우저에서 바로 해볼 수 있습니다. 소식은 이 블로그와 인스타그램(@fadongkwon.soft) (새 창에서 열림)에서 전해드립니다.
이 글의 구조와 규칙은 2026년 10월 17~26일 작업 기준입니다. 공용 패키지와 빌드 도구는 계속 바뀌므로 숫자와 임계값은 당시 값으로 읽어 주세요.
