Expo(React Native)アプリをGoogle Playに公開するまで ─ DevPickの構成を流用して4日で出した「終業ログ」と、それでも詰まった箇所

当ページのリンクには広告が含まれています。
この記事の結論
1本目のアプリ(DevPick)と同じ技術構成を流用したことで、2本目のアプリ「終業ログ」は着手から4日でGoogle Playに公開できました。ただ、流用できたのは「何を使うか」の部分で、ライブラリやビルド環境のバージョンの違い、1本目で記録していなかった初回だけの手続き、アプリごとに作り直す文書では、それぞれ詰まっています。2本目で効いたのは構成そのものより、1本目の経験を手順書と検査スクリプトに残しておくことだったと考えています。

お疲れ様です!IT業界で働くアライグマです!

個人開発している技術記事キュレーションサービス「DevPick」には、Expo(React Native)で作ったAndroidアプリがあります。この構成をそのまま流用して、2本目のAndroidアプリ「終業ログ」を作り、2026年10月10日の着手から10月14日にGoogle Playで公開しました。この記事では、何を流用できて、それでもどこで詰まったのかを、リポジトリに残っている記録をもとに書きます。

目次

2本目のアプリ「終業ログ」を作った理由

終業ログは、始業・休憩・終業をワンタップで記録し、終業時のチェックリストで一日を締めるAndroidアプリです。主な機能は次のとおりです。

  • 始業・休憩・終業の打刻と、あとからの記録の追加・修正
  • 終業チェックリスト(項目の編集、途中からの再開)と、チェックし終えたあとの「今日も終了」画面(その月の累計と、目標時間との差を表示)
  • 週・月ごとの勤務時間の集計
  • 終業予定と打刻忘れのお知らせ(端末のローカル通知)
  • 記録と設定のファイルへのバックアップと復元

何を作るかは、候補を調べてから決めました。最初に調べた段階の上位案は、シフト勤務のカレンダー、ペットのお世話記録、掃除のルーティン管理などでした。どれも、自分が使わないか、収益の見込みが弱いという理由で見送っています。そこで「自分が毎日使う」を条件に加えて調べ直し、終業のルーティンと勤務時間の記録を1つにまとめた案に決めました。

この案を選んだ理由は3つあります。

  • 自分が平日に毎日使う:会社員として平日に働いているので、毎日使いながら改善点を見つけられる
  • 1日に2回、自然に開く:始業と終業で開くので、広告だけで収益を得る形と相性が良い
  • サーバーが要らない:記録は端末の中だけに保存するので、最初の版を小さく作れる

収益を広告だけにしているのは、DevPickのときに決めた前提をそのまま引き継いでいるからです。アプリ内課金をすると、特定商取引法に基づく表記やGoogle Playの仕組みで自宅の住所が公開されることになります。この判断は個人開発アプリで課金をやめて広告のみにした判断で書いています。

技術は、DevPickと同じExpo(React Native)にしました。広告・障害の診断・ビルドと配布・スタイル・テストの構成をそのまま使えること、管理する技術を1つにそろえられることが理由です。実装は主にAI(Claude Code)に任せているので、AIの生成品質が安定しやすいTypeScriptとReactであることも決め手になっています。

IT女子 アラ美
「自分が使わないアプリは作らない」って、個人開発だと一番大事な条件かもね。

ITアライグマ
そう思います。自分で毎日使えば、不便なところに真っ先に気づけますからね。

DevPickから流用したものと、公開までの4日間

DevPickのアプリから流用したのは、次のものです。

領域 流用したもの
アプリの土台 Expo・TypeScript・Expo Routerのタブ構成、スタイルのNativeWind
テスト・CI Jestの単体テスト、Web版をPlaywrightで動かすE2E、GitHub ActionsのCI、PRへのAI自動コードレビュー
収益・診断 AdMobのバナー広告(react-native-google-mobile-ads)、Firebase Crashlytics
ビルド・配布 EAS Build/EAS Submit、リリース手順書の流れ、Play APIで提出するためのサービスアカウント
アカウント Google Playのデベロッパーアカウント

一方で、DevPickのアプリID・広告ID・Firebaseの設定は持ち込んでいません。EASのプロジェクトもFirebaseのプロジェクトも、終業ログ専用に作り直しました。

公開までの流れを、コミットの記録から日ごとにまとめると次のとおりです。

日付 やったこと
10月10日 Expoの土台・3つのタブ・CI・自動コードレビューを用意。端末内への保存の基盤、打刻、終業チェックリスト、勤務履歴の集計、設定画面を実装
10月11日 「今日も終了」画面の月累計、通知、バックアップと復元を実装。AdMob広告とCrashlyticsを導入し、表示名・起動画面・ストア掲載文・提出前の検査スクリプト・リリース手順書を用意
10月12日 本番用ビルドを作って検査。Play Consoleでアプリを作り、内部テストに提出。スクリーンショットを作り、製品版を審査へ送信
10月14日 審査を通過し、Google Play(日本)で公開されたことを確認

アプリの機能は、2日目の午前までにひととおりそろっています。残りの時間の多くは、広告・障害の診断・ストアへの提出といった「公開するための作業」に使いました。

4日で出せた理由には、構成の流用のほかにアカウントの事情もあります。Google Playでは、新しく作った個人のデベロッパーアカウントだと、製品版を出す前に一定人数・一定期間のクローズドテストが求められます。終業ログは以前から持っているアカウントで出したので、この要件がかかりませんでした。また、実機での通しの動作確認は、初回の公開では省略しています。各機能はJestとPlaywrightのテストで確認したうえでの判断です。

IT女子 アラ美
機能は2日目の午前でそろって、そこから先は公開の準備ばっかりって、アプリあるあるだよね。

ITアライグマ
はい。構成を流用できても、ストアに出すための作業は思ったより残りました。

ビルドで詰まったこと ─ 流用元とのバージョン差分

同じ構成を使っていても、新しいリポジトリでパッケージを入れ直すと、DevPickとは違うバージョンが入ります。ビルドで詰まった3件は、どれもこの「流用元との差分」が原因でした。いずれも公開前、開発中のビルドで見つかっています。

NativeWindのスタイルがAndroidで効かない

最初の土台づくりの段階で、Androidで表示するとNativeWindで指定したスタイルが失われる問題が出ました。NativeWindは、JSXの変換とスタイルの登録に react-native-css-interop という部品を使います。アプリから見たこの部品と、NativeWindから見たこの部品が別のものとして読み込まれる「二重ランタイム」の状態になり、スタイルの登録が画面に届いていませんでした。

DevPickではNativeWindのバージョンを範囲指定(^4.2.6)にしていましたが、終業ログではNativeWindと react-native-css-interop の両方をバージョン固定にしてそろえました。あわせて、同じ問題が戻ってきたときに気づけるよう、2つの読み込み先が一致することを確かめるテストを入れています。

import { createRequire } from "node:module";
import path from "node:path";

test("JSX変換とNativeWindのスタイル登録が同じランタイムを使います", () => {
  const fromApp = createRequire(path.resolve(__dirname, "../package.json"));
  const fromNativeWind = createRequire(fromApp.resolve("nativewind/package.json"));
  expect(fromApp.resolve("react-native-css-interop/jsx-runtime")).toBe(
    fromNativeWind.resolve("react-native-css-interop/jsx-runtime"),
  );
});

広告SDKを入れたらGradleの段階でビルドが落ちる

AdMobの広告を入れたあと、EASでのAndroidビルドがGradleの段階で次のエラーになりました。

A problem occurred evaluating project ':react-native-google-mobile-ads'.
> Cannot get property 'googleMobileAdsJson' on extra properties extension as it does not exist

原因は、その時点の最新版だった react-native-google-mobile-ads 17.2.0の不具合です。app.json にこのライブラリ用のキーが無いと、Gradleの設定ファイルが誤った変数名に値を入れ、直後に未定義の変数を参照して失敗します。

DevPickでは起きていませんでした。DevPickは16系のバージョンを使っていて、広告のアプリIDも app.json に直接書いています。終業ログでは、本番とテストでIDを切り替えるために設定を app.config.js 側へ移していたので、app.json にキーが無い状態になり、この不具合に当たりました。

直し方は、問題の参照よりも先に評価されるGradleのプロパティに、ライブラリの既定値(classic)を明示的に書くことです。既定値なので動作は変わりません。Expoのconfig pluginとして書いています。

const { withGradleProperties } = require("expo/config-plugins");

const KEY = "RNGMA_ANDROID_BACKEND";

module.exports = function withGoogleMobileAdsBackend(config) {
  return withGradleProperties(config, (config) => {
    config.modResults = config.modResults.filter(
      (item) => !(item.type === "property" && item.key === KEY),
    );
    config.modResults.push({ type: "property", key: KEY, value: "classic" });
    return config;
  });
};

ライブラリ側で直れば、このpluginは消せます。そのこともコードのコメントに残しました。

EASのビルド環境で npm ci が失敗する

もう1件は、EASのビルド環境で依存パッケージのインストールが止まった問題です。

npm error `npm ci` can only install packages when your package.json and package-lock.json ... are in sync.
npm error Missing: @emnapi/core@1.11.3 from lock file
npm error Missing: @emnapi/runtime@1.11.3 from lock file

EASのビルド環境の既定はNode 22.23.1(npm 10.9.8)でした。一方、ロックファイルは手元とCIで使っているNode 24.13.0(npm 11)で作っています。npmのバージョンが違うため、ロックファイルが package.json と一致していないと判定されていました。eas.json のすべてのビルドプロファイルでNodeのバージョンを指定して解決しています。

{
  "build": {
    "production": {
      "node": "24.13.0"
    }
  }
}

(実際の設定から、関係する部分だけを抜き出した例です)

DevPickの eas.json には、Nodeのバージョンの指定がありません。流用元に書かれていない設定は、流用しても当然付いてきませんでした。

IT女子 アラ美
同じ構成のはずなのに、入ったバージョンが違って全部ハマるの、地味にキツいね。

ITアライグマ
はい。「同じ構成」は使うライブラリの名前が同じというだけで、中身のバージョンまでは同じになりませんでした。

公開手続きで詰まったこと ─ 記録していなかった初回だけの手順

ストアへの提出では、「アプリを新しく作るときに一度だけ必要な手順」や、普段の更新では通らない手順で詰まりました。どれも、DevPickのリリース手順書には書かれていなかったものです。

最初のApp BundleはAPIで提出できない

DevPickでは、ビルドしたApp Bundle(AAB)を eas submit でGoogle Playへ送っています。中ではGoogle PlayのAPIを使うので、そのためのサービスアカウントも流用しました。ところが、Google PlayのAPIは新しいアプリの最初のApp Bundleを受け付けません。初回だけは、Play Consoleの内部テストの画面からAABを手で上げる必要があります。

終業ログでは、初回をPlay Consoleから手でアップロードし、2回目以降は eas submit で送る、という手順をリリース手順書に書き足しました。

ストアのアイコンは32ビットPNGが要件

ストアの掲載用画像は、アプリのアイコンと同じ図柄からスクリプトで生成しています。このスクリプトが、アイコンを24ビットのPNG(アルファチャネルなし)で書き出していました。Google Playの要件を整理すると、画像の種類ごとに形式が違います。

素材 サイズ 形式
アプリアイコン 512×512 32ビットPNG(アルファ付き)、1MB以下
フィーチャーグラフィック 1024×500 JPEGまたは24ビットPNG(アルファなし)、15MB以下
スクリーンショット 2〜8枚、16:9または9:16 JPEGまたは24ビットPNG(アルファなし)、8MB以下

アイコンだけがアルファ付きで、ほかの2つはアルファなしです。生成スクリプトでは、アイコンを保存するときだけRGBAに変換するようにしました。

# Playのアプリアイコンは32ビットPNG(アルファ付き)が要件のため、RGBAで保存します。
store_icon(mark).convert("RGBA").save(os.path.join(OUT, "icon-512.png"), optimize=True)

公開後に気づいたアイコンの見た目と、製品版の差し替え

アイコンの見た目の問題には、公開された1.0.0で気づきました。時計の図柄の時針と分針が中心でつながっていなかったことと、ランチャーで表示すると角丸の外側に薄い背景色の枠が出ていたことです。Androidのアダプティブアイコンは、前景の画像と背景色を重ねてから、端末ごとの形に切り抜きます。それまでの設定は背景色が薄い色だったので、前景の図柄の外側にその薄い色がはみ出し、枠のように見えていました。

修正版の1.0.1では、時計の図柄を図形から描き直すスクリプトを作り、前景を白い時計だけにしました。地色は、アダプティブアイコンの背景色の設定を濃い色に変えて塗っています。

この1.0.1を審査に出したあと、同じ日のうちに日時の入力を選択式に変えた1.0.2を作りました。ここでもう1つ、初めての手順に当たっています。製品版に未公開のリリース(審査に出した1.0.1)が残っている間は、内部テストのリリースを製品版へ昇格できません。製品版の未公開リリースを開き、App Bundleを1.0.2のものに差し替えてから、審査に送り直しました。リリースノートは eas submit では設定できないので、Play Consoleで入力しています。

DevPickのストア審査では、ニュースアプリ向けのポリシーで否承認を受けています(Google Playのニュース&雑誌ポリシーで否承認された話)。終業ログは最初の審査で通りましたが、詰まったのは審査の中身ではなく、その手前の手続きでした。

IT女子 アラ美
初回だけ手動とか、アイコンだけアルファ付きとか、一回やったら忘れそうなものばっかりね。

ITアライグマ
そうなんです。手順書に書かれていないと、流用元があっても頼れませんでした。

流用できなかった作業 ─ プライバシーポリシーと申告

技術の構成と違って、アプリごとに中身が変わる文書や申告は、最初から流用できません。終業ログで作り直したのは次の3つです。

プライバシーポリシーの置き場所

設定画面からプライバシーポリシーを開けるようにした時点では、本文をこのブログ(it-araiguma.com)の固定ページに置いていました。そのあとCrashlyticsを入れたので、障害の診断で何を送り、何を送らないかを本文に書き足す必要が出ました。Crashlytics入りのビルドを配る前に、記載を済ませておかなければなりません。

書き足すついでに、置き場所自体を見直しました。ブログとアプリは別の取り組みなので、アプリの規約をブログの固定ページで管理すると、ブログ側にアプリの作業が混ざります。本文を変えるたびにWordPressの管理画面で手作業が要るのも負担です。比べた案は次の3つです。

案 内容 判断
Firebase Hosting Crashlytics用に作ったFirebaseプロジェクトで、リポジトリ内の静的HTMLを公開する 採用。無料枠の範囲で、リポジトリを非公開のまま公開できる
GitHub Pages リポジトリからページを公開する 見送り。非公開リポジトリでは有料プランが要り、公開用のリポジトリを分けると管理先が増える
ブログの固定ページのまま Crashlyticsの記載だけを足す 見送り。作業は最小だが、ブログとアプリの作業が混ざったままになる

Firebase Hostingに移したことで、プライバシーポリシーの変更もアプリのコードと同じようにPRでレビューできるようになりました。アプリの設定画面のリンク、ストアの登録内容、AdMobの同意メッセージは、すべて新しいURLにそろえています。

データセーフティの申告

Google Playのデータセーフティでは、アプリが集めるデータを申告します。終業ログの勤務記録・チェックリスト・設定は端末の中だけに保存し、運営者は受け取らないので、申告の対象にはなりません。申告するのは、組み込んだSDKが集めるデータです。AdMobとFirebaseについてはGoogleが開示資料を出しているので、それに沿って次の内容を申告しました。

  • おおよその位置情報(IPアドレスからの推定):AdMob
  • アプリの操作(起動・タップ・広告の表示):AdMob
  • クラッシュログ:Crashlytics
  • 診断情報:AdMob、Crashlytics
  • 広告IDなどのデバイスの識別子:AdMob、Crashlytics

申告とプライバシーポリシーの記載は、内容をそろえておく必要があります。そのため、Crashlyticsの記載をプライバシーポリシーに載せ終えるまでは、審査に送らない順番にしました。あわせて、対象年齢は18歳以上(仕事の記録が目的で、子ども向けではない)、広告は「含まれる」と申告しています。

app-ads.txt

AdMobは、Google Playに登録した「ウェブサイト」のドメインに置かれた app-ads.txt を見て、広告の販売者が正しいかを確かめます。終業ログはウェブサイトをこのブログのドメインにしたので、ブログの app-ads.txt に終業ログ用の1行を足しました。ブログの広告用に書いてあった既存の行は残しています。DevPick用のサブドメインにも同じ形式のファイルがありますが、ストアに登録したサイトとは別なので、そちらは使えませんでした。

IT女子 アラ美
コードは使い回せても、ポリシーとか申告はアプリごとに書き直すしかないのね。

ITアライグマ
はい。しかも申告とポリシーの内容をそろえる必要があるので、公開の順番にも影響しました。

2本目で分かった、構成の流用が効く範囲

振り返ると、流用が効いたものと効かなかったものは、次のように分かれました。

分類 例 流用の効き方
何を使うか Expo、NativeWind、AdMob、Crashlytics、EAS、テストとCI そのまま効いた。技術選定で迷う時間がほぼ無かった
どのバージョンで動かすか NativeWindの関連部品、広告SDK、EASのビルド環境のNode 効かなかった。新しく入れた最新版や、流用元に書かれていない既定値で詰まった
初回だけの手続き 最初のAABの手動アップロード、製品版リリースの差し替え 効かなかった。流用元の手順書に記録が無かった
アプリごとの文書 プライバシーポリシー、データセーフティ、app-ads.txt そもそも流用できない。中身を作り直す必要がある

構成を流用するときにもう1つ気をつけたのが、流用元の値の持ち込みです。設定ファイルをコピーして始めると、DevPickの広告IDやFirebaseの設定がそのまま残る危険があります。テスト用の広告IDのまま公開すると収益が発生しませんし、別アプリのFirebase設定を使うと障害の記録が混ざります。そこで終業ログでは、本番ビルドの設定を読み込む段階で検査し、条件に合わなければビルドを止めるようにしました。次のコードは、実際の設定ファイルから広告IDの検査部分を抜き出して短くした例です。

// Google公式のテスト用広告IDのパブリッシャー
const TEST_PUBLISHER = "ca-app-pub-3940256099942544";
const publisherOf = (id) => id.split(/[~/]/)[0];

function verifyProductionAds(appId) {
  if (!/^ca-app-pub-\d+~\d+$/.test(appId) || publisherOf(appId) === TEST_PUBLISHER) {
    throw new Error("Set ADMOB_ANDROID_APP_ID to the workday-closer AdMob app ID for production builds");
  }
  // 広告ユニットIDの形式と、アプリIDと同じアカウントかどうかも確かめる(省略)
}

Firebaseの設定ファイルも、中に終業ログのパッケージ名のアプリが含まれているかを照合しています。

さらに、ストアに出す直前のAABも、提出前にスクリプトで検査しています。確かめている項目は次のとおりです。

  • パッケージ名・表示名・versionNameが設定ファイルと一致し、versionCodeが前回の提出より大きいこと
  • 本番の広告IDが入っていて、テスト用のIDが残っていないこと
  • 終業ログのFirebase設定とCrashlyticsが入っていること
  • Crashlyticsの送信確認のために一時的に入れる、わざとクラッシュさせるコードが残っていないこと

公開後の1.0.1と1.0.2も、この検査を通してから提出しました。提出した内容はリリース手順書の「実施記録」に、日付・バージョン・結果の形で残しています。

2本目を作ってみて分かったのは、構成の流用で省けるのは「何を使うか決める時間」までだということです。バージョンの違い、初回だけの手続き、アプリごとの文書は、流用元があっても自分で通る必要があります。そのとき助けになるのは流用元のコードより、流用元で通った手順と判断が書き残されているかどうかでした。DevPickの手順書に無かったものは、今回終業ログの手順書に書き足しています。

IT女子 アラ美
結局、流用元のコードより、流用元で書き残した手順書の方が2本目で効くってことね。

ITアライグマ
はい。1本目のときに面倒でも書き残しておけば、2本目でそのまま使えたはずでした。

まとめ

DevPickのExpo構成を流用して、2本目のAndroidアプリ「終業ログ」を着手から4日でGoogle Playに公開しました。技術を選ぶ手間はほぼ無くなり、機能は2日目の午前までにそろっています。

それでも、次の3つの場面では詰まりました。

  • バージョンの差分:NativeWindの二重ランタイム、広告SDK 17.2.0のGradleエラー、EASのビルド環境のNodeの違い
  • 初回だけの手続き:最初のAABはAPIで提出できない、ストアのアイコンは32ビットPNG、製品版に未公開のリリースが残っていると昇格できない
  • アプリごとの文書:プライバシーポリシーの置き場所、データセーフティ、app-ads.txt

構成の流用は、技術選定の時間を省くのには十分に効きます。ただ、2本目を速く出すために本当に効くのは、1本目で通った手順と判断を書き残しておくことだと、今は考えています。

IT女子 アラ美
次にアプリを作るときは、終業ログの手順書から始められるってわけね。

ITアライグマ
はい。DevPickの手順書に無かった初回だけの手順も、今は書き足してあります。

運営サービス・姉妹メディア

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次