quad/.claude/session/2026-08-12-09-slot-retract-same-pattern.md
qwreey 27adad9c1a
decide(slot): store-bound Slot rebind uses Relate identity check, not retract
Same gap as Ref: slotA -> slotB via State<Slot> matches the same
SlotHandler both times, so Dispatch skips retract and process must
diff. Slot's confirmed no-portal/discard-only policy makes an identity
short-circuit a correctness requirement, not just an optimization --
without it, any spurious same-value re-emit would tear down and
rebuild the entire mounted subtree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 13:49:58 +09:00

3.3 KiB

2026-08-12 아홉 번째 세션 — Slot의 store 재바인드도 Ref와 같은 Relate diff 패턴

배경

직전 세션(여덟 번째, Ref의 retract를 TagHandler와 같은 메커니즘으로 확정)에서 발견한 패턴이 다른 곳에도 있는지 사용자가 확인 요청: Slot도 store 값으로 재바인드될 수 있는가(State<Slot>), 그렇다면 같은 Relate diff 패턴을 써야 하는가 — 그리고 이미 같은 바인딩이면 전부 빼고 다시 넣는 것 자체를 하지 말고 완전히 무시해야 하지 않겠냐는 제안.

확인

slot-plan.md "Slot과 Store 바인드의 관계" 절을 다시 읽어보니, Ref에서 고쳤던 것과 정확히 같은 종류의 부정확한 서술이 있었음: "store 바인드 핸들러가 이전 slot 상태를 retract하고 새 slot 상태로 다시 process하는 사이클을 돈다"는 문장은, bind-system-plan.md의 일반 계약("핸들러 타입이 안 바뀌면 retract 없이 process가 diff 담당")을 적용하면 틀림 — slotA→slotB도 같은 SlotHandler가 매치하는 경우라 retract가 아니라 process가 처리해야 함. 게다가 실제 Dispatch/Slot.luauprocess(inst, k,slotValue) 구현(attachSlot(slotValue, inst, inst, k) 한 줄)을 보면 이전 값과의 비교 자체가 아예 없었고, destroySlotTree(파괴 함수)도 CRUD 경로(rawRemove)에서만 쓰이고 store-bind retract 경로에 연결된 적이 없었음 — 실제로 와이어링 자체가 안 돼 있던 진짜 갭.

결정

Ref와 같은 Relate 기반 패턴을 재사용하되, Slot의 이미 확정된 "폐기, 옮기지 않음"(portal 없음) 정책 때문에 diff가 아니라 identity 비교로 단순화:

local relate = Relate()

function SlotHandler.process(inst, k, slotValue)
    local old = relate:GetStrong(inst, k)
    if old == slotValue then return end  -- 이미 같은 바인딩, no-op
    if old then destroySlotTree(old) end  -- 폐기, 옮기지 않음(기존 정책)
    attachSlot(slotValue, inst, inst, k)
    relate:SetStrong(inst, k, slotValue)
end

function SlotHandler.retract(inst, k, v)
    assert(v == nil)
    local old = relate:GetStrong(inst, k)
    if old then destroySlotTree(old) end
    relate:SetStrong(inst, k, nil)
end

이 no-op 가드가 Tag/Ref보다 Slot에서 더 중요한 이유: Tag/Ref의 diff는 값이 같아도 기껏해야 헛계산만 하고 넘어가지만, Slot은 "폐기, 옮기지 않음" 정책이 이미 확정돼 있어서 이 가드가 없으면 재귀 재emit이 있을 때마다(예: 상위 :Compute 재계산 결과가 우연히 같은 Slot 레퍼런스인 경우) 마운트된 서브트리 전체가 파괴됐다 다시 만들어짐 — 자식이 들고 있던 스크롤/포커스/애니메이션 상태 전부 유실되는, Tag의 diff가 막으려던 "깜빡임"보다 훨씬 파괴적인 버전.

반영

  • base/slot-plan.md "Slot과 Store 바인드의 관계" 절 — 부정확했던 "retract 사이클" 서술을 정정, 위 pseudocode와 근거 추가. "확정: 폐기, 옮기지 않음" 정책 자체는 그대로 유지(메커니즘만 정정).
  • base/bind-system-plan.md 일반 retract 계약 절 — Tag/Ref에 이어 Slot도 같은 패턴의 세 번째 예시로 교차 참조 추가.