広告とサブスクを同じアプリに入れる、というアイデアに対して、私自身は長らく否定的でした。「広告でうるさいアプリにサブスクを入れても、ユーザーは離れていくだけだろう」と思っていたからです。
ところが、ここ1年ほど自分のアプリで実験を重ねた結果、設計を間違えなければ広告とサブスクは喧嘩しない、という結論に至りました。むしろ「広告だけ」「サブスクだけ」のときと比べて、月の総収益が1.7倍ほどに伸びています。Rork で開発しているアプリも、現在はハイブリッド構成にしているものが大半です。
ここでは個人開発者として実際に組み立てているハイブリッド収益モデルを、設計の意図と、Rork が生成する React Native コードに私が手で足している実装と一緒にお話しします。広告の出し分けやサブスク状態の同期は、Rork が自動で書いてくれる範囲の外側にあります。だからこそ、ここを丁寧に詰められるかどうかが、個人開発の収益を左右すると感じています。
なぜ「広告かサブスクか」という二択は時代遅れなのか
そもそも、広告かサブスクかという二択は、ユーザーを単一の集団として扱う前提で成立する話です。しかし実際のアプリ利用者は、課金意欲・利用頻度・広告耐性のすべてが大きくばらついています。
広告に強い耐性があり、無料で使い続けたい層。広告は嫌いだが、月数百円は払いたくない層。月額数百円なら払うが、年額一括は心理的な抵抗がある層。年額で買い切りたい層。最初に1度だけ買い切って、以後は何も払いたくない層。
少なくともこの5つの層は、私の運用しているアプリで明確に観測できます。1つのプランしか用意しないと、4層を切り捨てることになります。ハイブリッド設計は、この層分けに正面から応えるためのものです。
私が運用している壁紙系アプリの実数値で、この層の分布を共有しておきます。あくまで私の1アプリの一例ですが、設計の妥当性を考える叩き台にはなるはずです。
| ユーザー層 | 構成比 | 1人あたり月間収益(目安) | 収益貢献度 |
| 課金しない(広告のみ) | 約80% | 10〜25円 | 全体の約35% |
| サブスク加入 | 約8% | 280〜480円 | 全体の約50% |
| 買い切り課金 | 約4% | 初月のみ約120円相当 | 全体の約8% |
| 広告のみで定着 | 約8% | 15〜30円 | 全体の約7% |
この表で私がいちばん伝えたいのは、収益の半分はサブスクの8%が、もう半分は課金しない層の薄い積み上げが支えている、という二層構造です。どちらか一方だけを最大化しようとすると、もう一方を「捨てる」設計になります。ハイブリッドは、この両方を同時に拾うための工夫です。
「広告とサブスクの境界」は機能で切らない
ハイブリッド設計でやってしまいがちな失敗は、機能で境界を切ることです。「機能 A は無料で広告あり、機能 B はサブスクのみ」という線引きは、ユーザーから見ると「サブスクを買わないと使わせない機能を増やしているだけ」に映ります。
私が採用している境界の切り方は、頻度と質です。
頻度面では、たとえば「壁紙の保存は1日3枚まで無料、4枚目以降は広告視聴か、サブスク加入で解放」とします。機能そのものは全員に解放されており、上限を超えたいときに2つの選択肢が用意されている形です。広告で乗り切りたい人と、煩わしさを月額で買い取りたい人の両方に居場所があります。
質面では、たとえば「無料は標準解像度、サブスクは4K対応」とします。これも機能を奪っているわけではなく、より高品質を求めるユーザーが選べる構造です。
この2つの軸で境界を切ると、ユーザーは「サブスクに強制誘導されている」とは感じにくくなります。私の体感ですが、機能で線を引くやり方と比べて、サブスクのコンバージョン率が顕著に高くなりました。
日次頻度上限の実装
頻度の境界は、概念だけ語っても実装で崩れます。私が React Native(Rork が生成するのはこの形です)で組んでいる、最小限の日次カウンタを共有します。日付が変わったらリセットし、無料上限を超えたら「広告で解放」か「サブスクで解放」を返す関数です。
// useDailyQuota.ts — 日次の無料利用回数を管理する最小実装
import AsyncStorage from "@react-native-async-storage/async-storage";
const FREE_DAILY_LIMIT = 3;
function todayKey() {
// 端末ローカルの日付で1日を区切る(UTC で切ると深夜にズレます)
const d = new Date();
return `quota:${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
}
export async function getUsedCount(): Promise<number> {
const raw = await AsyncStorage.getItem(todayKey());
return raw ? parseInt(raw, 10) : 0;
}
export async function consumeOne(): Promise<void> {
const key = todayKey();
const used = await getUsedCount();
await AsyncStorage.setItem(key, String(used + 1));
}
// 上限を超えているか、超えているなら「広告」と「サブスク」のどちらで解放できるかを返す
export async function checkQuota(isSubscriber: boolean) {
if (isSubscriber) return { blocked: false, unlockBy: "subscription" as const };
const used = await getUsedCount();
if (used < FREE_DAILY_LIMIT) return { blocked: false, unlockBy: "free" as const };
return { blocked: true, unlockBy: "ad" as const };
}
ここで意図的に避けているのは、サーバー側でカウントを持つことです。個人開発の初期段階では、端末ローカルで十分に機能します。サーバーを挟むと、起動のたびに通信が走り、オフラインで壁にぶつかったユーザーが混乱します。まずは AsyncStorage で始め、不正利用が無視できない規模になってからサーバー検証を足す、という順番を私は勧めます。
なぜ日付を端末ローカルで切っているかというと、UTC で日付を区切ると、日本のユーザーは深夜9時にカウントがリセットされてしまうからです。実際に私はこれで「夜だけ上限が早く戻る」という挙動を作ってしまい、レビューで指摘されて気づきました。小さなことですが、頻度設計の信頼性に直結します。
広告フォーマットは「邪魔にならない場所」を最優先
広告とサブスクが喧嘩する典型は、広告がユーザー体験を壊しているケースです。アプリを起動した瞬間に全画面の動画広告、画面遷移ごとにポップアップ、というアプリは、サブスクの価値を「広告が消えること」に矮小化します。
私が個人開発で採用している広告配置は、概ね次の通りです。
バナー広告は最小限。画面下部に常時表示するのではなく、リスト末尾やセッション切れ目だけに置きます。インタースティシャル広告は、ユーザーがタスクを終えた瞬間にだけ挟みます。タスク途中で出すのは厳禁です。リワード広告は、ユーザーが自発的に「広告を見る代わりに何かをアンロックする」場面でのみ使います。
この配置だと、広告そのものへの不快感は強くなりません。結果として、サブスクが「広告から逃げるための駆け込み寺」ではなく、「もっと快適に使うためのアップグレード」として認識されるようになります。
リワード広告で上限を解放する実装
頻度上限に達したユーザーに「広告を見れば1回分解放されます」と提示する箇所は、ハイブリッド設計の心臓部です。リワード広告のコールバックで、報酬が確定したときだけカウンタを巻き戻します。視聴を途中でやめたユーザーには解放しない、という線引きが大事です。
// unlockByRewardedAd.ts
import { RewardedAd, RewardedAdEventType, TestIds } from "react-native-google-mobile-ads";
import AsyncStorage from "@react-native-async-storage/async-storage";
// 本番では自分の広告ユニット ID に置き換えます(テスト中は TestIds.REWARDED 推奨)
const AD_UNIT_ID = __DEV__ ? TestIds.REWARDED : "YOUR_REWARDED_AD_UNIT_ID";
export function unlockByRewardedAd(onUnlocked: () => void) {
const rewarded = RewardedAd.createForAdRequest(AD_UNIT_ID, {
requestNonPersonalizedAdsOnly: true, // 同意状況に応じて切り替えます
});
const loaded = rewarded.addAdEventListener(RewardedAdEventType.LOADED, () => {
rewarded.show();
});
// 報酬が確定したときだけ「使用済みカウントを1つ戻す」
const earned = rewarded.addAdEventListener(RewardedAdEventType.EARNED_REWARD, async () => {
const key = todayKeyFromUseDailyQuota(); // useDailyQuota の todayKey と必ず一致させる
const used = parseInt((await AsyncStorage.getItem(key)) ?? "0", 10);
await AsyncStorage.setItem(key, String(Math.max(0, used - 1)));
onUnlocked();
});
rewarded.load();
return () => {
loaded();
earned();
};
}
EARNED_REWARD 以外のイベント(閉じた・失敗した)では絶対に解放しないこと。ここを横着して「広告を表示したら解放」にすると、広告を1秒で閉じても解放されてしまい、リワード広告の収益が立たなくなります。私は最初これをやってしまい、eCPM が想定の半分以下に沈んで原因究明に丸2日を溶かしました。
サブスク状態の確認と広告ロードの順序 — 私が実際に踏んだ落とし穴
ここが、この記事でいちばんお伝えしたい実装です。
最初の実装で、サブスク加入ユーザーにも一部の広告が表示されてしまっていた時期がありました。原因は、AdMob 側の「広告を表示するかどうか」のフラグと、サブスク状態の同期が遅延していたことです。アプリ起動直後の数秒間、サブスク状態の確認が完了する前に広告のロードが走っていて、有料ユーザーに広告を見せていました。
修正は、広告ロードを「サブスク状態の確認完了後」に意図的に遅らせることでした。1〜2秒のロード遅延は許容範囲ですが、有料ユーザーに広告を見せるのは絶対に許されません。これは Rork のように高速で開発できる環境ほど踏みやすい罠で、初日から両者の状態管理を慎重に設計するべきだと痛感しました。
実装としては、サブスク状態を解決する Promise を起点にして、それが「非加入」と確定するまで広告の初期化を待たせます。
// adGate.ts — サブスク状態の確定を待ってから広告を初期化する
import mobileAds from "react-native-google-mobile-ads";
import { getSubscriptionState } from "./subscription"; // RevenueCat 等のラッパー
let adsReady = false;
export async function initAdsAfterSubscriptionCheck() {
// ここで必ず「確定値」を待つ。タイムアウトしても "non_subscriber" に倒さない
let state: "subscriber" | "non_subscriber" | "unknown" = "unknown";
try {
state = await Promise.race([
getSubscriptionState(),
new Promise<"unknown">((r) => setTimeout(() => r("unknown"), 4000)),
]);
} catch {
state = "unknown";
}
// 加入者、または「不明」のときは広告を初期化しない(疑わしきは表示しない)
if (state === "subscriber" || state === "unknown") {
adsReady = false;
return;
}
await mobileAds().initialize();
adsReady = true;
}
export function canShowAds() {
return adsReady;
}
この設計の肝は、状態が「不明(unknown)」のときに広告を表示しない、という一点です。通信が遅い・タイムアウトした、という状況で non_subscriber に倒すと、まさに有料ユーザーへの広告表示が起きます。広告収益を1回取りこぼすことより、有料ユーザーの信頼を1回壊すことのほうが、はるかに高くつきます。疑わしいときは表示しない、を徹底してください。
サブスクの設計は「最初の30日」で完結させる
ハイブリッド設計でサブスクのコンバージョンを高めるには、最初の30日のユーザー体験設計が決定的です。具体的には、以下の3つを押さえています。
最初の3日は、広告を意図的に少なめに表示します。新規ユーザーがアプリを覚える期間に、広告でうんざりさせるのは長期的に損です。
7日目に、無料での頻度上限に達するように設計します。早すぎても遅すぎてもいけません。早すぎるとアプリの価値を理解する前に壁にぶつかり、遅すぎるとサブスクを検討する前に習慣化が完了します。7日目あたりが、私のアプリでは最もコンバージョン率が高い窓でした。
14日目に、限定オファーを提示します。「最初の月だけ50%オフ」のような形で、まだ離脱していないユーザーを最後に押します。この時点で離脱しなかったユーザーは、本気で使い続ける可能性が高いので、オファーで取り込む価値が十分にあります。
30日を超えても加入しないユーザーは、サブスクではなく「買い切り」「広告のみ」など別の選択肢を提示します。サブスクに固執すると、機会を逃します。
この出し分けは、インストールからの経過日数で素直に分岐できます。私が使っているのは、初回起動時に保存したインストール日との差分で「今、どのオファーを出すべきか」を返す関数です。
// offerStage.ts — インストールからの経過日数でオファーを出し分ける
import AsyncStorage from "@react-native-async-storage/async-storage";
export async function ensureInstallDate() {
const existing = await AsyncStorage.getItem("install_date");
if (!existing) await AsyncStorage.setItem("install_date", String(Date.now()));
}
export async function currentOffer(isSubscriber: boolean) {
if (isSubscriber) return "none";
const raw = await AsyncStorage.getItem("install_date");
const installed = raw ? parseInt(raw, 10) : Date.now();
const days = Math.floor((Date.now() - installed) / 86_400_000);
if (days < 3) return "quiet"; // 広告控えめ・オファー出さない
if (days < 7) return "soft_hint"; // サブスクの存在を軽く案内
if (days < 14) return "cap_reached"; // 頻度上限の文脈で正式に提案
if (days < 30) return "discount_50"; // 初月50%オフの限定オファー
return "alt_oneshot"; // 買い切り・広告のみへ誘導
}
完璧な設計を初日から作り込む必要はありません。まずは days < 14 と days >= 14 の2分岐だけでも、サブスクの提案タイミングは大きく改善します。私も最初は2分岐から始めて、数字を見ながら段階を足していきました。
「課金しない層」も丁寧に扱う
ハイブリッド設計の真価は、課金しないユーザーに対しても、ちゃんと収益が立つ点にあります。
冒頭の分布表で見たとおり、私のアプリでは課金しない層が約80%を占めます。サブスク加入だけを目標にすると、この80%を「捨てる」設計になります。しかし広告で80%から少しずつ収益が立ち、サブスクは8%から大きく収益が立つ、という二層構造にすると、月の総収益が安定します。
具体的には、課金しないユーザーには「広告は最小限、ただし日次の頻度上限がある」という体験を、丁寧に保ちます。彼らも翌日にはまたアプリに戻ってきて、また広告を見てくれます。これは小さな収益ですが、ユーザー数が積み上がると無視できないボリュームになります。
私が AdMob を12年運用してきて確信しているのは、課金しない層を雑に扱うアプリは、長期的にレビュー評価が下がり、結局はサブスク層への入口も細る、ということです。無料ユーザーは「将来の有料ユーザーの母集団」でもあります。彼らの体験を守ることは、回り回ってサブスク収益を守ることでもあります。
締めくくり — 今日できる最小の一歩
すでに広告だけのアプリを運用している方は、まず「日次の頻度上限」を設定することから始めてみてください。この記事の useDailyQuota.ts のような構造を1つ追加するだけで、ハイブリッド設計の入り口に立てます。
その上で1ヶ月、コンバージョン率と離脱率を観察してみてください。数字が見えた段階で、サブスクを正式に追加するかどうかを判断するのが、無理のない順番です。最初から完璧なハイブリッド構成を作り込もうとすると、設計が壊れます。1つずつ、ユーザーの反応を見ながら積み上げていきましょう。
同じように個人開発で収益と向き合っている方の、設計の叩き台になれば幸いです。お読みいただきありがとうございました。