2026-08-03 확정 사항 전체를 AskUserQuestion으로 하나씩 재검증. 대부분 그대로 확인됐으나 State 프리미티브 존재 여부(있어야 함으로 정정), Pipe copy-on-write 후보(폐기, state(state) 조합으로 대체), Slot retract 시 동작(폐기로 확정) 등 실제 정정이 발생 — Store/State/Source 온톨로지가 다음 세션 최우선 열린 설계 스레드로 새로 부상. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
26 KiB
Bind 시스템 — pluggable key/value 핸들러 (핵심 모델 확정, 세부 사항만 남음)
상태: research — 핵심 디스패치 모델(process/retract, 핸들러 4종 계약,
Signal 미채택, Ref 역할)은 사용자 확인 완료로 사실상 확정. 남은 건 세부
시그니처(dependency array API, CreatedRef 모양) 뿐 — 이것들이 정리되면
base/로 승격 예정. 원본: .claude/initreq/raw-userinput.md
"key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를
저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 base/quad-v1-architecture.md
("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 .claude/initreq/tbox
(레지스트리)와 Fusion/Vide 비교는 base/comparison-fusion-vide.md 참고.
문제
v1의 ProcessQuadProperty(.claude/initreq/quad/src/class.lua:134-214)는
숫자 키(children/style) vs 문자열 키(prop/event) vs __type 태그 테이블
(register/linker/style)을 하드코딩된 if/elseif 체인으로 구분한다. 새 특수 키
([Attribute "X"], [Tag ""], PropertyChangedEvent "" 등)를 추가하려면 이
중앙 함수 자체를 고쳐야 한다 — 라이브러리로서 확장 불가능한 구조.
핸들러 계약 (확정 — 아래 "확정된 디스패치 모델" 절과 통합해서 읽을 것)
핸들러는 다음 4개를 제공하는 등록 가능한 객체:
isHandlable(key, value): boolean— 이 핸들러가 이 key/value 쌍을 처리할 수 있는지 판별하는 predicate. 부작용 없이, 빠르게 — tbox의 type-check/ constraint-check 분리 원칙(.claude/initreq/tbox/CLAUDE.md의 "타입 체크는 분기 선택에 쓰이므로 순수해야 함")을 그대로 적용:isHandlable은 오직 "이 핸들러가 맞는가" 판별에만 쓰이고, 실제 유효성 검사는 핸들러가 선택된 이후 별도 단계에서.priority: number— 우선순위. 등록 순서(Fusion의 4단계 고정 stage, Vide의 action() 우선순위)보다 일반화된 열린 숫자 공간으로.process(inst, key, value)— 실제 처리 수행(아래 "확정된 디스패치 모델" 절 참고). v1/기존 논의에서 "bind"라 부르던 것과 동일한 역할.retract(inst, key, value)— 이전 처리를 무르는/멈추는 함수(아래 절,base/lifecycle-pattern.md참고). 모든 핸들러가 의미 있게 구현할 필요는 없음(예: 일반 프로퍼티 핸들러는 보통 no-op).
디스패치는 등록된 핸들러를 우선순위 순으로 스캔하며 isHandlable을 호출,
첫 매치가 처리(Fusion의 SpecialKey 우선순위 스캔과 유사하되 4단계 고정이 아니라
열린 레지스트리). tbox의 TUnion 런타임 체커가 이미 이 "순서대로 스캔, 첫 매치
반환, 실패 정보는 클로저로 지연 생성" 패턴을 구현해뒀음(.claude/initreq/tbox/ src/schema/union.luau:48-68) — 에러 메시지는 즉시 문자열로 만들지 말고 매치
실패 시에만 클로저 호출.
확정된 디스패치 모델: process(inst, k, v) / retract(inst, k, v)
사용자가 직접 준 구체적인 모델 — 이 문서의 이전 초안보다 우선함. 아래가 실제로 구현할 모양:
- 모든 핸들러는 대상 Instance를 직접, 항상 받는다. quad는 "인스턴스를 생성하고
그 인스턴스를 처리하는" 라이브러리다 — 다른 라이브러리가 만든 값(예: Store)을
그 인스턴스에 적용하도록 돕는 역할에 가깝다. 그래서 핸들러가 "나중에 생길
대상"을 비동기로 기다릴 필요 자체가 없음(아래 Ref 절 참고 — Ref는 다른 이유로
존재).
- 보강(2026-08-04):
inst가 항상 살아있는 엔진 객체(Roblox Instance)일 필요는 없음 — 특정 백엔드에서 실제 엔진 객체 생성/바인딩 비용이 비싸면 (예: 웹 DOM) 중간 표현으로 평범한 테이블을 만들고 나중에 그 테이블을 렌더링하는 것도 가능. 이건 core(base)가 신경 쓸 일이 아니라 각 최종 엔드포인트 백엔드(quad-roblox/quad-web등)가 알아서 결정할 문제 — base 인터페이스는 "무언가를 inst로 받아 process/retract한다"는 계약만 지키면 됨, 그 inst의 실체가 뭔지는 백엔드 재량.
- 보강(2026-08-04):
process(inst, k, v)— 우선순위 순으로 등록된 핸들러를 스캔,isHandlable(k,v)를 만족하는 최상위 핸들러가 실제 처리를 담당.- 예시: Tween의 store-bind 핸들러는
k는 무엇이든 받고v가 Store인 경우를 잡아내는, 우선순위가 매우 높은 핸들러 —v가 store이면 그 값을 처리(구독)함. 이 핸들러 안에서:- 지금 이 처리가 실행되어도 되는지 라이프타임(
Connected)을 확인 — 확인 안 하면 이미 Destroy된 대상에 대해 처리가 실행되는 문제가 생김. GC가 결국 정리하긴 하지만, GC 되기 전에도 store 값이 업데이트될 수 있으므로 그 시점엔 그냥Connected를 보고 무시(no-op). - 처리해도 되면, 사용자가 넘긴 함수들을 거쳐 실제 값(
realv)을 계산. realv를 들고 다시process(inst, k, realv)를 재귀 호출 — 이게 바로 "store 바인드는 pluggable 바인드를 재실행하는 래핑"이라는 이 문서 이전 초안의 결론과 일치.realv가 store가 아니라면 자연히 Tween의 store-bind 핸들러isHandlable을 통과 못 하고 우선순위상 다음 핸들러(일반 프로퍼티 세터 등)로 흘러감 — 무한 재귀 걱정 없음.
- 지금 이 처리가 실행되어도 되는지 라이프타임(
retract(inst, k, v)(이전 초안의 "cleanup", 이름 변경 근거는base/lifecycle-pattern.md참고) — 이전 처리를 무르는/멈추는 함수. 오직 "같은 key에 새 값이 들어와서 이전 처리를 갈아치우는" 시나리오에만 존재 — 인스턴스/바인드 전체가 Destroy될 때는retract가 호출되지 않음(base/ lifecycle-pattern.md의 "quad는 라이프사이클 중간에 있지 않다" 원칙 참고).- 일반 프로퍼티는 애초에 "unset" 개념이 없음(
nil로 셋하는 것도 그냥 셋 동작) — 그래서 프로퍼티 핸들러는 보통retract가 필요 없음. retract가 실제로 의미 있는 곳: Tag를 지운다, Attribute 엔트리 자체를 지운다, 실행 중인 Tween을 멈춘다 같은, "값을 새로 셋하는 것"과 "이전 상태를 명시적으로 되돌리는 것"이 다른 케이스.- store bind가 새 값으로 넘어갈 때 이전 핸들러의
retract(inst, k, v)를 한 번 호출해주면 됨.
- 일반 프로퍼티는 애초에 "unset" 개념이 없음(
- 핸들러 내부 상태 저장:
retract가 "이전에 생성한 것"(예: 실행 중이던 Tween 객체)에 접근하려면 상태를 어딘가에 저장해야 함 —inst를 키로 하는 weak-keyed 테이블에k별로 저장(예: 생성된 Tween을 담아뒀다가 나중에 멈추거나 끝냄). base가 이걸 범용 유틸로 제공(base.perInstanceState(inst)류, 정확한 이름/모양은 구현 단계에서 확정) — 모든 핸들러가 재사용, 각자 WeakMap을 새로 만들지 않음.base/lifecycle-pattern.md의 "생명 바인드 유틸"과 짝을 이루는 유틸. - 다른 값 변경을 추적하는 것도 process 함수의 정상 범위: 예를 들어 Slot
핸들러는 자기가 감시하는 값(배열/스토어)이 바뀌면 그에 따라 child를
갱신해야 함 —
retract시점엔 그 추적(구독)만 풀면 됨.
Store 바인드는 특수 경우인가, 아니면 pluggable 바인드를 재실행하는 래핑인가
사용자 원 메모: "스토어 바인드는 특수 경우로 둘지, 아니면 다른 pluggable 바인드를 재실행하는 래핑으로 쓸지 생각해봐야함... 충분히 확장 가능하게 둘 수 있음."
확정: 래핑 쪽. 위 "확정된 디스패치 모델" 절 참고 — store 바인드 핸들러도
다른 핸들러와 동일한 isHandlable/priority/process/retract 계약을
따르되, process가 내부적으로 "실제 값이 바뀔 때마다 (원래 key, 새 value)로
process(inst,k,realv)를 재귀 호출"하는 식으로 구현됨. 이러면 store 값
자체가 어떤 타입이든(원시값, 인스턴스, 심지어 다른 store) 상관없이 동일한
재귀적 디스패치로 처리 가능 — 아래 "store가 store를 저장 가능한가"와 직결.
Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 retract 핸들러가
필요하다는 점(부모가 slot을 정리하고 다시 process하는 방식)도 이 래핑 방식과
자연스럽게 맞음 — research/slot-plan.md 참고.
Store가 Store를 저장 가능한가
사용자 원 메모: "슬롯을 스토어처럼 생각 가능하다면 이건 가능하다고 봐야하는가? 아니면 아예 다른 값으로 둬야 하는가? table/number 같은 프리미티브 타입이나 ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플러그를 만드는 걸로."
이 문서의 제안: "Store 안의 값이 Store"인 경우도 그냥 하나의 (key,value) 쌍일 뿐이고, 그 값 타입(Store)을 인식하는 핸들러가 pluggable 레지스트리에 등록되어 있으면 됨 — 위 "재실행 래핑" 방식과 동일한 메커니즘으로 커버됨. 별도 특수 케이스 코드 불필요.
Ref — 도입 확정, 단 용도는 재정의됨
중요한 정정: Ref는 Tween이 대상을 얻기 위해 필요한 게 아님(Tween 핸들러도
process(inst,k,v)처럼 항상 대상 Instance를 직접 받으므로 — 위 "확정된 디스패치
모델" 참고, research/tween-plan.md도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
- v1의
Frame "id" {}+Store.GetObject(id)식 id 매핑은 폐기 확정 (base/architecture.md5번 항목) — "비현실적"이라는 게 이유. - 하지만 "라이브러리가 자기 자신이 만들어낸 instance를 나중에 다루기 편하게"
하는 용도로 Ref는 여전히 필요. 구체 시나리오: 기존에 다른 라이브러리로
관리되던 instance를 당장 quad로 옮기지 않고, ref를 따서 그 안에 자식을
Parent로 마운트한다든가, 점진적으로 마이그레이션한다든가, 래퍼를 만든다든가 하는 다양한 용도. - Store는 이미 바깥에서 옵저빙 가능한 존재라 별도 취급 불필요 — Ref는 그와 달리 "원하는 객체 자체를 직접 얻어오는" 경로. 얻어진 뒤에 그 참조를 어디에 저장하고 어떻게 쓰는지는 라이브러리 책임 범위 밖(사용자 자유).
- 바인드 방법: 특수 처리 없이, children을 배열 아이템으로 넣듯
CreatedRef같은 값을 숫자 키 슬롯에 넣는 방식(정확한 이름/시그니처는 미정, 예:[1] = CreatedRef(function(inst) ... end)) — child와 동일한 층위에서process(inst,k,v)디스패치를 그대로 타게 함. 즉 Ref도 pluggable 핸들러 레지스트리의 평범한 참가자. - 코루틴 기반 "채워질 때까지 대기" 지원 여부는 여전히 미정(별도 확인 필요 없이 구현 우선순위 낮음 — 필요성이 명확해지면 그때 추가).
- 콜백 호출 시점 확정: 생성 직후(construction 시점)와 트리 마운트 후(Parent
세팅 완료) 둘 다 지원 — 옵션으로 선택(
CreatedRef(fn, {phase="created"| "mounted"})류, 정확한 API 이름은 구현 단계에서 확정).
여러 Store 값을 묶어 파생값 만들기 — :With + :Compute, 포지셔널 인자 지양
사용자 확인 완료, 상세 방향 확정. 후보로 검토했던 두 방식 모두 기각:
- 암묵적 자동 추적(Vide식 ambient stack) 기각 — "함수 실행 중과 끝 사이를 확인하고 부작용이 필요"한 방식이라 Lua에서 깔끔한 방법이 아니라고 판단.
- 명시적 디펜던시 배열 + 포지셔널 인자(
Store.Combine({a,b}, function(av,bv) ...))도 기각 — 두 가지 이유: (1) 팩토리 함수로 store-bind 처리기를 쉽게 못 만들어줌, (2) 여러 팩토리를 체이닝하면 인자 순서가 꼬일 수 있고, 타입 표기도 어려워짐.
채택 방향: :With(...)로 필요한 의존성을 모으고, 그 뒤 :Compute(function() ... end)에서 with한 값을 포지셔널 인자로 받지 않고 클로저로 직접 읽는다
(정확히 어떤 방식으로 "직접 읽는지"는 아래 열린 질문 — :fromState 후보 참고).
Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 기술적 난이도 미확정
중요한 배경: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
이상적으로는 store에서 한 값을 추적(track)하면 "State"가 나오고, 거기에
compute를 적용하면 또 다른 "State"가 나오는 식 — Unix의 (cat a; cat b) | while read ...처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최종 목표. :With의
두 번째 인자(b)도 다른 :Compute의 결과물(State)을 그대로 받을 수 있어야
이상적.
미해결 긴장 관계: 이걸 구현하는 두 갈래 방식이 있고 어느 쪽이 맞는지 아직 결정 안 됨:
- Compute 체인이 항상 자기 자신을 mutable하게 바꾼다 — 엔지니어링 비용은 낮지만, 다른 코드가 나중에 그 체인 뒤에 새 compute를 붙이는(다른 소비자가 동일 State에 독립적으로 파생값을 추가하는) 것이 불가능해짐 — 공유/합성이 깨짐.
- 명시적
State:fromState(state)류의 비-mutating 생성자 — 합성은 안전해지지만 엔지니어링 비용이 더 큼(정확히 얼마나 큰지 미확정).
이건 base/architecture.md의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과
같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가
아직 검증 안 됨.
Store/State/Source 온톨로지 — 진행 중인 설계 스레드 (2026-08-04 검증 라운드에서 새로 열림)
이 절은 아직 결론난 설계가 아니다 — 검증 라운드 중 사용자가 실시간으로
설계를 전개하며 나온 내용을 그대로 기록. 다음 세션에서 이어서 다룰 것.
base/store-semantics.md의 "State 프리미티브는 실제로 필요하다" 정정과
직결됨.
핵심 온톨로지:
- Source — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원").
- Store — source들의 집합체.
store.a/store "a"처럼 키로 접근하면 그 source를 감싼 새 State를 매번 만들어 반환(state가 store에 캐시되어 재사용되는 게 아님 — source만 store에 귀속된 유일한 실체). - State — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의
독립적 value 개념이 없음.
state(state)로 기존 state의 결과를 받아 새 state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리 합성 가능"이라는 원래 목표를 구현하는 방식. :With(...)/:Compute(fn)은 실제로는 state 위의 연산 —store "key1":With(store "key2"):Compute(function(key1) return key1 + store.key2.value end)처럼, with한 값을 fn이 클로저로 직접 읽는 모양.
미해결 세부 사항:
:Compute캐싱/무효화 전략 — 매번 새로 계산할지, 캐싱해두고 무효화 플래그로 관리할지 미정. 후보: 값이 바뀌었는데 듣는 소비자가 없으면 연산은 미루고invalid=true만 세워두고, 필요해질 때(듣는 사람이 생기거나 값을 읽을 때) 실제로 연산하고true→캐싱 후false로 되돌리는 dirty-flag 방식. "store가 state를 만드느냐 아니면 저장하느냐"의 문제와 직결 — 사용자 판단은 "만든다" 쪽(저장한다고 하면 한 곳에서:Compute를 붙이면 다른 소비자도 전부 그 compute된 값을 읽게 되어버리는 오염 문제 발생).emit필요 여부 — store가 값 변경 시 관련된 모든 state에 emit해야 하는 구조가 맞는지 확신은 없지만, 그 외의 방법이 안 보인다는 게 사용자 현재 판단.state(from) / state() -> (state, setState)같은 팩토리 모양도 후보로 언급됨(React의useState류 페어 반환과 유사).- Luau 타입 시스템 제약 —
store "key"같은 커링 호출로state<T>의T를 정확히 추론하려면 오버로드 함수 타입((("a") -> number) | (("b") -> boolean))이 필요한데, Luau는 문자열 리터럴 인자를 자동으로as const취급하지 않아서 타입이 좁혀지지 않는 문제가 있음.store.states.a처럼 필드 접근으로 우회하거나,store.a가 바로 state를 반환하고state.value = x로 설정 가능하게 하는 대안도 검토됐으나, 후자는 "다른 source로부터 파생된 state에 value를 직접 설정하면 안 된다"는 문제와 충돌(store가 실제 값을 담는 유일한 주체여야 함).state<Mapped>(compute 결과)가 제대로 바인딩 안 됐을 때 생기는 타입 문제는 일단 UB로 두기로 함. Pipe(quad2-try 후보)는 사실상 폐기 쪽으로 기움 — 별도Pipe타입에 소유권/버전 가드를 넣어 재설계하는 대신, State 자체를 파이핑 결합체로 보고state(state)로 분기하는 쪽이 엔지니어링상 더 쉬워 보인다는 게 사용자의 최신 판단(2026-08-03 라운드의 "Pipe COW가 유력 후보"보다 우선함).
quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지
.claude/initreq/quad2-try/out/quad-core에 정확히 이 문제(Unix 파이프 영감의
State/스트림)를 다뤘던 이전 시도가 있었음. 조사 결과 요약:
확인된 죽은 접근 — 절대 반복하지 말 것:
- OOP 상속(
Base:Extends) 구조가Source/State/Pipe/Store/Event/Action+8개 서브타입 전체에 퍼져 있었음 — 모든 서브클래스 생성자마다self._super._constructor(self, ...)를 수동으로 호출해야 하고(빼먹기 쉬움, 컴파일러가 검증 안 함), private/protected는_접두사 관례일 뿐 실제 캡슐화가 전혀 없었으며,Base:IsInstance가 수동 유지되는_proto/_super연결 리스트를 순회하는 런타임 전용 타입 체크라 Luau 정적 타입 시스템이 전혀 못 봄. 사용자가 우려한 그대로 확인됨 — 상속 기반 설계 금지. --&커스텀 파서 시도는 완전히 죽은 코드였음 — 6개 파일에 156줄의 주석 기반 타입/가시성 어노테이션이 있었지만, 이걸 실제로 소비할 도구 (quad-gen,quad-lang)는 둘 다 완전히 빈 디렉토리였음. 오타(@clsas를@class로 못 고침)가 안 잡힌 채 남아있었고, 같은 주석 마커 아래 전혀 다른 Lua5.1-호환 트랜스파일러 지시어까지 섞여 있었음 — 파서가 한 번도 제대로 동작한 적 없다는 명백한 증거. 확인대로 반복 금지.- Slot은 이 시도에서도 사실상 빈 스텁이었음 —
Insert의 실제 구현부가 전부 주석 처리되어 있고,Notify()도 빈 함수. 심지어 구 v1(quad-2)의DEV_CHANGELOG.txt에도 "TODO: slot 기능 구현"이 마지막까지 미완료로 남아있었음 — 가져올 게 전혀 없음,research/slot-plan.md의 from-scratch 설계를 그대로 진행하면 됨(재조사 불필요). - 다른 서브패키지(
quad-roblox/quad-gtk/quad-lang/quad-gen/quad-compat/quad-debug/quad-docs)는 전부 파일이 0개인 빈 디렉토리 —quad-core밖엔 참고할 게 없음. Store:Pipe/Store:Value연동이 담긴 유일한 두 예제 콜사이트(slot.luau:31-41)조차 존재하지 않는Store:Value메서드를 호출하는 등 실제로 동작 검증된 적이 없는 죽은 스크래치 코드였음 — 이 프로토타입은 끝까지 실사용 검증을 통과한 적이 없음.
건질 만한 것 (인체공학/아이디어만, 코드는 아님):
store:Pipe(key):Compute(fn)같은 왼쪽에서 오른쪽으로 읽히는 파이프 문법 자체는 목표로 유지할 가치가 있음.Pipe가 mutate-vs-fromState긴장 관계에 제시한 절충안 — "체이닝된Compute/Add/... 호출은 자신이 액션 리스트의 유일한 '끝(tip)'일 때만 공유 배열에 그대로 append(뮤테이션), 이미 다른 코드가 그 지점 이후로 체인을 확장해버렸다면 배열을 복사한 뒤 새Pipe객체를 반환"하는 copy-on-write 방식 — 이건 이 문서의 "mutate-in-place vsfromState" 긴장을 실제로 풀어보려 한 유일한 시도라 quad-v2에서 제대로 다시 설계해볼 만한 후보. 단, 원본은 "내가 지금 유일한 tip인가" 체크에 소유권/버전 관리가 전혀 없어서 경쟁 상황에 취약했고 테스트/실사용 검증도 없었음 — 그대로 베끼지 말고, 같은 아이디어를 소유권 가드를 제대로 넣어 재설계할 것.Depend(...)액션 — 계산값에는 관여하지 않고 오직 "이 소스가 바뀌면 다시 계산하라"는 추가 의존성만 등록하는 값-투명(value-transparent) no-op 액션. 작지만 깔끔한 아이디어라 이름 그대로 채택할 만함.- 흥미로운 발견: 스크래치 파일(
out/asdf)에 남아있던 더 이전 버전의 파이핑 스케치가 정확히Pipe(store.background):With(store.transparency, globalStore.test):Compute(fn)모양이었음 — 실제 구현으로 넘어가며:Depend()+포지셔널 인자로 바뀌었지만,:With(...)네이밍은 사용자가 이번 라운드에서 다시 요청한 것과 정확히 일치 — 우연이 아니라 원래 지향점이었던 것으로 보임,:With이름 채택에 힘을 실어줌.
종합: 이 프로토타입은 사실상 죽은 시도가 맞음(확인됨) — Depend/:With
네이밍은 quad-v2 설계에 그대로 살려볼 가치가 있는 아이디어로 남지만, Pipe의
copy-on-write 절충안은 2026-08-04 검증 라운드에서 사실상 폐기 쪽으로 재평가됨
(위 "Store/State/Source 온톨로지" 절 참고 — State 자체가 파이핑 결합체이고
state(state)로 분기하는 쪽이 더 간단하다는 사용자의 최신 판단).
확정된 것 (더 이상 열린 질문 아님)
- 핸들러 계약:
isHandlable(k,v)+priority+process(구bind) +retract(구cleanup) 4종 조합으로 확정 — tbox식 6-hook 세분화는 지금은 안 함. 실제 구현하며 부족한 지점이 보이면 그때 hook 추가(점진적 확장). - Signal 클래스: 안 만듦, 콜백 +
Connected계산 속성만(base/ lifecycle-pattern.md). - Ref: 도입 확정(위 절 참고), 용도는 "id 기반 조회 대체"가 아니라 "외부 관리 instance를 점진적으로 다루기 위한 직접 참조 획득".
base 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입 (2026-08-04 보강)
base/lifecycle-pattern.md가 말하는 "범용 유틸"(per-instance 상태 저장소,
생명 바인드 유틸)은 base가 직접 구현하는 게 아니라 인터페이스만 정의 —
inst는 base 입장에선 any일 수 있음(다른 엔진일 수도 있으므로). 실제
구현은 RobloxFactory(BaseModule) 같은 팩토리 함수가 BaseModule을
뮤테이션해서 그 안에 실 구현체(canExecute 등)를 채워넣는 방식 — 사용자는
quad-base/quad-roblox를 각각 import해서 const quad = RobloxFactory(QuadBase) 세 줄 정도로 직접 조립하면 됨(별도 번들 quad
패키지로 재수출할 필요 없음, 필요하면 만들어도 됨).
열린 질문: RobloxFactory를 같은 BaseModule에 여러 번 호출하면 어떻게
되어야 하는가 — 이미 초기화됐으면 무시(rbvm의 InitNamespace류 가드와
유사하되, "라이브러리마다 수동 init" 패턴과는 다름)하는 쪽으로 기울어짐.
서로 다른 두 곳에서 같은 base를 require해서 RobloxFactory와
AnotherFactory(가상의 예)를 각각 실행하는 경우처럼 충돌 가능성이 있는
시나리오가 향후 모듈 스코핑(New(), base/architecture.md 13번) 논의를
다시 촉발할 수 있음 — 지금은 열어만 둠.
인스턴스 생성 / 이벤트 네이밍 인체공학 (2026-08-04 검증 라운드에서 새로 나온 열린 질문)
Quad "Frame"처럼 문자열로 인스턴스 종류를 지정하는 방식은 타입 추론이
어려움(위 온톨로지 절의 Luau 오버로드 문제와 같은 원인). PA님 DI 스타일은
DI.Frame/DI.TextLabel처럼 필드 접근으로 만들어서 자동완성이 자연스럽게
됨 — 목록에 없는 타입은 DI.New<<Frame>> "Frame"류로 폴백. 이벤트도
Event "" 대신 On...류 이름으로 필드 접근하면 타입/자동완성이 쉬워질 수
있음. 아직 방향 결정 안 됨 — 다음 세션에서 다룰 것.
남은 열린 질문 (.claude/question.md에도 취합)
:Compute가with한 값을 정확히 어떻게 읽는가 — 클로저로 직접 캡처하는 방향은 확정(위 온톨로지 절), 캐싱/무효화 전략(dirty-flag 등)과emit필요 여부는 아직 미정.CreatedRef(가칭)의 정확한 함수/옵션 이름 — children 배열에 아이템으로 넣는다는 방향과 생성/마운트 두 시점 모두 지원한다는 것은 확정, 정확한 API 이름만 남음.- 매
process()호출마다 우선순위 스캔 비용 — 실제 구현/벤치마크 단계에서 확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현 검증 대상). - Store가 Store를 담는 경우의 실제 소유권(누가 내부 Store를 destroy하는가) —
이 문서의 "재실행 래핑" 제안이 맞다면 자연히 바깥 Store bind가 내부 Store의
라이프타임도 감싸게 될 텐데, 이중 해제(double-dispose) 방지가 필요한지 확인.
단,
base/lifecycle-pattern.md의 "destroy 시점엔 아무것도 안 함" 원칙상 이중 해제 자체가 걱정할 필요 없는 개념일 수도 있음 — 재검토 필요. RobloxFactory중복 호출/충돌 시나리오, 인스턴스 생성·이벤트 네이밍 인체공학 — 위 두 절 참고, 둘 다 새로 열린 질문.