본문 바로가기
카테고리 없음

미니앱 개발하기 4 - 수익화 마케팅

by 테크구루스 2026. 8. 24.

무료로 심사부터, 광고는 그다음 — 쫀득 슬라임 수익화 설계기라는 제목 아래, 앱인토스에 올린 3D 슬라임 토이 '쫀득 슬라임'에 광고를 정책 위반 없이 붙이고 커스터마이징으로 다시 오게 만든 뒤 심사까지 통과시킨 과정을 정리한 개발기입니다.

무료 토이 앱은 애초에 돈을 낼 이유가 없습니다. 슬라임을 주무르는 화면에 결제 버튼을 들이밀 수는 없으니, 남는 카드는 사실상 광고 하나뿐이죠. 그런데 광고는 잘못 붙이면 앱 경험을 망치고, 정책을 어기면 정산이 막힙니다. 이번 편은 이 앱에서 실제로 내린 수익화 결정과, 그 과정에서 밟은 함정들을 정리합니다.

광고는 나중에 — "빌드 플래그 두 개"로 무료부터 심사에 올리기

가장 먼저 정한 원칙은 "무료 빌드로 먼저 심사에 올리고, 광고는 그다음 출시 빌드에서 켠다"였습니다. 광고 정책은 앱 심사와는 또 다른 리스크라, 처음부터 광고를 잔뜩 얹은 빌드로 검수 테이블에 올라가면 트집 잡힐 표면적만 넓어지거든요. 그래서 SlimeCanvas 상단에 스위치 두 개를 뒀습니다.

// 등록용 빌드는 두 플래그 모두 false로 빌드 — 개발·출시 빌드는 true.
// MONETIZATION_ENABLED=false면 젤리 경제·잠금·보상형·전면형이 전부 꺼지고
// 모든 아이템이 무료로 열린다 (경제 도입 전과 동일한 UX)
const BANNER_AD_ENABLED = true;
const MONETIZATION_ENABLED = true;

MONETIZATION_ENABLED가 false면 젤리 경제·잠금·보상형·전면형 광고가 통째로 꺼지고 색·글리터·파츠가 전부 무료로 열립니다. 잠금 판정을 하는 함수 자체가 !MONETIZATION_ENABLED || isUnlocked(...)라, 플래그만 내리면 앱이 "순수 무료 토이"로 돌변하죠. 그래서 등록용 clean 빌드는 두 플래그를 모두 false로 제출했습니다. 광고 없이 완결된 토이로 먼저 통과시킨 다음, 검증된 코드 위에 광고만 스위치로 올리는 셈입니다.

개인적으로는, 광고 코드를 지우는 게 아니라 플래그 하나로 껐다 켠 게 이 앱에서 잘한 결정이었다고 봅니다. 심사용 빌드와 출시용 빌드가 서로 다른 코드가 아니라 같은 코드의 두 상태라, "심사 때는 없던 버그가 광고 켜니 튀어나오는" 사고를 원천적으로 줄였거든요. 광고 로직 자체는 무료 빌드 시절에도 계속 컴파일되고 타입 검사를 받고 있었으니까요.

젤리 경제 — 광고를 "강매"가 아니라 "모으는 재미"로

리텐션을 광고로 푸는 가장 게으른 방법은 화면을 광고로 막고 "보면 뚫어준다"고 강요하는 것입니다. 저는 반대로 갔습니다. 보상형 광고를 한 번 끝까지 보면 젤리 +10, 그 젤리를 모아 색·글리터·파츠를 하나씩 영구 해금하게 했죠. 소모성이 아니라 한 번 열면 끝인 영구 해금이라, "광고를 계속 봐야 유지되는" 압박이 없습니다. 다시 오는 이유가 '못 쓰게 될까 봐'가 아니라 '다음 아이템을 열고 싶어서'가 되는 거죠.

가격표는 economy.ts에 상수로 박아 뒀습니다.

export const ITEM_PRICES: Record<string, number> = {
  "color:1": 20, "color:2": 20, "color:3": 20, "color:4": 20, "color:5": 20,
  "color:custom": 30,                              // 무지개 팔레트 (60→30 인하)
  star: 30, heart: 30, moon: 30, ring: 30,         // 글리터 4종
  pearl: 40, charm: 40, flower: 40, icecream: 40,  // 파츠 4종
};

기본 그린 색, 가루 글리터, 도넛 파츠는 무료라 여기 목록에 없습니다. 첫 실행 웰컴 보너스 +20젤리로 시작하니, 광고 없이도 프리셋 색 하나는 바로 열어볼 수 있죠. 하루 시청 캡은 10회입니다.

여기서 실제로 한 번 데인 대목이 지급 금액이었습니다. 보상형 광고 SDK는 userEarnedReward 이벤트에 콘솔에서 설정한 unitAmount를 실어 주는데, 개발 중 테스트 광고가 이 값을 1로 보내는 바람에 광고를 완주해도 +1젤리만 들어오는 일이 있었습니다. 그래서 지급량은 콘솔 값을 아예 무시하고 앱에서 +10으로 고정했습니다.

onReward: () => {
  // 지급량은 앱에서 고정 +10 — 테스트 광고의 unitAmount=1
  // 같은 콘솔 값에 흔들리지 않는다
  const next = grantAdReward(economyRef.current, AD_REWARD_FALLBACK); // 10
  setEconomy(next);
},

 

미니앱 개발하기 5 - 수익화 마케팅
미니앱 개발하기 5 - 수익화 마케팅

 

보상은 userEarnedReward를 받는 즉시 저장합니다. 광고를 중간에 끄면 이벤트가 오지 않으니 보상도 없고(정상이죠), 반대로 이벤트가 왔다면 직후에 앱을 꺼도 젤리는 안전하게 남습니다. "광고 진행 중" 같은 상태는 아예 저장하지 않아, 재진입하면 늘 깨끗한 상태에서 광고만 새로 로드하면 됩니다.

광고 정책의 함정, 그리고 아이콘 심사 반려

앱인토스 광고 정책은 어기면 광고 제한이나 정산 보류로 돌아옵니다. 그래서 지킨 선은 분명했습니다.

  • 보상은 시청 완료에만: userEarnedReward 이벤트에서만 젤리를 줍니다. 클릭을 유도하거나 클릭에 보상을 엮는 것은 금지라, 코드에도 '광고 보기' 버튼 외에 클릭 보상 경로를 두지 않았습니다.
  • 광고 UI는 손대지 않는다: 위장·가림·임의 스타일 변경 금지.
  • 개발은 테스트 ID로만: 실제 adGroupId로 반복 시청·클릭하면 부정 트래픽으로 간주될 수 있어, 광고 로직을 만질 땐 테스트 ID로 바꾸고 확인은 1~2회만 합니다.

배너에서는 하루를 통째로 날린 버그가 있었습니다. TossAds.initialize()가 비동기인데 이걸 모르고 곧바로 attachBanner를 부르니, 브라우저에선 멀쩡하다가 실기기에서만 배너가 안 뜨더군요. 예외도 안 나고 조용히 실패해서 원인 찾기가 고약했습니다. 결국 onInitialized 콜백 안에서 attach하도록 옮겨 잡았습니다.

TossAds.initialize({
  callbacks: {
    onInitialized: () => {            // 동기 attach는 조용히 실패한다
      if (TossAds.attachBanner.isSupported()) {
        banner = TossAds.attachBanner(AD_GROUP_ID, adEl, { theme: "auto" });
      }
    },
  },
});

배너의 실제 렌더 높이는 ResizeObserver--ad-h CSS 변수에 반영해, 소리·탭바·공유·패널이 자동으로 그만큼 내려가게 했습니다. 광고가 UI를 덮지 않도록 한 거죠.

전면형은 빈도가 생명이라 에스컬레이션으로 뒀습니다. 슬라임 잡기 100회마다·최소 90초 간격으로 띄우되, 4회차부터는 300회·180초로 완화하고, 하루 총 20회를 넘기지 않습니다. 진입 후 60초는 아예 안 띄우고, 무엇보다 손을 뗀 순간에만 표시합니다. 주무르는 도중에 광고로 끊으면 이 앱의 유일한 가치인 촉감이 깨지니까요.

마지막은 사람이 밟기 쉬운 함정, 앱 아이콘이었습니다. 2026-08-04 심사 피드백은 "크롭되지 않은, 배경색까지 꽉 찬 정사각형"을 요구했는데, 제가 낸 건 둥근 카드와 흰 여백이 이미 그려진 이미지였습니다. 플랫폼이 알아서 둥글릴 자리를 제가 미리 둥글려 버린 거죠. 풀블리드 정사각으로 다시 만들어 재제출했습니다. 에셋도 결이 같은 문제라 처음부터 CC0만 썼습니다. CC-BY 곰돌이·데이지 모델은 표기 의무가 생겨 폐기하고, 표기가 필요 없는 CC0 에셋으로 갈아끼웠죠. 무료 토이 하나 올리는 데도, 코드보다 이런 '규칙의 함정'을 지우는 일이 더 많았습니다.