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>
352 lines
26 KiB
Markdown
352 lines
26 KiB
Markdown
# 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의 실체가 뭔지는 백엔드 재량.
|
|
- `process(inst, k, v)` — 우선순위 순으로 등록된 핸들러를 스캔, `isHandlable(k,v)`를
|
|
만족하는 최상위 핸들러가 실제 처리를 담당.
|
|
- 예시: Tween의 store-bind 핸들러는 **`k`는 무엇이든 받고 `v`가 Store인 경우를
|
|
잡아내는, 우선순위가 매우 높은 핸들러** — `v`가 store이면 그 값을 처리(구독)함.
|
|
이 핸들러 안에서:
|
|
1. 지금 이 처리가 실행되어도 되는지 라이프타임(`Connected`)을 확인 —
|
|
확인 안 하면 이미 Destroy된 대상에 대해 처리가 실행되는 문제가 생김. GC가
|
|
결국 정리하긴 하지만, GC 되기 전에도 store 값이 업데이트될 수 있으므로
|
|
그 시점엔 그냥 `Connected`를 보고 무시(no-op).
|
|
2. 처리해도 되면, 사용자가 넘긴 함수들을 거쳐 실제 값(`realv`)을 계산.
|
|
3. **`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)`를
|
|
한 번 호출해주면 됨.
|
|
- **핸들러 내부 상태 저장**: `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.md` 5번 항목) — "비현실적"이라는 게 이유.
|
|
- 하지만 **"라이브러리가 자기 자신이 만들어낸 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)을 그대로 받을 수 있어야
|
|
이상적.
|
|
|
|
**미해결 긴장 관계**: 이걸 구현하는 두 갈래 방식이 있고 어느 쪽이 맞는지 아직
|
|
결정 안 됨:
|
|
1. **Compute 체인이 항상 자기 자신을 mutable하게 바꾼다** — 엔지니어링 비용은
|
|
낮지만, 다른 코드가 나중에 그 체인 뒤에 새 compute를 붙이는(다른 소비자가
|
|
동일 State에 독립적으로 파생값을 추가하는) 것이 불가능해짐 — 공유/합성이
|
|
깨짐.
|
|
2. **명시적 `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 vs `fromState`" 긴장을 실제로
|
|
풀어보려 한 유일한 시도라 **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` 중복 호출/충돌 시나리오, 인스턴스 생성·이벤트 네이밍
|
|
인체공학 — 위 두 절 참고, 둘 다 새로 열린 질문.
|