RORK LABEN
DEADLINE — Google Play は2026年8月31日以降、新規アプリと既存アプリの更新の双方に対象 API レベル36(Android 16)以上を要求します。残り13日ですEXTENSION — 期限に間に合わない場合は Play Console の期限延長フォームから申請することで11月1日まで配信を継続できます。延長は自動ではなく、申請自体を期限内に行う必要がありますTARGET SDK — Rork などのビルダーが出力する Expo / React Native アプリでも targetSdkVersion は自分で確認が必要です。テンプレートが古い SDK に固定されていると生成しただけでは要件を満たしませんPOLICY — スパムと最低限の機能に関するポリシーが改定され、高品質な機能とコンテンツ体験が求められます。薄いアプリを量産する運用は正面から影響を受ける領域ですPRIVACY — 収集するデータの項目・用途・第三者への共有の有無について詳細な説明が求められます。ビルダーが自動で組み込む解析 SDK や広告 ID も申告の対象ですRORK — 従来の Rork が React Native / Expo を出力するのに対し、Rork Max は SwiftUI を生成します。無料枠から始められ、有料プランは月25ドルからですDEADLINE — Google Play は2026年8月31日以降、新規アプリと既存アプリの更新の双方に対象 API レベル36(Android 16)以上を要求します。残り13日ですEXTENSION — 期限に間に合わない場合は Play Console の期限延長フォームから申請することで11月1日まで配信を継続できます。延長は自動ではなく、申請自体を期限内に行う必要がありますTARGET SDK — Rork などのビルダーが出力する Expo / React Native アプリでも targetSdkVersion は自分で確認が必要です。テンプレートが古い SDK に固定されていると生成しただけでは要件を満たしませんPOLICY — スパムと最低限の機能に関するポリシーが改定され、高品質な機能とコンテンツ体験が求められます。薄いアプリを量産する運用は正面から影響を受ける領域ですPRIVACY — 収集するデータの項目・用途・第三者への共有の有無について詳細な説明が求められます。ビルダーが自動で組み込む解析 SDK や広告 ID も申告の対象ですRORK — 従来の Rork が React Native / Expo を出力するのに対し、Rork Max は SwiftUI を生成します。無料枠から始められ、有料プランは月25ドルからです
記事一覧/アプリ開発
アプリ開発/2026-08-03上級

1.10.0 の端末が強制アップデートに落ちる — 最小サポートバージョン判定を4実装で実測する

リモート設定で最小サポートバージョンを引き上げるゲートを、バージョン比較の4実装で実測しました。localeCompare が同値と言い切る箇所、fail-open の境界、審査中バージョンを閾値にした事故までを記録しています。

Expo171React Native227リモート設定3バージョン管理アプリ運用4

プレミアム記事

強制アップデートの画面が出たまま先へ進めない、という問い合わせが届いたのは、バージョン 1.10.0 を配信した数日後でした。

こちらの管理画面では最小サポートバージョンを 1.9.0 に設定しています。1.10.0 は当然それより新しい。落ちる理由がありません。

手元の端末で再現しませんでした。1.10.0 を入れた自分の iPhone は素通りします。再現したのは、ゲートの判定関数を切り出して Node で走らせたときでした。"1.10.0" < "1.9.0"true を返します。

文字列として比べれば "1" の次の文字が "1""9" の勝負になり、"1" < "9" で 1.10.0 の負けです。当たり前の話なのですが、9 系のマイナーバージョンを踏むまでの1年半、この条件は一度も偽になりませんでした。バグは静かに待っていました。

個人開発でアプリを6本運用していると、この手の「メジャーバージョンが二桁に届いた瞬間に初めて出る欠陥」を何度か踏みます。今回は判定の実装そのものを実測で選び直したので、その過程を残しておきます。

バージョン文字列を比べる方法は4通りあり、3つは壊れる

まず素朴に思いつく実装を並べます。文字列比較、localeCompare の numeric オプション、parseFloat、セグメントごとの数値比較の4つです。

const sgn = (n) => (n < 0 ? -1 : n > 0 ? 1 : 0);
 
// ① 文字列としてそのまま比較
const naive = (a, b) => sgn(a < b ? -1 : a > b ? 1 : 0);
 
// ② localeCompare の numeric オプション
const loc = (a, b) => sgn(a.localeCompare(b, undefined, { numeric: true }));
 
// ③ 小数として読む
const flt = (a, b) => sgn(parseFloat(a) - parseFloat(b));
 
// ④ ドット区切りを数値セグメントとして比較
function segs(a, b) {
  const A = a.split("."), B = b.split(".");
  const n = Math.max(A.length, B.length);
  for (let i = 0; i < n; i++) {
    const x = parseInt(A[i] ?? "0", 10) || 0;
    const y = parseInt(B[i] ?? "0", 10) || 0;
    if (x !== y) return sgn(x - y);
  }
  return 0;
}

これを、実運用で実際に現れるバージョン文字列の組み合わせに当てました。Node v22.22.3 での結果です。-1 は左が小さい、1 は左が大きい、0 は同値を意味します。

比較ペア① 文字列② localeCompare③ parseFloat④ セグメント
1.9.0 vs 1.10.01-11-1
1.0 vs 1.0.0-1-100
1.02.0 vs 1.2.0-10-10
2.0.0 vs 2.0.0.1-1-10-1
1.4.0 vs 1.41100
1.4.0-beta.1 vs 1.4.01101
10.0.0 vs 9.99.99-1111
1.2.10 vs 1.2.9-1101
3.0.0 vs 3.01100
1.10 vs 1.9-11-11

太字が期待と食い違った箇所です。①は12ペア中6つ、③は5つを取り違えます。

私が事故を起こしたのは①でした。10.0.0 vs 9.99.99 も同じ形で壊れます。メジャーが二桁へ届いた瞬間、そのアプリの全ユーザーが一斉に「古い」と判定されます。

③の parseFloat1.2.101.2 として読むため、パッチバージョンが丸ごと消えます。1.2.10 vs 1.2.9 が同値になる理由がこれです。二番目のドット以降が存在しないものとして扱われます。

localeCompare が「同じです」と言い切る二つの場所

②の localeCompare(b, undefined, { numeric: true }) は、①と③が壊れた数値まわりを全て正しく処理します。1.9.0 vs 1.10.010.0.0 vs 9.99.99 も期待どおりです。ここまで見ると、これで十分に思えます。

私も一度これで書き直しかけました。実際に表へ落としてから気づいた箇所が二つあります。

一つ目。1.02.01.2.00(同値) と判定します。numeric コレーションは数字の並びを数値として読むため、先頭のゼロが無視されます。ビルド番号を桁揃えして 1.02.0 のように振る運用をしていると、二つの別バージョンが区別できません。

二つ目のほうが実害があります。1.4.01.4 を比べると 1(左が大きい) を返します。セグメント数の違いが、そのまま大小の差として扱われます。

Android の versionName1.4 のような二桁表記を使っている場合、リモート設定の閾値を 1.4.0 と書いた瞬間、1.4 の端末は「閾値より古い」と判定されます。同じバージョンを指しているのに、表記のゆれだけで強制アップデート対象へ落ちます。

これは私の予想と逆でした。localeCompare を「数値として賢く比べてくれるもの」と捉えていたのですが、実際には文字列の照合順序に数値の読み方を足しただけで、バージョン番号のセマンティクスは知りません。セグメントが欠けたら短いほうが小さい、というのは辞書順の規則であってバージョン番号の規則ではありません。

④のセグメント比較はこの二つを正しく処理します。ただし 1.4.0-beta.1 vs 1.4.01 を返します。parseInt("0-beta")0 を返し、そのあと4番目のセグメントに 1 が残るため、ベータ版がリリース版より新しいと判定されます。プレリリースを配る運用があるなら、④も素のままでは使えません。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
バージョン比較の4実装を12ペアで実測した対照表。localeCompare が「1.02.0 と 1.2.0 は同値」「1.4.0 は 1.4 より大きい」と判定する箇所まで含めて掲載
16ケースを通した比較関数の完全なコードと、20万回で286.4ms という測定値
リモート設定の取得が遅れたときに fail-open へ倒す実装と、1,500ms のバジェットで切り替わる境界の実測
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

アプリ開発2026-08-16
グラフが壊れて見えるのは、データが多いときではなく最初の数日でした
記録アプリの折れ線グラフが、データの少ない最初の数日だけ線が消える。原因はY軸の幅が0になり座標がNaNになることでした。実測した挙動と、描画前に置く小さな正規化層を紹介します。
アプリ開発2026-08-06
壁紙の上の文字が読めるかを、端末ではなく取り込み時に決める — 判定指標を4つ測り比べた記録
壁紙の上に置くステータスバーや見出しが読めるかを、端末での画像解析ではなく取り込み時のメタデータで決める設計です。240枚のコーパスで判定指標を4つ実測し、縮小画像での判定が何と一致するのかまで記録しました。
アプリ開発2026-07-14
壁紙を長押しして、プレビューと保存メニューを出す — Rork の Expo アプリで context menu を組む
壁紙ギャラリーのカードを長押ししても何も起きない。iOS利用者はプレビュー付きのメニューを期待します。Pressableだけでは届かない理由から、zeegoでのネイティブcontext menu実装、スクロールとの競合解消、保存・共有アクション、Android向けの自前フォールバックまで、動くコードでまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →