`question.md` 최우선 두 건을 한 패스로 닫고, 배너를 달고 있던 7개 문서 +
인덱스 레이어 전체를 갱신했다.
0-Z (Attribute 이름 소유권):
- 그룹은 비공개 `GetKey`로 이름마다 자기 전용 키를 써서 위임 → 교차 오염
구조적 제거. 이름 소유권은 `AttributeKeyHandler`의 이름 claim이 판정
(`nameClaims` Relate, 다른 키가 같은 이름을 노리면 즉시 error).
- 권고안 (a)(그룹 안 claimant Relate)는 그룹↔직접 쓰기를 못 잡아 기각 —
두 경로가 만나는 말단 핸들러에서 공개 키는 같은 객체라 소유자 구분 불가.
0-A (재디스패치 = 하강 diff):
- 래핑 핸들러의 선행 `retractFrom` 폐기, `Dispatch.process`가 슬롯의
`handler`를 먼저 비교(같으면 클로저에 새 값 전달 후 재process, 다르면
그 자리부터 전량 철거).
- 귀결: `retractFrom`이 3-인자로 축소(힌트를 외부에서 만들어 넣을 자리
소멸), `isX(hintValue)` 가드 규칙 폐지, 깊은 체인 힌트 유실 캐비엇 삭제,
Dispatch의 점유 체크 폐지.
- 9차 세션이 미뤄둔 2단계 분할을 같이 수행 — 디스패치 코어를
`base/dispatch-core-plan.md`로 분리하며 재작성(bind-system-plan은
2263→1219줄). 옛 모델은 archive/dispatch-hintvalue-model-reversed.md.
패키지 재배치 (사용자 제기):
- Tag/Attribute의 부기 알고리즘 전체를 quad-base로, 백엔드는
`addTag`/`removeTag(inst,{string})`/`setAttribute(inst,name,v)` 3개 op만
주입(웹 className/data-* 대응). 엔진 고유 타입 패밀리만 백엔드.
- `HANDLER_PRIORITY_FALLBACK` 신설 — base 제공 핸들러의 밴드, 백엔드가
평범한 우선순위로 덮어쓰면 언제나 이김.
부수: 스파이크 04/19가 옛 모델을 검증 중이라 rewrite-required로 이동,
question.md 최우선 칸 비움, 새 소소 항목 2건 등록(Merged 이름 중복,
`hintValue` 이름 재검토). doc-check ERROR 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KckSawrsJSJmDBcSojJPxZ
86 lines
6.3 KiB
Markdown
86 lines
6.3 KiB
Markdown
# `OnChange` 특수 키 — `GetPropertyChangedSignal` 바인딩
|
|
|
|
**상태**: base — 2026-08-10 세션에서 확정. quad-roblox 전용(값 타입/API
|
|
레이어 없음, `Attribute`와 같은 패키지 배치). **[2026-08-11 아홉 번째
|
|
세션 후속]** `AttributeKey`와 동일한 이름별 weak 캐시로
|
|
`OnChange(a) == OnChange(a)` 동등성도 확정 — 아래 "확정" 절 참고.
|
|
|
|
## 문제
|
|
|
|
이벤트 바인딩은 이미 평범한 문자열 키 + reflection(`GetEventsOfClass`)으로
|
|
확정돼 있음(`bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학"
|
|
절) — `inst[key]`가 이미 `RBXScriptSignal`이라 그냥 `Connect`하면 됨.
|
|
`GetPropertyChangedSignal(name)`은 이 패턴이 그대로 안 통함: 프로퍼티 이름을
|
|
인자로 받아 **별도 메소드 호출**로 시그널을 얻어야 하고, 그 프로퍼티 이름은
|
|
이미 "값 세팅" 키 네임스페이스(`Frame.Position = x`)와 겹침 — 값 타입만으론
|
|
"세팅"과 "변경 리스닝"을 구분할 방법이 없어서 별도 마커가 필요함.
|
|
|
|
## 확정
|
|
|
|
- **`OnChange(propertyName): OnChangeKey`** — 프로퍼티 이름을 감싸는 DI 키
|
|
팩토리, `AttributeKey(name)`/`Tag(...)`와 같은 패턴(`AttributeKey`는 구
|
|
`Attribute` — 2026-08-11 아홉 번째 세션에 여러 Store를 묶는 그룹
|
|
`Attribute(...)` 프리미티브가 신설되며 이름 충돌 방지로 리네임됨,
|
|
`base/attribute-plan.md` 참고). 사용 예:
|
|
`Frame { [OnChange "Position"] = function(v: UDim2) ... end }`.
|
|
- **제네릭 타입 파라미터 없음 — `OnChange<<T>>` 같은 타입 파라미터화는 안
|
|
함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시
|
|
(`function(v: UDim2) ... end`) — Luau가 그 타입이 실제 프로퍼티 타입과
|
|
일치하는지 검증해주지 않음. 이미 확정된 "이벤트 바인딩은 콜백 시그니처를
|
|
Luau가 검증 못 하는 대가를 받아들인다"는 결정(`bind-system-plan.md` "이벤트
|
|
바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도
|
|
포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려
|
|
`AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더
|
|
엄격한 걸 요구하는 셈이라 일관성이 깨짐.
|
|
- **기각안 — 프로퍼티별 정적 `OnChange.PropertyName` 전량 코드 생성**:
|
|
`archive/onchange-per-property-codegen-rejected.md` 참고. Attribute의
|
|
"제네릭 + 자주 쓰는 것만 정적 지름길" 절충과 겉보기엔 비슷해 보이지만
|
|
규모가 다른 문제라 기각.
|
|
- **패키지 경계: 전부 quad-roblox** — `Handlers/OnChange.luau`에 `OnChange(name)`
|
|
키 팩토리와 Handler를 같이 둠. **[정정, 2026-08-13 열네 번째 세션]**
|
|
예전엔 "단일 키 `AttributeKey.luau`와 같은 배치"라고 적었으나 그
|
|
`AttributeKey`는 같은 세션에 **quad-base로 옮겨갔음**(부기가 엔진 지식을
|
|
요구하지 않아서, `base/attribute-plan.md` "패키지 배치" 절) — `OnChange`가
|
|
quad-roblox에 남는 이유는 그것과 달리 **`GetPropertyChangedSignal`
|
|
자체가 로직**이라 "한 줄 op 주입"으로 줄어들지 않기 때문
|
|
(`base/dispatch-core-plan.md` "base가 소유하는 핸들러와 주입되는 엔진
|
|
op" 절의 분할 기준).
|
|
`GetPropertyChangedSignal` 자체가 Roblox 엔진 API라 base에
|
|
둘 이유가 없음 — Tag처럼 백엔드 무관한 값/API 레이어가 따로 있는 경우와
|
|
다름.
|
|
- **`process(inst,k,v,index)`**: `inst:GetPropertyChangedSignal(name):Connect(
|
|
function() v(inst[name]) end)` 후 **그 Connection을 `:Disconnect()`하는
|
|
클로저를 반환**. 일반 `Handlers/Event.luau`와 같은 결(Connection
|
|
관리뿐, 새 메커니즘 없음). **[정정, 2026-08-13 다섯 번째 세션]** 원래는
|
|
별도 `retract(inst,k,v)` 필드가 Disconnect를 담당한다고 적혀 있었으나,
|
|
Handler 계약이 `process` 1-메소드로 합쳐지며 그 로직이 반환 클로저로
|
|
이동 — `connection`은 `process`의 로컬 변수를 클로저가 upvalue로 그대로
|
|
캡처하므로 별도 `Relate` 저장/재조회가 필요 없음(`bind-system-plan.md`
|
|
"핸들러 내부 상태 저장" 절).
|
|
- **`State<function>` 지원 — 새 메커니즘 없음.** 이미 확정된 "이벤트도
|
|
store-bind 가능 — `false`로 disconnect" 메커니즘(`bind-system-plan.md`)이
|
|
`OnChange` 키에도 그대로 적용됨 — `OnChangeHandler`는 `process`(와 그
|
|
반환 클로저)만 구현하면 되고, `v`가 State/Source면 범용 `Dispatch/StoreBind.luau`가 알아서
|
|
언랩+재귀 재-dispatch해서 `process`를 다시 호출해줌. `OnChange` 전용 분기
|
|
불필요.
|
|
- **`OnChange(name)`도 `AttributeKey`와 같은 이름별 weak 캐시 적용
|
|
(2026-08-11 아홉 번째 세션 후속)** — `AttributeKey<<T>>(name)`이
|
|
이름만으로 캐시되는 것과 정확히 같은 모양(이름 → 키, 다른 가변 정보
|
|
없음)이라 같은 기법 그대로 재사용(`base/attribute-plan.md` "동등성"
|
|
절). `State<function>`이 되더라도 문제 없이 작동하고(캐시는 키
|
|
객체 자체의 identity만 다루지, 그 키에 바인딩된 값/콜백과는 무관),
|
|
`OnChange "a" == OnChange "a"`가 외부에서 관찰 가능해지는 것도
|
|
의도적으로 허용해도 되는 동작 — 문제 없음(사용자 확인). `Handlers/OnChange.luau`
|
|
안 `OnChange(name)` 팩토리에 `AttributeKey`와 동일한 캐시 구현.
|
|
|
|
## 다른 특수 DI 키와의 대조
|
|
|
|
| | 소스 | 값 타입 | 패키지 경계 |
|
|
|---|---|---|---|
|
|
| 이벤트(`MouseButton1Click = fn`) | `inst[key]`가 이미 Signal | 콜백, 타입 미검증 | quad-roblox(`Handlers/Event.luau`) |
|
|
| `AttributeKey(name)` | 주입된 `setAttribute` op | 값(제네릭 또는 정적 타입 패밀리로 타입 파라미터화) | **quad-base**(키+Handler, 2026-08-13 열네 번째 세션 재배치) / 엔진 op만 백엔드 |
|
|
| `OnChange(name)` | `GetPropertyChangedSignal(name)` | 콜백, 타입 미검증(제네릭 없음) | quad-roblox(`Handlers/OnChange.luau`) |
|
|
|
|
`OnChange`가 Attribute처럼 제네릭화되지 않은 이유는 "콜백을 받는다"는
|
|
성질이 Attribute(값을 직접 받음)보다 이벤트에 더 가깝기 때문 — 카테고리가
|
|
헷갈리지 않도록 표로 명확히 구분해둠.
|