Dessert Rush는 모바일 portrait 기준으로 만든 캐주얼 퍼즐/러시 게임이다. 처음에는 Godot 프로젝트 자체가 게임의 중심이었지만, Toss 미니앱으로 내보내려면 단순히 Web export를 올리는 것으로 끝나지 않았다.
Toss 쪽에는 홈, 티켓, 코인, 결과 화면, 광고, 결제, 약관, 출시용 빌드 같은 shell이 필요했고, 게임 본체는 Godot의 조작감과 연출을 유지해야 했다. 이 글은 Godot 게임을 React/TypeScript 기반 Apps in Toss shell 안에 넣으면서 어떤 구조를 선택했고, 출시 준비 단계에서 무엇을 검증했는지 정리한 기록이다.
문제
처음 문제는 명확했다.
Godot Web export는 보통 index.html, index.js, index.wasm, index.pck 묶음으로 나온다. 가장 쉬운 통합 방식은 iframe으로 Godot export 페이지를 넣는 것이다. 하지만 Apps in Toss에서는 YouTube embed 같은 예외를 제외하면 iframe 사용이 적합하지 않다.
그래서 선택지는 두 가지였다.
- 게임을 React/TypeScript로 다시 구현한다.
- Godot Web runtime을 host document의 top-level canvas에 직접 붙인다.
1번은 빠르게 보이지만, 이미 Godot 쪽에 있던 조작감, 게임 루프, 시각 연출을 다시 만들어야 했다. 결과적으로 게임의 핵심 품질이 흔들릴 가능성이 컸다. 그래서 2번을 선택했다.
구조
최종 구조는 이렇게 나눴다.
React/Granite Toss shell
- 홈
- 티켓/코인
- 결과 화면
- 광고/IAP adapter
- 약관/출시 shell
Godot Web runtime
- 실제 게임 플레이
- 조작/점수/콤보/서빙 결과
- Web export 산출물: index.js, index.wasm, index.pck
Bridge contract
- Godot -> React: CustomEvent
- React -> Godot/Toss shell: 상태 반영, 보상, 결과 화면 이동
핵심은 게임과 shell의 책임을 분리한 것이다. Godot는 “게임 한 판”에 집중하고, Toss shell은 “서비스로 운영되는 게임”에 필요한 주변 상태를 담당한다.
iframe 대신 직접 canvas로 붙이기
Godot export 파일은 public/godot/ 아래에 두었다.
public/godot/index.js
public/godot/index.wasm
public/godot/index.pck
React에서는 src/components/GodotCanvas.tsx가 Godot loader script를 직접 불러오고, host document 안의 <canvas>를 Godot Engine에 넘긴다.
const GODOT_BASE = '/godot/index';
const GODOT_SCRIPT = `${GODOT_BASE}.js`;
const engine = new window.Engine({
...GODOT_CONFIG_BASE,
canvas,
onProgress: (current, total) => {
if (current > 0 && total > 0) setProgress({ current, total });
},
});
await engine.startGame();
이 방식의 장점은 production surface가 명확하다는 점이다.
- Toss shell 안에서 직접 canvas를 제어한다.
/godot/index.html을 production embed 대상으로 쓰지 않는다.- 로딩 상태, 에러 상태, retry 버튼을 React shell에서 일관되게 처리한다.
- canvas size를 shell layout에 맞춰 직접 조정할 수 있다.
Godot와 React 사이의 이벤트 계약
Godot와 React를 직접 결합하면 나중에 수정이 어려워진다. 그래서 bridge는 문자열 이벤트와 payload 타입으로 고정했다.
대표 이벤트는 다음과 같다.
dessert-rush:ready
dessert-rush:run-started
dessert-rush:run-ended
dessert-rush:game-event
dessert-rush:ad-requested
dessert-rush:haptic-requested
React 쪽에서는 src/integrations/godotBridge.ts에서 이벤트 이름과 payload 타입을 관리한다. 예를 들어 한 판이 끝나면 Godot가 score, served, combo, coins 같은 결과 payload를 보내고, React shell은 이 값을 result screen과 reward flow에 반영한다.
export type DessertRushRunEndedPayload = {
source: 'godot';
runId?: number;
title: string;
score: number;
served: number;
targetServed?: number;
bestCombo: number;
moves: number;
coinsEarned: number;
};
이렇게 해두면 Godot 쪽 게임 루프를 바꾸더라도 shell에서 필요한 계약은 유지할 수 있다. 반대로 Toss SDK, 광고, 결제 쪽이 바뀌어도 Godot는 “결과를 이벤트로 보낸다”는 책임만 지키면 된다.
출시 준비에서 중요했던 검증
이 프로젝트에서 가장 중요했던 것은 “로컬에서 잘 돌아감”이 아니라 “제출 가능한 산출물인지”였다. 그래서 release verification script를 따로 두었다.
npm run verify:release는 다음을 한 번에 확인한다.
- Godot smoke tests 실행
- Godot Web export 갱신
- Web export sanitizer 실행
- Toss production integration contract 확인
- lint
- production build
- AIT build
.ait,.pck,.wasm파일 존재 및 크기 확인- AIT 50MB 초과 방지
- Godot loader에 기록된 runtime file size와 실제 파일 크기 일치 확인
검증 스크립트가 특히 막아주는 것은 “실수로 오래된 runtime을 넣은 채 빌드하는 문제”다. Godot export는 파일이 크고 산출물이 여러 개라서, 수동으로 확인하면 놓치기 쉽다.
이와 별개로 GRAC 제출용 설명서, 등급분류 자가진단 자료, 화면 흐름 영상, 모바일 스크린샷 묶음도 따로 정리했다. 구현만 한 프로젝트와 출시 준비까지 간 프로젝트의 차이는 이런 주변 산출물에서 드러난다.
수동 QA 체크리스트
자동화만으로는 Toss WebView와 모바일 조작감을 전부 보장할 수 없다. 그래서 수동 QA 항목도 따로 잡았다.
- iOS Toss app에서 home, PLAY, READY countdown, drag, pause, result, HOME, retry 확인
- Android Toss app에서 동일 flow와 back button/overscroll 확인
- notch와 Toss game menu가 timer, score, pause button과 겹치지 않는지 확인
- 앱을 background로 보냈다가 돌아왔을 때 canvas와 audio 상태 확인
- 네트워크가 약하거나 reload가 실패했을 때 retry UI가 보이는지 확인
게임 프로젝트는 “빌드 성공”과 “사용자가 한 판을 정상적으로 끝낼 수 있음” 사이의 간격이 크다. 그래서 release checklist에는 둘 다 필요했다.
iOS/App Store 쪽에서 다시 배운 점
Toss 미니앱과 별도로 iOS/TestFlight 가능성도 같이 정리했다. 여기서는 monetization scope를 줄이는 판단이 중요했다.
초기 App Store 경로에서는 IAP UI를 비활성화하고, unlimited free play로 두었다. mock purchase나 StoreKit이 완전히 준비되지 않은 상태에서 결제 UI를 노출하는 것보다, 첫 제출 경로에서는 게임 플레이와 안정성을 먼저 검증하는 편이 낫다고 봤다.
광고도 active gameplay 중간에 넣지 않고, 보수적인 post-run interstitial 후보로 제한했다. 게임에서 monetization은 기능을 붙이는 것보다 사용자 경험을 망치지 않는 위치를 고르는 쪽이 더 중요했다.
배운 점
이번 작업에서 배운 것은 세 가지다.
첫째, 플랫폼 제약은 구현 후반에 붙이는 체크리스트가 아니라 아키텍처를 바꾸는 입력값이다. Apps in Toss에서 iframe을 피해야 한다는 조건 때문에 Godot를 top-level canvas로 직접 붙이는 구조가 나왔다.
둘째, 게임 본체와 서비스 shell의 책임을 나누면 출시 준비가 쉬워진다. Godot는 게임 플레이를 책임지고, React shell은 티켓, 코인, 결과, 광고, 약관, 빌드를 책임진다. 두 세계는 event contract로만 연결한다.
셋째, 출시 준비는 문서보다 검증 스크립트가 강하다. npm run verify:release처럼 실제 산출물, 파일 크기, integration contract, build를 한 번에 확인하는 명령이 있어야 매번 같은 기준으로 판단할 수 있다.
다음에 보강할 것
- 실제 기기 Toss WebView QA 결과를 캡처해서 첨부한다.
npm run verify:release실행 로그를 공개 가능한 범위로 정리한다.- Godot canvas integration 전후 구조를 다이어그램으로 만든다.
- 공개 가능한 범위에서 데모 링크와 repo 링크를 정리한다.