はじめに
React Native + Expo で実際に運用するアプリを1人で開発していると、AIコーディングの便利さと同時に、かなり厄介な問題に直面しました。
それは、
AIが新しいバグを修正する過程で、以前すでに直した機能を再び壊す
という問題です。
最初は単純に「AIが間違えた」と考えていました。
しかし、複数のAIツールを使いながら機能追加・リファクタリング・UI修正を繰り返すうちに、これは偶発的なミスではなく、AIを強く使う個人開発では継続的に発生しやすい回帰問題だと感じるようになりました。
この記事では、私が実際の React Native / Expo プロジェクトで導入した、非常にシンプルな回帰防止方法を共有します。
高度なテスト基盤の話ではありません。
むしろ、
- 1人開発
- 大きな
App.js - AIによる頻繁な修正
- テストコードが十分ではない
- Android Build 前に最低限の安全確認をしたい
という状況で使っている現実的な方法です。
1. 「アプリが起動する」と「プロダクトが正しい」は別
以前の私は、修正後の確認をだいたい次のようにしていました。
アプリ起動
↓
画面表示
↓
ボタンを押す
↓
エラーがない
↓
完了
しかし、FinTech 系の比較ロジックを持つアプリを作る中で、この確認方法では不十分だと分かりました。
少なくとも次の3つは分けて考える必要があります。
1. アプリは起動するか
2. 機能は動作するか
3. 結果は依然として正しいか
例えば、画面が正常に表示されても、
- 特定の市場だけ計算ロジックが消えている
- 高額帯だけ spread が過小評価される
- P2P とローカル取引所の分類が変わっている
- Adapter が削除され
undefinedが出る - Partner URL が旧URLに戻っている
ということがあります。
つまり、
クラッシュしない = 正常
ではありません。
2. 実際に起きた問題その1:Spread Floor が消えるリスク
私のプロジェクトでは、国際送金ルートを比較するために、通貨ペア・市場タイプ・金額帯ごとに spread を推定しています。
ある時、高額送金や一部の新興国市場で推定値が楽観的すぎる問題がありました。
そこで、次のような考え方を導入しました。
const CORRIDOR_SPREAD_FLOOR = {
// corridor tier × amount band
// minimum spread floor
};
目的は単純です。
例えば、
- corridor type
- amount band
- provider type
に応じて、最低 spread を適用することです。
概念的には次のような処理です。
const finalSpread = Math.max(
estimatedSpread,
corridorSpreadFloor
);
また、Dealer 型 Provider では同じ floor をそのまま適用せず、別係数を使うようにしました。
このロジックを実測値と比較しながら調整し、ある程度期待する結果に近づけました。
問題はその後です。
別の機能をAIに修正させた後、ふと疑問が出ました。
CORRIDOR_SPREAD_FLOORはまだ残っているのか?
AIは新しい問題を直していました。
アプリも起動しました。
しかし、大きなファイルを編集・整理・置換する過程で、以前追加した重要ロジックが消える可能性があります。
3. 実際に起きた問題その2:データ構造変更で undefined
別の例です。
Crypto → Fiat の off-ramp 表示で、ローカル取引所を表示する処理がありました。
例えば、
USDT → BRL
で、ブラジルのローカル取引所を表示したいケースです。
ところがデータソースを変更した後、一部の行で次のような問題が出ました。
undefined
UIコンポーネントが期待しているフィールドと、新しいデータ構造が一致していなかったためです。
そこで Adapter を追加しました。
function adaptOffRampRow(row) {
return {
name: row.providerName ?? row.name,
logo: row.logoUrl ?? row.logo,
receiveAmount: row.estimatedReceive ?? row.receiveAmount,
type: row.routeType ?? "local",
};
}
実際のコードはもう少し複雑ですが、考え方は同じです。
異なるデータ構造を、Render Component が期待する形式に変換します。
これで問題は解決しました。
しかし次の修正でまた同じ疑問が出ます。
AIがこのAdapterを「不要な重複」と判断して削除しないか?
4. そこで「保護対象」を先に決めるようにした
現在は、大きな修正を始める前に、
今回変更してよいもの
絶対に消してはいけないもの
を分けています。
例えば現在のプロジェクトでは、次のようなものを保護対象として扱います。
CORRIDOR_SPREAD_FLOOR
adaptOffRampRow
特定市場のP2P除外ルール
Partner URL
推定受取額の計算ロジック
特定通貨向け例外処理
AIに依頼するときも、単に
このバグを修正して
ではなく、
この問題を修正してください。
ただし以下の既存ロジックは変更・削除しないでください。
- CORRIDOR_SPREAD_FLOOR
- adaptOffRampRow
- KRW/JPY の direct exchange routing
- existing partner links
のように明示します。
これだけでも事故率は下がりました。
ただし、AIが「守った」と言っても、それだけでは信用しません。
5. Build 前に非常に単純な文字列チェックをする
私が追加した方法は、とても原始的です。
Windows では findstr を使います。
findstr /C:"CORRIDOR_SPREAD_FLOOR" App.js
findstr /C:"adaptOffRampRow" App.js
findstr /C:"partner.bitget.com" App.js
重要な文字列が消えていないか確認します。
Git Bash なら grep でも同じです。
grep -n "CORRIDOR_SPREAD_FLOOR" App.js
grep -n "adaptOffRampRow" App.js
grep -n "partner.bitget.com" App.js
もちろん、これは自動テストの代わりではありません。
文字列が存在していても、ロジックが正しく呼ばれていない可能性があります。
それでも、
完全に削除された回帰
をBuild前に発見するための最低限の安全装置としては役立ちます。
6. 「存在確認」だけでは足りないのでシナリオテストもする
現在は Build 前に、実際のビジネスシナリオを確認しています。
Scenario 1: USDT → BRL
確認内容:
Mercado Bitcoin が表示されるか
Bitso が表示されるか
undefined がないか
Scenario 2: Crypto → KRW
確認内容:
Upbit が P2P と誤分類されていないか
Local direct route として表示されるか
Scenario 3: High Amount
例:
10,000 USD → BRL
確認内容:
推定結果が実際の観測レンジから極端に離れていないか
Scenario 4: Partner Link
確認内容:
Sign up URL が旧URLに戻っていないか
Scenario 5: Light Mode
確認内容:
文字が存在するだけでなく、実際に読めるか
最後の項目も意外と重要です。
コード上は正常でも、文字色のコントラストが低すぎるとユーザーには見えません。
7. AI修正後は「新しく入ったもの」だけ見ない
以前は修正後に、
新しいコードが入ったか?
だけ確認していました。
現在は必ず、
重要な古いコードが消えていないか?
も確認します。
この考え方は非常に重要でした。
Before:
新しい修正が入ったか?
After:
新しい修正が入ったか?
+
以前の重要ロジックは残っているか?
AIコーディングでは、新規生成速度が非常に速いため、どうしても「追加されたもの」に意識が向きます。
しかし実運用では、削除されたものの方が危険な場合があります。
8. 大きな変更の前にGit checkpointを作る
現在は、大きな修正前に checkpoint を作るようにしています。
git status
git add .
git commit -m "checkpoint before off-ramp refactor"
その後AIに修正させます。
修正後:
git diff
で差分を確認します。
特に見るポイントは、
大量削除
関係ない関数の変更
URLの置換
条件分岐の簡略化
fallback削除
です。
AIは「コードをきれいにする」ために、プロダクト固有の例外処理を消すことがあります。
人間から見ると汚いコードでも、実際には過去の障害対応で必要になった条件かもしれません。
9. 「なぜ存在するコードか」をコメントに残す
AIは文脈がないと、特殊処理を不要と判断することがあります。
そこで最近は、重要な例外処理に理由を書くようにしています。
悪い例:
if (amount >= 10000) {
spread = Math.max(spread, floor);
}
少し改善:
// Keep this floor for high-value emerging-market corridors.
// Without it, estimates became too optimistic in observed quotes.
if (amount >= 10000) {
spread = Math.max(spread, floor);
}
さらに必要なら:
// DO NOT REMOVE without re-validating real quote samples.
// Added after high-value USD/BRL and KRW corridor checks.
if (amount >= 10000) {
spread = Math.max(spread, floor);
}
これは人間向けでもありますが、AIに対しても効果があります。
「不要なコード」ではなく、
検証結果から意図的に存在するコード
だと伝えられるためです。
10. 私が現在使っている簡易フロー
現在の流れは次のようになりました。
1. 問題を定義
↓
2. 変更禁止ロジックを列挙
↓
3. Git checkpoint
↓
4. AIに修正依頼
↓
5. git diff確認
↓
6. 重要symbolの存在確認
↓
7. 実シナリオ確認
↓
8. Web確認
↓
9. Android Build
Expo の production build は次のように行っています。
npx eas-cli build --platform android --profile production
Windows では local build を使わず、EAS側でBuildしています。
11. 文字列チェックの限界
ここは重要です。
例えば、
grep -n "adaptOffRampRow" App.js
で関数が見つかっても、
function adaptOffRampRow() {
// ...
}
が存在するだけで、実際には呼ばれていない可能性があります。
つまり存在確認は、
削除検知
には役立ちますが、
正しい動作保証
にはなりません。
そのため最低でも次を組み合わせる必要があります。
symbol check
+
git diff
+
scenario test
理想的には自動テストも追加すべきです。
しかし、小規模な個人開発でテスト基盤が十分でない段階でも、何もしないよりはかなり良いと感じています。
12. 複数AIを使うと回帰リスクはさらに増える
私は複数のAIツールを使います。
例えば、
AI A → Bug修正
AI B → Refactor
AI C → UI改善
AI D → データ分析
ということがあります。
すると次のようなことが起きます。
AI A:
この関数は必要です
AI B:
重複しているので削除しました
AI C:
旧機能が壊れているので復元しました
最終的にプロダクト全体の文脈を管理するのは人間です。
AIが増えるほど、
コード生成能力
より、
変更履歴と意図の管理
の方が重要になると感じています。
まとめ
AIコーディングを実運用で使って感じたのは、
AIの最大の問題はコードを書けないことではない
ということです。
むしろ、
新しい修正をしながら、過去に検証済みのロジックを壊すこと
の方が危険でした。
現在私が最低限行っているのは次の通りです。
- 保護対象を先に決める
- Git checkpointを作る
- git diffを見る
- 重要symbolをgrep/findstrで確認
- 実際のビジネスシナリオを確認
- 「なぜ必要か」をコメントで残す
特に個人開発では、
新しいコードが入ったか
だけではなく、
重要な古いコードが消えていないか
を見ることが重要だと感じています。
実際に運用しているプロジェクト
この記事の例は、私が開発・運用している TransferIQ で実際に発生したものです。
Web:
Android:
国際送金、FX、Crypto off-ramp ルートを比較するプロダクトです。
記事内で紹介した、
- spread floor
- local exchange mapping
- off-ramp adapter
- route classification
などは、実際のプロダクト改善中に発生した課題です。
AIを使った個人開発をしている方に聞いてみたいです。
AIが1つのBugを直した代わりに、過去に直した機能を再び壊した経験はありますか?
また、テストコードがまだ十分でない初期プロジェクトでは、どのように回帰を防いでいますか?