Article
🖼️ ImagePickerのfile URIをDBに保存しない:React Nativeで画像を永続化する設計
Expo ImagePickerで選んだ画像をそのままDBへ保存すると、あとから画像が読めなくなることがあります。
理由は、ImagePickerが返すuriが端末内のローカルファイルを指すことがあるためです。Expo公式ドキュメントの例でも、file:///.../cache/... のようなURIが返っています。
端末内URIは「その端末で今読める場所」であって、複数端末や再インストール後まで使える共有URLではありません。
まず結論
共有データとして画像を扱うなら、保存境界を次のようにします。
ImagePicker
↓
端末内のlocal URI
↓
Cloud Storageへアップロード
↓
Storage上の参照/URL
↓
DBへ保存
DBへ書く前に、画像を永続ストレージへ移すのがポイントです。
local URIをそのまま保存すると何が起きるか
const result = await ImagePicker.launchImageLibraryAsync();
if (!result.canceled) {
const localUri = result.assets[0].uri;
}
このlocalUriをFirestoreなどへそのまま保存すると、別端末からは読めません。
さらにURIがキャッシュやアプリ管理下の一時領域を指す場合、そのファイルを共有データの永続参照として扱うのは危険です。
そのため、ローカルURIは「アップロード前の一時入力」として扱います。
永続化の責務をrepositoryに寄せる
作成画面と編集画面でアップロード処理を個別実装すると挙動がずれやすいため、保存境界へ寄せます。
type PhotoInput =
| { type: 'keep' }
| { type: 'remove' }
| { type: 'replace'; localUri: string };
keep: 既存画像を変更しないremove: 画像を削除するreplace: 新画像をアップロードして差し替える
string | null | undefinedだけで扱うより、意図を型で表せます。
アップロード後にDBを書き込む
async function saveItem(input: PhotoInput) {
let uploadedPath: string | null = null;
try {
const photoUrl =
input.type === 'replace'
? await uploadPhoto(input.localUri, path => {
uploadedPath = path;
})
: input.type === 'remove'
? null
: undefined;
await saveToDatabase({ photoUrl });
} catch (error) {
if (uploadedPath) {
await deleteUploadedFile(uploadedPath).catch(() => {
// cleanup失敗は別ログへ
});
}
throw error;
}
}
アップロード失敗時にDBだけ成功した状態を作らないことが重要です。
逆に、アップロード後のDB保存で失敗した場合は、その保存試行で新規作成したファイルだけを削除します。
これは補償処理という考え方で、複数システムをまたぐ途中失敗を打ち消します。
Storage Rulesでも検証する
Firebase Storage Security Rulesでは、認証だけでなくファイルサイズやMIMEタイプも検証できます。
allow write: if request.auth != null
&& request.resource.size < 10 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
10MiBはFirebaseの上限ではなく、アプリ側で決めた例です。
クライアントの事前チェックだけに頼らず、Storage Rulesでも制限します。
上書きより新規パスを使う
差し替え時に同じStorageパスを上書きすると、DB更新失敗時に以前の画像まで失う可能性があります。
新画像はUUIDなどを含む別パスへ保存し、
- 新画像をアップロード
- DBの参照先を更新
- 必要なら旧画像を削除
という順序にすると途中失敗に強くなります。
テストはデータの寿命をまたぐ
最低限、次を確認します。
- DBにはlocal URIではなく永続参照が入る
- 元のローカル画像を消しても表示できる
- アプリ再起動後も表示できる
- 別画面や共有画像生成からも読める
- Rulesで未認証・権限なし・不正MIME・過大ファイルを拒否できる
- DB保存失敗時に今回アップロードしたファイルだけcleanupされる
選択直後に同じ画面で見えるだけでは不十分です。
まとめ
- local URIは一時入力として扱う
- DB書き込み前にCloud Storageへ移す
- keep/remove/replaceを型で分ける
- StorageとDBの途中失敗には補償処理を入れる
- Security Rulesでもサイズ・MIME・権限を検証する
- 再起動や元ファイル削除まで含めてテストする
この設計は、自分が開発している旅行サポートアプリ TripSupporter の予定写真保存でも使っています。
この修正は 2026年10月4日時点では未リリース です。コードのmain反映と、App Storeでユーザーへ配布される状態は分けて管理しています。
参考
- Expo ImagePicker: https://docs.expo.dev/versions/latest/sdk/imagepicker/
- Cloud Storage for Firebase: https://firebase.google.com/docs/storage/
- Firebase Storage Security Rules: https://firebase.google.com/docs/storage/security/
- Firebase Storage Rules conditions: https://firebase.google.com/docs/storage/security/rules-conditions