quad/.claude/research/existing-mount-plan.md
qwreey ecc6b0e538
qa: 10라운드 후속 H-158~H-162 반영 — :Block 폐기(__apply), _rerunRequired 홀드(_installed 흡수), Claim M5, Void export
- H-158 state:Block → state:Apply(blocker), Blocker:__apply 메소드형(호출 규약 명시)
- H-159 사용자 제안 _rerunRequired 홀드: fire=Update→Rerun, rawRerun이 실행 불가 상태의 요청을 홀드,
  _installed 폐기, Observer 대칭(전파 루프 else + bind/subscribe 1회 발화, 생성자 순서 fn→_subs)
- H-160 홀드로 정정 + "error 나면 그 Effect는 죽는다" 계약 / H-161 Claim M5 스코프 / H-162 Void.luau 잎 모듈
- 감사 7→6→1→0, /code-review high 10건 중 8 반영, 둘(H-163 Slot 내부 Observer×홀드, H-164 emitFrom nil)은 문항으로

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
2026-08-28 14:43:56 +09:00

12 KiB

이미 있는 트리를 quad가 소유하기 — Claim + D.Mapper (가칭)

[2026-08-28 신설, 사용자 발의] 10라운드 H-148(Parent 거부 문구)을 논의하다 더 큰 표면의 공백이 드러나 만든 문서. 상태: 설계 논의 중 — 방향은 사용자 확정, 갈래 몇 개 미결(§5). M2 착수 게이트 아님, M5 스코프 ([2026-08-28 H-161] "M5 이후"에서 당김 — 루트 예외 폐기 뒤 M5의 유일한 루트 부착 경로; 프로바이더 op가 필요하니 M5가 자연스러운 자리). 결정이 나면 base/로 승격한다.

archive/existing-instance-bind-rejected.md(2026-08-14 기각)와의 관계: 그 기각은 *"이미 있는 Instance에 나중에 새 props를 다시 바인드"*였고 사유는 "quad가 만들지 않은 트리의 자식 구성을 바깥이 밀고 당기면 setLength/ setOffsetSource 부기가 깨진다"였다. 이 문서는 그 반대 방향 — 한 번 claim하면 quad가 소유하고 직계 자식은 사용자가 전부 매핑한다는 계약이라 claim 뒤엔 quad가 만든 트리와 같은 불변식이 성립한다. 재바인드는 여전히 미지원(claim은 1회, 디스크립터는 Processed로 소진). 그 archive엔 "좁은 형태로 부활"이라는 배너를 달 것(반영 시).

1. 왜 필요한가 (사용자 원문)

H-146이 "루트는 사용자가 밖에서 .Parent ="로 닫혔는데, 사용자가 이어서 지적했다: "slot 은 물리 장치에 mount 할 방법이 거의 존재하지 않음. Parent = 처럼 마운트 할 방법이 없는데? 그럼 PlayerGui 가 상위에 있고 거기에 GUI 를 여럿 바운딩 해야해서 Slot { Shop{} … } 하는게 안 될것 같은 느낌이 듦. 이건 Parent 이상의 문제인것 같아." — 즉 루트가 Slot일 수 없다(Slot은 quad가 부기를 가진 부모 inst 아래에만 산다). 그리고: "생성할 요소들 자체가 너무 많은 경우 Clone 이 엇청 더 싸서, 그 Clone 된 것 아래 quad 를 바인딩 할 방법이 있으면 좋은것도 사실인듯. … web 에서도 템플릿에 의해 유효한 요소일꺼고, roblox 에서도 보면서 만들어낸 GUI를 바인딩하는건 흔한 요구라서 이 역시 흔한 필요일꺼야."

결론(사용자): "이런 방식으로, 이미 있는 PlayerGui 아래 마운트를 거는거지. … 이것도 똑같이 Quad 가 소유하게 될 요소가 되는거지. … 이렇게 끝내면 parent 를 설정할 문제 자체가 사라져"H-146의 루트 예외와 H-148의 문구 문제가 같이 소멸한다.

2. 모양 (사용자 스케치 + 확정된 것)

local M = D.Mapper                      -- 정의는 D 안에 산다. 유저가 필요하면 꺼낸다
local cloned = Claim(template:Clone(), M.Frame "root" {   -- ← 루트 이름은 미결(§5-1)
    M.TextLabel "Title" { Text = title },
    M.Frame "List" {
        Slot {  },                     -- 기존 부모 아래 Slot — 이제 가능
    },
    BackgroundColor3 = color,           -- props는 New와 같은 derive 테이블
}) -- -> cloned (claim한 루트 Instance)
  • D.Mapper.<Class> "Name" { … }는 Instance를 만들지 않고 디스크립터만 만든다(브랜드 Mapper류) — derive 테이블 + 매칭 키. D.Frame에 직접 얹지 않는다(사용자: "D.Frame 에 바로 바인딩은 위험한듯. 의미가 겹쳐버려").
  • Claim(inst, descriptor) -> inst가 최상위. inst는 quad 밖에서 온 것 (PlayerGui, Clone() 결과, Studio에서 만든 GUI). 이 호출로 inst와 매핑된 하위 전부가 quad 소유가 된다 — New가 만든 것과 같은 gcconn/gchold·부기.
  • 처리 순서는 New와 반대 방향에서 시작한다. New는 안쪽 생성자가 먼저 평가돼 자연히 bottom-up이지만, 매퍼의 안쪽은 평가 시점에 자기 Instance를 모른다(부모가 아직 없다). 그래서 ClaimDFS로 내려가며 이름으로 해석 → 자식부터 drive → 올라오며 부모 drive. 사용자: "핸들러가 된다면 위험해. 일반 생성과 다르게, 상위 부터 처리하거든 … DFS 로써, 내려가는게 먼저고 그 뒤에서 derive 를 걸어야해. 이건 derive 에선 구현하지 않고, 그 위의 무언가로써 구현되어야할듯."drive/핸들러 층은 안 바뀌고 그 위의 한 겹이다.
  • 매핑된 정적 자식은 InstanceChildHandler와 같은 부기(setLength(inst,k,1))만 하고 .Parent =는 안 한다(이미 거기 있다). Slot은 평소처럼 native*로 기존 부모 아래 끼운다.
  • 디스크립터는 1회용 — claim 뒤 Processed로 소진(PreRef와 같은 관용구). 재사용·이중 claim은 error.
  • 매칭은 프로바이더 주입 op nativeFindChild(inst, key)(가칭) — Roblox는 Name, web은 id/selector. quad-base가 순회·부기 전반을 구현하고 프로바이더는 이 핸들만 낸다(사용자: "quad-base 에서 전반을 구현해주고 필요 핸들을 구현하라고 남기는건 괜찮은 생각").
  • 여러 quad 인스턴스가 한 트리를 claim — UB(사용자 확정).
  • H-142(props에 Parent 금지)는 그대로. 루트가 quad 소유가 되므로 H-146의 "루트는 밖에서 .Parent =" 예외는 폐기.

3. 계약 — 자식은 전부 매핑한다

사용자: "모든 개체를 유저가 직접 네임을 매핑해서 derive 테이블 안에서 내부 요소를 전부 매핑해준다를 계약으로 잡으면 문제가 없다고 생각해."

  • 부기 대상(그려지는 자식)은 전부 매핑해야 한다. 안 된 자식이 남으면 nativeInsert의 삽입 위치(web은 곧 DOM 순서)와 Length/Offset이 어긋난다.
  • 숏핸드(UICornerUI*)는 부기 대상이 아니다 — 그려지지 않고 Roblox에만 있으며 단순 Parent 대입 요소. 사용자 확정: "숏핸드를 quad 에서만 직접 쓰거나 … 아니면 실제 UI 객체를 바인딩해서 숏핸드를 안 쓰거나" — 둘 중 하나: (i) 템플릿엔 UI*가 없고 quad가 숏핸드 키로 만든다, (ii) 템플릿의 UI*M.UICorner "UICorner" {…}처럼 실제 객체로 매핑하고 그 부모에 숏핸드 키는 안 쓴다. 섞으면(템플릿에 UICorner가 있는데 숏핸드 키도 씀) 둘이 생기는 것은 UB.
  • 이름 중복·부재는 UB(사용자 확정). debug 모드에선 seen 맵으로 중복을 잡아 error(사용자 제안) — 부재·클래스 불일치도 같은 자리에서 검사.

4. 이 문서가 여는 것

  • 루트: Claim(PlayerGui, M.ScreenGui "…" { Slot {…} }) — PlayerGui가 quad 소유 부모가 되어 Slot이 그 아래 산다. ScreenGuiNew로 만들어 붙이는 경우도 Claim(PlayerGui, M.PlayerGui(...) { New "ScreenGui" {…} })처럼 정적 자식으로 들어간다(→ §5-3 루트 이름 문제).
  • 템플릿 대량 생성: template:Clone()Claim — 각 사본이 독립 소유. Claim이 Instance를 돌려주므로 Slot 요소로도 그대로 쓸 수 있다(요소는 inst) — "요소가 너무 많은 경우"의 답.
  • 비루트 사용: New "Frame" { Claim(clone, …) } — 반환된 inst가 정적 자식으로 들어가면 InstanceChildHandlerParent =와 부기를 한다. 평가 순서상 Claim이 먼저 끝나므로 bottom-up이 유지된다.

5. 미결 — 다음 배치 문항

  1. 루트 디스크립터의 이름 — 루트는 Claiminst를 직접 받으니 매칭 키가 필요 없다. 사용자: "최상위는 이름을 뭐로 둬야할지 아직 모르겠음. 비워두는걸 D.Mapper.Frame{} 으로 제공하는건 더 나빠보이는데. 아니면 테이블로써 MapperRoot = {} Mapper.Frame (MapperRoot) {} 모양이 되어도 될것같음." 갈래: (a) 센티널 MapperRoot(M.Frame(MapperRoot) {…}) / (b) 루트는 Claim(inst, { … })처럼 클래스 없는 맨 테이블(클래스는 inst가 이미 안다) / (c) 이름을 받되 무시. 권고 (b) — 루트 클래스를 두 번 말하지 않고, M.<Class>는 "찾아야 하는 자식"에만 쓰여 뜻이 하나가 된다. 단 타입(D 생성기가 만든 props 타입)을 잃으므로 Claim<<"Frame">>(inst, {…})처럼 타입 인자로 보완 — New<<X>>와 같은 관용구.
  2. 물리 순서 계약 — 디스크립터 배열 순서와 기존 트리의 실제 순서가 다를 때. Roblox는 물리 순서가 의미 없어 무관, web은 DOM 순서라 nativeInsert 위치가 어긋난다. 갈래: (a) 디스크립터 순서가 정본이고 일치는 사용자 책임(UB, debug 검사) / (b) claim 시 quad가 nativeMove로 실제 순서를 디스크립터에 맞춘다. 권고 (a) — "이미 있는 걸 그대로"의 취지, Roblox에선 비용 0.
  3. New로 만든 자식을 claim된 부모에 넣는 것 — 위 §4 첫 항목. 정적 자식이니 InstanceChildHandlerParent =를 하면 되고 새 결정은 없어 보이나, "매핑(이미 있음)"과 "생성(새로 붙임)"이 한 배열에 섞이는 것이 계약상 괜찮은지 확인 문항.
  4. debug 검사의 범위 — 이름 중복 / 부재 / 클래스 불일치 / 미매핑 부기 대상 자식 / 같은 quad의 이중 claim 중 어디까지. 권고: 전부(debug에선 싸다).
  5. 표면 이름Claim/Mount/Adopt, D.Mapper/D.Existing. Mount(root, parent)H-146에서 기각한 사유("부기 없는 대상에 quad 객체 주입")는 여기 반대로 적용된다 — claim은 부기를 세우는 행위. 권고 Claim.
  6. 마일스톤 — M5 이후(nativeFindChild가 프로바이더 표면). ROADMAP.md 백로그에 포인터만. [2026-08-28 /code-review, H-161]H-146 루트 예외를 폐기한 지금 M5에 승인된 루트 부착 경로가 없다 — (a) Claim을 M5 스코프로 당김 / (b) Claim 전까지 M5 한정 임시 예외 / (c) 루트 컨테이너용 얇은 표면. 사용자 확정 (a)("M5 스코프로 올라가도 될것으로 보임") — 헤더와 ROADMAP.md M5 체크박스에 반영. §5-7(다중 스크립트)은 여전히 미결.
  7. [2026-08-28 /code-review, H-161] 여러 스크립트/여러 quad가 같은 루트 컨테이너를 쓰는 경우 — 위 "이중 claim error / 다중 quad UB / 부기 대상 자식 전부 매핑"을 그대로 두면 Shop.client.luauInventory.client.luau가 각각 Claim(PlayerGui, …)하는 가장 흔한 사례가 막힌다. 이건 "전부 매핑" 계약이 루트 컨테이너(부기 대상이 아닌 PlayerGui·CoreGui류 — 자식 순서가 의미 없고 quad가 그 형제들을 관리하지 않는다)에는 안 맞는다는 신호다. 갈래: (a) Claim부기를 갖는 노드에만, 루트 컨테이너엔 "quad가 만든 자식 하나를 붙이는" 별개 표면(부기 없음, 여러 스크립트 공존, 이름은 §5-5와 같이) / (b) Claim에 "이 노드의 다른 자식은 관리하지 않는다"(부분 매핑) 모드 — 단 web처럼 물리 순서가 의미 있는 엔진에선 위험 / (c) 다중 claim을 허용하되 각 claim이 자기가 매핑한 자식만 소유(UB 대신 정의) — 부기 충돌 없음이 조건. 권고 없음 — (a)는 H-146에서 기각한 Mount(root, parent)의 재개방과 경계가 얇고, (b)(c)는 계약을 약화시킨다. 사용자 판단.
  8. 매핑된 정적 자식의 Parent 대입 — §2는 "부기만, .Parent =는 안 한다"인데 InstanceChildHandlerv.Parent = inst가 계약(dispatch-core-plan.md H-134). 같은 핸들러를 쓰면 이미 거기 있는 자식에 같은 값을 재대입(엔진 no-op)하는 것뿐이라 별도 핸들러가 필요 없어 보인다 — 확인 문항(권고: 같은 핸들러, 재대입 감수).

6. 반영 시 고칠 자리 (승격 때 체크리스트)

archive/existing-instance-bind-rejected.md 배너 / base/bind-system-plan.md H-142 항목의 H-146 루트 bullet(폐기 → 이 문서 포인터) / base/slot-plan.md "동적 자식은 반드시" 절 각주 / ROADMAP.md M5 Property.luau 전용 문구 삭제 + 백로그 항목 / research/documentation-content-map.md / question.md.