同じ「外す」という言葉が、実装では3つの違う操作になる話です。決めるときに層を言わないと話が噛み合わなくなる、というところまで。
「web に出す」を外したら、サーバは何をするべきか
綴Photo はスマホで綴って、選んだものだけを web に出す作りになっている。スマホ側に、写真を web に出すかどうかのチェックがあり、押すとサーバを経由して web の公開一覧に載る。出す方は簡単だった。迷ったのは外した方で、選べる範囲が3つある。
- 公開の一覧から下ろすだけ
- 綴りも下ろす
- 写真ごと消す
一番浅いところ(公開の一覧から下ろすだけ)を選んだ。理由は再公開の値段だった。気が変わってもう一度出したくなったとき、上2つなら送り直すのは公開の意思表示だけで済む。写真まで消してしまうと、もう一度アップロードからやり直しになる。
一番浅いところでも、サーバが何もしないわけではない。判断ログに書いた「取り消せる最小の範囲」を素直に当てると、これしか残らなかった。
このとき決めたことを、判断ログにはこう書いた。
「外す」操作は取り消せる最小の範囲に留める。実データを消すのは退会のように「消すと分かって押す」導線でだけ。
残した結果、別の場所に出続けた
一番上を選ぶと、当然そうなる帰結がある。写真も綴りもサーバに残っているので、それを全部読む画面には出続ける。
翌日それが実際に見つかった。web でページを作るときの「素材を選ぶ」一覧に、スマホで取り消したはずの写真が並んでいた。
これはバグに見えるが、バグではない。「残す」と決めた側の当然の続きで、決めたときに詰めていなかった部分——残したデータを、どの画面まで見せるのか——が空白のままだっただけだ。
なお、このあと実際に一覧を絞ったときには、逆向きの症状も出た。公開中のはずの写真が3枚だけ出てこなくなった方で、こちらは表示の線引きではなくデータ側のズレが原因だった。そちらは「公開中のはずの写真が3枚だけ出てこない — 導出フィールドの backfill 漏れ」に書いた。
「棚も同じにする?」と聞かれて、身構えた
素材の一覧を絞るのは決まっていた。同じ穴が本棚(振り返りの画面)にもあったので、そちらもどうするか、と AI から聞かれた。
読んだ瞬間に身構えた。消す話だと思ったからだ。
消すとなると、サーバのデータを消すのか、端末側に残っている「出す/出さない」の印(フラグ)はどうするのか、消したのに次の同期でまた上がってきたらどうするのか——考えることが一気に増える。「これはちょっと慎重に決めたい」と返した。
AI が実際に提案していたのは、そういう話ではなかった。データベースには一切触らず、一覧を取ってくるときの条件に1つ足すだけ。
# qs = これから返す一覧。この時点ではまだ「全部」
# query_params = URL の末尾に付く注文(?favorite=1 の部分)
if self.request.query_params.get("favorite") in ("1", "true"):
# is_favorite は内部の名前で、意味は「web に出す」が ON かどうか
qs = qs.filter(photo__is_favorite=True) # 公開中のものだけを残す画面側も、読み込むときに1つ指定するだけになっている。
// useRemoteMemories = サーバから一覧を取ってくる係
// { publishedOnly: true } = 「公開中のものだけ」という注文(引数)
// memories = 受け取った一覧が入る先
const { memories } = useRemoteMemories({ publishedOnly: true });
// この { publishedOnly: true } ごと消せば元通り全部出る=いつでも戻せる引数1つで戻ると言い切れるのは、既定の動きに手を付けなかったからだ。「何も指定しなければ今まで通り全部返す」という約束はそのままにして、絞りは指定したときだけ効くように足してある(この既定はテストで固定してあるので、知らないうちに変わることもない)。もし逆に既定そのものを「公開中だけ」に変えていたら、全部見たい画面が出てきたときに直すのは条件式ではなく約束の方になる。表示で外すやり方でも、既定を書き換えたらそれは安い変更ではない。
分かってしまえば5秒で決まる話だった。噛み合わなかったのは、提案に「どの層で外すのか」が書かれていなかったからだ。 層が書かれていないと、読む側は一番強い方——データが消える方——を勝手に補う。そして決めなくていい怖さを判断材料に混ぜてしまう。実装を AI に任せていると、判断だけがこちらに回ってくる。回ってきた言葉がどの層の話なのかは、こちらで確かめるしかない。
層で並べ直すと、迷うところが1つに減る
同じ「外す」を、実装の層で並べるとこうなる。
ただし一番上にもコストがある。絞るのが画面ごとの仕事になることだ。今回も素材の一覧と本棚の2箇所に同じ条件を足した。画面が増えれば、そのたびに足し忘れが起きうる。逆に実体を消してしまえば絞る場所は1箇所も要らず、漏れようがない——「消す方」が一瞬もっともらしく見えた正体はこれだった。戻せることを取るなら、絞り忘れが起きる前提で受け止めるしかない。
決めたのは「表示するかしないかだけ」だった。そしてデータを消す導線はアカウント削除に一本化することにした。あちこちに「消える」操作があると、どれが戻せてどれが戻せないのかを覚えていられない。消えると分かって押す場所は、1つでいい。
なお同じ日のうちに、web の本棚からも「web から下ろす」を押せるようにした。これはスマホで外したときと同じ層(公開の意思表示だけ消す)で止まっていて、写真も綴りも残る。文言も「消えません」ではなく「写真も綴った言葉も残ります」と、残る側を数えて書いた。戻せない層に触る導線は、やはりアカウント削除の1箇所だけ。
学んだこと
- 「外す」「隠す」「下ろす」は日本語では1語だが、実装では層が違う別の操作になる。だから賛否を聞く前に層を1語添えるだけで足りた。「これは表示の話です」の一言があれば、今回の身構えは起きていない
- 「表示層なら安い」も無条件ではなかった。安かったのは既定(何も指定しなければ全部返す)に手を付けなかったからで、既定そのものを書き換えていたら、層は表示のままでも直すのは条件式ではなく約束の方になる。層が決めるのは戻しやすさの上限までで、実際の値段は「既定を触ったか」で変わる
- 「残す」と決めたときは、残したデータをどの画面まで見せるかまで決めておかないと、あとで「バグに見えるが仕様」が生える
この記事は2026年7月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。