From 448b961e7f025a8ce7d816a5035b57ecdbb13a6b Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 21 Aug 2026 11:31:11 +0900 Subject: [PATCH] =?UTF-8?q?research:=20attachSlot=20=EB=B6=84=ED=95=B4=20?= =?UTF-8?q?=E2=80=94=20=EC=9D=BC=EA=B4=84=20=EB=A7=88=EC=9A=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=B0=A8=EC=9D=B4=20=EA=B2=80=ED=86=A0,=20=ED=8C=90=EB=8B=A8?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EC=B2=9C=EC=9C=BC=EB=A1=9C=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자가 짚은 "물리 마운트가 관측 이후 일괄로 밀린다"는 차이를 따져본 결과 감수하는 비용이 아니라 개선이라는 결론. - 마운트 순서 자체는 안 바뀐다(둘 다 깊이 우선 같은 순서) — 바뀌는 건 각 Parent 대입 시점에 부기가 얼마나 완성돼 있는가뿐. - 그 차이를 실제로 관측하는 주체가 있다: Parent 대입이 ChildAdded/ DescendantAdded를 동기 발화시키므로 사용자 핸들러가 마운트 도중에 끼어든다. 현행은 A의 ChildAdded 시점에 inner.Length가 0이고 뒤 형제 offset도 stale인 미완성 스냅샷을 보여주는데, 분해하면 첫 발화 때 서브트리 전체가 최종값이다. slot.Length 구독자도 0→최종 두 번이 아니라 최종값으로 한 번 발화. - "합치는 거대 함수"는 만들어도 목적을 못 이룬다 — 완전 병합은 자식 길이를 그 자식 재귀가 끝나야 알므로 C6를 못 지키고(그게 지금 코드), 부분 병합은 인터리브는 유지하지만 부모 레벨 C7 위반이 그대로 남는다. 실질 선택지는 (A) 인터리브+자기교정 / (B) 분해+일괄 둘뿐. - 부수 이득 둘: Blocker가 materialize만 감싸게 되어 "배치 등록 게이팅"이라는 정의와 코드 범위가 일치하고, mountSlotTree가 순수 walk라 일괄 삽입이 유리한 백엔드(웹 DocumentFragment 등)가 그 함수 하나만 갈아끼울 수 있는 seam이 생긴다. 판단을 "약한 추천"에서 "추천"으로 갱신 — RC-1/RC-3/RC-4가 전부 한 함수 안의 줄 순서를 잘못 잡아 난 버그였는데, 분해하면 C2/C3/C7이 함수 경계로 강제되어 그 실수 클래스가 구조적으로 사라진다는 사용자 지적이 결정적. 다만 M6 착수 전 실제로 짜보며 확정해도 늦지 않다는 판단은 유지. Co-authored-by: qwreey --- .claude/research/slot-attach-decomposition.md | 89 ++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/.claude/research/slot-attach-decomposition.md b/.claude/research/slot-attach-decomposition.md index 1e349b2..a751e4e 100644 --- a/.claude/research/slot-attach-decomposition.md +++ b/.claude/research/slot-attach-decomposition.md @@ -184,8 +184,8 @@ R1(부모 등록)까지 따로 떼는 안. `Dispatch.drive`의 최상위 배열 ## 5. 지금 상태 요약 > **[2026-08-21 갱신] 사용자 판단으로 (B) 방향 + "공개 표면은 안 쪼갬"까지 -> 좁혀졌다** — 구체안은 아래 **6절**이 최신이고, 이 절과 3절은 거기까지 온 -> 경로다. +> 좁혀졌다** — 구체안은 **6절**, "물리 마운트가 일괄로 밀리는" 차이에 대한 +> 검토와 판단 갱신은 **7절**이 최신이고, 이 절과 3절은 거기까지 온 경로다. - **확정된 건 없다.** `base/slot-plan.md`의 단일 `attachSlot`이 여전히 정본. - **급하지 않다** — (A)로 두어도 동작은 맞고(자기 교정), M2/M3 착수를 막지 @@ -321,3 +321,88 @@ end **반대로 안 쪼갤 이유**도 정당하다 — 동작이 지금도 맞고(자기 교정), 순회가 하나 늘며, 확정된 의사코드를 건드리는 변경이다. **M6 착수 시점에 실제로 짜보며 정해도 늦지 않다.** + +--- + +## 7. "물리 마운트가 일괄로 밀린다"는 차이 — 손해가 아니라 이득 (2026-08-21) + +**사용자 관찰**: *"얼마나 nested 든, 하나 마운트되고 하나 마운트되고… 각 +객체들이 `Parent = inst` 되어가며 피지컬에 붙어가는 일자 진행인데, 이건 관측 +이후 일괄 등록이라는 차이가 있는듯 … 그러나 이런 사소한, 문제를 야기하기 어려운 +동작을 원래와 동치시키기 위해 디버깅이 어려운 함수를 만드는건 옳지 않아보임."* + +**결론에 동의한다. 그리고 그 "차이"는 감수하는 비용이 아니라 개선이다.** + +### 7-1. 마운트 **순서**는 안 바뀐다 — 바뀌는 건 부기와 섞이느냐뿐 + +`outer { A, inner { B, C }, D }` 기준: + +| | 현행(인터리브) | 분해(일괄) | +|---|---|---| +| `Parent` 대입 순서 | A → B → C → D | A → B → C → D (**동일**) | +| 각 대입 시점의 부기 상태 | 부분적 — A가 붙을 때 `inner.Length == 0`, outer의 뒤 offset은 아직 stale | **전부 완결** | + +두 방식 다 `_elements`를 깊이 우선으로 같은 순서로 훑으므로 **관측 가능한 +마운트 순서는 동일**하다. 달라지는 건 "그 시점에 부기가 얼마나 완성돼 +있는가"뿐이다. + +### 7-2. 그 차이를 실제로 관측하는 주체가 있고, 분해 쪽이 더 정확하다 + +Roblox의 `Parent` 대입은 **동기적으로 `ChildAdded`/`DescendantAdded`/ +`AncestryChanged`를 발화**시킨다. 즉 사용자 핸들러가 마운트 도중에 실제로 +끼어들어 상태를 읽는다. + +- **현행**: A의 `ChildAdded`가 뜰 때 `inner.Length`는 아직 `0`이고, `outer`의 + 뒤 형제 offset도 아직 안 밀려 있다 — **완성 전 스냅샷**을 보게 된다. +- **분해**: 첫 `ChildAdded`가 뜰 때 **서브트리 전체의 `Length`/`Offset`이 이미 + 최종값**이다. + +`slot.Length`를 구독하는 사용자 State/Observer("n개 검색됨" 라벨 등)도 같다 — +분해 쪽이 **한 번, 최종값으로** 발화한다(현행은 `0` → 최종으로 두 번). +`PostRef`가 "자기 아래는 전부 끝난 뒤"를 보장하는 것과 결이 같아진다. + +**즉 원래 동작을 보존할 이유가 약한 정도가 아니라, 원래 동작 쪽이 덜 정확하다.** + +### 7-3. "합치는 거대 함수"는 애초에 목적을 못 이룬다 + +*"액티베이션 재귀와 실측시 마운트를 합치는 거대한 함수를 만드는게 아닌이상 +해결하지 못할 문제"* — 맞는데, **그 거대 함수를 만들어도 안 풀린다.** 두 +변형을 다 따져보면: + +- **완전 병합(측정하면서 그 자리에서 마운트)** — 자식의 최종 길이는 그 자식의 + 재귀가 **끝나야** 알 수 있으므로, 자식을 마운트하는 시점엔 아직 자기 길이를 + 모른다. **C6를 못 지킨다** — 그게 정확히 지금 코드(= `Length=0`으로 등록 후 + 자기 교정)다. 새로 얻는 게 없다. +- **부분 병합(자식마다 `materialize` → 바로 `mount` → 다음 형제)** — 실제로 + 가능하고 인터리브도 유지된다. 하지만 **자기 길이를 부모에게 등록하는 건 + 여전히 루프가 다 끝난 뒤**라, 고치려던 부모 레벨 C7 위반이 그대로 남는다. + 구조만 복잡해지고 목적은 못 이룬다. + +**그래서 실질적 선택지는 둘뿐이다** — (A) 인터리브 + 자기 교정(현행), +(B) 분해 + 일괄. "인터리브하면서 최종값도 아는" 제3의 안은 구조적으로 없다. + +### 7-4. 부수 이득 둘 + +1. **Blocker의 목적이 선명해진다** — `mountSlotTree`는 부기를 전혀 안 + 건드리는 순수 walk라 Blocker가 필요 없다. 결과적으로 Blocker가 + `materializeSlotTree` **하나만** 감싸게 되고, "이건 배치 *등록*을 게이팅하는 + 물건"이라는 정의와 코드 모양이 일치한다(지금은 물리 마운트까지 같이 감싸고 + 있어 이름과 범위가 어긋난다). +2. **백엔드가 갈아끼울 seam이 생긴다** — `mountSlotTree`가 "부기는 끝났고 + 붙이기만 하면 되는" 순수 함수라, 일괄 삽입이 유리한 백엔드(웹의 + `DocumentFragment` 등)가 **이 함수 하나만** 자기 방식으로 바꿔 끼울 수 있다. + 인터리브 구조에서는 부기 호출 사이사이에 물리 조작이 박혀 있어 불가능하다. + +### 7-5. 판단 갱신 — 약한 추천에서 **추천**으로 + +6-5절은 "약하게 추천"이었는데, 위 7-2(관측 정확도)와 사용자가 짚은 **구현 +실수 위험**을 더하면 근거가 한 단계 올라간다: + +- 안 쪼개면 C1~C7 일곱 제약이 **한 함수 안에서 줄 순서로만** 지켜진다 — + `RC-1`/`RC-3`/`RC-4`가 전부 그 줄 순서를 잘못 잡아서 난 버그였다. 분해하면 + 그중 C2/C3/C7이 **함수 경계로 강제**되어 줄 순서 실수로 깨질 수 없게 된다. +- 남는 반대 근거는 "확정된 의사코드를 건드린다" 하나인데, 위 위험과 견주면 + 약하다. + +**단 여전히 M6 착수 전까지 시간이 있고, 실제로 짜보며 확정해도 늦지 않다** — +지금 확정할 필요는 없다는 판단은 안 바뀐다.