docs(base): dispose(value) 시그니처/범위 확정 — question.md 0-B 해소
범위를 Slot+엔진 객체로 좁히고 Observer/Effect는 명시적으로 제외 (GC-native bindLifetime만으로 충분, 트리 부기 없음). isSlot이 아니면 disposeInst 주입 op으로 위임(addTag/removeTag/setAttribute와 같은 패턴). 부수적으로 OnDestroyed 이름 재검토 조건도 종결. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2c575d91f0
commit
573dd452af
11 changed files with 294 additions and 84 deletions
File diff suppressed because one or more lines are too long
|
|
@ -627,6 +627,42 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
`useMemo` deps도 대부분 정적) — 의도적 비지원으로 확정. 상세는
|
||||
`research/framework-comparison-findings.md` 3번 절.
|
||||
|
||||
- **[해소됨, 2026-08-14 열 번째 세션]** 0-B. `dispose(any)` — 시그니처/범위.
|
||||
**결론**: 범위를 `Slot`+엔진 객체(Instance)로만 좁히고 **Observer/Effect는
|
||||
명시적으로 제외** — 시그니처는 `dispose(value: Slot | Instance)`,
|
||||
내부적으로 `if isSlot(value) then <Slot 자체 소유권 판정 재사용> else
|
||||
disposeInst(value) end`로 분기. `unbindLifetime`과의 역할 분담도 이걸로
|
||||
해소: `dispose`는 **트리 소유권 부기(elementOwner/lengthList 등)가 있는
|
||||
대상**이 아직 요구되는데 강제로 죽이려는 경우를 막는 게 목적이고,
|
||||
`unbindLifetime`은 Observer/Effect류의 GC 앵커(gcconn)를 조기 해제하는
|
||||
것 — 서로 다른 축이라 대체 불가.
|
||||
- **Observer/Effect가 빠지는 이유**: 이 둘은 children 배열 leaf 위치에서
|
||||
`Dispatch/Leaf.luau`가 매치해 내부적으로 `bindLifetime`/`canExecute`/
|
||||
`unbindLifetime`(GC-native, gcconn 기반)로만 관리됨(`base/
|
||||
source-state-plan.md` "이중 바인딩 금지" 절) — Slot의 `elementOwner`/
|
||||
`lengthList`/`sourceList` 같은 "죽으면 offset/length가 깨지는" 트리
|
||||
부기 자체가 없어서, 도중에 GC되거나 `unbindLifetime`으로 조기 해제돼도
|
||||
구조적으로 안전함. 즉 dispose가 막아야 하는 문제(트리 부기 붕괴)가
|
||||
Observer/Effect에는 원천적으로 발생하지 않음. `State<Observer>`/
|
||||
`State<Slot>` 등 반응형 leaf 값은 이미 확정된 일반 원칙("모든
|
||||
`(inst,k)`는 `T`든 `State<T>`든 `StoreBind`가 균일하게 재귀 처리")의
|
||||
자연스러운 귀결일 뿐 별도 설계가 필요 없었음 — Modifier 필드/Slot
|
||||
원소 금지 규칙(핸들러 계층 값 즉시 error)은 **다른 컨텍스트**(Modifier
|
||||
필드, `Slot:Add`/`:List`의 원소)라 여기 적용 안 됨, 혼동하지 말 것.
|
||||
- **base/backend 분리**: `dispose`가 `isSlot`이 아닐 때 위임하는
|
||||
`disposeInst(inst: any): ()`는 `addTag`/`removeTag`/`setAttribute`와
|
||||
같은 "base가 소유하는 핸들러와 주입되는 엔진 op" 패턴(`base/
|
||||
dispatch-core-plan.md`) — quad-roblox는 `inst:Destroy()`로 구현.
|
||||
- **네이밍**: `free()`는 GC 언어 맥락과 안 맞아 기각, `Destroy`는 엔진
|
||||
`:Destroy()` 메소드와 동명이라 사용자가 착각할 위험이 있어 기각 —
|
||||
`dispose` 유지.
|
||||
- 정본은 `base/slot-plan.md` "`dispose(any)`" 절 +
|
||||
`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op"
|
||||
절. 이 해소로 `base/lifecycle-hooks-plan.md`의 `OnDestroyed` 이름
|
||||
재검토 조건("0-B가 'quad가 만드는 모든 것의 유일한 파괴 경로'로
|
||||
풀리면")도 **발동하지 않는 쪽으로 영구 종결** — `OnDestroyed`가
|
||||
최종 이름.
|
||||
|
||||
## 참고: 지금까지 확정된 것 (요약)
|
||||
|
||||
전부 `base/`에 문서화되어 더 이상 열려있지 않음 — 상세 근거/논의 과정이
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ quad/
|
|||
├── wally.toml
|
||||
└── src/
|
||||
├── RobloxFactory.luau # BaseModule 뮤테이션, 재호출 가드(같은 팩토리=무시/다른=에러) — 주입 대상엔 bindLifetime/canExecute 외에 addTag/removeTag/setAttribute도 포함(2026-08-13 열네 번째 세션)
|
||||
├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제) (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절)
|
||||
├── EngineOps.luau # 주입되는 엔진 op 구현: addTag(inst,{string})/removeTag(inst,{string})=CollectionService, setAttribute(inst,name,v)=inst:SetAttribute(v==nil이면 삭제), disposeInst(inst)=inst:Destroy()(`dispose(value)`가 `isSlot`이 아닐 때 위임, `base/slot-plan.md`) (`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진 op" 절)
|
||||
├── LifetimeHandle.luau # bindLifetime/canExecute 실제 구현 — GetPropertyChangedSignal("ClassName") 연결 트릭으로 gcconn 확보, Relate:SetStrong으로 gcconn/gchold 저장(`base/lifecycle-pattern.md`). Relate 자체는 순수 Lua라 quad-roblox 쪽 재구현 없음(quad-base 그대로 재사용)
|
||||
├── Handlers/
|
||||
│ ├── Property.luau # 일반 프로퍼티 세팅 + `isTween(realv)` 분기(3-상태 릴레이션 슬롯 `RobloxTween|true|nil`, hasBeenSet 억제, override 정책) — 구 `Handlers/Tween.luau`(높은 우선순위 store-bind 핸들러)는 폐기(`archive/tween-special-bind-key-reversed.md`)
|
||||
|
|
|
|||
|
|
@ -526,7 +526,12 @@ end
|
|||
백엔드마다 재구현하면 **같은 참조 카운트/소유권 알고리즘이 통째로
|
||||
복제**됨 — `architecture.md`의 "엔진마다 큰 구현을 중복하지 않기 위해
|
||||
디스패치 엔진을 base가 인터페이스로 소유한다"는 원칙이 그대로 적용되는
|
||||
자리(2026-08-13 열네 번째 세션, 사용자 판단으로 재배치).
|
||||
자리(2026-08-13 열네 번째 세션, 사용자 판단으로 재배치). **같은 패턴이
|
||||
Dispatch 바깥에도 적용됨** — `dispose(value)`(`base/slot-plan.md`)는
|
||||
Dispatch 핸들러가 아니라 독립 탑레벨 유틸이지만, `isSlot`이 아닌 값은
|
||||
`elementOwner` 같은 순수 부기 판정 뒤에 마지막 한 줄만
|
||||
`disposeInst(inst: any): ()`로 위임(quad-roblox는 `inst:Destroy()`) —
|
||||
2026-08-14 열 번째 세션에 같은 원칙으로 확정.
|
||||
- **backend 소유**: `Property`/`Event`/`OnChange`(Reflection·시그널 같은
|
||||
엔진 개념 자체가 로직), `InstanceChild`, `Slot`의 실제 부모 조작
|
||||
(재조정 알고리즘은 base `Dispatch/Slot.luau`, 물리 마운트만 backend) —
|
||||
|
|
|
|||
|
|
@ -304,15 +304,14 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
있어 위 "①" 절의 대조 설명 없이 이름만 보면 헷갈릴 수 있음, 문서화
|
||||
시 명시할 것.
|
||||
- `OnDestroyed`는 최초 가칭이던 `OnDisposed`보다 사용자가 선호 —
|
||||
**이 이름으로 확정하되, `dispose()` 범위(0-B)가 풀리면 재검토 여지만
|
||||
남겨둠**(아래 "열린 질문" 절). `OnDisposed`가 제안된 이유는
|
||||
미래 `dispose()` 함수(`question.md`
|
||||
"0-B. `dispose(any)` — 시그니처/범위")와 이름을 맞추자는 발상이었는데,
|
||||
대조해보면 트리거 자체가 다름:
|
||||
**`OnDestroyed`로 영구 확정**(2026-08-14 열 번째 세션, `dispose()` 범위
|
||||
확정으로 재검토 조건 자체가 종결됨 — 아래). `OnDisposed`가 제안된
|
||||
이유는 미래 `dispose()` 함수(`archive/question-resolved.md` 0-B)와
|
||||
이름을 맞추자는 발상이었는데, 대조해보면 트리거 자체가 다름:
|
||||
- `dispose(value)`는 사용자가 **의도적으로** 부르는 명시적 파괴
|
||||
API로 설계 중(대상이 아직 트리에 의해 살아있길 요구되면 파괴를
|
||||
**거부하고 error**, `question.md` 0-B). "언제 부를지"를 호출자가
|
||||
고르는 능동적 경로.
|
||||
API(대상이 아직 트리에 의해 살아있길 요구되면 파괴를 **거부하고
|
||||
error**, `base/slot-plan.md`). "언제 부를지"를 호출자가 고르는
|
||||
능동적 경로.
|
||||
- 반면 이 문서의 훅은 `Effect`의 leaf-death cleanup에 얹히므로,
|
||||
실제 트리거는 **물리 Instance가 죽는 시점**(엔진 `Destroying`
|
||||
신호, `bindLifetime`이 감시하는 이벤트)임 — 그 죽음이 `dispose()`를
|
||||
|
|
@ -320,16 +319,14 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
정리했는지는 이 훅 입장에서 구분도 안 되고 상관도 없음.
|
||||
- 그래서 `OnDisposed`는 "`dispose()`를 불렀을 때만 발화한다"는
|
||||
잘못된 인상을 줄 위험이 있고, `OnDestroyed`가 실제 트리거(엔진
|
||||
`Destroying`)를 더 정직하게 반영함 — **지금 추천은 `OnDestroyed`**.
|
||||
- 단 이 판단은 **0-B가 아직 미확정**이라는 전제 위에 있음: 만약
|
||||
나중에 `dispose()`가 "Slot뿐 아니라 Instance/Effect까지 포함해
|
||||
quad가 만드는 모든 것의 유일한 파괴 경로"로 확정되면(0-B의
|
||||
"미확정: 대상 범위" 항목이 그쪽으로 풀리면), 그때는 `dispose()`와
|
||||
이름을 맞추는 재검토가 자연스러워질 수 있음.
|
||||
- 이름 자체는 런타임에 아무 의미가 없는 순수 네이밍(값은 그냥
|
||||
`PreRef`/`EffectHandle`)이라 바꾸는 비용은 0에 가까움 — 그래서
|
||||
위험 부담이 낮은 `OnDestroyed`로 확정하고, `dispose()` 범위가
|
||||
확정되면 재검토하는 게 결론.
|
||||
`Destroying`)를 더 정직하게 반영함 — **`OnDestroyed`가 최종 이름**.
|
||||
- **[해소, 2026-08-14 열 번째 세션]** 재검토 조건이던 "`dispose()`가
|
||||
Slot뿐 아니라 Instance/Effect까지 포함해 quad가 만드는 모든 것의
|
||||
유일한 파괴 경로로 확정되면"은 **발동하지 않는 쪽으로 결정됨** —
|
||||
`dispose()`의 범위는 오히려 `Slot`+`Instance`로 좁혀지고
|
||||
`Observer`/`Effect`는 명시적으로 제외됐음(`archive/
|
||||
question-resolved.md` 0-B). `OnDestroyed`↔`dispose()` 이름을 맞출
|
||||
이유 자체가 사라져 더 이상 재검토 대상 아님.
|
||||
- **`OnRendered`/`PostRef` — 이름 확정(2026-08-14 아홉 번째 세션).**
|
||||
`PostRef`는 `PreRef`와의 대칭성이 이름에서 바로 읽혀 이견 없음.
|
||||
`OnRendered`는 채택되며 이름도 그대로 확정 — 다만 **"렌더"가 quad엔
|
||||
|
|
@ -362,16 +359,9 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
|
||||
## 열린 질문
|
||||
|
||||
**[2026-08-14 아홉 번째 세션] 설계상 열린 질문 없음** — 채택 여부/
|
||||
메커니즘/스코프/패키지 배치가 전부 확정됨(위 각 절). 하나만 성격이
|
||||
다른 항목으로 남음:
|
||||
|
||||
- **`OnDestroyed` 이름 재검토 여지** — 지금 이름은 확정이지만, 미래
|
||||
`dispose()`의 대상 범위(`question.md` "0-B. `dispose(any)` —
|
||||
시그니처/범위")가 "quad가 만드는 모든 것의 유일한 파괴 경로"로 풀리면
|
||||
`OnDisposed`와 맞추는 재검토가 자연스러워질 수 있음(위 "이름 컨벤션"
|
||||
절의 대조 참고). `question.md`의 **용어 정리 대기열**에 3순위로 올려둠
|
||||
— `Slot`/`Brand`처럼 "base에 확정돼 있지만 이름만 재검토 대상"인
|
||||
기존 항목들과 같은 취급이고, 이름은 런타임에 아무 의미가 없어 바꾸는
|
||||
비용이 0에 가까움.
|
||||
- 그 외 확정된 결정 없음 — 착수 시점에 위 항목들을 순서대로 확인.
|
||||
**[2026-08-14 열 번째 세션] 열린 질문 없음** — 채택 여부/메커니즘/
|
||||
스코프/패키지 배치에 이어, 마지막까지 남아있던 `OnDestroyed` 이름
|
||||
재검토 조건도 `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고
|
||||
`Observer`/`Effect`는 제외되는 쪽으로 확정되며 발동 없이 종결됨(위
|
||||
"이름 컨벤션" 절) — `OnDestroyed`가 최종 이름. 착수 시점에 위 각 절을
|
||||
순서대로 확인하면 됨.
|
||||
|
|
|
|||
|
|
@ -1780,10 +1780,10 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
|
|||
것과 정확히 같은 문제 — quad는 그 값이 이미 죽었다는 걸 모른 채 언마운트
|
||||
경로를 탐. 순서는 항상 **`Set`(언마운트) → 그 다음 정리**.
|
||||
|
||||
#### `dispose(any)` — quad가 만든 것을 안전하게 지우는 유일한 경로 (신설, 사용자 제안)
|
||||
#### `dispose(value)` — quad가 관리 중인 값을 안전하게 지우는 유일한 경로 (신설, 사용자 제안, **2026-08-14 열 번째 세션에 시그니처/범위 확정 — `question.md` 0-B 해소**)
|
||||
|
||||
위 UB를 "조심하세요"로만 두지 않기 위해, **base 레벨 탑레벨 유틸
|
||||
`dispose(value)`** 를 제공하는 방향으로 확정:
|
||||
`dispose(value: Slot | Instance): ()`** 를 제공:
|
||||
|
||||
- 의미(**[정정, 사용자 확정]** 최초안은 "마운트돼 있으면 먼저 떼어낸 뒤
|
||||
파괴"였으나 더 단순하게 확정): **대상이 아직 어느 트리에 의해 살아있길
|
||||
|
|
@ -1804,11 +1804,52 @@ Slot이 마운트될 때 **자기 하위 요소들까지 `bindLifetime`으로
|
|||
거꾸로 읽으면 되므로 **새 부기가 필요 없음**. 사용자 지적: "이미 두
|
||||
곳에 넣는 게 에러나도록 하기로 했으니, 어디 마운트되었냐가 따져지고,
|
||||
그래서 이미 가능한 일".
|
||||
- 네이밍/시그니처(`dispose(any)`가 맞는지, 타입을 어떻게 좁힐지),
|
||||
Slot 외의 대상(Instance/Observer/Effect)까지 커버하는 범위,
|
||||
`unbindLifetime`과의 역할 분담은 **미확정** — `.claude/question.md`에
|
||||
올림. 여기서는 "언마운트로 바꾼 대신 명시적 파괴 수단을 제공한다"는
|
||||
방향만 확정.
|
||||
|
||||
**범위 — `Slot` + 엔진 객체(Instance)만, `Observer`/`Effect`는 명시적으로
|
||||
제외**(2026-08-14 열 번째 세션, 사용자 확정):
|
||||
|
||||
```lua
|
||||
function dispose(value)
|
||||
if isSlot(value) then
|
||||
-- 위 elementOwner 기반 판정 재사용 — 요구 중이면 error, 아니면 재귀 파괴
|
||||
...
|
||||
else
|
||||
disposeInst(value) -- 아래 주입 op
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- **왜 `Observer`/`Effect`는 dispose 대상이 아닌가**: 이 둘은 children
|
||||
배열 leaf 위치에 놓이면 `Dispatch/Leaf.luau`가 매치해 내부적으로
|
||||
`bindLifetime(inst, value)`를 호출하고(`base/source-state-plan.md`
|
||||
"이중 바인딩 금지" 절), 생존은 그 GC 앵커(gcconn)만으로 판정됨 —
|
||||
Slot처럼 "죽는 순간 `elementOwner`/`lengthList`/`sourceList`가
|
||||
어긋나는" 트리 부기 자체가 없음. 즉 dispose가 막으려는 문제(부기
|
||||
붕괴)가 Observer/Effect에는 원천적으로 발생하지 않음 — 아무도 안 들고
|
||||
있으면 그냥 GC, 조기에 끊고 싶으면 `unbindLifetime`으로 충분하고
|
||||
`dispose`가 다룰 이유가 없음.
|
||||
**주의 — Modifier 필드/`Slot:Add`·`:List` 원소 금지 규칙("핸들러 계층 값이
|
||||
들어오면 즉시 error")과 헷갈리지 말 것.** 그건 Modifier 필드나 Slot의
|
||||
CRUD 원소 자리에 관한 별개 규칙이고, children 배열의 leaf 위치(정적
|
||||
`Frame{observer}`류)나 그 leaf가 `State<Observer>`/`State<Effect>`로
|
||||
반응형으로 바뀌는 경우는 전혀 다른 컨텍스트 — 후자는 이미 확정된 일반
|
||||
원칙("모든 `(inst,k)`는 `T`든 `State<T>`든 `StoreBind`가 균일하게 재귀
|
||||
처리")의 자연스러운 귀결이라 별도 설계 없이 그냥 됨.
|
||||
- **`unbindLifetime`과의 역할 분담**: `dispose`는 **트리 소유권 부기가
|
||||
있는 대상**(Slot/Instance)이 아직 요구되는데 강제로 죽이려는 시도를
|
||||
막는 것이고, `unbindLifetime`은 Observer/Effect류의 GC 앵커를 조기
|
||||
해제하는 것 — 축이 달라 서로 대체 불가.
|
||||
|
||||
**base/backend 분리 — `disposeInst`는 주입 op**(`base/dispatch-core-plan.md`
|
||||
"base가 소유하는 핸들러와 주입되는 엔진 op" 절과 같은 패턴, `addTag`/
|
||||
`removeTag`/`setAttribute`가 선례): `dispose`가 `isSlot`이 아닌 값을
|
||||
받으면 base가 시그니처만 소유하는 `disposeInst(inst: any): ()`로 위임 —
|
||||
quad-roblox는 `inst:Destroy()`로 구현. 웹 등 다른 백엔드는 자기 방식으로
|
||||
매핑.
|
||||
|
||||
**네이밍**: `free()`는 GC-native 언어 맥락과 안 맞아 기각, `Destroy`는
|
||||
엔진 자체 `:Destroy()` 메소드와 동명이라 사용자가 "그냥 `:Destroy()`
|
||||
부르는 거 아님?"으로 착각할 위험이 있어 기각 — `dispose` 유지.
|
||||
|
||||
#### 구현상 바뀌어야 하는 것
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
## 결정 대기 — M0는 안 막음
|
||||
|
||||
### 0-W. 같은 `Ref` 객체가 두 자리에 놓이는 걸 막을 것인가 (2026-08-13 열세 번째 세션 신설, 0-Z 확인 중 발견)
|
||||
### 0-W. 같은 `Ref` 객체가 두 자리에 놓이는 걸 막을 것인가 (2026-08-13 열세 번째 세션 신설, 0-Z 확인 중 발견, 2026-08-14 열 번째 세션 기준 M4 구현 세부만 막는 유일한 잔여 항목 — 0-B는 해소돼 `archive/question-resolved.md`로 이전)
|
||||
|
||||
**0-Z(Attribute, 열네 번째 세션에 해소됨)를 보다가 "Ref에도 같은 문제가
|
||||
있냐"는 사용자 질문에서 나온 것 — 있고, 막는 장치가 전혀 없음.** 단
|
||||
|
|
@ -76,26 +76,6 @@
|
|||
단 증상이 "조용한 값 소실"이라 다른 UB보다 나쁨), (c) 마지막 쓰기 승리를
|
||||
정식 동작으로 인정(비권장, `Ref`의 "확정된 값 박스" 의미와 충돌).
|
||||
|
||||
### 0-B. `dispose(any)` — 시그니처/범위 (2026-08-13 여섯 번째 세션 신설, 사용자 제안)
|
||||
|
||||
`State<Slot>` 교체를 파괴가 아니라 **언마운트**로 확정하면서(`state<Frame>`와
|
||||
동일, `base/slot-plan.md`), 명시적 파괴 수단으로 base 탑레벨 `dispose(value)`를
|
||||
제공하기로 방향 확정. "이 값이 지금 어디 마운트돼 있는가"는 이미
|
||||
`elementOwner`가 들고 있어(다중 마운트 error 판정용) 새 부기가 필요 없음.
|
||||
|
||||
**[확정, 사용자] 시맨틱은 "거부"** — 대상이 아직 어느 트리에 의해
|
||||
살아있길 요구되고 있으면 **파괴를 거부하고 즉시 error**. 떼어내주지
|
||||
않음(떼어내는 건 `Set`=언마운트의 몫, `dispose`는 그 뒤). 근거: 엔진은
|
||||
`Destroy`/`Clear`에 에러를 안 내지만 quad의 `_elements`/`lengthList`/
|
||||
`sourceList`/`elementOwner`는 그 순간 어긋나므로, **quad가 관리 중인 값을
|
||||
안전하게 지우는 유일한 경로**가 이것이고 "지금 지우면 안 되는 상태"를
|
||||
잡아주는 게 존재 이유. 이걸로 "`Set` 전에 직접 `Destroy()`"가 UB에서
|
||||
명확한 에러로 바뀜.
|
||||
|
||||
**미확정**: 시그니처(`dispose(any)`가 맞는지, 타입을 어떻게 좁힐지),
|
||||
대상 범위(Slot 외에 Instance/Observer/Effect까지 커버하는지),
|
||||
`unbindLifetime`과의 역할 분담.
|
||||
|
||||
## 1. 용어 정리 — 아직 안 정해진 것만 (사용자 요청, 진행 중)
|
||||
|
||||
사용자 원 메모: "quad는 register라던가 좀 부정확하거나 느낌이 바로 와닿지
|
||||
|
|
@ -167,16 +147,6 @@
|
|||
이미 없앴으니 급하지 않지만, 최종 이름은 여전히 이 목록의 다른
|
||||
가칭들과 함께 검토 대상. `base/attribute-plan.md` "그룹 `Attribute(...)`"
|
||||
절 참고.
|
||||
- **`OnDestroyed`(3순위, 사소함, 2026-08-14 아홉 번째 세션 추가)**:
|
||||
`base/lifecycle-hooks-plan.md`가 이 이름으로 **확정**하되, 위 0-B
|
||||
(`dispose(any)` — 시그니처/범위)가 "quad가 만드는 모든 것의 유일한 파괴
|
||||
경로"로 풀리면 `OnDisposed`와 맞추는 재검토 여지를 남겨둠. 지금
|
||||
`OnDestroyed`인 이유는 실제 트리거가 `dispose()` 호출이 아니라 엔진
|
||||
`Destroying` 신호라서(그 문서 "이름 컨벤션" 절). 이름은 런타임에 아무
|
||||
의미가 없는 순수 네이밍이라 바꾸는 비용이 0에 가까움 — **0-B가 풀리기
|
||||
전엔 이 항목을 열지 말 것**(형제 `OnCreated`/`OnRendered`는 재검토
|
||||
대상 아님, 다만 `OnRendered`엔 "부모에 붙기 전에 불린다"는 캐비엇이
|
||||
있어 이름이 아니라 *문서화*로 대응하기로 확정됨).
|
||||
- **참고 — 이미 지나간 사례**: `register`(v1) → `State`(v2) 리네임은
|
||||
"모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든
|
||||
셈 — 이번 정리에서 같은 패턴을 조심할 것.
|
||||
|
|
|
|||
136
.claude/session/2026-08-14-10-dispose-scope-resolved.md
Normal file
136
.claude/session/2026-08-14-10-dispose-scope-resolved.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# 2026-08-14 열 번째 세션 — `dispose(value)` 시그니처/범위 확정 (`question.md` 0-B 해소)
|
||||
|
||||
## 배경
|
||||
|
||||
이전 대화(다른 세션이 동시에 작업 중이라 이 세션은 처음엔 읽기 전용으로
|
||||
시작)에서 사용자가 `question.md` 0-B(`dispose(any)` — 시그니처/범위,
|
||||
2026-08-13 여섯 번째 세션 신설)에 대해 물었고, 남은 미확정 항목(시그니처,
|
||||
Slot 외 대상 범위, `unbindLifetime`과의 역할 분담)을 확인하는 것으로
|
||||
시작했다.
|
||||
|
||||
## 1차 논의 — Observer/Effect 범위 관련 오류와 정정
|
||||
|
||||
사용자가 먼저 "Observer/Effect는 dispose 범위에서 빼야 한다"는 결론을
|
||||
제시하며 근거로 "`State<Observer>`/`State<Effect>`가 이미 지원 사양"이라고
|
||||
주장했다. 어시스턴트는 처음에 이걸 검증하며 `modifier-plan.md`의 "Modifier
|
||||
필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이
|
||||
들어오면 즉시 error" 규칙과 `slot-plan.md`의 Slot 원소 금지 규칙을 근거로
|
||||
"State<Observer>가 지원된다는 명시적 문서는 없다"고 (잘못) 답했다.
|
||||
|
||||
사용자가 "modifier가 왜 나온 말인지 전혀 모르겠다"며 반박 — Modifier
|
||||
필드 금지 규칙은 이 논의와 무관한 별개 컨텍스트였다. 재조사 결과 사용자
|
||||
말이 맞았음이 확인됨:
|
||||
|
||||
- `base/architecture.md`(Leaf.luau 파일 설명)와 `base/effect-plan.md`가
|
||||
이미 **children 배열의 leaf 위치**(`k=number`)에 `Ref`/`PreRef`/`PostRef`
|
||||
뿐 아니라 **`Observer`/`Effect`도 명시적으로 지원 대상**으로 확정해뒀음
|
||||
(`Dispatch/Leaf.luau`가 `(i:number, v=Observer/Effect)`를 매치).
|
||||
- `base/source-state-plan.md` "이중 바인딩 금지" 절이 "children 배열에
|
||||
Observer를 직접 놓으면 `Dispatch/Leaf.luau`가 매치해 내부적으로
|
||||
`bindLifetime(inst, observer)`를 호출한다"고 이미 명시 — leaf 부착이
|
||||
곧 `bindLifetime` 호출 그 자체.
|
||||
- `base/dispatch-core-plan.md`의 `Dispatch/StoreBind.luau`는 "범용,
|
||||
엔진 무관"이고 **`k`는 무엇이든 받음** — 즉 children 배열의 `k=number`
|
||||
슬롯에도 똑같이 적용되는 일반 재귀 재디스패치 계층이라, `State<Observer>`/
|
||||
`State<Effect>`가 그 자리에 놓여도 StoreBind가 실체를 뽑아 재귀
|
||||
`Dispatch.process`로 넘기고 `Leaf.luau`가 풀린 값을 매치 — 새 메커니즘
|
||||
없이 이미 있는 일반 원칙("모든 (inst,k)는 T든 State\<T\>든 균일하게
|
||||
처리")의 자연스러운 귀결.
|
||||
- Modifier 필드 금지 규칙(`modifier-plan.md:240`)과 Slot 원소 금지 규칙
|
||||
(`slot-plan.md:81`)은 **완전히 다른 컨텍스트**(Modifier 자신의 필드,
|
||||
`Slot:Add`/`:List`의 CRUD 원소)라 children 배열 leaf 위치와 무관함 —
|
||||
어시스턴트가 이 두 컨텍스트를 혼동한 게 오류의 원인이었음.
|
||||
|
||||
어시스턴트가 이 오류를 인정하고 정정, `State<Observer>`/`State<Effect>`가
|
||||
실제로 확정 사양임을 사용자에게 재확인했다.
|
||||
|
||||
## 2차 논의 — 모델을 Opus로 올릴지 질문, 반영 시작
|
||||
|
||||
사용자가 "모델을 opus로 올리고 반영 시작할까? 너는 어렵게 느껴? 아니면
|
||||
가능한 정도야?"라고 질문. 어시스턴트는 이번 반영 작업(정해진 패턴을
|
||||
따라 문서 여러 곳에 확정된 결정을 박아넣는 기계적 작업)이 Sonnet으로
|
||||
충분하다고 판단해 답변, CLAUDE.md의 "토큰 맥싱" 방침과도 일치한다고
|
||||
설명. 사용자가 "가능하다면 문서 반영하고, 세션 기록 남겨. 다른쪽 전부
|
||||
꺼서, 작업해도 좋음. 핸드오버 준비하고 커밋해줘"로 승인.
|
||||
|
||||
## 최종 확정 — `dispose(value)`
|
||||
|
||||
**범위**: `Slot` + 엔진 객체(`Instance`)만. **`Observer`/`Effect`는
|
||||
명시적으로 제외**.
|
||||
|
||||
- **이유**: Observer/Effect는 children 배열 leaf에서 `bindLifetime`/
|
||||
`canExecute`/`unbindLifetime`(GC-native, gcconn 기반)로만 관리되고,
|
||||
Slot처럼 "죽는 순간 `elementOwner`/`lengthList`/`sourceList`가
|
||||
어긋나는" 트리 부기 자체가 없음. dispose가 막아야 하는 문제(quad 내부
|
||||
자료구조 붕괴)가 Observer/Effect에는 원천적으로 발생하지 않아 dispose가
|
||||
다룰 이유가 없음. 아무도 안 들고 있으면 그냥 GC, 조기에 끊고 싶으면
|
||||
`unbindLifetime`으로 충분.
|
||||
|
||||
**시그니처**: `dispose(value: Slot | Instance): ()` —
|
||||
```lua
|
||||
function dispose(value)
|
||||
if isSlot(value) then
|
||||
-- 기존 elementOwner 기반 소유권 판정 재사용: 요구 중이면 error, 아니면 재귀 파괴
|
||||
...
|
||||
else
|
||||
disposeInst(value) -- 백엔드 주입 op
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**base/backend 분리**: `isSlot`이 아닌 값은 `disposeInst(inst: any): ()`로
|
||||
위임 — `base/dispatch-core-plan.md`의 "base가 소유하는 핸들러와 주입되는
|
||||
엔진 op" 패턴(`addTag`/`removeTag`/`setAttribute`가 선례) 그대로 재사용.
|
||||
quad-roblox는 `inst:Destroy()`로 구현.
|
||||
|
||||
**네이밍 경위**(사용자가 직접 검토): `free()`는 GC-native 언어 맥락과 안
|
||||
맞아 기각. `Destroy`는 엔진 자체 `:Destroy()` 메소드와 동명이라 사용자가
|
||||
"그냥 `:Destroy()` 부르는 거 아님?"으로 착각할 위험이 있어 기각. `dispose`
|
||||
유지.
|
||||
|
||||
**`unbindLifetime`과의 역할 분담**: `dispose`는 트리 소유권 부기가 있는
|
||||
대상(Slot/Instance)이 아직 요구되는데 강제로 죽이려는 시도를 막는 것,
|
||||
`unbindLifetime`은 Observer/Effect류 GC 앵커의 조기 해제 — 축이 달라
|
||||
대체 불가.
|
||||
|
||||
## 부수 해소 — `OnDestroyed` 이름 재검토 조건 종결
|
||||
|
||||
`base/lifecycle-hooks-plan.md`가 "0-B가 'quad가 만드는 모든 것의 유일한
|
||||
파괴 경로'로 풀리면 `OnDisposed`와 이름을 맞추는 재검토가 자연스러워질 수
|
||||
있다"는 조건부 열린 항목을 갖고 있었음. 이번 해소로 `dispose()`의 범위가
|
||||
오히려 좁아지고(Slot+Instance만) Observer/Effect가 제외됐으므로, 그 조건은
|
||||
**발동하지 않는 쪽으로 영구 종결** — `OnDestroyed`가 최종 이름, 용어 정리
|
||||
대기열에서도 제외.
|
||||
|
||||
## 반영한 문서
|
||||
|
||||
- `.claude/question.md` — 0-B 섹션 제거(해소), 0-W만 남은 "결정 대기"
|
||||
항목으로 정리, `1. 용어 정리`의 `OnDestroyed` 조건부 항목 제거.
|
||||
- `.claude/archive/question-resolved.md` — 0-B 결론+근거 추가(원 문서
|
||||
형식과 동일하게 "해소됨" 표시, 원문 요약 포함).
|
||||
- `.claude/base/slot-plan.md` — `dispose(value)` 절 전면 갱신(범위/시그니처/
|
||||
disposeInst 위임/네이밍 근거 확정 반영).
|
||||
- `.claude/base/dispatch-core-plan.md` — "base가 소유하는 핸들러와
|
||||
주입되는 엔진 op" 절에 dispose/disposeInst를 재사용 사례로 추가.
|
||||
- `.claude/base/architecture.md` — `EngineOps.luau` 파일 설명에
|
||||
`disposeInst` 추가.
|
||||
- `.claude/base/lifecycle-hooks-plan.md` — `OnDestroyed` 이름 컨벤션 절과
|
||||
"열린 질문" 절 모두 조건 종결로 갱신.
|
||||
- `ROADMAP.md` — M6 `dispose(value)` 체크리스트 항목에 확정된 시그니처/
|
||||
분기 반영, "0-B 미확정" 문구 제거.
|
||||
- `HUMAN_TODO.md` — 3번 항목에서 0-B를 해소로 갱신.
|
||||
- `CLAUDE.md` — "지금 할 일" 0번 섹션에서 0-B를 해소로 갱신(0-W만 잔여).
|
||||
- `.claude/README.md` — `slot-plan.md`/`lifecycle-hooks-plan.md`/
|
||||
`dispatch-core-plan.md` 세 행에 이번 세션 반영 내용 추가.
|
||||
|
||||
`doc-check.py` ERROR 0 유지 확인(WARN 84, 편집 전과 동일 — 새로 생긴
|
||||
불일치 없음).
|
||||
|
||||
## 참고 — 세션 중 발견한 별개 사실
|
||||
|
||||
다른 세션(quad-4a)이 동시에 커밋한 `2c575d9`(PostRef 반영 후 코퍼스
|
||||
정합성 감사)가 이 세션 시작 시점엔 이미 반영돼 있었음 — 대화 시작
|
||||
시점의 CLAUDE.md 스냅샷(시스템 리마인더)이 살짝 stale했으므로, 실제
|
||||
편집 전 `git log`/파일 재확인으로 최신 상태를 다시 잡고 작업함(세션
|
||||
번호를 "아홉 번째" 다음인 "열 번째"로 올바르게 잡을 수 있었던 것도 이
|
||||
재확인 덕분).
|
||||
29
CLAUDE.md
29
CLAUDE.md
|
|
@ -163,8 +163,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
`question.md`의 최우선 항목이 **전부 비었음** — `0-Y`(`:Compute` lazy
|
||||
핸들 계약)는 13차 세션에, **`0-Z`(Attribute 이름 소유권)와 `0-A`(재디스패치
|
||||
하강 diff)는 14차 세션에 확정·`base/` 반영 완료**. 남은 `0-W`(같은 `Ref`
|
||||
이중 배치)/`0-B`(`dispose` 시그니처)는 M0가 아니라 각각 M4/M6 구현 세부를
|
||||
막을 뿐임.
|
||||
이중 배치)는 M0가 아니라 M4 구현 세부만 막음 — **[2026-08-14 열 번째
|
||||
세션] `0-B`(`dispose` 시그니처/범위)도 해소**되어 `question.md`의
|
||||
"결정 대기"엔 이제 `0-W` 하나만 남음.
|
||||
|
||||
**M0 착수 전 반드시 읽을 것 — 이 두 개는 "결정"이 아니라 "구현 규약"이라
|
||||
여전히 유효**:
|
||||
|
|
@ -1343,3 +1344,27 @@ base 승격**
|
|||
건 `OnDestroyed` 이름 재검토 여지 하나(0-B 확정 시, `question.md` 용어
|
||||
대기열 3순위). ROADMAP M8/백로그·README·brand/architecture/slot/modifier/
|
||||
typing-limits 전파 완료, `doc-check.py` ERROR 0.
|
||||
|
||||
**2026-08-14 열 번째 세션 — `dispose(value)` 시그니처/범위 확정, `question.md` 0-B 해소**
|
||||
(`session/2026-08-14-10-dispose-scope-resolved.md`)
|
||||
사용자가 0-B의 남은 미확정(시그니처/대상 범위/`unbindLifetime`과의 역할
|
||||
분담)을 직접 확정: **범위는 `Slot`+엔진 객체(`Instance`)만, `Observer`/
|
||||
`Effect`는 명시적으로 제외**(둘은 children 배열 leaf에서 `bindLifetime`/
|
||||
`canExecute`(GC-native)만으로 관리되고 Slot 같은 트리 부기 자체가 없어
|
||||
dispose가 막는 문제가 원천적으로 안 생김). 시그니처는 `dispose(value:
|
||||
Slot | Instance)` — `isSlot`이면 기존 `elementOwner` 판정 재사용, 아니면
|
||||
`disposeInst(inst)`(`addTag`/`removeTag`/`setAttribute`와 같은 "base
|
||||
소유+op 주입" 패턴)로 위임. 네이밍은 `free`(GC 언어 맥락과 안 맞음)/
|
||||
`Destroy`(엔진 `:Destroy()`와 혼동 위험) 둘 다 기각하고 `dispose` 유지.
|
||||
과정에서 어시스턴트가 "Observer/Effect가 `State<>`로 지원되는지"를 처음에
|
||||
Modifier 필드 금지 규칙과 혼동해 잘못 답했다가 사용자 지적으로 정정 —
|
||||
실제로는 children 배열 leaf(`Dispatch/Leaf.luau`)가 이미 `Observer`/
|
||||
`Effect`를 지원 대상으로 확정해뒀고, `StoreBind`가 "범용, `k`는 무엇이든
|
||||
받음"이라 `State<Observer>`도 별도 설계 없이 기존 재귀 디스패치 원칙만으로
|
||||
됨. 부수 해소로 `base/lifecycle-hooks-plan.md`의 `OnDestroyed` 이름
|
||||
재검토 조건("0-B가 모든 것의 유일한 파괴 경로로 풀리면")도 반대 방향
|
||||
(범위가 좁아짐)으로 확정되며 발동 없이 영구 종결. `question.md`/
|
||||
`archive/question-resolved.md`/`base/slot-plan.md`/
|
||||
`base/dispatch-core-plan.md`/`base/architecture.md`/
|
||||
`base/lifecycle-hooks-plan.md`/`ROADMAP.md`/`HUMAN_TODO.md`/`README.md`
|
||||
전부 반영, `doc-check.py` ERROR 0 유지.
|
||||
|
|
|
|||
|
|
@ -149,8 +149,9 @@ git branch -D worktree-debounce-throttle-plan
|
|||
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
|
||||
진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이
|
||||
마음에 안 드는 것만 답해주면 됨 — **[2026-08-13 열네 번째 세션 기준]
|
||||
M0 착수를 막는 항목은 하나도 없음**(0-W `Ref` 이중 배치는 M4, 0-B
|
||||
`dispose` 시그니처는 M6 구현 세부만 막음).
|
||||
M0 착수를 막는 항목은 하나도 없음**(남은 건 0-W `Ref` 이중 배치, M4 구현
|
||||
세부만 막음 — 0-B `dispose` 시그니처/범위는 2026-08-14 열 번째 세션에
|
||||
해소돼 `archive/question-resolved.md`로 이전됨).
|
||||
|
||||
---
|
||||
Sources (MCP 리서치): [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server), [How to Connect Claude Code to Roblox Studio — Clauder Navi](https://www.clauder-navi.com/en/claude-roblox-studio)
|
||||
|
|
|
|||
18
ROADMAP.md
18
ROADMAP.md
|
|
@ -370,12 +370,18 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
클로저가 early-return해도 체인에서 항상 소비하므로).
|
||||
- 전부 `base/slot-plan.md`에 반영돼 있고, `luau-test/19` C 섹션이
|
||||
소유권 분기를 음성 대조군까지 포함해 실측 검증함.
|
||||
- [ ] **`dispose(value)`** — 대상이 아직 어느 트리에 의해 살아있길 요구되면
|
||||
**파괴를 거부하고 즉시 error**(떼어내주지 않음 — 떼는 건 `Set`=언마운트의
|
||||
몫). 엔진은 `Destroy`/`Clear`에 에러를 안 내지만 quad 자료구조가 깨지므로,
|
||||
quad가 관리 중인 값을 안전하게 지우는 유일한 경로. 마운트 위치는
|
||||
`elementOwner`가 이미 알고 있어 새 부기 불필요. 시그니처/대상 범위는
|
||||
`.claude/question.md` 0-B에서 미확정
|
||||
- [ ] **`dispose(value: Slot | Instance)`** — 대상이 아직 어느 트리에 의해
|
||||
살아있길 요구되면 **파괴를 거부하고 즉시 error**(떼어내주지 않음 —
|
||||
떼는 건 `Set`=언마운트의 몫). 엔진은 `Destroy`/`Clear`에 에러를 안
|
||||
내지만 quad 자료구조가 깨지므로, quad가 관리 중인 값을 안전하게
|
||||
지우는 유일한 경로. 마운트 위치는 `elementOwner`가 이미 알고 있어
|
||||
새 부기 불필요. `isSlot(value)`면 그 경로, 아니면 백엔드가 주입하는
|
||||
`disposeInst(inst): ()`(`addTag`/`removeTag`/`setAttribute`와 같은
|
||||
"base 소유+op 주입" 패턴, quad-roblox는 `inst:Destroy()`)로 위임.
|
||||
**`Observer`/`Effect`는 범위 밖**(GC-native `bindLifetime`/
|
||||
`unbindLifetime`만으로 충분, 트리 부기 없음) — 2026-08-14 열 번째
|
||||
세션에 `question.md` 0-B 해소, 정본은 `base/slot-plan.md`
|
||||
"`dispose(value)`" 절
|
||||
|
||||
- [x] **"여러 Slot이 형제로 섞일 때 순서 보장" 해소**(2026-08-09 여섯 번째
|
||||
세션) — `Dispatch.setLength`/`setOffsetSource` 메커니즘, `base/
|
||||
|
|
|
|||
Loading…
Reference in a new issue