decide(base): Tag를 array-part 값 객체로 재설계, Dispatch 체인+retractUnder로 retract 전파 확정
Tag를 해시 파트 boolean DI 키에서 Modifier식 immutable clone 체이닝 값 객체로 재설계(구 모델은 archive/tag-hash-key-model-reversed.md로 보존). 이 과정에서 재귀 재-dispatch(StoreBind/Tween/NoneHandler)의 retract가 다단 체인까지 정확히 전파되지 않던 설계 공백(pre-implementation-audit.md 1-2번)이 드러나, Dispatch가 (inst,k)별 핸들러 체인을 직접 소유하고 retractUnder로 꼬리부터 정리하는 방식으로 해결.
This commit is contained in:
parent
75cae39c7c
commit
54a46aaf7e
8 changed files with 399 additions and 75 deletions
|
|
@ -37,7 +37,7 @@
|
||||||
| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤(M3)과 함께 개발. 메커니즘+이름 확정 |
|
| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤(M3)과 함께 개발. 메커니즘+이름 확정 |
|
||||||
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, state?)` — `state` 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료 |
|
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, state?)` — `state` 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료 |
|
||||||
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님) |
|
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님) |
|
||||||
| `tag-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** `[Tag "Name"] = boolean` — `CollectionService` 얇은 래퍼, `process`가 add/remove 전부 처리, `retract` 불필요 |
|
| `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. 이제 `retract`가 의미 있음(타입이 바뀌면 전체 삭제, 같은 Tag끼리는 `process`가 diff). 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md` |
|
||||||
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** `[Attribute "Name"]` — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐, `retract` 불필요. 타입 파라미터화 이름(`Attribute<T>` vs `BooleanAttribute`류)만 미확정 |
|
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** `[Attribute "Name"]` — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐, `retract` 불필요. 타입 파라미터화 이름(`Attribute<T>` vs `BooleanAttribute`류)만 미확정 |
|
||||||
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘 |
|
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘 |
|
||||||
|
|
||||||
|
|
|
||||||
45
.claude/archive/tag-hash-key-model-reversed.md
Normal file
45
.claude/archive/tag-hash-key-model-reversed.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# [역전됨] Tag = 해시 파트 boolean DI 키(`[Tag "Name"] = true`) — array-part 값 객체로 대체됨
|
||||||
|
|
||||||
|
**역전 일시**: 2026-08-08 (세 번째 세션). **원 확정 일시**: 2026-08-07
|
||||||
|
여덟 번째 세션(`base/tag-plan.md` 최초 작성, "상태: base — 전부 확정").
|
||||||
|
**현재 유효한 설계**: `base/tag-plan.md`(전면 재작성됨)가 최종 소스. 이
|
||||||
|
파일은 능동적으로 참고할 필요 없음(구현에 안 씀) — 왜 "태그 하나당 키
|
||||||
|
하나"에서 "여러 태그를 조합하는 값 객체"로 넘어갔는지가 `quadnomicon`
|
||||||
|
소재로 가치 있어서 사유·원문을 통째로 보존해둔 것.
|
||||||
|
|
||||||
|
## 역전된 사례 — 원래 무엇을 확정했었나
|
||||||
|
|
||||||
|
**값 모양**: `[Tag "Name"] = boolean | State<boolean>` — 태그 이름 하나당
|
||||||
|
해시 파트 키 하나, 값은 store-bind 가능한 boolean.
|
||||||
|
|
||||||
|
**메커니즘**: `isHandlable`이 `[Tag "Name"]` 모양의 키를 매칭하는
|
||||||
|
`TagHandler` 하나로 충분. `process(inst,k,v)`가 `v`가 참이면 `AddTag`,
|
||||||
|
거짓/`nil`이면 `RemoveTag`. **`retract` 불필요**로 결론 — "값이 뭐든
|
||||||
|
(`true`/`false`/`nil`) 항상 같은 `TagHandler`가 이 키를 계속 담당하니
|
||||||
|
핸들러 *타입*이 안 바뀐다"는 게 근거였음.
|
||||||
|
|
||||||
|
## 왜 역전됐나
|
||||||
|
|
||||||
|
사용자가 실사용 시나리오를 제시하며 기각: 상호배타적인 스타일 상태
|
||||||
|
(`btn1`/`btn2`/`btn3`류, 실제로는 20개까지도 가능)를 표현하려면 이 모델은
|
||||||
|
**태그 이름 개수만큼 boolean 키를 각각 만들어야** 함 — 상태 전환마다
|
||||||
|
여러 키를 동시에 갱신해야 하고, 스타일 조합(여러 태그를 합쳐 쓰는 것)도
|
||||||
|
자연스럽게 표현이 안 됨. "하나의 값을 통째로 바꿔서 태그 집합을 바꾼다"는
|
||||||
|
요구를 이 모델은 구조적으로 못 담음.
|
||||||
|
|
||||||
|
## 대체 모델과의 비교
|
||||||
|
|
||||||
|
| | 구 모델(해시 파트) | 신 모델(array-part 값 객체) |
|
||||||
|
|---|---|---|
|
||||||
|
| 값 모양 | `[Tag "이름"] = boolean` | `Tag(...)`/`Tag.Merged(...)` 값 객체, array 슬롯에 놓임 |
|
||||||
|
| 상태 전환 | 태그 개수만큼 키 갱신 | 값 하나를 store-bind로 교체 |
|
||||||
|
| 조합 | 안 됨(키가 독립적) | `:Added`/`:Removed`/`Merged`로 조립 |
|
||||||
|
| retract | 불필요(핸들러 타입 안 바뀜) | 필요(값이 `nil`이 되면 핸들러 자체가 안 바뀜, 전체 삭제) — `Dispatch` 체인 메커니즘(`bind-system-plan.md` "Dispatch 체인" 절)과 맞물려 재설계됨 |
|
||||||
|
|
||||||
|
부수적으로, 이 역전이 `Dispatch.process`/`retract`의 "이전 매치 핸들러
|
||||||
|
추적" 문제(`pre-implementation-audit.md` 1-2번)를 실제로 파고드는 계기가
|
||||||
|
됐음 — Tag가 재귀 재-dispatch(`Source<Tag|nil>`가 store-bind를 거쳐
|
||||||
|
TagHandler로 위임)에 진입하는 첫 구체 사례가 되면서, "핸들러 타입이 안
|
||||||
|
바뀌니 retract 불필요"라는 구 모델의 전제 자체가 신 모델에서 깨졌고, 그
|
||||||
|
자리를 메우려다 `Dispatch.retractUnder`(체인 기반 retract 전파) 설계로
|
||||||
|
이어짐.
|
||||||
|
|
@ -26,9 +26,12 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
|
||||||
테이블을 계속 쌓는 방식, `reference/quad-v1-architecture.md` 참고)은 폐기.
|
테이블을 계속 쌓는 방식, `reference/quad-v1-architecture.md` 참고)은 폐기.
|
||||||
store 바인드에 대한 변경은 "전체 변경"으로 간주(UB 아님, 문서화된 의미론) —
|
store 바인드에 대한 변경은 "전체 변경"으로 간주(UB 아님, 문서화된 의미론) —
|
||||||
부분 복사/오버레이가 필요하면 팩토리 함수로 필요한 곳만 명시적으로 복사.
|
부분 복사/오버레이가 필요하면 팩토리 함수로 필요한 곳만 명시적으로 복사.
|
||||||
4. **PA님 스타일 DI 키 계속 지원**: `[Attribute "Name"]`, `[Tag ""] = true` 같은
|
4. **PA님 스타일 DI 키 계속 지원**: `[Attribute "Name"]` 같은 특수 바인드 키,
|
||||||
특수 바인드 키. Tag는 `retract`(구 cleanup, `base/lifecycle-pattern.md` 참고)가
|
store 컴퓨티드 바인드도 가능해야 함(`retract`, 구 cleanup,
|
||||||
내장되어 store 컴퓨티드 바인드도 가능해야 함.
|
`base/lifecycle-pattern.md` 참고). **[정정, 2026-08-08 세 번째 세션]**
|
||||||
|
`Tag`는 더 이상 `[Tag ""] = true` 해시 파트 DI 키가 아님 — array-part
|
||||||
|
값 객체(`Tag(...)`)로 재설계됨, `base/tag-plan.md` 참고
|
||||||
|
(`archive/tag-hash-key-model-reversed.md`에 구 모델 보존).
|
||||||
5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/
|
5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/
|
||||||
`Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유.
|
`Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유.
|
||||||
네임스페이싱 문제는 있지만 별도 네임스페이스 개념을 추가하면 라이브러리
|
네임스페이싱 문제는 있지만 별도 네임스페이스 개념을 추가하면 라이브러리
|
||||||
|
|
@ -134,9 +137,10 @@ quad/
|
||||||
│ ├── Store.luau # source 집합체, dot-access로 Source 그대로 반환
|
│ ├── Store.luau # source 집합체, dot-access로 Source 그대로 반환
|
||||||
│ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`), State/Source와 밀접 연관돼 같은 위치
|
│ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`), State/Source와 밀접 연관돼 같은 위치
|
||||||
│ ├── Modifier.luau # flatten-before-dispatch, immutable 체이닝, 제네릭 `__index` 필드 setter 합성 + `:Apply`/`:Peek`/`Override`(`base/modifier-plan.md`)
|
│ ├── Modifier.luau # flatten-before-dispatch, immutable 체이닝, 제네릭 `__index` 필드 setter 합성 + `:Apply`/`:Peek`/`Override`(`base/modifier-plan.md`)
|
||||||
|
│ ├── Tag.luau # 값 타입+immutable clone 체이닝(`Tag(...)`/`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), CollectionService 글루는 quad-roblox Handlers/Tag.luau(`base/tag-plan.md`, 2026-08-08 세 번째 세션)
|
||||||
│ ├── Effect.luau # `Effect(fn, state?)` — state 없으면 설치1회+leaf사망시 정리, 있으면 State.Observer를 조합해 재실행(`base/effect-plan.md`)
|
│ ├── Effect.luau # `Effect(fn, state?)` — state 없으면 설치1회+leaf사망시 정리, 있으면 State.Observer를 조합해 재실행(`base/effect-plan.md`)
|
||||||
│ ├── Dispatch/
|
│ ├── Dispatch/
|
||||||
│ │ ├── init.luau # process/retract 엔진, isHandlable 우선순위 스캔
|
│ │ ├── init.luau # process/retract 엔진, isHandlable 우선순위 스캔, `chains`(inst,k별 핸들러 체인)+`retractUnder`(`bind-system-plan.md` "Dispatch 체인" 절, 2026-08-08 세 번째 세션)
|
||||||
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process/retract)
|
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process/retract)
|
||||||
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
|
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
|
||||||
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/PreRef) children-array leaf 매칭 Handler, StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/PreRef) children-array leaf 매칭 Handler, StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
||||||
|
|
@ -155,7 +159,7 @@ quad/
|
||||||
│ ├── Property.luau
|
│ ├── Property.luau
|
||||||
│ ├── Event.luau # ReflectionService 기반 자동 판별
|
│ ├── Event.luau # ReflectionService 기반 자동 판별
|
||||||
│ ├── Attribute.luau
|
│ ├── Attribute.luau
|
||||||
│ ├── Tag.luau # CollectionService
|
│ ├── Tag.luau # CollectionService 글루만(process/retract) — 값 타입/API는 quad-base Tag.luau(`base/tag-plan.md`)
|
||||||
│ ├── Tween.luau # 높은 우선순위 store-bind 핸들러
|
│ ├── Tween.luau # 높은 우선순위 store-bind 핸들러
|
||||||
│ ├── Slot.luau # base Slot 재조정 로직의 실제 적용/해제(Instance Parent 조작)
|
│ ├── Slot.luau # base Slot 재조정 로직의 실제 적용/해제(Instance Parent 조작)
|
||||||
│ └── InstanceChild.luau # k:number, v:Instance — 중첩 인스턴스 자식(예: Frame { Frame {} })
|
│ └── InstanceChild.luau # k:number, v:Instance — 중첩 인스턴스 자식(예: Frame { Frame {} })
|
||||||
|
|
|
||||||
|
|
@ -92,13 +92,16 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
|
||||||
결국 정리하긴 하지만, GC 되기 전에도 store 값이 업데이트될 수 있으므로
|
결국 정리하긴 하지만, GC 되기 전에도 store 값이 업데이트될 수 있으므로
|
||||||
그 시점엔 그냥 `Connected`를 보고 무시(no-op).
|
그 시점엔 그냥 `Connected`를 보고 무시(no-op).
|
||||||
2. 처리해도 되면, 사용자가 넘긴 함수들을 거쳐 실제 값(`realv`)을 계산.
|
2. 처리해도 되면, 사용자가 넘긴 함수들을 거쳐 실제 값(`realv`)을 계산.
|
||||||
3. **`realv`를 들고 다시 `Dispatch.process(inst, k, realv)`를 재귀 호출**
|
3. **재귀 호출 전에 먼저 `Dispatch.retractUnder(inst, k, self, realv)`를
|
||||||
(오케스트레이터 이름 공식화는 아래 `None` 센티널 절 참고, 2026-08-07
|
불러 자기 밑에 위임돼 있던 걸 정리한 뒤, `realv`를 들고
|
||||||
여덟 번째 세션) — 이게 바로 "store 바인드는 pluggable 바인드를
|
`Dispatch.process(inst, k, realv)`를 재귀 호출**(정확한 메커니즘은
|
||||||
재실행하는 래핑"이라는 이 문서 이전 초안의 결론과 일치. `realv`가
|
아래 "Dispatch 체인" 절, 2026-08-08 세 번째 세션 — 오케스트레이터
|
||||||
store가 아니라면 자연히 Tween의 store-bind 핸들러 `isHandlable`을
|
이름 공식화는 아래 `None` 센티널 절 참고, 2026-08-07 여덟 번째
|
||||||
통과 못 하고 우선순위상 다음 핸들러(일반 프로퍼티 세터 등)로 흘러감
|
세션) — 이게 바로 "store 바인드는 pluggable 바인드를 재실행하는
|
||||||
— 무한 재귀 걱정 없음.
|
래핑"이라는 이 문서 이전 초안의 결론과 일치. `realv`가 store가
|
||||||
|
아니라면 자연히 Tween의 store-bind 핸들러 `isHandlable`을 통과 못
|
||||||
|
하고 우선순위상 다음 핸들러(일반 프로퍼티 세터 등)로 흘러감 — 무한
|
||||||
|
재귀 걱정 없음.
|
||||||
- **`retract(inst, k, v)`** (이전 초안의 "cleanup", 이름 변경 근거는
|
- **`retract(inst, k, v)`** (이전 초안의 "cleanup", 이름 변경 근거는
|
||||||
`base/lifecycle-pattern.md` 참고) — 이전 처리를 무르는/멈추는 함수. **오직
|
`base/lifecycle-pattern.md` 참고) — 이전 처리를 무르는/멈추는 함수. **오직
|
||||||
"같은 key에 새 값이 들어와서 이전 처리를 갈아치우는" 시나리오에만 존재** —
|
"같은 key에 새 값이 들어와서 이전 처리를 갈아치우는" 시나리오에만 존재** —
|
||||||
|
|
@ -111,14 +114,19 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
|
||||||
세션, 정정) — 예: 이전엔 Tween 핸들러가 매치돼 애니메이션이 실행
|
세션, 정정) — 예: 이전엔 Tween 핸들러가 매치돼 애니메이션이 실행
|
||||||
중이었는데, 다음 값이 더 이상 Tween 대상이 아니게 되어 일반
|
중이었는데, 다음 값이 더 이상 Tween 대상이 아니게 되어 일반
|
||||||
PropertyHandler로 매치가 넘어가는 경우, 이전 Tween을 멈추는 게
|
PropertyHandler로 매치가 넘어가는 경우, 이전 Tween을 멈추는 게
|
||||||
`retract`의 일. **Tag/Attribute는 여기 해당 안 함** — 처음엔 이
|
`retract`의 일. **Attribute는 여기 해당 안 함** — UICorner 숏핸드와
|
||||||
둘도 예시로 들었으나, 실제로는 UICorner 숏핸드와 같은 패턴(값의
|
같은 패턴(값의 참/거짓/nil 여부와 무관하게 항상 같은 핸들러가 계속
|
||||||
참/거짓/nil 여부와 무관하게 항상 같은 핸들러가 계속 담당하고,
|
담당, 추가/제거를 전부 `process` 자신이 처리)이라 핸들러 교체 자체가
|
||||||
추가/제거를 전부 `process` 자신이 처리)이라 핸들러 교체 자체가 안
|
안 일어남 — `base/attribute-plan.md`. **[정정, 2026-08-08 세 번째
|
||||||
일어나서 `retract`가 발화할 조건이 생기지 않음 — 구체 설계는
|
세션] Tag는 더 이상 여기 해당하지 않음** — array-part 값 객체로
|
||||||
`base/tag-plan.md`/`base/attribute-plan.md`.
|
재설계되며(`base/tag-plan.md`, 구 모델은 `archive/
|
||||||
- store bind가 새 값으로 넘어갈 때 이전 핸들러의 `retract(inst, k, v)`를
|
tag-hash-key-model-reversed.md`) `Tag(...)`↔`nil` 사이에서 핸들러
|
||||||
한 번 호출해주면 됨.
|
타입 자체가 바뀌므로 `retract`가 의미 있어짐(전체 삭제), 같은 Tag끼리
|
||||||
|
바뀌는 diff는 `process`가 담당.
|
||||||
|
- store bind가 새 값으로 넘어갈 때 이전 핸들러의 `retract`를 호출해주면
|
||||||
|
됨 — **정확한 전파 메커니즘은 아래 "Dispatch 체인" 절 참고**(재귀
|
||||||
|
재-dispatch에서 여러 단계가 겹칠 때 어느 슬롯에 뭘 추적하는지가
|
||||||
|
2026-08-08 세 번째 세션에 구체화됨, 여기 한 줄 설명은 그 요약).
|
||||||
- **핸들러 내부 상태 저장**: `retract`가 "이전에 생성한 것"(예: 실행 중이던
|
- **핸들러 내부 상태 저장**: `retract`가 "이전에 생성한 것"(예: 실행 중이던
|
||||||
Tween 객체)에 접근하려면 상태를 어딘가에 저장해야 함 — **`inst`를 키로 하는
|
Tween 객체)에 접근하려면 상태를 어딘가에 저장해야 함 — **`inst`를 키로 하는
|
||||||
weak-keyed 테이블에 `k`별로 저장**(예: 생성된 Tween을 담아뒀다가 나중에
|
weak-keyed 테이블에 `k`별로 저장**(예: 생성된 Tween을 담아뒀다가 나중에
|
||||||
|
|
@ -252,15 +260,12 @@ NoneHandler.process(inst, k, v) = process(inst, k, nil) -- 재귀 재호출
|
||||||
"`v`가 `nil`이 됨"과는 다른 문제. `None → nil` 재디스패치는 항상
|
"`v`가 `nil`이 됨"과는 다른 문제. `None → nil` 재디스패치는 항상
|
||||||
`Dispatch.process` 경로로만 흐름 — `NoneHandler` 자신도 `retract`가
|
`Dispatch.process` 경로로만 흐름 — `NoneHandler` 자신도 `retract`가
|
||||||
딱히 할 일이 없음(재귀 호출 자체가 이미 process이므로).
|
딱히 할 일이 없음(재귀 호출 자체가 이미 process이므로).
|
||||||
- **M2 착수 시 확인할 것 (`pre-implementation-audit.md` 우선순위1
|
- **[해소됨, 2026-08-08 세 번째 세션]** "이 키를 지금 누가 담당 중인가"
|
||||||
"이전에 실제로 매치됐던 핸들러 추적" 항목에 추가)**: "이 키를 지금 누가
|
bookkeeping — `pre-implementation-audit.md` 우선순위1 "이전에 실제로
|
||||||
담당 중인가" bookkeeping은 바깥 순회 루프(`Dispatch.drive`)가 아니라
|
매치됐던 핸들러 추적" 항목이 여기서 다시 언급됐던 것. 아래 "Dispatch
|
||||||
`Dispatch.process` 호출 자체 내부에서 갱신돼야 함. 안 그러면 값이 계속
|
체인" 절의 `chains`/`Dispatch.retractUnder`로 구체화됨 — `NoneHandler`의
|
||||||
`None`으로 유지되는 매 사이클마다 "1차 매치는 `NoneHandler`, 재귀 호출 뒤
|
재귀 재호출도 이 메커니즘 위에서 동일하게 동작(`None`으로 유지되는 매
|
||||||
실제 담당은 다른 핸들러"로 바깥 루프가 오판해 불필요한 `retract`를 반복
|
사이클마다 담당자가 자연히 정확하게 갱신됨, 별도 특수 처리 불필요).
|
||||||
호출할 위험이 있음 — `Dispatch.process`가 재귀 호출 시에도 자기 자신을
|
|
||||||
통해 담당자 기록을 갱신하게만 해두면(`Dispatch.drive`가 별도로 기록 안
|
|
||||||
하고 `Dispatch.process` 내부에 위임) 자연히 해소됨.
|
|
||||||
|
|
||||||
### Dispatch는 프리미티브가 아니다 — 탑레벨 싱글톤 확정 (2026-08-08 두 번째 세션)
|
### Dispatch는 프리미티브가 아니다 — 탑레벨 싱글톤 확정 (2026-08-08 두 번째 세션)
|
||||||
|
|
||||||
|
|
@ -313,6 +318,92 @@ NoneHandler.process(inst, k, v) = process(inst, k, nil) -- 재귀 재호출
|
||||||
딸려가고, 호출부는 `module.Dispatch.process(...)`처럼 그 인스턴스
|
딸려가고, 호출부는 `module.Dispatch.process(...)`처럼 그 인스턴스
|
||||||
테이블을 통해 접근하게 됨 — 지금 미리 프리미티브화해둘 이유가 없음.
|
테이블을 통해 접근하게 됨 — 지금 미리 프리미티브화해둘 이유가 없음.
|
||||||
|
|
||||||
|
### Dispatch 체인 — 재귀 재-dispatch의 retract 전파, `Dispatch.retractUnder` (2026-08-08 세 번째 세션)
|
||||||
|
|
||||||
|
**문제**: Tween/`NoneHandler`/`StoreBind`처럼 자기 `process` 안에서
|
||||||
|
`Dispatch.process(inst,k,realv)`를 다시 부르는 래핑 핸들러가 있으면, 같은
|
||||||
|
`(inst,k)`에 대해 "지금 누가 담당 중인가"를 슬롯 하나로 추적하는 순간
|
||||||
|
깨짐 — 래핑 핸들러 A 자신의 생명주기(예: StoreBind의 Observer 구독)와,
|
||||||
|
A가 재귀로 위임한 핸들러 B의 생명주기가 **같은 슬롯을 두고 서로
|
||||||
|
덮어씀**. 구체적으로: A의 재귀 진입 시점에 슬롯을 A→B로 갱신해두면, A가
|
||||||
|
스스로 다시 값을 재계산해 재-dispatch할 때(예: store 값이 또 바뀜) 그
|
||||||
|
슬롯엔 이미 B가 적혀있어 "A로 바뀌었다"고 오판해 A 자신을 엉뚱하게
|
||||||
|
retract하거나, 반대로 A가 자길 스스로 retract하는 오작동이 남 — 처음
|
||||||
|
검토했던 "Dispatch 전역 소유자맵 슬롯 하나" 안은 이 이유로 기각됨(당시
|
||||||
|
대화에서 직접 반례로 확인).
|
||||||
|
|
||||||
|
**해법 — Dispatch가 `(inst,k)`별 핸들러 체인(순서 있는 배열)을 소유**:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Dispatch/init.luau
|
||||||
|
local chains = Relate() -- {[inst(weak)] = {[k] = {handler, handler, ...}(strong, 순서 있는 배열)}}
|
||||||
|
|
||||||
|
function Dispatch.process(inst, k, v)
|
||||||
|
local h = Dispatch.getHandler(inst, k, v)
|
||||||
|
if h then
|
||||||
|
local list = chains:GetStrong(inst, k) or {}
|
||||||
|
table.insert(list, h) -- 항상 꼬리에 추가
|
||||||
|
chains:SetStrong(inst, k, list)
|
||||||
|
h.process(inst, k, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dispatch.retractUnder(inst, k, keep, v)
|
||||||
|
local list = chains:GetStrong(inst, k)
|
||||||
|
if not list then return end
|
||||||
|
local cutoff = 0
|
||||||
|
if keep then
|
||||||
|
for i, h in list do if h == keep then cutoff = i break end end
|
||||||
|
end
|
||||||
|
for i = #list, cutoff + 1, -1 do
|
||||||
|
list[i].retract(inst, k, i == cutoff + 1 and v or nil)
|
||||||
|
list[i] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **재귀/래핑 핸들러는 재-dispatch 전에 반드시 `Dispatch.retractUnder(inst,
|
||||||
|
k, self, newV)`를 먼저 부른 뒤 `Dispatch.process(inst, k, newV)`를
|
||||||
|
부름** — "나 밑에 있던 걸 전부 정리하고 새로 위임". `keep`(자기 자신)
|
||||||
|
바로 다음 항목만 실제 `newV`를 받고, 그보다 더 안쪽(다단 체인이 있을
|
||||||
|
경우)은 `nil`을 받음 — 더 안쪽 항목엔 "구체적으로 뭐로 대체됐는지"
|
||||||
|
정보가 없고 "완전히 사라진다"는 것만 사실이라서.
|
||||||
|
- **개별 핸들러의 `retract`는 더 이상 자기 위임 대상을 수동으로 안
|
||||||
|
쫓아가도 됨** — `retractUnder`가 꼬리부터 `keep` 앞까지 한 번의
|
||||||
|
루프로 체인 전체를 순서대로 정리해주므로, A→B→C처럼 몇 단계든 각
|
||||||
|
핸들러는 **자기 자신의 자원만** 정리하면 자동으로 전파됨(질문
|
||||||
|
제기됐던 "다단 체인에서 안쪽까지 retract가 안 간다" 문제가 이걸로
|
||||||
|
해소 — `retractUnder`의 루프 자체가 체인 전체를 훑으므로 각 핸들러가
|
||||||
|
수동으로 cascade할 필요가 원천적으로 없음).
|
||||||
|
- **구멍 걱정 없음** — 이 배열은 항상 꼬리에서만 추가/삭제되는 스택
|
||||||
|
모양이라(`retractUnder`가 항상 꼬리부터 연속으로 지움), "촘촘하지
|
||||||
|
않은 정수 키는 순회 순서가 깨진다"는 문제(위 "PreRef" 절의 `None`
|
||||||
|
소진 이슈)가 애초에 발생할 구조가 아님.
|
||||||
|
- **`retract`는 여전히 `(inst,k,v)` 3-인자** — 드롭하자는 제안이 대화
|
||||||
|
중 한 번 나왔으나 기각(전체 삭제 vs 부분 diff를 갈라야 하는 핸들러가
|
||||||
|
있어서, `base/tag-plan.md` 참고). 다만 `v`가 실제로 필요한지는
|
||||||
|
핸들러마다 다름 — Tag는 구조상 retract가 "더 이상 매치 안 될 때만"
|
||||||
|
불리므로 `v`를 안 봐도 항상 전체 삭제가 맞음(무조건), Tween 같은
|
||||||
|
경우는 자기 `Relate` 저장분만 보고 `Cancel`하면 되니 역시 `v`를 꼭
|
||||||
|
안 봐도 됨 — `v`는 "계약상 항상 주어지지만 안 쓰는 핸들러가 있어도
|
||||||
|
됨" 정도로 이해할 것.
|
||||||
|
- **순환은 UB, 방어 로직 없음** — Handler 간 순환 참조(A가 B를 부르고
|
||||||
|
B가 다시 A로 돌아오는 것)는 재귀 호출이 안 끝나 바로 스택오버플로가
|
||||||
|
나므로 애초에 일어날 수 없는 구조(각 핸들러는 최대 한 번씩만 그
|
||||||
|
키에서 호출됨을 전제) — 값에 별도 플래그를 심어 의도적으로 순환을
|
||||||
|
만드는 것도 이론상 가능하지만 use case가 없어 문서화 대상 밖,
|
||||||
|
2026-08-04 세션에 이미 확정된 "일반적 무한루프 방어 안 함" 원칙과
|
||||||
|
같은 결로 UB 취급.
|
||||||
|
- **부수 효과 — 미래 재바인드/quad-debug에 유리**: 이 체인이 Dispatch에
|
||||||
|
중앙화돼 있으므로, `research/existing-instance-bind-plan.md`가 다룰
|
||||||
|
미래의 재바인드는 `Dispatch.retractUnder(inst, k, nil, newV);
|
||||||
|
Dispatch.process(inst, k, newV)` 두 줄로 "이 키의 체인을 통째로 갈아
|
||||||
|
끼우기"가 자연스럽게 됨(각 래핑 핸들러가 자기 전용 `Relate`에 위임
|
||||||
|
대상을 비공개로 숨겨두는 대안 설계는 이게 안 됨 — 대화 중 검토 후
|
||||||
|
기각). `research/debug-tooling-plan.md`의 "무엇이 무엇에 연결됐는가"
|
||||||
|
그래프도 이 `chains` 구조를 그대로 읽으면 됨 — quad-debug 착수 시점에
|
||||||
|
새로 설계할 필요 없음.
|
||||||
|
|
||||||
## Store 바인드는 특수 경우인가, 아니면 pluggable 바인드를 재실행하는 래핑인가
|
## Store 바인드는 특수 경우인가, 아니면 pluggable 바인드를 재실행하는 래핑인가
|
||||||
|
|
||||||
사용자 원 메모: "스토어 바인드는 특수 경우로 둘지, 아니면 다른 pluggable 바인드를
|
사용자 원 메모: "스토어 바인드는 특수 경우로 둘지, 아니면 다른 pluggable 바인드를
|
||||||
|
|
@ -335,7 +426,8 @@ value)로 `Dispatch.process(inst,k,realv)`를 재귀 호출"하는 식으로 구
|
||||||
```lua
|
```lua
|
||||||
-- 예: 일반 프로퍼티 store-bind 핸들러의 process(inst, k, state)
|
-- 예: 일반 프로퍼티 store-bind 핸들러의 process(inst, k, state)
|
||||||
local observer = state:Observer(function()
|
local observer = state:Observer(function()
|
||||||
Dispatch.process(inst, k, state:Get())
|
Dispatch.retractUnder(inst, k, StoreBind, state:Get()) -- 나 밑에 있던 거 정리
|
||||||
|
Dispatch.process(inst, k, state:Get()) -- 새로 위임(체인에 push)
|
||||||
end)
|
end)
|
||||||
observer:Subscribe()
|
observer:Subscribe()
|
||||||
relate:SetStrong(inst, k, observer) -- retract에서 :Unsubscribe() 하려면 들고 있어야 함
|
relate:SetStrong(inst, k, observer) -- retract에서 :Unsubscribe() 하려면 들고 있어야 함
|
||||||
|
|
@ -346,12 +438,12 @@ relate:SetStrong(inst, k, observer) -- retract에서 :Unsubscribe() 하려면
|
||||||
보는 leaf가 아니기 때문(위 "이중 바인딩 금지" 원칙과 정합적: 한 Observer
|
보는 leaf가 아니기 때문(위 "이중 바인딩 금지" 원칙과 정합적: 한 Observer
|
||||||
핸들은 두 바인딩 경로 중 하나만 써야 하는데, 이건 애초에 leaf가 아니므로
|
핸들은 두 바인딩 경로 중 하나만 써야 하는데, 이건 애초에 leaf가 아니므로
|
||||||
`:Subscribe()`가 유일한 선택).
|
`:Subscribe()`가 유일한 선택).
|
||||||
- **`retract`가 할 일은 `observer:Unsubscribe()` 호출뿐** — 이게 위 "이벤트도
|
- **`retract`가 할 일은 `observer:Unsubscribe()` 호출뿐 — 위임 대상까지
|
||||||
|
수동으로 안 쫓아가도 됨.** `Dispatch.retractUnder`가 자기 밑에 위임된
|
||||||
|
걸 알아서 정리해주므로(위 "Dispatch 체인" 절), 이 핸들러의 `retract`는
|
||||||
|
정확히 자기 자신의 자원(Observer)만 정리하면 끝 — 이게 위 "이벤트도
|
||||||
store-bind 가능" 절에서 이미 "엔지니어링 비용이 낮다"고 서술한 것과 같은
|
store-bind 가능" 절에서 이미 "엔지니어링 비용이 낮다"고 서술한 것과 같은
|
||||||
이유(새 디스패치 메커니즘 없이 기존 4종 계약만 구현), 다만 그 절이
|
이유(새 디스패치 메커니즘 없이 기존 계약만 구현).
|
||||||
구체적으로 가리키는 재사용 대상은 "재귀 process+retract 래핑 패턴"이었고
|
|
||||||
Observer 자체를 구독 메커니즘으로 쓴다는 것까지는 명시가 안 돼 있었던
|
|
||||||
갭이 이번에 메워짐.
|
|
||||||
- **핸들러가 직접 `canExecute`/liveness를 재구현할 필요 없음** — Observer가
|
- **핸들러가 직접 `canExecute`/liveness를 재구현할 필요 없음** — Observer가
|
||||||
이미 자기 `Subscribed` 상태로 게이팅됨(아래 `base/lifecycle-pattern.md`의
|
이미 자기 `Subscribed` 상태로 게이팅됨(아래 `base/lifecycle-pattern.md`의
|
||||||
`canExecute(inst, value)` 절 참고, Observer/Effect는 그 함수 안에서
|
`canExecute(inst, value)` 절 참고, Observer/Effect는 그 함수 안에서
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,111 @@
|
||||||
# Tag 특수 키 — `CollectionService` 얇은 래퍼
|
# Tag — array-part 값 객체, `CollectionService` 얇은 래퍼
|
||||||
|
|
||||||
**상태**: base — `[Tag "Name"] = true` DI 키의 존재 자체는 `architecture.md`
|
**상태**: base — 2026-08-08 세 번째 세션에서 값 모양을 전면 재설계(구
|
||||||
4번 항목에서 이미 확정. 이 문서는 UICorner 숏핸드(`base/ui-shorthand-plan.md`)/
|
모델은 `archive/tag-hash-key-model-reversed.md`에 원문·역전 이유 보존).
|
||||||
Tween(`research/tween-plan.md`)처럼 별도 전용 문서가 없던 걸 2026-08-07
|
새 결정만 반영, 열린 질문 없음.
|
||||||
여덟 번째 세션에 메꾼 것 — Tag/Attribute도 "1 프리미티브 1 파일" 관례
|
|
||||||
(Blocker/Effect/Ref/PreRef 분리 선례)를 따라야 한다는 사용자 지적으로 신설.
|
|
||||||
새 설계 내용은 없음 — 이미 여기저기 흩어져 있던 결정을 한 곳에 모으고,
|
|
||||||
오늘 논의한 `None`/`process`/`retract` 동작을 반영.
|
|
||||||
|
|
||||||
## 값 모양
|
## 왜 재설계됐나
|
||||||
|
|
||||||
`[Tag "Name"] = boolean | State<boolean>` — store-bind 가능(일반 프로퍼티와
|
구 모델(`[Tag "Name"] = boolean`, 태그 하나당 해시 파트 키 하나)은 상호
|
||||||
동일하게 취급). PA님의 `EventDrivenProgramming/Observer.luau`
|
배타적인 스타일 상태(`btn1`/`btn2`/`btn3`류, 실사용에서 20개까지도 가능)를
|
||||||
`subscribeTaggedInstance`도 얇은 `CollectionService` 래퍼일 뿐이라
|
표현하려면 태그 이름 개수만큼 키를 각각 갱신해야 해서 끔찍함, 스타일
|
||||||
(`bind-system-plan.md` "PA님 코드와 대조" 절) **Instance 태그는
|
조합(여러 태그를 합쳐 쓰는 것)도 구조적으로 안 됨 — 상세 경위는
|
||||||
`CollectionService` 직접 사용 그대로 유지** — 별도 자체 태그 시스템(v1이
|
`archive/tag-hash-key-model-reversed.md`.
|
||||||
검토했던 것 같은) 안 만듦.
|
|
||||||
|
|
||||||
## 메커니즘 — 새 아키텍처 개념 불필요
|
## 값 모양 — `Modifier`와 같은 immutable clone 체이닝
|
||||||
|
|
||||||
`isHandlable`이 `[Tag "Name"]` 모양의 키를 매칭하는 `TagHandler` 하나로
|
```
|
||||||
충분:
|
Tag(name1, name2, ...) -- 생성자, 가변인자. Tag() 빈 값도 유효
|
||||||
|
tag:Added(name): Tag -- clone 후 이름 추가, 원본 안 건드림
|
||||||
|
tag:Removed(name): Tag -- clone 후 이름 제거
|
||||||
|
tag:Contains(name): boolean -- 멤버십 확인
|
||||||
|
tag:Apply(factory): U -- factory(self) 체이닝 설탕(Modifier와 동일 패턴)
|
||||||
|
Tag.Merged(tag1, tag2, ...): Tag -- 여러 Tag의 합집합(무손실). Modifier의
|
||||||
|
Override(필드 단위 덮어쓰기, 손실 있음)와
|
||||||
|
다른 연산이라 이름도 다름 — Override는
|
||||||
|
"이미 계산된 걸 합침", Merged는 "집합을
|
||||||
|
합침"
|
||||||
|
```
|
||||||
|
|
||||||
- `process(inst, k, v)` — `v`가 참이면(`true`) `CollectionService:AddTag(inst,
|
`Added`/`Removed`가 `-ed` 어미인 이유는 **`Add`/`Remove`로 쓰면 뮤테이션
|
||||||
name)`, 거짓/`nil`이면 `RemoveTag(inst, name)`. `None → nil` 재디스패치
|
API처럼 보이기 때문** — 실제로는 항상 `table.clone` 후 반환(Modifier
|
||||||
(`base/bind-system-plan.md`의 `None` 센티널 절)가 그대로 이 경로를 탐 —
|
3번 절과 동일한 immutable 확정 이유: 형제 서브트리 오염 방지). `Tag(a,b)`
|
||||||
`nil`을 "태그 없음"으로 자연스럽게 해석하면 되므로 특별 처리 불필요.
|
자체가 `Tag():Added(a):Added(b)`의 sugar라고 생각하면 됨 — 별도 런타임
|
||||||
- **`retract` 불필요** — 값이 `true`/`false`/`nil` 무엇이든 항상 같은
|
경로 아님.
|
||||||
`TagHandler`가 이 키를 계속 담당(핸들러 *타입*이 안 바뀜), 추가/제거를
|
|
||||||
전부 `process` 자신이 처리. `retract`는 "매치되는 핸들러 타입 자체가
|
|
||||||
바뀌는" 경우에만 의미 있다는 게 확정된 원칙(`bind-system-plan.md`
|
|
||||||
"확정된 디스패치 모델" 절, Tween↔일반 프로퍼티가 그 유일한 실사례) —
|
|
||||||
Tag는 여기 해당 안 됨. **처음엔 이 문서 없이 "확정된 디스패치 모델"
|
|
||||||
절이 Tag를 retract 필요 예시로 잘못 들었던 걸 여기서 바로잡음.**
|
|
||||||
|
|
||||||
## 패키지 배치
|
**children 배열 슬롯(array-part)에 직접 놓임** — `Frame { Tag("selected") }`.
|
||||||
|
정적으로 여러 개 놓아도(`Frame { Tag("a"), Tag("b") }`) 각자 독립적으로
|
||||||
|
자기 태그만 추가하면 되므로 `Merged` 없이도 됨(`Merged`/`Added`/`Removed`는
|
||||||
|
"하나의 Tag 값을 프로그래밍적으로 조립"하는 용도).
|
||||||
|
|
||||||
UICorner 숏핸드/Tween과 같은 판단 재사용 — 작고 항상 켜져 있어도 비용이
|
**동적 토글은 `Source`/`State`로, `None` 불필요** — 상호배타 상태 전환은
|
||||||
무시할 만한 기능은 `quad-roblox` 코어에 직접 포함(`base/ui-shorthand-plan.md`
|
`store.activeTag:Compute(function(name) return name == "btn1" and
|
||||||
"패키지 배치" 절 참고, 별도 opt-out 패키지로 안 쪼갬).
|
Tag("selected") or nil end)`처럼 그냥 `nil`을 리턴하면 됨. `None` 센티널은
|
||||||
|
"정적 테이블 리터럴에서 `키 = nil`이 키 없음과 구별 안 되는" 문제의
|
||||||
|
해법이지(`bind-system-plan.md` "`None` 센티널" 절), 이건 함수 리턴값이
|
||||||
|
동적으로 흘러가는 경우라 그 문제 자체가 없음 — `nil`을 인자로 넘기는 건
|
||||||
|
아무 문제 없음. (단, `Frame { cond and Tag("a") or nil, sibling }`처럼
|
||||||
|
**정적 리터럴**에서 조건부로 Tag를 넣거나 빼고 싶은 경우엔 다른 array-part
|
||||||
|
값들과 마찬가지로 `cond and Tag("a") or None` 관용구가 여전히 유효 —
|
||||||
|
이건 nil-hole 문제라 Tag만의 특수 규칙이 아니라 `props.Modifier`/
|
||||||
|
`props.Ref`와 같은 일반 array-part 관용구.)
|
||||||
|
|
||||||
|
## 메커니즘 — `TagHandler`, retract가 이제 의미 있어짐
|
||||||
|
|
||||||
|
구 모델과 달리 **핸들러 타입이 사이클마다 바뀔 수 있음**(`Tag(...)` ↔
|
||||||
|
`nil`, 값이 `Tag`가 아니게 되면 `TagHandler.isHandlable`이 더 이상 안
|
||||||
|
맞음) — 그래서 `retract`가 실제로 필요해짐(`bind-system-plan.md` "확정된
|
||||||
|
디스패치 모델" 절의 일반 원칙 그대로).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local relate = Relate() -- TagHandler 전용, 이전에 반영한 Tag 값 저장
|
||||||
|
|
||||||
|
TagHandler.priority = <일반>
|
||||||
|
TagHandler.isHandlable(inst, k, v) = isTag(v) -- Brand 기반, array-part 전용
|
||||||
|
|
||||||
|
function TagHandler.process(inst, k, v)
|
||||||
|
local old = relate:GetStrong(inst, k)
|
||||||
|
-- diff: old에 있고 v에 없는 이름만 RemoveTag, v에 있고 old에 없는 이름만 AddTag
|
||||||
|
-- (모두 지웠다 다시 붙이지 않음 — 랙/스타일 깜빡임 방지가 이 diff의 존재 이유)
|
||||||
|
relate:SetStrong(inst, k, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TagHandler.retract(inst, k, v)
|
||||||
|
local old = relate:GetStrong(inst, k)
|
||||||
|
if old then for name in old:Names() do CollectionService:RemoveTag(inst, name) end end
|
||||||
|
relate:SetStrong(inst, k, nil)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Tag(A) → Tag(B)`(같은 핸들러, 타입 안 바뀜)**: `retract`는 아예 안
|
||||||
|
불림 — `Dispatch`의 "핸들러가 안 바뀌면 retract 없이 process만 다시"
|
||||||
|
원칙 그대로(`bind-system-plan.md` "Dispatch 체인" 절). **diff는 여기,
|
||||||
|
`process` 안에서만** 일어남 — 전체 삭제 후 재생성하면 스타일이 순간
|
||||||
|
전부 사라졌다 다시 붙어 랙/깜빡임을 유발하므로(사용자 지적), 반드시
|
||||||
|
이전 값과 diff.
|
||||||
|
- **`Tag(A) → nil`(핸들러가 TagHandler → 없음으로 바뀜)**: `retract`가
|
||||||
|
불림 — **`v`를 굳이 안 봐도 됨**: retract는 구조상 "더 이상 Tag가
|
||||||
|
아니게 됐을 때만" 불리므로, 뭐가 새로 들어왔든 전체 삭제가 항상 맞는
|
||||||
|
동작. `Handler.retract`가 여전히 `(inst,k,v)` 3-인자를 받는 건 계약
|
||||||
|
일관성 때문이지(다른 핸들러는 `v`를 실제로 씀) Tag가 그걸 필수로
|
||||||
|
요구해서가 아님.
|
||||||
|
- **`retract`가 자기 위임 대상까지 수동으로 안 쫓아가도 됨** —
|
||||||
|
`Dispatch.retractUnder`가 체인 전체를 알아서 훑어주므로 TagHandler는
|
||||||
|
자기 자원(위 `relate` 저장분)만 정리하면 됨. 상세 메커니즘은
|
||||||
|
`bind-system-plan.md` "Dispatch 체인" 절.
|
||||||
|
|
||||||
|
## 패키지 배치 — base는 값+API, roblox는 process/retract 글루
|
||||||
|
|
||||||
|
**Tag의 "값 타입과 clone 체이닝 API"(`Tag(...)`/`:Added`/`:Removed`/
|
||||||
|
`:Contains`/`:Apply`/`Merged`)는 quad-base 소속** — `Modifier`와 정확히
|
||||||
|
같은 층위(엔진 무관, 순수 데이터+연산). `CollectionService` 실제 호출
|
||||||
|
(`TagHandler.process`/`retract`)만 quad-roblox 소속 — 이미 확정된 "base는
|
||||||
|
인터페이스/값, backend는 process·retract 글루" 패턴(`LifetimeHandle`,
|
||||||
|
`Dispatch.addHandler` 자체가 이 패턴)을 값 타입 수준까지 그대로 확장한
|
||||||
|
것뿐, 새 아키텍처 개념 아님.
|
||||||
|
|
||||||
## 열린 질문
|
## 열린 질문
|
||||||
|
|
||||||
없음 — 값 모양/메커니즘/retract 여부 전부 확정. 이름 자체(`Tag`)는 이미
|
없음 — 값 모양/메커니즘/retract/패키지 배치 전부 확정. 이름 자체
|
||||||
쓰기 시작한 v1/PA님 관례와 일치해 특별히 재검토 대상 아님.
|
(`Tag`/`Added`/`Removed`/`Merged`)는 다른 가칭들과 같이 용어 정리 대상
|
||||||
|
(`.claude/question.md`).
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,17 @@ store.color }`처럼 애니메이션 없이 그냥 반응형으로 값만 바뀌
|
||||||
`Tween.luau`는 그 위에 얹히는 "값에 tween 설정이 붙어있으면 가로채는" 더
|
`Tween.luau`는 그 위에 얹히는 "값에 tween 설정이 붙어있으면 가로채는" 더
|
||||||
높은 우선순위의 특수 케이스로 재정리하는 게 자연스러워 보임.
|
높은 우선순위의 특수 케이스로 재정리하는 게 자연스러워 보임.
|
||||||
|
|
||||||
### 1-2. retract 시 "이전에 실제로 매치됐던 핸들러"를 누가 추적하는지 불명
|
### 1-2. retract 시 "이전에 실제로 매치됐던 핸들러"를 누가 추적하는지 불명 — [해소됨, 2026-08-08 세 번째 세션]
|
||||||
|
|
||||||
|
**해소**: `Dispatch`가 `(inst,k)`별 핸들러 체인(순서 있는 배열, `chains`)을
|
||||||
|
직접 소유하고, `Dispatch.retractUnder(inst,k,keep,v)`가 꼬리부터 `keep`
|
||||||
|
앞까지 정리해주는 걸로 확정 — 아래 원래 제안(`Dispatch/StoreBind.luau`가
|
||||||
|
"마지막 선택된 핸들러"를 직접 들고 있는 방식)은 재귀/래핑 핸들러가
|
||||||
|
여러 단계(A→B→C)로 겹칠 때 자기 자신의 상태와 위임한 핸들러의 상태가
|
||||||
|
슬롯 하나를 두고 충돌하는 문제가 있어 기각되고, 대신 Dispatch 자신이
|
||||||
|
전체 체인을 배열로 들고 있는 쪽으로 정리됨. 상세는 `base/
|
||||||
|
bind-system-plan.md` "Dispatch 체인" 절, `ROADMAP.md` M2/M4. 아래는
|
||||||
|
원래 발견 당시 기록.
|
||||||
|
|
||||||
**위치**: `base/bind-system-plan.md` "확정된 디스패치 모델" 절 90-91행 —
|
**위치**: `base/bind-system-plan.md` "확정된 디스패치 모델" 절 90-91행 —
|
||||||
"store bind가 새 값으로 넘어갈 때 이전 핸들러의 `retract(inst, k, v)`를
|
"store bind가 새 값으로 넘어갈 때 이전 핸들러의 `retract(inst, k, v)`를
|
||||||
|
|
|
||||||
89
CLAUDE.md
89
CLAUDE.md
|
|
@ -1539,3 +1539,92 @@ architecture.md`의 "코드 스타일 — 네이밍 케이싱" 절 신설.
|
||||||
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수
|
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수
|
||||||
설계/문서 정리라 M0 착수 우선순위 자체는 그대로. 위 2026-08-08 첫 세션이
|
설계/문서 정리라 M0 착수 우선순위 자체는 그대로. 위 2026-08-08 첫 세션이
|
||||||
남긴 "M0/M2 스파이크 검증 목록"에 새로 추가되는 항목 없음.
|
남긴 "M0/M2 스파이크 검증 목록"에 새로 추가되는 항목 없음.
|
||||||
|
|
||||||
|
## 2026-08-08 세 번째 세션 — Tag를 array-part 값 객체로 재설계, Dispatch
|
||||||
|
체인+`retractUnder`로 재귀 재-dispatch의 retract 전파 문제 해결
|
||||||
|
|
||||||
|
같은 날 이어진 세션. 사용자가 "Tag를 해시 파트 boolean 키 대신 array-part
|
||||||
|
값 객체로 바꾸는 게 낫지 않냐"는 질문으로 시작 — 상호배타 스타일 상태
|
||||||
|
(`btn1`/`btn2`/`btn3`류, 20개까지도 가능)를 표현하려면 구 모델은 태그
|
||||||
|
개수만큼 키를 갱신해야 해서 끔찍하다는 실사용 근거. 이 논의가 "retract가
|
||||||
|
새 값의 타입에 따라 이전 핸들러를 정확히 찾아 부를 수 있는가"라는 훨씬
|
||||||
|
근본적인 구멍(`pre-implementation-audit.md` 1-2번이 이미 지적해뒀던 것)을
|
||||||
|
직접 건드리게 됐고, 몇 차례 시행착오 끝에 사용자가 제시한 "체인+
|
||||||
|
`retractUnder`" 설계로 수렴. 세 갈래로 정리:
|
||||||
|
|
||||||
|
**1. Tag 재설계 — array-part 값 객체, `Modifier`와 같은 immutable clone
|
||||||
|
체이닝.** `Tag(name1, name2, ...)`(가변인자 생성자, 빈 `Tag()`도 유효),
|
||||||
|
`:Added`/`:Removed`(뮤테이션처럼 안 보이게 `-ed` 어미 — 실제로는 항상
|
||||||
|
clone 후 반환), `:Contains(name):boolean`, `:Apply(factory)`(Modifier와
|
||||||
|
동일한 순수 체이닝 설탕), `Tag.Merged(tag1,tag2,...)`(집합 합집합, 무손실
|
||||||
|
— Modifier의 `Override`는 필드 단위 덮어쓰기라 손실 있음, 그래서 이름도
|
||||||
|
다름). `None` 센티널은 불필요로 확인 — 동적 토글은 `Source`/`State`가
|
||||||
|
계산 결과로 `nil`을 리턴하면 되는 함수 인자 전달이라 테이블 리터럴의
|
||||||
|
nil-hole 문제 자체가 없음(정적 리터럴에서 조건부로 Tag를 넣고 뺄 때는
|
||||||
|
다른 array-part 값과 마찬가지로 기존 `None` 관용구가 그대로 유효, Tag
|
||||||
|
전용 규칙 아님). 구 모델(해시 파트 boolean, "핸들러 타입이 안 바뀌니
|
||||||
|
retract 불필요"가 결론이었음)은 `archive/tag-hash-key-model-reversed.md`로
|
||||||
|
역전 보존, `base/tag-plan.md` 전면 재작성. 값 타입+API(`Tag.luau`)는
|
||||||
|
quad-base, `CollectionService` 글루(`Handlers/Tag.luau`)만 quad-roblox —
|
||||||
|
이미 확정된 "base는 인터페이스/값, backend는 process·retract 글루"
|
||||||
|
패턴(`LifetimeHandle`)을 값 타입 수준까지 그대로 확장한 것으로 확인,
|
||||||
|
새 아키텍처 개념 아님.
|
||||||
|
|
||||||
|
**2. Tag 재설계가 "retract가 실제로 필요해지는" 첫 array-part store-bind
|
||||||
|
사례가 되며, 기존 "이전 핸들러 추적" 설계 공백이 정면으로 드러남.**
|
||||||
|
`pre-implementation-audit.md` 1-2번이 이미 "store-bind 재실행 모델에서
|
||||||
|
realv 타입이 매 갱신마다 바뀔 수 있는데 '이전 핸들러'를 누가 추적하는지
|
||||||
|
불명"이라고 짚어뒀던 것 — Tag가 `Tag(...)`↔`nil` 사이를 오가며 실제로
|
||||||
|
핸들러 타입이 바뀌는 구체 사례가 되어 더 이상 미룰 수 없어짐. 시행착오
|
||||||
|
과정:
|
||||||
|
- **1차 제안(제가 냄, 기각됨)**: Dispatch가 `(inst,k)`별로 "지금 누가
|
||||||
|
담당 중인가"를 슬롯 하나로 추적. **재귀/래핑 핸들러(StoreBind 등)
|
||||||
|
에서 깨짐** — 사용자가 직접 "A→B 구조에서 A가 바뀌면 B의 retract가
|
||||||
|
실행되고, 재귀로 B로 다시 내려오면 retract가 없는 거 아니냐"고 반례를
|
||||||
|
제시 — A 자신의 생명주기(예: Observer 구독)와 A가 재귀로 위임한 B의
|
||||||
|
생명주기가 슬롯 하나를 두고 서로 덮어써서, A가 스스로 재-dispatch할
|
||||||
|
때 자길 엉뚱하게 retract하거나 반대로 안 해야 할 때 안 하는 오작동이
|
||||||
|
생김이 실제 트레이스로 확인됨.
|
||||||
|
- **2차 제안(제가 냄, 부분 기각)**: 각 래핑 핸들러가 자기 전용 `Relate`에
|
||||||
|
위임 대상을 비공개로 저장(A→B→C면 A.retract가 수동으로 B.retract를
|
||||||
|
부르고 B.retract가 수동으로 C.retract를 부르는 linked 구조). 동작은
|
||||||
|
하지만 사용자가 두 가지 지적: (a) 나중에 재바인드(`existing-instance-
|
||||||
|
bind-plan.md`) 지원을 생각하면 위임 정보가 핸들러별로 비공개 분산돼
|
||||||
|
있어 외부에서 못 들여다봄, (b) 각 핸들러 작성자가 "내 retract에서
|
||||||
|
위임 대상도 cascade해야 한다"는 걸 매번 기억해야 하는 규율 의존적
|
||||||
|
설계.
|
||||||
|
- **최종 채택(사용자 제안) — Dispatch가 `(inst,k)`별 핸들러 체인(순서
|
||||||
|
있는 배열)을 직접 소유, `Dispatch.retractUnder(inst,k,keep,v)`가
|
||||||
|
꼬리부터 `keep` 앞까지 훑으며 정리.** `Dispatch.process`가 매치될
|
||||||
|
때마다 체인에 push, 재귀/래핑 핸들러는 재-dispatch 전에
|
||||||
|
`retractUnder(inst,k,self,newV)`를 먼저 불러 자기 밑을 정리 — 이
|
||||||
|
한 번의 루프가 다단 체인(A→B→C) 전체를 순서대로 정리해주므로 개별
|
||||||
|
핸들러의 `retract`는 더 이상 자기 위임 대상을 수동으로 안 쫓아가도
|
||||||
|
됨(2차 제안의 (b) 해소), 체인이 Dispatch에 중앙화돼 있어 미래
|
||||||
|
재바인드도 `retractUnder(inst,k,nil,newV);process(inst,k,newV)`
|
||||||
|
두 줄로 자연스럽게 됨((a) 해소) — quad-debug의 "무엇이 무엇에
|
||||||
|
연결됐는가" 그래프도 이 구조를 그대로 읽으면 됨. 배열이 항상 꼬리에서만
|
||||||
|
추가/삭제되는 스택 모양이라 `None` 소진 이슈(구멍 있는 정수 키 순회
|
||||||
|
문제)도 애초에 안 생김. **`retract`는 여전히 `(inst,k,v)` 3-인자
|
||||||
|
유지** — 한 차례 제가 "v 제거"를 제안했다가 틀렸음(사용자가 Tag의
|
||||||
|
전체삭제 vs diff 분기를 근거로 정정) — 다만 최종 설계에서 diff는
|
||||||
|
`process`(같은 핸들러 유지 시)의 몫이고 `retract`는 항상 "더 이상
|
||||||
|
매치 안 될 때만" 불리므로 Tag 한정으로는 `v`를 안 봐도 항상 전체
|
||||||
|
삭제가 맞다는 것도 확인. 순환은 기존 "일반적 무한루프 방어 안 함"
|
||||||
|
원칙(2026-08-04) 그대로 UB.
|
||||||
|
|
||||||
|
**3. 전부 `base/bind-system-plan.md`(신규 "Dispatch 체인" 절 + "확정된
|
||||||
|
디스패치 모델"/"None 센티널"/"Store 바인드는 특수 경우인가" 절 갱신)/
|
||||||
|
`base/tag-plan.md`(전면 재작성)/`archive/tag-hash-key-model-reversed.md`
|
||||||
|
(신규)/`base/architecture.md`(소스트리 `Tag.luau` 추가, 4번 항목 정정)/
|
||||||
|
`ROADMAP.md`(M2/M4/M10)/`research/pre-implementation-audit.md`(1-2번
|
||||||
|
해소 표시)에 반영 완료.**
|
||||||
|
|
||||||
|
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M2/M4 스파이크
|
||||||
|
검증 목록에 `chains`/`retractUnder`가 다단 체인에서 실제로 정확히
|
||||||
|
동작하는지가 새로 추가됨(추론만으로 확정된 것, `pre-implementation-audit.md`
|
||||||
|
류 "실제 Luau로 부딪혀본 적 없는 것" 범주). `pre-implementation-audit.md`
|
||||||
|
1-1번(Tween이 유일한 store-bind 예시라 "일반 store-bind와 Tween이 같은
|
||||||
|
핸들러인지"가 불명확한 문제)은 Tag가 두 번째 구체 사례가 되면서 정황상
|
||||||
|
"별개 핸들러, 둘 다 `Dispatch/StoreBind.luau` 재사용"쪽에 힘이 실리지만
|
||||||
|
**아직 명시적으로 확정된 건 아님** — M2/M4 착수 전 마저 확인할 것.
|
||||||
|
|
|
||||||
25
ROADMAP.md
25
ROADMAP.md
|
|
@ -113,6 +113,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) —
|
leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) —
|
||||||
quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/
|
quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/
|
||||||
bind-system-plan.md` "Dispatch는 프리미티브가 아니다" 절)
|
bind-system-plan.md` "Dispatch는 프리미티브가 아니다" 절)
|
||||||
|
- [ ] `chains`(Relate 기반, `{[inst(weak)]={[k]={handler,handler,...}
|
||||||
|
(strong 순서 배열)}}`) + `Dispatch.retractUnder(inst,k,keep,v)` —
|
||||||
|
재귀 재-dispatch(StoreBind/Tween/NoneHandler)의 retract를 다단
|
||||||
|
체인까지 정확히 전파(2026-08-08 세 번째 세션, `base/
|
||||||
|
bind-system-plan.md` "Dispatch 체인" 절 — `pre-implementation-audit.md`
|
||||||
|
1-2번 "이전 핸들러 추적" 항목 해소). `Dispatch.process`가 매치될
|
||||||
|
때마다 체인에 push하는 것도 이 항목에 포함
|
||||||
- [ ] mock 대상 테스트
|
- [ ] mock 대상 테스트
|
||||||
|
|
||||||
## M3 — Store/State/Source
|
## M3 — Store/State/Source
|
||||||
|
|
@ -143,8 +150,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
|
|
||||||
## M4 — 첫 end-to-end 반응형 업데이트
|
## M4 — 첫 end-to-end 반응형 업데이트
|
||||||
|
|
||||||
- [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관)
|
- [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관 — 재-dispatch
|
||||||
- [ ] mock 대상으로 "store 값 바꾸면 `process`가 다시 호출된다" 확인
|
전 `Dispatch.retractUnder(inst,k,self,realv)` 호출 필수, `base/
|
||||||
|
bind-system-plan.md` "Dispatch 체인" 절)
|
||||||
|
- [ ] mock 대상으로 "store 값 바꾸면 `process`가 다시 호출된다" +
|
||||||
|
"이전 값이 다른 타입이면 이전 핸들러의 `retract`가 정확히 불린다"
|
||||||
|
확인
|
||||||
|
|
||||||
## M5 — quad-roblox 최소 프로바이더
|
## M5 — quad-roblox 최소 프로바이더
|
||||||
|
|
||||||
|
|
@ -235,7 +246,15 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
|
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
|
||||||
- [ ] `Handlers/Attribute.luau`(`base/attribute-plan.md` — 메커니즘/`None`/
|
- [ ] `Handlers/Attribute.luau`(`base/attribute-plan.md` — 메커니즘/`None`/
|
||||||
`retract` 불필요 확정, 타입 파라미터화 이름만 착수 전 확인)
|
`retract` 불필요 확정, 타입 파라미터화 이름만 착수 전 확인)
|
||||||
- [ ] `Handlers/Tag.luau`(`CollectionService`, `base/tag-plan.md` — 전부 확정)
|
- [ ] `Tag.luau`(quad-base — 값 타입+immutable clone 체이닝: `Tag(...)`/
|
||||||
|
`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`, `base/tag-plan.md`
|
||||||
|
— 2026-08-08 세 번째 세션 array-part 값 객체로 재설계, 구 해시 파트
|
||||||
|
모델은 `archive/tag-hash-key-model-reversed.md`)
|
||||||
|
- [ ] `Handlers/Tag.luau`(quad-roblox — `CollectionService` process/retract
|
||||||
|
글루만, `isHandlable`은 `isTag(v)`. `retract`는 이제 의미 있음(값이
|
||||||
|
Tag가 아니게 되면 전체 삭제), 같은 Tag끼리 바뀌는 diff는 `process`가
|
||||||
|
자기 `Relate` 저장분과 비교해서 처리 — 전체 삭제 후 재생성 금지(랙
|
||||||
|
유발), `base/tag-plan.md` 참고)
|
||||||
|
|
||||||
## M11 — Tween
|
## M11 — Tween
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue