약 14분 읽기
애니믹스는 같은 동물 두 마리를 가운데 게이트로 보내 합치는 캐주얼 게임이다. 알이 병아리가 되고, 병아리가 강아지가 되고, 그렇게 고양이, 여우, 판다, 사자, 코끼리, 유니콘, 드래곤을 거쳐 마지막 ‘전설의 동물’까지 11단계를 올라간다. 판에는 동물이 최대 12마리까지만 있을 수 있고, 꽉 찬 상태로 4초가 지나면 게임이 끝난다.
이 게임을 9월에 Google Play에 제출했고, 한 번 거절된 뒤 10월 초에 Play와 토스 앱 안의 미니앱(앱인토스)으로 출시했다. 지금은 웹 게임 포털용 빌드까지 만들어 두었다. 코드는 하나인데 나가는 곳이 셋이다. 커밋 기록을 보면 게임 자체보다 세 곳의 규칙에 맞추는 수정이 더 많다. 이 글은 그 과정에서 실제로 고친 것들의 기록이다.

게임 코드는 1벌, 바뀌는 건 SDK뿐
플랫폼마다 코드를 복사하지 않는다. 게임 로직(game.js)은 어디서 돌든 같고, src/platform.js가 실행 환경을 감지해서 저장소와 광고 SDK만 갈아끼운다.
- 앱인토스: 저장은 토스 네이티브 Storage, 광고는 앱인토스 광고 SDK
- Google Play: Capacitor로 감싼 안드로이드 앱. 저장은 localStorage, 광고는 AdMob
- 브라우저(개발용): localStorage, 광고 자리에는 목업 화면
광고만은 두 벌로 나눠야 했다. 앱인토스 미니앱 안에서는 AdMob 같은 외부 광고 네트워크를 직접 붙일 수 없고, 반대로 Play 빌드에는 앱인토스 SDK가 없다. 처음에는 한 번들 안에서 분기했는데, 그러다 AdMob 키가 앱인토스 번들에 섞여 들어가는 문제가 생겼다. 그래서 광고 SDK를 아예 빌드 타깃으로 분리해서, 앱인토스용 결과물에는 AdMob 관련 코드가 들어가지 않게 했다.
환경 감지 자체는 단순하다. 토스 웹뷰인지는 사용자 에이전트와 SDK가 심어두는 전역 값으로, 네이티브 앱인지는 Capacitor의 isNativePlatform()으로 본다.
Play 첫 심사에서 거절된 이유: 런처 아이콘
9월 7일에 Play에 제출했는데 14일이 지나도록 심사가 끝나지 않았다. 안내된 기간(7일)의 두 배였다. 같은 시기에 낸 다른 앱도 멈춰 있어서 계정 문제라고 짐작하고 지원 티켓을 넣었는데, 답변은 “계정 수준의 제한은 없다”였다. 지연 원인은 대기열 증가와 개별 앱 심사였고, 티켓을 넣은 덕에 검토가 앞당겨졌다.
이때 지원팀이 알려준 규칙 하나가 중요했다. 새로 제출한 것은 가장 최근 변경일부터 평가 기간을 다시 센다. 등록정보를 고쳐 다시 내면 기다린 날짜가 처음으로 돌아갈 수 있다는 뜻이라, 심사가 끝날 때까지 콘솔에서 아무것도 바꾸지 않기로 했다.
그리고 9월 23일에 거절됐다. 사유는 “혼동을 야기하는 주장 정책 — 앱의 설치된 아이콘 또는 이름이 스토어 등록정보와 다름”이었다. 원인은 두 가지였다.
- 런처 아이콘이 Capacitor 기본값 그대로였다. 안드로이드 리소스 폴더의 아이콘 파일 날짜가 전부 프로젝트를 만든 날이었다. 스플래시와 스토어 아이콘은 만들면서 정작 폰 홈 화면에 뜨는 아이콘만 빠뜨렸다. 적응형 아이콘 배경색도 기본값 흰색이었다.
- 앱 이름이 한국어 하나뿐이었다. 영어·일본어 기기에서도 홈 화면에 ‘애니믹스’가 떴는데, 스토어 이름은 언어마다 달랐다. 거절 메일의 “이 문제는 다른 위치에서도 발생할 수 있습니다”가 이것이었다.
아이콘을 스토어 아이콘과 같은 그림으로 5개 밀도 전부 바꾸고, 적응형 배경색을 아이콘 모서리색(#482B7D)으로 맞췄다. 앱 이름은 영어 Animix, 일본어 アニミックス를 따로 넣었다. 런처 라벨은 짧게 잘리기 때문에 스토어 이름 전체가 아니라 앞부분과 맞췄다. 지적이 사실이었기 때문에 이의신청은 하지 않고 고쳐서 다시 냈다.
그래서 Capacitor 새 앱 체크리스트에 한 줄을 넣었다. npx cap add android 직후에 런처 아이콘, 배경색, 언어별 앱 이름부터 바꾼다. 빠뜨려도 빌드도 되고 실행도 되기 때문에, 심사에서 거절되기 전까지는 모른다.
AdMob 승인이 늦어진 이유: app-ads.txt
Play 출시 뒤에는 AdMob 앱 인증이 실패했다. AdMob은 Play 등록정보의 ‘개발자 웹사이트’ 도메인에서 app-ads.txt 파일을 찾아 이 앱의 광고 판매자가 맞는지 확인한다. 파일은 이 블로그 도메인에 미리 올려두었지만, 개발자 웹사이트 지정은 심사 중에 등록정보를 건드리지 않으려고 출시 뒤로 미뤄둔 상태였다. 출시 직후에 지정하고 나니, 남은 건 AdMob이 그 도메인을 다시 크롤링하기를 기다리는 것뿐이었다. 다음 날인 10월 7일에 인증이 통과되고 광고 게재 제한이 풀렸다.
유럽 사용자를 위한 동의 메시지(UMP)도 이때 붙였다. 동의가 필요한 지역에서 동의를 받지 못하면 광고 없이 게임만 돌린다. 이 지역에서는 인증된 동의 관리 도구가 없으면 광고 게재가 제한된다.
전면광고 빈도는 플랫폼마다 다르다
전면광고는 한 판이 끝나고 결과 화면이 나오기 직전에만 띄운다. 게임 도중에는 절대 끼어들지 않는다. 그런데 몇 판마다 띄울지는 플랫폼마다 다르게 정했다.
// 바꿀 때는 이 표만 고치면 된다. game.js 는 window.__INTERSTITIAL_POLICY 를 읽기만 한다.
// first : 처음 전면광고가 나오는 판(누적). 첫인상 보호 — 그 전엔 안 나온다
// every : 그 뒤 몇 판마다
// minGapSec : 직전 전면광고 뒤 이 시간(초) 안이면 건너뛴다(같은 실행 안에서만 잰다)
// 앱인토스: 노출 정책 UX 원칙 2 "결과를 볼 때마다 전면광고를 반복하지 말 것" → 3판마다.
// 플레이: 그런 추천 평가가 없어 2판마다, 대신 60초 간격으로 연달아 뜨지 않게.
// 웹 포털: 포털 SDK 외 광고 금지라 전면광고가 아예 없다(first 무한대).
const INTERSTITIAL_POLICY = __WEB_BUILD__ ? { first: Infinity, every: 1, minGapSec: 0 }
: __AIT_BUILD__ ? { first: 3, every: 3, minGapSec: 0 }
: { first: 3, every: 2, minGapSec: 60 };
window.__INTERSTITIAL_POLICY = INTERSTITIAL_POLICY;
- 앱인토스: 3판째부터 3판마다. 앱인토스의 노출 정책 UX 원칙에 “결과를 볼 때마다 전면광고를 반복하지 말 것”이 있다.
- Play: 3판째부터 2판마다. 대신 직전 광고 뒤 60초 안이면 건너뛰어서, 짧은 판이 연달아 끝나도 광고가 붙어 나오지 않게 했다.
- 웹 포털: 포털 SDK 외의 광고가 금지라 전면광고가 아예 없다.
처음 두 판에 광고가 없는 이유도 있다. 첫인상에서 광고를 보면 그대로 삭제하기 때문이다. 그리고 이 표는 한 곳에만 둔다. 게임 코드는 이 값을 읽기만 하고, 값을 못 읽으면 가장 보수적인 앱인토스 기준으로 돌아간다.
const FALLBACK_INTERSTITIAL = { first: 3, every: 3, minGapSec: 0 };
let lastInterstitialAt = -Infinity; // performance.now() ms, 이번 실행 안에서만
function interstitialDue(plays) {
const p = window.__INTERSTITIAL_POLICY || FALLBACK_INTERSTITIAL;
if (plays < p.first || (plays - p.first) % p.every !== 0) return false;
return (performance.now() - lastInterstitialAt) / 1000 >= p.minGapSec;
}
광고 직전에는 1.5초 동안 “잠깐 광고 후 결과를 보여드려요”를 띄운다. 무엇이 나오고 그 뒤에 무엇을 보는지 먼저 알리라는 것도 앱인토스 UX 원칙이다. 광고를 불러오지 못하거나 차단돼도 결과 화면은 반드시 뜬다.

광고 버튼에서도 버그가 있었다. 보상형 광고 버튼을 연타하면 광고가 줄줄이 뜨고 보상이 여러 번 적용돼 게임 상태가 꼬였다. 지금은 광고를 불러오는 동안 버튼을 “불러오는 중”으로 바꿔 잠근다. 반대로 광고 안전장치가 먼저 풀렸는데 보상 신호가 늦게 도착하는 경우에는, 늦게 받은 보상도 지급하도록 고쳤다. 광고를 끝까지 봤는데 보상을 못 받는 게 사용자에게는 가장 화나는 일이다.
스폰 하한 0.32초를 0.8초로: 봇을 돌려 난이도를 맞췄다
출시 후 받은 피드백 중에 “유니콘 이후 너무 어렵다”가 있었다. 동물이 나오는 간격은 2.1초에서 시작해 점점 짧아지는데, 처음에는 하한이 0.32초였다. 사람이 탭해서 합칠 수 있는 속도는 0.5초 간격 탭, 대략 초당 1회 합체 정도다. 하한이 그보다 2~3배 빨랐으니, 실력과 상관없이 2분 안에 판이 밀려 끝났고 드래곤과 전설은 사실상 불가능했다.
감으로 숫자를 바꾸는 대신 봇을 만들었다. 실수율을 넣은 봇이 정해진 속도로 탭하게 하고, 탭 간격마다 24판씩 돌려서 어디까지 가는지 쟀다.
const SPAWN_START = 2.10; // 시작 스폰 간격(초) — 초반은 여유롭게
// 최종 하한. 예전 0.32초는 사람이 합칠 수 있는 속도(0.5초 간격 탭 ≈ 초당 1회 합체)보다 2~3배 빨라서
// 실력과 상관없이 2분 안에 밀려 끝났고 드래곤·전설은 사실상 불가능했다("유니콘 이후 너무 어렵다", 2026-10).
// 수박게임처럼 '잘하면 가끔 닿는' 목표가 되도록 잘하는 손 속도 근처로 맞췄다.
// 봇 시뮬레이션(24판, 실수율 포함, SPAWN_TIER_MAX 5·연장전·연쇄(CHAIN_MAX 2, 고양이↑) 포함):
// 0.7초 탭 → 판다 91%·유니콘 4%, 약 2분 반 / 0.5초 탭 → 유니콘 100%·드래곤 25%, 약 4분 /
// 0.4초 탭 → 드래곤 100%·전설 12%, 약 5분 반. 봇은 연쇄용 사다리를 일부러 만들지 않으니
// 그걸 노리는 사람은 이보다 잘 간다(머리 쓴 만큼 보상). 다시 조정할 땐 같은 방식으로 재고 바꾼다.
const SPAWN_FLOOR = 0.8;
// 연장전: 하한만 두면 손 빠른 사람은 영영 안 죽는다(봇 0.4초 탭이 15분 상한까지 생존).
// LATE_START 초가 지나면 1분에 LATE_PER_MIN 초씩 다시 빨라져 LATE_MIN 까지 간다.
const LATE_START = 300, LATE_PER_MIN = 0.08, LATE_MIN = 0.45;
const SPAWN_TAU = 95; // 감쇠 시간상수(초). 클수록 더 천천히 어려워진다
목표는 ‘잘하면 가끔 닿는’ 정도로 잡았다. 그래서 하한을 0.8초로 올렸다. 이 값에서 0.7초 간격으로 탭하면 판다까지 91%가 가고 유니콘은 4%, 0.5초면 유니콘 100%에 드래곤 25%, 0.4초면 드래곤 100%에 전설 12%가 나온다. 봇은 연쇄 합체를 노린 배치를 일부러 만들지 않으니, 그걸 노리는 사람은 이보다 더 간다.

하한만 두면 생기는 문제도 봇 결과에서 보였다. 0.4초로 탭하는 봇은 15분 상한까지 죽지 않았다. 그래서 5분이 지나면 1분에 0.08초씩 다시 빨라져 0.45초까지 내려가는 ‘연장전’을 넣었다.
연쇄 합체도 같은 방식으로 맞췄다. 합쳐서 나온 동물과 같은 단계가 판에 있으면 저절로 끌려와 또 합쳐지는 기능인데, 알·병아리 같은 흔한 단계까지 연쇄되게 했더니 누르지 않아도 판이 정리돼 버렸다. 보통 실력 봇의 드래곤 도달률이 0%에서 81%로 뛰었다. 그래서 연쇄는 고양이 단계부터만, 한 번 합칠 때 최대 2번까지로 제한했다.
작은 버그들
- 아이폰에서 동물이 판 밖으로: 앱인토스로 아이폰에서 열면 동물이 판 밖으로 나가고 게이트가 아래로 치우쳤다. 토스 웹뷰는 노치·홈 인디케이터 여백(safe-area)을 첫 화면을 그린 뒤에 반영하는데, 그때
resize이벤트가 오지 않았다. 그래서 판은 줄었는데 예전 크기로 계산하고 있었다. 390×844 화면에서 재현하니 판은 592px인데 653px로 계산하고 있었다. 창 크기 대신 판 요소 자체를ResizeObserver로 지켜보게 바꿨다. - 소리 버튼이 안 눌림: 앱인토스 화면 오른쪽 위에는 토스의 ‘··· ✕’ 버튼이 겹쳐 있다. 그 자리에 있던 소리 버튼을 메뉴 옆으로 옮겼다.
- 배경음악이 갑자기 끊김: “노래가 갑자기 안 나온다”는 피드백이 있었다. iOS와 안드로이드 웹뷰는 광고, 전화, 알림음, 백그라운드 전환 뒤에 오디오(AudioContext)를 멈춤 상태로 두고 스스로 돌아오지 않는다. 다시 깨우는 것도 사용자 조작 안에서만 허락하는 환경이 있어서, 탭할 때마다, 화면으로 돌아올 때, 광고가 끝날 때 깨우기를 시도한다.
- 결과창 오터치: 판이 끝나자마자 결과창이 뜨면, 계속 누르던 손가락이 ‘다시하기’를 눌러버렸다. 끝난 판을 0.7초 보여준 뒤 결과창을 띄우고, 결과창 버튼도 0.8초 동안 잠근다.
- 저절로 나오던 용: 예전에는 드래곤까지 무작위로 나와서 합쳐 만든 적 없는 용이 판에 있었다. 저절로 나오는 동물은 판다 단계까지로 묶었다.

지금 상태
Play와 앱인토스 양쪽에 출시돼 있다. 앱인토스 버전에는 토스 게임센터 리더보드와, 동의한 사람에게만 매일 20시에 가는 ‘오늘의 도전’ 알림이 붙어 있다. 최근에는 결과 화면을 이미지로 공유하는 카드와 웹 게임 포털용 빌드를 추가했다. 웹 포털 빌드는 그 포털의 SDK 외 광고를 쓸 수 없어서 전면광고가 빠진 버전이다.
세 곳에 내면서 배운 건, 게임 코드보다 각 플랫폼의 규칙을 코드 한 곳에 모아두는 게 더 중요하다는 점이다. 광고 빈도도, 저장소도, SDK도 플랫폼마다 다르지만, 그 차이를 표 하나와 파일 하나에 몰아두니 게임 로직(game.js)은 세 곳이 같은 파일을 그대로 쓴다.
직접 해보고 싶다면 아래에서 받을 수 있다. 직접 만든 게임이다.
- Google Play에서 애니믹스 보기
- 토스 앱: 토스에서 ‘애니믹스’를 검색하면 미니앱으로 바로 열린다.