# [역전됨, 2026-08-21] "부기가 물리 트리 조작보다 항상 먼저 끝난다" **상태**: 역전됨. 2026-08-20 구현 전 QA 4라운드 `C-7`에서 **모든 `raw*`가 따르는 일반 계약**으로 승격됐다가, **2026-08-21 5라운드에 `native*` 주입 op 계층이 확정되면서 폐기**됐다. **왜 뒤집혔나**: 이 계약은 *"`Length`를 먼저 올려 뒤 형제를 밀어내고 비워진 공간에 넣는다"*는 그림 위에 서 있었는데, **base에는 물리적으로 자리를 비워둘 수단이 없다.** 미는 주체는 언제나 백엔드의 삽입 연산 자신이다 — 사용자 지적: *"밀어내고 null을 넣어둘 수도 없는데, 그리고 nativeInsert 라는것 자체가 밀어내기 동작을 강제하는데, 그렇다면 length 를 나중에 설정한 다음 offset들이 무시되어야함."* **무엇이 남았나**: "빼기는 물리 먼저 / 넣기는 부기 먼저"라는 두 얼굴 대신 **"자기 자리를 정하는 것 먼저(`setOffsetSource`) / 뒤를 미는 것 나중 (`setLength` → `recompute`)"** 하나로 줄었다. 배치 경로(`materializeSlotTree` → `mountSlotTree`)가 부기 전량을 먼저 끝내는 것은 **C6가 요구하는 별개 사안**이라 그대로다. 지금 유효한 서술은 `base/dispatch-core-plan.md`의 "일반 계약 — 물리와 부기의 순서" 절. --- ## 역전 전 원문 ### ⭐ 일반 계약 — 부기가 물리 트리 조작보다 항상 먼저 끝난다 (2026-08-20 구현 전 QA 4라운드 `C-7` 승격) **지금까지 이 규칙은 `rawAdd` 한 곳에만 적혀 있었다**(바로 아래 "동기 순서" 문단). `rawRemove`/`rawUnmount`는 의사코드가 우연히 같은 모양이었을 뿐 계약화돼 있지 않았고, `Splice`는 물리 detach/attach와 부기의 선후가 **아예 안 적혀 있었다.** 사용자 판정으로 **모든 `raw*`가 따르는 일반 계약으로 승격**한다(*"각각의 동작에 따라 다른 동작 보다, 일관성 있는 동작을 제공하는게 나아보이고, 이것을 단순 일반 계약 승격으로 도달 될 수 있기 때문"*). > **계약**: 어떤 CRUD/재조정 연산이든 **`Length`/`offset` 부기 갱신이 물리 > 트리 조작(`Parent` 대입/해제)보다 먼저 완료**돼야 한다. 즉 "**먼저 밀어내고 > 그 공간에 넣는다**" / "**먼저 비운 걸 반영하고 그 다음 당긴다**". - **왜 일반 계약이어야 하나**: 백엔드가 "밀어내기"를 물리적으로 구현해야 하는 경우(DOM `insertBefore` 밖의 백엔드 등), **부기가 이미 정확하다**는 걸 전제할 수 있어야 자기 일을 할 수 있다. 연산마다 선후가 다르면 백엔드 작성자가 매번 다시 확인해야 한다. - **각 연산에 적용하면**: - `rawAdd` — 부기(`setOffsetSource`/`setLength` 등록 → `recompute`) 완료 → 물리 마운트(`mountInst`). **[정정, 2026-08-21 5라운드 `C-2`]** 예전엔 여기 `Length:Set(newCount)`라고 적혀 있었으나 **틀렸다** — 아래 문단 참고. - `rawRemove`/`rawUnmount`/**`rawDetach`**(**[2026-08-21]** `Detach` 경로용으로 신설된 세 번째 형제 — 소유권을 **유지**한 채 언마운트만 한다는 점만 다르고 순서는 같음, `base/slot-plan.md`) — 파괴/언마운트 → `spliceArraysDown` → `recompute`가 지금 의사코드인데, **이건 물리 조작이 먼저**라 계약과 어긋나 보인다. 다만 여기선 "빼는" 방향이라 부기를 먼저 줄이면 아직 트리에 있는 요소가 순서 계산에서 빠지는 역전이 생긴다 — **"빼기는 물리 먼저, 넣기는 부기 먼저"**가 실제로는 같은 원칙(항상 **좁은 쪽이 먼저**)의 두 얼굴이다. 문서화 시 이 대칭으로 적을 것. - `Splice` — 제거 구간과 삽입 구간이 겹치므로 위 두 규칙을 그대로 이어 붙이면 된다(제거는 물리 먼저, 삽입은 부기 먼저). **shift/recompute를 1회로 묶는다는 최적화는 그 사이에서만** 일어난다. - `rawMove`/`rawSwap` — `Parent`를 안 건드리므로 이 계약의 대상이 아님. - **⚠️ 프레임 경계는 어차피 안 낀다** — `process`/`attachSlot` 체인 도중 코루틴 yield가 금지돼 있으므로(위 "Handler 작성 체크리스트" 9번) 이 순서가 어긋나도 사용자에게 보일 프레임이 그 사이에 없다. 그래서 이 계약은 "안 지키면 깜빡인다"가 아니라 **"백엔드가 전제할 수 있게 하나로 고정한다"**가 진짜 이유다.