はじめに
記録時点: 2024-09〜2026-01
この時期の画像機能は、保存APIの成功率だけを見ると問題がないように見えていました。 ただ運用では「保存できたのに使えない」という問い合わせが繰り返し発生し、 編集フロー全体としては失敗扱いになっていました。
この記事で扱う範囲
- URL生成の不整合で発生した表示崩れ
- デプロイ直後のみ発生する取得エラー
- 画像件数増加時の探索不能(見つけにくさ)
個別不具合の話に見えますが、実際は「保存成功」を過信した設計の見直しです。
症状は3種類に分かれていた
1. アップロード成功なのにプレビューが出ない
特定のファイル名でのみ画像URLが壊れ、表示できないケースがありました。 利用者視点では「保存は成功したのに画像だけない」ため、失敗理由が伝わりません。
2. ローカルでは再現せず、デプロイ後だけ取得APIが落ちる
開発環境で再現しないため、最初はデータ不整合を疑いました。 実際は、デプロイ時の参照構成に依存したエラーでした。
3. 画像はあるのに一覧で見つけられない
件数が増えると初回読み込みが重くなり、探索の手数も増えます。 機能としては成立していても、運用では「使えない」に近い状態です。
切り分けの順番
この回で効いたのは、症状の頻度順ではなく責務順で切り分けたことです。
- URL生成の契約をフロント/バックで突き合わせる
- デプロイ後の実行形とローカル実行形の差分を見る
- 一覧UIを性能ではなく探索完了までの手数で評価する
この順番にすると、見た目上つながって見える不具合を別問題として扱えます。
実装で変えたポイント
1. URL生成ルールを共通化した
まず、S3キーをURLへ載せる時点で必ず同じ正規化を通すようにしました。
export function toAssetUrl(baseUrl: string, objectKey: string): string {
const normalizedBase = baseUrl.replace(/\/$/, "");
const encodedKey = encodeURIComponent(objectKey).replace(/%2F/g, "/");
return `${normalizedBase}/${encodedKey}`;
}「保存時は問題ないが表示時に壊れる」症状は、この共通化で大きく減っています。
2. デプロイ後の実行形に合わせて参照構成を修正した
共通化した処理の置き場所が、デプロイアセットでは解決できない経路になっていました。 見た目の共通化より、実行時に確実に読める配置を優先して整理しました。
3. 一覧を分割取得に切り替え、探索導線を追加した
無限スクロールを導入し、重複を除外しながら継ぎ足し表示する形へ変更しました。
export function mergeUniqueById(current: ImageItem[], next: ImageItem[]): ImageItem[] {
const map = new Map<string, ImageItem>();
for (const item of current) map.set(item.id, item);
for (const item of next) map.set(item.id, item);
return [...map.values()];
}加えて、ファイル名フィルタ・ソート・表示切り替えを入れ、 「表示できる」ではなく「見つけて選べる」状態へ寄せました。
検討して見送った案
1. 取得失敗時に毎回再試行で吸収する
一時的には改善しますが、根本原因を隠して監視を難しくするため見送りました。
2. 一覧を常に全件取得してクライアントで完結させる
件数増加時の初回体験が悪化するため採用していません。
3. URL崩れを画面側の例外処理だけで吸収する
表示層で吸収すると、他画面で同じ不具合が再発するため契約側で直しました。
回帰確認で固定した観点
| 観点 | 期待値 |
|---|---|
| 空白/記号を含むキー | URLに空白が残らない |
| デプロイ直後の画像取得 | 参照エラーで落ちない |
| 追加読み込み | 重複表示が出ない |
| フィルタ/ソート変更後 | 選択状態が壊れない |
| アップロード直後 | 同ダイアログで即選択できる |
この回の判断が次に効いたこと
「保存成功」ではなく「使える状態」までを完了条件にする考え方は、次の記事にもつながっています。
まとめ
今回の改善は、画像表示の不具合修正ではなく、画像利用フローの再設計でした。 URL契約、実行環境、探索導線の3層を分けて直したことで、 「保存できたのに使えない」状態を実運用で減らせています。