flowoodz.
← blog
17분 읽기글 flowoodz

앱인토스 게임 출시: 웹게임 RAYFRAME을 토스 미니앱으로 옮기고 등급분류에서 멈춘 기록

앱인토스 게임 출시를 준비하며 웹 메카 슈팅 RAYFRAME을 토스 미니앱 빌드로 옮겼고, 지금은 출시 전 게임 등급분류를 기다리고 있습니다. 가로 고정, 안전 영역과 ⋯/X 버튼, 안드로이드 뒤로가기, 백그라운드 음소거, 토스 공유 초대를 고친 과정과 mTLS 인증 코드 교환으로 사용자를 식별한 방법, 등급분류를 받는 두 길과 비용을 적었습니다.

목차
  1. 웹 게임은 WebView SDK로 감쌌습니다
  2. 출시 가이드대로 바꾼 다섯 가지
  3. 사용자 식별은 키 대신 한 번 쓰는 코드로 했습니다
  4. 출시를 막은 것은 게임 등급분류입니다
  5. 남은 순서

RAYFRAME은 제가 만드는 웹용 3대3 무중력 메카 슈팅입니다. 2026년 10월 22일 0시 19분에 이 게임을 토스 앱 안에서 도는 미니앱으로 빌드하는 커밋을 넣었습니다.

결과부터 적으면 아직 출시 전입니다. 코드는 끝났고, 게임 등급분류를 받지 못해서 앱인토스 콘솔에 검토를 요청할 수 없는 상태입니다.

7월에 웹게임 포털 6곳을 비교한 글에서 앱인토스는 등급분류가 먼저라고 제가 직접 써 두었습니다. 그래 놓고 코드부터 끝냈습니다.

이 글은 그 코드에서 바꾼 것과, 앞을 막고 있는 등급분류를 정리한 기록입니다.

앱인토스 콘솔용 RAYFRAME 썸네일. 흰 메카가 우주에서 전갈 모양 기체에 빔을 쏘고, 왼쪽 위에 게임 제목이 있습니다

위 그림은 콘솔의 게임 썸네일 칸에 넣으려고 만든 1932×828 이미지입니다. 로고는 600×600으로 따로 만들었습니다.

웹 게임은 WebView SDK로 감쌌습니다#

7월 글에는 앱인토스 개발에 Bedrock(React Native 기반) SDK나 Unity SDK를 쓴다고 적었습니다. 그게 전부는 아니었습니다.

공식 문서의 기존 웹 프로젝트에 SDK 연동하기대로 하면 이미 있는 웹 프로젝트에 @apps-in-toss/web-framework를 설치해 미니앱으로 바꿀 수 있습니다.

RAYFRAME은 이 WebView SDK 3.x를 썼습니다. 구조는 CrazyGames에 낼 때와 같습니다.

빌드 변수 VITE_PLATFORM으로 플랫폼 구현을 고르고, 토스용 빌드(npm run build:toss)만 토스 SDK를 싣습니다. SDK 호출은 예전 토스 앱이나 시험 환경에서 예외를 던져도 게임이 멈추지 않게 전부 감쌌습니다.

npm run build:toss는 dist-toss 폴더를 만든 뒤 ait build로 콘솔에 올릴 rayframe.ait 파일 하나로 묶습니다. 압축을 풀었을 때 100MB가 상한이고, 지금은 27MB입니다.

CrazyGames 때와 같은 문제도 하나 있었습니다. 토스는 게임 파일을 자기 도메인(rayframe.apps.tossmini.com)에서 내보냅니다. 페이지 주소를 기준으로 게임 서버 주소를 만들면 토스 도메인으로 접속하게 되므로, 토스 빌드는 소켓 주소를 빌드 변수로 고정하고 서비스 워커를 넣지 않았습니다.

출시 가이드대로 바꾼 다섯 가지#

앱인토스의 게임 출시 가이드는 체크리스트입니다. 하나라도 어기면 검수가 승인되지 않는다고 적혀 있습니다. 웹판에 없던 동작 다섯 가지를 토스 빌드에만 넣었습니다.

1. 화면을 가로로 고정했습니다#

RAYFRAME은 가로 화면 게임입니다. 시작할 때 Screen.setOrientation({ type: 'landscape' })를 부릅니다.

WebView 설정에서는 당겨서 새로고침, 끝에서 튕기는 스크롤, 스와이프로 뒤로 가는 제스처를 껐습니다. 조작하다가 게임 화면이 손가락에 끌려 움직이거나 페이지가 넘어가면 안 되기 때문입니다.

webView: {
  bounces: false,
  pullToRefreshEnabled: false,
  overScrollMode: 'never',
  allowsBackForwardNavigationGestures: false,
},

2. 안전 영역과 ⋯/X 버튼을 피해 무대를 그립니다#

토스는 게임 화면 오른쪽 위에 ⋯ 버튼과 X 버튼이 든 알약 모양 막대를 띄웁니다. 가이드는 Safe Area와 iOS Dynamic Island를 침범하지 말라고 합니다.

SafeArea.get()으로 받은 여백을 CSS 변수로 넘기고, 여백이 바뀌면 SafeArea.subscribe로 다시 넘깁니다. 좌우 여백은 안전 영역과 알약이 차지하는 폭 중 큰 값을 양쪽에 똑같이 줍니다. 그래야 무대가 가운데에 남습니다.

const side = Math.max(i.left, i.right + PILL);
s.setProperty('--safe-t', `${i.top}px`);
s.setProperty('--safe-b', `${i.bottom}px`);
s.setProperty('--safe-x', `${side}px`);

여기에 못 푼 게 하나 있습니다. 알약이 오른쪽 끝에서 얼마나 떨어져 있는지는 Safe Area 문서에 있는데(오른쪽 여백 + 10px), 알약의 폭은 문서에서 찾지 못했습니다.

그래서 PILL을 간격 10px에 폭 96px를 더한 106px로 추정해 두었습니다. 실제 폭은 기기에서 확인해야 합니다.

3. 안드로이드 뒤로가기는 Esc와 같게 했습니다#

가이드는 안드로이드 시스템 뒤로가기 버튼을 누르면 뒤로 가거나 미니앱이 닫혀야 한다고 합니다. RAYFRAME에는 이미 Esc가 하는 일이 있었습니다. 열린 창을 닫고, 로비에서는 돌아가기, 경기 중에는 메뉴를 엽니다.

그래서 토스의 backEvent가 오면 Esc를 누른 것처럼 처리했습니다. 열린 창이 있으면 그 창을 닫고, 없으면 Esc 키 이벤트를 페이지에 보냅니다. 아무도 그 키를 받지 않았으면 첫 화면이라는 뜻이니 「정말 나갈까요?」를 묻고 Screen.close()로 미니앱을 닫습니다.

const down = new KeyboardEvent('keydown', esc);
window.dispatchEvent(down);
window.dispatchEvent(new KeyboardEvent('keyup', esc)); // input.ts tracks held keys
if (!down.defaultPrevented) askExit();

가이드에는 미니앱을 닫을 때 확인 창이 나와야 한다는 항목도 있습니다. 뒤로가기로 닫을 때 묻는 이유가 이것입니다. 오른쪽 위 X 버튼은 따로 손대지 않았습니다. X를 누르면 닫기 전에 확인하는 창을 토스가 직접 띄웁니다.

4. 백그라운드로 가면 소리를 바로 끕니다#

가이드는 미니앱이 백그라운드로 넘어가면 소리를 즉시 끄고, 돌아오면 다시 재생하라고 합니다. 토스 빌드는 document.hidden을 플랫폼 설정의 음소거 값으로 씁니다.

CrazyGames 빌드에서 포털의 음소거 설정을 받던 통로를 그대로 썼습니다. 숨으면 음소거 관문이 닫히고, 돌아오면 AudioContext를 다시 시작합니다.

5. 친구 초대는 토스 공유 링크로 보냅니다#

웹판의 「초대 링크」는 페이지 주소 뒤에 방 코드를 붙입니다. 토스 안에서는 그 주소가 토스 도메인이라 쓸 수 없습니다.

토스 빌드는 Share.createLink로 intoss://rayframe?room=방 코드 공유 링크를 만들고, Share.sendMessage로 토스 공유 창을 엽니다. 받은 사람이 링크로 들어오면 Environment.initialURL에서 방 코드를 읽어 그 방으로 들어갑니다. rayframe은 콘솔에 등록할 appName이고, 설정 파일의 값과 같아야 업로드가 됩니다.

사용자 식별은 키 대신 한 번 쓰는 코드로 했습니다#

체크리스트에는 사용자 식별키를 저장하고, 미니앱을 껐다 켜도 랭킹 같은 플레이 기록이 유지돼야 한다는 항목이 있습니다. RAYFRAME은 계급과 전적이 서버에 쌓이므로 토스 사용자를 서버가 알아봐야 합니다.

가장 짧은 길은 User.getAnonymousKey()로 받은 키를 그대로 서버에 보내는 것이었습니다. 그렇게 하지 않았습니다. 기기에서 키를 복사해 가면 다른 사람이 그 파일럿 행세를 할 수 있습니다.

익명 사용자 식별 키 검증 API 설명에도 이 API는 요청한 사람이 그 사용자임을 증명하지 않으니, 로그인에는 인증 코드 교환 API를 쓰라고 적혀 있습니다.

앱인토스 사용자 식별 흐름. 게임 서버가 미니앱의 일회용 코드를 mTLS로 anonKey와 바꿔 파일럿 신원을 만듭니다

사용자 식별키 인증 코드 발급 흐름은 이렇습니다.

  1. 미니앱이 User.createAnonymousKeyAuthCode()로 인증 코드를 받습니다. 코드는 300초 동안 한 번만 쓸 수 있습니다.
  2. RAYFRAME은 이 코드를 게임 서버에 접속할 때 보내는 첫 메시지에 싣습니다.
  3. 게임 서버는 콘솔에서 미니앱마다 발급하는 mTLS 인증서로 앱인토스 파트너 API를 불러 코드를 anonKey로 바꿉니다.
  4. 서버는 sha256('toss:' + anonKey)를 파일럿 신원으로 씁니다. 코드와 키는 저장하지 않습니다.

교환 API는 실패도 HTTP 200으로 돌려줍니다. 코드가 틀리면 4011, 요청 한도(앱당 분당 3,000회)를 넘으면 4095가 resultType: FAIL과 함께 옵니다. 그래서 서버는 상태 코드가 아니라 resultType이 SUCCESS일 때만 키를 꺼냅니다.

미니앱에는 3초 제한을 걸었습니다. 예전 토스 앱이나 일반 브라우저에서는 SDK 다리가 응답하지 않을 수 있는데, 그러면 첫 메시지가 끝없이 기다리게 됩니다.

3초 안에 코드가 오지 않으면 게스트로 들어갑니다. 서버에 인증서가 없을 때도 이 기능은 꺼지고 게스트가 됩니다. 게스트는 기기마다 다른 파일럿입니다.

문서에는 anonKey가 같은 미니앱에서 늘 같은 값이라고 적혀 있습니다. 그렇다면 같은 토스 사용자는 다른 기기에서도 같은 파일럿이 됩니다. 실제 기기 두 대로 확인한 것은 아닙니다.

출시를 막은 것은 게임 등급분류입니다#

앱인토스 콘솔은 게임이면 앱 정보에 등급분류 근거가 있어야 검토를 받습니다. 토스가 쓴 게임 등급분류 안내에 따르면 등급을 받는 길은 두 가지입니다.

길 걸리는 시간 비용 콘솔에 넣는 것
게임물관리위원회(GRAC)에 직접 신청 약 10~15일 등급분류 수수료(GRAC 모의 계산으로 미리 확인) 등급분류증명서 PDF
스토어에 실제로 출시하고 그 스토어의 자체등급분류를 받기 스토어 심사 며칠과 GRAC 목록 반영 Microsoft Store·원스토어 0원, 구글 플레이 25달러, 앱스토어 연 99달러 스토어 게임 페이지 주소, GRAC 자체등급분류 조회 정보, 스토어판·앱인토스판 플레이 화면 각 2장

GRAC에 직접 낼 때는 「오픈마켓/등급분류 신청」에서 플랫폼을 모바일 게임으로, 스토어명을 「기타-앱인토스」로 고릅니다. 청소년이용가 범위의 게임은 간소화 신청으로 갈 수 있다고 안내에 적혀 있습니다.

두 길 모두에 걸리는 조건이 있었습니다.

  • GRAC에 직접 내려면 사업자는 게임물제작업자등록증이나 배급업자등록증을 첨부해야 합니다. 저는 아직 이 등록이 없습니다. 등급 신청보다 먼저 끝나야 해서 가장 먼저 해야 할 일이 됐습니다. 처리 기간과 수수료는 아직 확인하지 못했습니다.
  • 스토어로 가면 스토어에 등록한 개발자 이름이 사업자명(flowoodz)과 같아야 합니다. 다르면 반려됩니다.
  • 구글 플레이는 새 개인 계정이면 프로덕션 출시 전에 테스터 12명으로 14일 동안 비공개 테스트를 해야 해서 가장 느립니다. RAYFRAME은 PWA라서 Microsoft Store가 가장 빠른 후보로 보입니다.

어느 길로 갈지는 아직 정하지 않았습니다. 신청서에 적을 내용은 미리 나눠 봤습니다. 사람 캐릭터가 없고 피 표현도 없지만 기체끼리 무기로 싸우니, 예상 등급은 전체이용가나 12세이용가입니다. 판단은 심의가 합니다.

남은 순서#

  1. 등급분류를 받습니다. 이것이 끝나야 앱 정보 검토(영업일 1~2일)를 요청할 수 있습니다.
  2. 콘솔에 미니앱을 등록합니다. appName은 rayframe입니다.
  3. 콘솔에서 mTLS 인증서를 발급해 게임 서버 환경 변수에 넣고 다시 배포합니다. 이게 없으면 토스 사용자는 모두 게스트입니다.
  4. rayframe.ait를 콘솔에 올리고 QR 코드로 토스 앱에서 엽니다. 알약 폭, 뒤로가기, 홈에 나갔다 왔을 때 소리 재개, 다른 기기에서 같은 파일럿인지, 초대 링크로 같은 방에 들어오는지를 이때 봅니다.
  5. 출시 검토를 요청합니다. 영업일 3~5일이 걸립니다.

체크리스트의 「10초 안에 첫 화면」은 로컬 헤드리스 폰 화면에서 타이틀이 0.25초에 떴습니다. 실제 토스 앱과 네트워크에서 다시 재야 합니다. 인앱 광고와 결제는 이번 판에 넣지 않았습니다.

국내 출시는 해외 포털보다 행정 절차가 길다는 7월의 판단은 맞았습니다. 다만 그 절차가 코드보다 오래 걸린다는 건 직접 막히고 나서야 실감했습니다. 등급분류 결과가 나오면 이 글에 덧붙이겠습니다.