`.claude/pre-implementation-qa.md`(사용자가 base/ 확정 문서를 문항으로 재심사한 결과)를 실제 문서에 반영하고, 그 문서를 qa-request/로 옮기며 1라운드임을 파일명·제목에 명시(2라운드는 새 파일). 그대로 구현하면 반대로 돌던 것 2건: - canBound의 판정 방향이 이름과 반대였음 → canBound(v) == not isBoundAlive(v), 게이트는 전부 `if not canBound(v) then error(...)`. canExecute와는 값이 같은 게 아니라 서로의 부정이고, 그게 오히려 이름 분리의 명분이 됨(옛 근거 "값이 항상 같다"는 폐기). - gcconn/gchold 보관이 SetStrong으로 적혀 있었음 → SetWeak. 근거 문장까지 틀렸던 것이라 같이 교체(그대로 짰으면 두-Relate 상호 강참조 누수). 설계가 바뀐 것: - Dispatch.drive의 None 스킵 분기 폐기 → NoneHandler는 재귀 전담, NilHandler 신설(k=number and v==nil 말단이 setLength/setOffsetSource 등록). 깨진 전제는 "배열 파트의 None은 process를 안 탄다". - Length/Offset 등록 책임이 "처음 매치한 Handler" → 말단 Handler. - 이벤트 disconnect 센티널 false → None/nil. - Ref 내부 구조를 .Callbacks 분리 + 평범한 .Value 필드로 단순화, RefLeafHandler에 빠져 있던 type(k)=="number" 추가(leaf는 배열 전용). - :List reconcile의 nil 리턴은 다시 파괴가 기본, 값 교체와 PopOnly(가칭)만 비파괴. - base 소유 Fallback Handler 등록 주체를 백엔드 팩토리 → quad-base 자신으로 재역전(백엔드 미로드 시 안내 에러 경로가 안 돌았음). - "이벤트 콜백 시그니처는 Luau가 검증 못 한다"가 거짓임이 사용자 반례로 확인 → onchange-plan.md의 파생 근거까지 교체(결론은 유지). 이름/표면: DI → D(Declarative) 확정 및 전수 반영, New 커링 + D는 전량 코드 생성, Attribute.Merged/Overridden 둘 다 제공, Quad.debug 신설, store "key" 문자열 커링 기각(→ store:GetDynamic). 판단이 갈리던 4건(PopOnly 채택 / D-7 재역전 / NoneHandler·NilHandler 역할 분담 / 동적 키 경로)은 사용자에게 물어 확정. 커밋 전 검증: quad-doc-auditor 1패스가 1건, 사용자가 돌린 `/code-review high`가 10건을 더 잡아 전부 반영(ROADMAP이 SL-3 역전을 안 따라오던 것, 설계 갭 2건은 새 열린 질문으로 등록). doc-check.py ERROR 0. Co-authored-by: qwreey <me@qwreey.moe>
7 KiB
7 KiB
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— 프로퍼티 이름을 감싸는 특수 키 팩토리,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가 그 타입이 실제 프로퍼티 타입과 일치하는지 검증해주지 않음. [근거 교체, 2026-08-18 구현 전 QA — 결론은 그대로] 옛 근거는 "이벤트 바인딩은 콜백 시그니처를 Luau가 검증 못 하는 대가를 받아들인다는 결정과 같은 급" 이었는데, 그 전제가 거짓이다(이벤트는 props 타입의 필드라D생성기가 콜백 타입을 정확히 줄 수 있음 —base/bind-system-plan.md의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절). 진짜 이유는OnChange(name)이 이름을 인자로 받는 팩토리라 그 경로가 없다는 것 — 필드가 아니므로 생성기가 미리 타입을 찍어둘 자리가 없고, 프로퍼티별로 전량 생성하는 안은 이미 기각돼 있다(아래 항목). 그래서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 계약이process1-메소드로 합쳐지며 그 로직이 반환 클로저로 이동 —connection은process의 로컬 변수를 클로저가 upvalue로 그대로 캡처하므로 별도Relate저장/재조회가 필요 없음(dispatch-core-plan.md"핸들러 내부 상태 저장" 절).State<function>지원 — 새 메커니즘 없음. 이미 확정된 "이벤트도 store-bind 가능" 메커니즘(base/event-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와 동일한 캐시 구현.
다른 특수 키와의 대조
| 소스 | 값 타입 | 패키지 경계 | |
|---|---|---|---|
이벤트(MouseButton1Click = fn) |
inst[key]가 이미 Signal |
콜백 — [2026-08-18 정정] 타입 검증됨(props 타입의 필드라 D 생성기가 콜백 시그니처를 찍어줌) |
판별은 quad-roblox(Handlers/Event.luau), 타입은 D 생성기 |
AttributeKey(name) |
주입된 setAttribute op |
값(제네릭 또는 정적 타입 패밀리로 타입 파라미터화) | quad-base(키+Handler, 2026-08-13 열네 번째 세션 재배치) / 엔진 op만 백엔드 |
OnChange(name) |
GetPropertyChangedSignal(name) |
콜백, 타입 미검증(제네릭 없음) | quad-roblox(Handlers/OnChange.luau) |
OnChange가 Attribute처럼 제네릭화되지 않은 이유는 위 "확정" 절의 정정된
근거대로 이름을 인자로 받는 팩토리라 타입을 미리 찍어둘 필드가 없기
때문 — "콜백을 받는다"는 성질이 이벤트에 가깝다는 분류 자체는 그대로지만,
이벤트 쪽은 필드라서 타입이 나온다는 게 2026-08-18에 확인됐으므로 그
유사성이 근거가 되지는 못한다.