이벤트 self 관습/Store Emit/Ref 일반화/Observer 논의(2026-08-06 후속 세션) 결과 반영

- 이벤트 핸들러 self(Instance) 관습 비채택 확정 (Ref로 충분, Modifier 정적
  flatten과의 충돌, quad-debug 추적성, 클로저 비용)
- rbvm GC 패턴이 실물 검증됐다는 근거 보강 (lifecycle-pattern.md)
- .claude 코퍼스 전체 stale 참조/모순 감사 및 정리
- Store:Emit(key) 확정 (Source 원천 한정, clone 불가 userdata 우선 근거)
- :Compute(fn, previous) 확정 (무거운 파생 객체 재사용, full diff 필수)
- state:Observer(fn) 확정 (children 배열에 직접 놓는 leaf 값, canExecute 게이팅)
- Ref 일반화 확정 (범용 값 박스, 반복 재설정 가능, State와 달리 non-lazy)
- CLAUDE.md 핸드오버 갱신

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-06 17:54:13 +09:00
parent bc0a8b9f5f
commit 936e0766b7
Signed by: qwreey
GPG key ID: D28DB79297A214BD
12 changed files with 440 additions and 34 deletions

View file

@ -53,7 +53,8 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
갱신됨 — 위 5번 항목의 6차 추가분 참고: 스타일시트는 적용 위치 제약과
스크립팅 난이도 때문에 오히려 안 쓰기로 하고 modifier kit으로 대체함.)
- **2026-08-04 세션: modifier 메커니즘 전체 확정, 상세는
`research/modifier-plan.md`로 분리.** 요지만: 런타임 pluggable 핸들러가
`base/modifier-plan.md`로 분리.**[정정: `research/`에서 `base/`
승격됨] 요지만: 런타임 pluggable 핸들러가
아니라 디스패치 이전에 정적으로 flatten되는 값(핸들러 레지스트리 미참여,
CSS cascade 문제 회피). Merge 우선순위는 "배열 순서상 나중 modifier가
우선"과 "인라인 키는 modifier보다 무조건 우선"이라는 독립된 두 규칙(Lua

View file

@ -167,6 +167,115 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
- **콜백 호출 시점 확정**: 생성 직후(construction 시점)와 트리 마운트 후(Parent
세팅 완료) 둘 다 지원 — 옵션으로 선택(`CreatedRef(fn, {phase="created"|
"mounted"})`류, 정확한 API 이름은 구현 단계에서 확정).
- **왜 값이 아니라 콜백인가**: quad는 React처럼 렌더 함수가 계속
재실행되지 않음(플레인 함수를 한 번 호출해 트리를 만들고 끝) — 그래서
"채워졌는지 매 렌더마다 다시 확인"하는 모델 자체가 없고, `useEffect`
의존성 배열 같은 것도 없음. 즉 값이 채워지는 시점을 외부에서 알아낼
방법이 콜백(또는 폴링, 채택 안 함 — `lifecycle-pattern.md`에서 폴링
방식은 이미 기각된 패턴) 말고 없음. 값 자체를 나중에 다루고 싶으면
콜백 안에서 원하는 곳(외부 변수, `self._button` 같은 필드, Store 등)에
직접 대입해 캡쳐하면 됨 — `component-composition-plan.md` 31행 예제
참고. 즉 "값으로도 얻어진다"는 요구는 별도 API가 아니라 콜백이 이미
충족함.
### Ref 일반화 — 엔진 instance 전용이 아니라 범용 값 박스 (2026-08-06 후속 세션)
**결정**: Ref는 "quad가 만든 instance를 얻는 통로"로 좁게 남지 않고,
**아무 사용자 값이나 담을 수 있는 범용 "채워지길 기다리는 값 박스"**로
확장한다. 위 "코루틴 기반 대기 지원 여부는 미정"이었던 항목은 이걸로
해소됨(더 이상 열린 질문 아님).
- **object-ref/function-ref로 나누지 않음.** React의 `useRef`가 DOM
노드든 임의의 사용자 값이든(함수 포함, `ref.current?.()`로 호출하는
imperative-handle 패턴 포함) 같은 API로 다루는 것과 동일한 선례 —
두 개념으로 쪼개면 사용자가 "이번엔 어느 쪽을 써야 하나" 매번
판단해야 해서 나쁨. 엔진 instance도 그냥 "사용자 값의 한 종류"일 뿐.
- **구체 유스케이스**: 자식 컴포넌트가 비싸고 온디맨드로만 필요한 계산
(예: 클릭 위치 기준 컨텍스트 메뉴를 그리기 위한 clip bounds 계산)을
부모에 노출하고 싶을 때, 매 변경마다 push하는 대신 부모가 필요할 때만
`ref.Value?.()`처럼 호출하는 함수를 Ref에 담아 넘기는 패턴 — React의
imperative handle과 동일한 이유(비싼 연산이라 온디맨드가 맞음, 값이
최신인지 아닌지도 애매해짐).
- **API 모양**: `.Value`(get/set) + `:Wait()`(coroutine 컨텍스트에서
사용 — 렌더 함수 바디 안에서 `return` 위에 바로 못 씀, 그래서 콜백도
같이 필요) + 콜백 등록(복수 허용, 이미 채워져 있으면 등록 즉시 그
값으로 1회 호출 — nil/미설정 상태여도 그 상태 그대로 호출. React의
`useEffect`가 매번 `.current` 존재 여부부터 체크하는 것과 같은 이유,
Ref가 자식으로 전달되는 경우 채워지는 시점이 더 늦어질 수 있어서
"이미 채워졌는지" 확인이 항상 필요함). `:Wait()`의 대기자 리스트와
콜백 리스트는 같은 구조 재사용 가능(발화 후 해당 인덱스만 nil 처리,
Luau의 일반화 for는 성긴 배열도 잘 순회함).
- **`CreatedRef`와의 관계**: 둘은 상충하지 않음 — 이 절의 Ref가 범용
프리미티브, `CreatedRef(fn, {phase=...})`는 그 위에 얹힌 "children
배열에 넣으면 dispatch가 자동으로 채워주는" 특수 편의 패턴(quad가
만든 instance에 한정된 경우).
- **해소됨 — 반복 재설정 가능(one-shot 아님), 사용자 확정.** React에서도
자식이 재생성되는 경우 같은 방식(ref가 다시 채워짐)을 씀 — 예: 마우스
호버/무브 시 `current` 확인 후 라벨 위치를 결정하는 라벨 컨테이너
하나를 두고 라벨 내용만 스왑해가며 Ref를 재사용하는 패턴. 이런 고급
패턴은 조심할 게 많지만 그건 라이브러리가 아니라 사용자가 신경 쓸
몫. **따라서 콜백은 "발화 후 소진"이 아니라 매 `:Set()`마다 다시
불림** — 소진되는 건 `:Wait()`가 만드는 개별 대기자(coroutine 재개는
본질적으로 1회성)뿐, 콜백 리스트 자체는 유지됨.
- **⚠️ Ref는 의도적으로 lazy가 아니고 `:Compute` 파생을 지원하지 않음
— State와의 이 차이가 중요함.** (예전엔 Store가 Ref와 비슷한 것도
겸해서 지원한 적이 있었는데, State의 lazy 재계산 모델과 Ref의 즉시
get/set 모델이 섞여서 좋지 않았음 — 그 경험에서 나온 의도적 분리.)
Ref는 그냥 "지금 뭐가 들어있나/누가 채워주길 기다리나"만 다루는 즉시
값 박스이고, 파생값이 필요하면 Store/State(`:With`+`:Compute`)를 쓸 것
— 둘을 섞으려 하지 말 것.
- **용어 정리 합류 대상**: Ref의 정의 자체가 "instance를 얻는 것"에서
"범용 값 박스"로 넓어졌으므로, 진행 중인 용어 정리(`question.md` 1번)
때 이름이 여전히 맞는지 같이 재검토할 것.
## 이벤트 핸들러는 self(Instance)를 받지 않는다 — 확정 (2026-08-06)
**결정**: v1의 `function(self, ...)` 관습(`self`/`this`로 이벤트 대상
Instance를 넘겨주는 것, `.claude/base/quad-v1-architecture.md` 참고 —
실제로 `event.lua``Bind``func(self or this, ...)`로 넘겨줌)은
**채택하지 않는다.** quad-roblox의 이벤트 핸들러는 엔진이 네이티브로
주는 이벤트 인자만 받는다(React의 `onXxx`가 DOM 노드가 아니라
SyntheticEvent만 주는 것과 같은 모양).
**근거**:
1. **Ref가 이미 이 자리를 채움.** "생성 직후/마운트 후 ref 채우기"가 되는
순간 Instance 접근이 필요하면 클로저 캡처로 해결됨(위 Ref 절) — self는
그와 중복되는 두 번째 채널일 뿐이고, 두 채널이 있으면 "어느 쪽이
authoritative냐"는 질문이 항상 따라붙음.
2. **thin wrapper를 제공하면 엔지니어링 구조 자체가 바뀜.** self로 얻는
값이 mutable한 재바인드 가능 wrapper라면, 그건 Modifier의 정적
flatten(`base/modifier-plan.md`)과 항상 경쟁하는 두 번째 쓰기 경로가
생긴다는 뜻 — flatten된 뒤엔 wrapper 쪽에서 "이전 modifier가 뭐였는지"
재구성할 방법이 없음. Modifier 핸들러가 KV 매치 기반이라는 걸 감안하면,
wrapper 값을 처리하려면 핸들러가 "이게 flatten된 정적 값이냐, 아니면
언제든 바뀔 수 있는 wrapper냐"를 매번 분기해야 함 — 오버엔지니어링이고
hot path(매 `process` 호출)에 분기 비용이 붙음. 반대로 raw Instance를
그대로 주는 선택지도 있지만, 그러면 quad가 스스로 지양하는 "quad가
모르는 직접 mutate 경로"를 공식 API로 만들어주는 셈이라 (3)과 충돌.
3. **디버깅 관점에서 더 결정적.** quad-debug의 가치 제안이 "무엇이
무엇에 연결됐는가"를 선언된 반응형 그래프로 추적하는 것인데
(`research/debug-tooling-plan.md`), self로 얻은 Instance를 이벤트
핸들러 안에서 직접 mutate하는 경로는 그 그래프 밖 — `base/
purity-and-effects-plan.md`의 "재사용 가능한 컴포넌트는 store만
파라미터로 받아야 한다"는 이식성 원칙과도 같은 결.
4. **성능/GC**: self를 넘겨주려면 원본 콜백을 클로저로 한 번 더 감싸야
함(`event:Connect(function(...) func(self, ...) end)`) — Connect마다
불필요한 클로저 할당 비용이 들고, 최적화에도 GC 흐름에도 좋을 게
없음. self가 없으면 사용자가 준 함수를 그대로 `:Connect`에 넘기면
충분함. quad는 어차피 라이프사이클 끝까지 바인딩을 들고 있으므로
(`base/lifecycle-pattern.md`, rbvm 선례 — GC-native), Destroy되면
해당 Connection도 자연히 같이 정리됨 — 별도 Disconnect 관리가 애초에
불필요. 동적으로 Connect/Disconnect를 반복해야 하는 최적화 케이스가
실제로 생기면, 그건 Ref로 얻은 Instance를 갖고 사용자 코드가 직접
처리하면 됨(사용자가 실사용 케이스로 확인한 바로도 이런 니즈는
드묾 — 드문 케이스를 위해 구조 전체를 복잡하게 만들 이유 없음).
**일반화**: 이 논거의 핵심은 Roblox에 국한되지 않는 원칙으로 정리됨 —
"엔진이 네이티브로 콜백에 뭘 주든, quad는 그걸 감싸지 않고 그대로
호출해줘도 무방하다"는 것. 다만 이벤트 등록 자체가 quad-roblox에만
있는 개념이라(다른 백엔드는 이벤트 모델이 다를 수 있음) 이건 base
문서가 아니라 quad-roblox 로컬 결정 — 다른 백엔드 구현체를 만들 때
참고할 만한 템플릿 정도로만 취급.
## 여러 Store 값을 묶어 파생값 만들기 — `:With` + `:Compute`, 포지셔널 인자 지양
@ -185,6 +294,97 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/
`:Compute`" 부분 참고).
### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06)
**배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진
객체일 수 있음(예: 큰 로케일 테이블을 Roblox `LocalizationTable`
Instance로 변환하는 경우 — `LocalizationTable``Set`/`Get`/`List`로
부분 갱신 가능한 userdata). 매번 새로 만들지 않고 이전 결과를 그대로
재사용해 필드만 patch하고 싶을 때를 위해, `fn(value, previous)` 형태로
**직전에 이 Compute 함수가 반환했던 값**을 두 번째 인자로 받을 수 있게
한다.
- **opt-in**: 안 쓰는 Compute 함수는 두 번째 인자를 그냥 무시하면 됨 —
비용 0. 대부분의 Compute는 이걸 쓸 필요 없음.
- **`previous`는 "바로 직전 버전"이 보장되지 않음.** lazy pull 모델이라
중간에 여러 번 무효화됐어도 실제로 관측(`Get()`) 안 됐으면 재계산
자체가 안 일어남 — 그래서 `previous`는 몇 세대 전 값인지 알 수 없음.
**따라서 `previous`를 다루는 로직은 반드시 "현재 입력 전체 대 이전
결과 전체"의 full diff여야 하고, "정확히 한 단계 전"이라고 가정하는
incremental delta 로직을 짜면 안 됨.** 이건 React 자체의 reconciler가
하는 것과 같은 모양(old tree/new tree 전체 비교 후 실제 host 객체에
패치 적용)이라 새로 발명하는 패턴은 아님.
- 최종 소비처가 patch된 값을 다시 한번 Set/Parent하게 되는 경우가
있어도(레퍼런스는 같은데 다시 대입) 대체로 치명적이지 않음(Roblox
프로퍼티 재대입은 저렴/멱등인 경우가 대부분) — 문서화만 해두면 충분.
**⚠️ 이 패턴을 쓸 때 반드시 같이 지켜야 하는 것 — "확정(관측)되기 전엔
연산이 없다".** `previous`를 mutate하는 로직은 Compute 함수 **본문
안**에 있으므로, 그 함수가 재실행되지 않으면(=아무도 다시 `Get()`하지
않으면) mutation 코드 자체가 아예 실행되지 않는다 — 단순히 "가끔
stale하다" 수준이 아니라 **영영 갱신이 안 일어날 수 있음**. 이 패턴으로
만든 State는 반드시 다음 중 하나로 계속 능동적으로 관측되어야 함:
1. quad의 정상적인 선언적 prop 바인딩 경로(`[Property "X"] = someState`
류)에 실제로 물려있어서, dispatch 엔진이 무효화 시 자동으로
재`Get()`하게 되어 있거나,
2. 아래 "Observer" 절의 `state:Observer(fn)` + 콜백 안에서 명시적
`Get()` 호출 + 그 결과를 children 배열에 넣어 라이프사이클에
묶어두기.
"Ref로 한 번 얻어서 수동으로 Parent만 하고 끝"처럼 능동적 관측 경로가
안 남아있으면, 이 최적화는 그냥 조용히 작동을 멈춘다.
### `state:Observer(fn)` — 값을 안 실어주는 구독, children 배열에 직접 놓는 leaf 값
**결정(2026-08-06 후속 세션, 사용자 확정)**: 별도 `ObserverHolder`
래퍼 타입은 안 만듦 — `state:Observer(fn)`가 반환하는 값 자체가 이미
"children 배열에 바로 놓을 수 있는 leaf 값"이라 감쌀 필요가 없음.
`CreatedRef`와 완전히 같은 층위:
```lua
local observer = state:Observer(function()
state.value
end)
Frame {
observer,
}
```
이러면 `observer``Frame`이 살아있는 동안만 유지되고, `Frame`
retract/Destroy되면 자동으로 정리됨.
- **값을 안 실어줌 — 반드시 `Get()`을 다시 해야 함.** 기존 "emit은
무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함" 원칙(아래
"Store/State/Source 온톨로지" 절)이 그대로 적용됨: `fn`은 "뭔가
바뀌었으니 다시 확인하라"는 신호만 받고 새 값 자체는 안 받음 —
위 예시처럼 `fn` 본문에서 `state.value`/`Get()`을 명시적으로 다시
읽어야 함. 자동으로 안 해주는 이유: 재계산이 진짜 필요한지가 다른
`:With`한 값에 따라 갈리는 경우가 있어서(위 "포지셔널 인자 지양" 절의
`noprint` 예시처럼 계산 자체를 통째로 생략하고 싶을 수 있음) — `Get()`
호출 여부를 작성자가 직접 결정하게 열어둔 것.
- **base가 제공하는 것은 `isObserver`류 타입 판별자 하나** — children
배열 dispatch가 숫자 슬롯 값을 훑을 때 "이게 Observer인가"를 판별해
`CreatedRef`와 같은 방식으로 라이프사이클에 묶어주는 것 말고는 base가
더 해줄 일이 없음. 새 dispatch 메커니즘이 아니라 기존 children-array
참가자 패턴의 반복.
- **콜백 실행은 기존 `canExecute` predicate로 게이팅**(Slot 생존 확인과
동일한 재사용 — "canExecute 하나로 통일" 원칙, 새 메커니즘 발명 아님)
— 발화 시점과 처리 시점 사이에 owning leaf가 이미 죽었으면 no-op.
- **구현 노트(사용자 제안, 확정된 아키텍처는 아니고 구현 시 참고)**:
살아있는 Observer 집합을 Observer 값 내부 필드로 안 두고, 외부에
weak table(`{[observer] = true}`, `__mode = "k"`)로 인덱싱하는 방식을
선호 — 포인터 해싱 비용만 들고 값 자체엔 부작용 없음. rbvm의
`getNamespaceOf`류가 비슷한 외부 weak-table 인덱싱을 씀
(`base/lifecycle-pattern.md` 참고).
- **인자 없는 `state:Observer()` — "항상 관측" 유틸.** `fn`을 생략하면
내부적으로 no-op 콜백을 쓰는 것으로 취급해, 그냥 "이 State를 계속
능동적으로 관측 상태로 유지"하는 용도로만 씀. 위 "`previous` 인자"
절의 캐비엇("능동적 관측 경로가 안 남아있으면 mutate 로직이 조용히
멈춘다")을 만족시키는 가장 단순한 도구 — 별道 콜백 로직 없이 그냥
이 State가 계속 재계산되게만 강제하고 싶을 때 씀 — 별도 콜백 로직 없이
이 용도로만 쓰고 싶을 때. 문서화만 확실히 하면 별문제 없음(사용자
판단).
## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨
**배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
@ -353,7 +553,9 @@ Observable/Observer)을 조사한 결과, 두 지점에서 기존 확정과 실
방식이면, 나중에 GC만으로 정말 부족한 케이스가 생겨도 그 connection을 얻어
`disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게 가능한 디자인 —
지금 마일스톤에서는 필요 없어서 안 함(사용자: "필요하다면 dispose 핸들러를
만들어주는 것도 가능한 디자인, 다만 지금까지 요구가 없었음").
만들어주는 것도 가능한 디자인, 다만 지금까지 요구가 없었음"). (rbvm의
GC-native 패턴이 실물에서 검증됐다는 근거는 `base/lifecycle-pattern.md` 상단
참고 메모 참고.)
## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지

View file

@ -56,8 +56,13 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자
## 추가로 기록해둘 것
- Vide의 암묵적(ambient stack) 의존성 추적 vs Fusion의 명시적 `use()` 축은
push/pull 축과 독립적인 별개 결정 — quad는 아직 미정(`base/bind-system-plan.md`
열린 질문 참고). Fusion의 명시적 `use()``checkLifetime` 같은 bind-time
체크를 가능하게 하는 부수 효과가 있음.
push/pull 축과 독립적인 별개 결정. **[정정] 리서치 당시(2026-08-03 이전)엔
"quad는 아직 미정"이었으나, `base/bind-system-plan.md`의 "여러 Store 값을
묶어 파생값 만들기 — `:With` + `:Compute`" 절에서 이미 명시적 모델로
확정됨** — Vide식 암묵적 ambient stack 추적은 "함수 실행 중과 끝 사이를
확인하고 부작용이 필요"한 방식이라 Lua에서 깔끔하지 않다는 이유로 기각,
`:With(...)` + `:Compute(fn)`(클로저로 직접 읽는 명시적 방식)를 채택.
Fusion의 명시적 `use()``checkLifetime` 같은 bind-time 체크를 가능하게
하는 부수 효과가 있음.
- 두 라이브러리 다 mount 시 단일 소유권 가드가 없다는 것 자체가 quad Slot의
차별점이라는 근거로 재사용 가능.

View file

@ -3,6 +3,14 @@
**상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(`retract`) 전략의 원본.
완료 개념 없음, 구현하면서 세부 조정 있을 수 있음.
**참고(2026-08-06)**: rbvm은 사용자가 직접 실행해 실제로 잘 동작하는 것을 확인한
코드베이스(프로덕션에서 검증됨) — 사람이 짠 코드라 무결성이 100% 보장되는 건
아니지만(실제로 아래 5번 항목에서 `Connected` 체크 방향 반전 버그, `__mode` 오타
버그가 발견됨), 다른 리서치 참고 레포보다 "실물로 돌아간다"는 근거가 확실한
비교 대상. 단 이건 참고용 비교 대상일 뿐 quad-v2가 반드시 따라야 할 규범은
아님 — 실제 채택 여부는 이 문서 각 절의 확정 내용(특히 아래 "확정" 두 절)을
따를 것, rbvm 코드를 그대로 베끼라는 뜻이 아님.
## 배경
`raw-userinput.md`(이하 사용자 원 메모): "레지스터의 실행은 내 생각엔 그냥 Destroy

View file

@ -3,8 +3,9 @@
**상태**: base — 핵심 메커니즘(런타임 plug 아님/정적 merge, immutable
값+clone 기반 체이닝, 이중 setter)은 2026-08-04 세션 채팅 논의로 확정. 남은
건 getter 정확한 이름뿐(구현 단계). Modifier가 컴포넌트 경계를 어떻게
통과하는지(다중 루트, 상속 방식)는 별개 문제로
`research/component-composition-plan.md`의 열린 질문에 남음 — 이 문서는
통과하는지(named parameter로 전달, multi-root 개념 폐기)는 별개 문제로
**[정정] `research/component-composition-plan.md`는 2026-08-04 세션에 수렴
완료돼 `base/component-composition-plan.md`로 승격됨 — 이 문서는
"Modifier 값 자체가 어떻게 동작하는가"만 다룸.
## 문제
@ -26,6 +27,12 @@ pluggable로 만들면 여러 modifier가 반응형으로 같은 키를 계속
cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드 변경은 전체
교체, 부분 오버레이 없음"(`base/architecture.md` 3번) 원칙과 충돌함.
관련: 이미 마운트된 Instance에 재바인드할 때 Default→실값 flatten을 다시
해야 하는지/clone이 필요한지는 별개 미정 문제로
`research/existing-instance-bind-plan.md`의 "Default 값과 얽히는 문제" 절
참고 — 여기서 다루는 건 컴포지션 타임의 modifier 값 자체 flatten이라 층위가
다름.
### 2. Merge 우선순위: 배열 순서와 인라인은 독립된 두 규칙
`Frame { modifier1, modifier2, Name = ... }` 평탄화 시:
@ -157,6 +164,7 @@ State에 담기면 그 값이 반응형으로 바뀔 수 있다는 뜻이 되어
- Getter 정확한 이름/모양(`:Get(key)` vs dot-access 겸용) — 후순위, 구현
단계에서 다른 세부 API 이름들과 같이 확정 가능.
- Modifier가 컴포넌트 경계를 어떻게 통과하는지(다중 루트, 상속 방식)는
`research/component-composition-plan.md`에서 계속 다룸 — 이 문서가 다루는
"값 자체의 동작"과는 별개 문제.
- Modifier가 컴포넌트 경계를 어떻게 통과하는지는 **[정정] 이미
`base/component-composition-plan.md`에서 해소됨**(named parameter로
전달, "다중 루트" 개념 자체는 폐기) — 더 이상 열린 질문 아님, 이 문서가
다루는 "값 자체의 동작"과는 별개 문제였다는 점만 참고로 남김.

View file

@ -88,6 +88,52 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
인체공학(사용자가 좋아하는 부분)은 유지하되 내부 구현(체이닝이 아니라 팩토리
함수)만 바꾼다.
## Store 값을 직접 mutate한 뒤 전파 — `:Emit(key)` (2026-08-06 후속 세션)
**결정**: Source가 들고 있는 값을 새 값으로 교체하지 않고 제자리에서
mutate한 뒤, `Store:Emit(key)`로 그 key의 무효화 신호만 별도로 쏘는
것을 **Source 원천(store가 직접 들고 있는 값)에 한해 허용**한다.
**존재 이유(우선순위순)**:
1. **clone이 아예 불가능한 값이 있음.** userdata나 외부 라이브러리
객체(엔진 Instance 등)는 `table.clone`으로 새 값을 만들 수 없음 —
이런 값은 "새 값을 만들어 Set"이라는 대안 자체가 없으므로, in-place
mutation + `Emit`이 변경을 전파하는 유일한 수단.
2. Lua의 불변 업데이트가 verbose함(JS의 `{...t, x=1}` 같은 문법이 없어
`table.clone` 후 필드 덮어쓰기 + 재대입 필요) — 이걸 줄여주는 부차적
이득도 있지만, 이게 주된 이유는 아님(1번이 진짜 이유).
**왜 새 구멍이 아닌가**: `Get()`은 원래도 라이브 테이블 레퍼런스를
돌려주므로, 그 레퍼런스를 mutate하는 것 자체는 `Emit` 유무와 무관하게
Lua에서 항상 가능한 일. `Emit`이 없으면 그 mutation은 "조용히 반영 안
되는"(dependent가 재계산 안 됨, UI가 stale한 채 멈춤) 상태로 남을 뿐이라
오히려 `Emit` 없는 쪽이 더 나쁜 버그 클래스 — `Emit`은 이미 가능한
mutation에 정식 신호를 붙여주는 것뿐.
**남는 캐비엇(문서에 반드시 명시)**: `Get()`으로 이전에 그 테이블을
읽어서 어딘가(로컬 변수, 다른 코드가 들고 있는 참조)에 캐시해둔 게
있다면, mutation 순간 그것도 같이 바뀐다 — 새 테이블이 아니라 같은
레퍼런스라서. **`Get()` 결과를 나중 비교(`==`)나 diff 캐시 용도로 들고
있으면 안 됨 — 항상 다시 `Get()`할 것.**
**하드 경계 — Source 원천에만 허용, 중간/파생 State에는 없음.** `:With`/
`:Compute`로 만들어진 파생 State에는 `Emit`이라는 개념 자체가 없다 —
허용하면 "이 State의 현재 값이 뭘 근거로 계산됐는가"를 아무도 설명할 수
없게 되어(quad-debug가 추적하려는 "무엇이 무엇을 계산했는가" 그래프가
깨짐) 디버깅이 사실상 불가능해짐. State의 값은 항상 "선언된 Compute
함수를 실제로 실행한 결과"여야 한다는 불변식이 깨지면 안 됨. 무거운
파생 객체를 재사용하고 싶은 경우(Compute의 결과 자체가 무거운 userdata인
경우)를 위한 별도 메커니즘은 `base/bind-system-plan.md`의 "`:Compute(fn)`의
선택적 두 번째 인자 — `previous`" 절 참고 — 이건 `Emit`과 다른 메커니즘.
**따름정리 — `Store<T>``T`는 Modifier가 될 수 없음.** Modifier는
정적 flatten으로 dispatch와 완전히 별개인 단계에서 처리되고
(`base/modifier-plan.md`), `State<Modifier>`가 UB로 확정된 것도 같은
이유(`modifier-plan.md` 7번) — Store/State/dispatch 경로엔 애초에
Modifier용 processor가 없음. 위 "하드 경계"와 같은 이유로, `Emit`
Modifier의 정적 flatten과 충돌할 걱정 자체가 성립하지 않음(둘이 만날
지점이 없음).
## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는

View file

@ -35,6 +35,11 @@
- **`CreatedRef`/`canExecute`(3순위, 사소함)**: `CreatedRef`는 과거분사형이라
생성자처럼 안 읽힘. `canExecute`는 실제로 "이 핸들이 아직 살아있나"
확인인데 이름이 범용 권한 체크처럼 들림 — `isAlive` 쪽이 더 직접적.
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/
그냥 "plug" 중 뭘로 할지 아직 안 정함(개념 자체는 확정). 이 문서가 자체적으로
"question.md에도 취합"이라고 표시해뒀던 항목이 누락돼 있어 이번에 추가.
- **이미 지나간 사례로 참고**: `register`(v1) → `State`(v2) 리네임은
"모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든
셈 — 이번 정리에서 같은 패턴을 조심할 것.
@ -55,11 +60,12 @@
- **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고.
채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를
넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은
다 해소됨, 남은 건 세부 API 이름과 "이벤트 함수가 self로 instance를
읽는 게 quad 관습"이라는 언급 확인(그 문서 "열린 질문" 절 참고)뿐.
사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정한 건
여전함 — base 설계(M2 Dispatch/M3 Source/M5 DI 생성자) 시점에 훅 확장
지점만 고려해두면 됨.
다 해소됨, 남은 건 세부 API 이름뿐("이벤트 함수가 self로 instance를
읽는 게 quad 관습"이라는 언급은 2026-08-06 후속 세션에서 해소 —
채택 안 함으로 확정, `base/bind-system-plan.md` "이벤트 핸들러는
self(Instance)를 받지 않는다" 절 참고). 사용자가 "quad 개발 완료 전엔
착수 못 함"으로 직접 후순위 지정한 건 여전함 — base 설계(M2 Dispatch/
M3 Source/M5 DI 생성자) 시점에 훅 확장 지점만 고려해두면 됨.
- **문서화 전략(UI 네이밍 컨벤션, Store 부작용을 게임 시스템에서 쓰는
패턴)** — `research/documentation-plan.md`(뼈대만). 정식 백로그 항목으로
올릴지, 착수 시점을 언제로 볼지 사용자 판단 필요.

View file

@ -491,13 +491,19 @@ Tween mock 등 동적 동작 포함")와 목적이 다름:
기술적 실현 가능성은 전부 해소됨(위 핸드오버 요약) — 아래는 우선순위별로만
분류, 다음 세션 진행을 막는 항목 없음.
**확인 필요(다음 세션에서 사용자에게 물어볼 것)**
**해소됨 (2026-08-06 후속 세션)**
- "이벤트 함수들이 실제 instance를 읽을 수 있게 self를 건네받는 게 quad의
관습"이라는 언급 — 사용자가 이번 논의 중 언급했으나 본인도 "문서화됐는지
모르겠다"고 함. 정확히 어떤 함수(이벤트 핸들러? Compute? `describe` 훅?)에
어떤 시그니처로 적용되는 관습인지 확인 후 해당 base 문서
(`bind-system-plan.md` 등)에 반영 필요 — 지금은 추측성 반영 안 함.
관습"이라는 언급 — v1 `event.lua``func(self or this, ...)` 관습이
실존함은 확인됐으나(v1 튜토리얼에도 문서화), **quad 재설계에서는
채택하지 않기로 확정**. Ref가 이미 인스턴스 접근 용도를 커버하고,
thin wrapper를 준다면 Modifier 정적 flatten과 경쟁하는 두 번째 쓰기
경로가 생겨 오버엔지니어링/성능/디버깅 추적성 문제가 생긴다는 게
이유 — 클로저 래핑 비용도 근거로 추가됨. 상세 근거와 결정문은
`base/bind-system-plan.md`의 "이벤트 핸들러는 self(Instance)를 받지
않는다" 절 참고. quad-debug 입장에서는 self/thin wrapper 경로가
아예 없어지므로 오히려 계측 대상이 단순해짐(추적 안 되는 경로 자체가
존재하지 않게 됨).
**세부 API 이름 (후순위, 구현 착수 시점에 자연히 정리)**

View file

@ -4,7 +4,8 @@
(2026-08-06) 도중 파생된 두 아이디어를 별도로 트래킹할 가치가 있다고
판단해 뼈대만 기록해둠. 세부 설계는 없음, 착수 시점도 미정 — 사용자가
"간단히 plan 작성은 하는 게 맞을 듯, 아주 뼈대만 있는 것이라 할지라도
적어줄 필요는 있겠다"고 판단해서 만든 문서.
적어줄 필요는 있겠다"고 판단해서 만든 문서. 같은 날 후속 세션에서
이벤트 핸들링 self 관습 논의가 확정되며 3번 항목이 추가됨.
## 1. UI 요소 네이밍 컨벤션 문서
@ -18,8 +19,12 @@ Roblox가 Play 중 라이브 UI 편집 도구를 꺼버려서 실제 화면의 U
**뼈대(아직 설계 아님, 물음표만)**:
- 어떤 단위에 이름을 붙이게 유도할까 — 모든 Frame? 컴포넌트 루트만?
- 자동 이름 부여 옵션이 있으면 좋을까(예: 컴포넌트 함수 이름을 기본
`Name`으로 쓰는 관례 — 있으면 편하지만 매직에 가까워 `architecture.md`
2번의 "마법 안 쓴다" 원칙과 긴장 있을 수 있음, 검토 필요).
`Name`으로 쓰는 관례 — 있으면 편하지만 매직에 가까워 "마법 안 쓴다" 원칙과
긴장 있을 수 있음, 검토 필요).[정정: 인용 위치 정정 —
`architecture.md` 2번(함수형 vs OOP 논의)이 아니라
`base/component-composition-plan.md` §1("컴포넌트 = 그냥 함수, '자기
store 자동 소유' 매직은 폐기")의 **사용자 확정** "마법 안쓴다 그것도
동의함" 발언이 근거임]
- 어디에 문서화할까 — 퀵스타트 튜토리얼? 별도 스타일 가이드? 린트 규칙으로
강제할까(과한 선택지, 참고만)?
@ -50,9 +55,32 @@ UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_`나 `QUAD_` 같
문서에 합칠지 분리할지는 미정.
- 안티패턴 vs 권장 패턴 대조 형식이 적당할지, 다른 형식이 나을지도 미정.
## 3. 권장 이벤트 핸들링 패턴 문서 (2026-08-06 추가)
**배경**: 이벤트 핸들러가 self(Instance)를 받지 않기로 확정된 결정
(`base/bind-system-plan.md` "이벤트 핸들러는 self(Instance)를 받지
않는다" 절)의 근거가 꽤 비직관적임 — v1을 써본 사용자는 self가 없어진
걸 보고 "왜 없어졌지, 불편한데"라고 느낄 수 있어서 근거까지 같이
문서화할 가치가 있음.
**뼈대(아직 설계 아님, 물음표만)**:
- **"왜 thin wrapper를 안 주는가"**를 설명하는 절 — 메모리 낭비(클로저
래핑 비용)와 오버엔지니어링(Modifier 정적 flatten과 경쟁하는 두 번째
쓰기 경로, KV 핸들러 분기 비용)이 핵심 논거. `bind-system-plan.md`
결정문을 그대로 요약하면 될 듯.
- **권장 이벤트 핸들링 패턴** 자체 — Instance가 필요하면 Ref로 캡쳐해서
쓰는 예제, Store/Source를 만들고 State로 파이핑되는 초기점을 바꿔가며
반응형을 구현하는 게 더 일관된 흐름이라는 사용자 원 논지를 코드
예제로 보여주는 게 좋아 보임.
- 일반화된 원칙("엔진이 네이티브로 콜백에 뭘 주든 그대로 호출해줘도
무방하다")을 quad-roblox 한정 설명으로 둘지, 다른 백엔드 구현자를
위한 일반 가이드로도 남길지는 미정.
- 어느 문서에 넣을지 — 퀵스타트? 위 1번 네이밍 컨벤션 문서와 마찬가지로
아직 미정.
## 다음 단계
둘 다 지금 당장 설계할 필요는 없음(`CLAUDE.md` "지금 할 일" 1번 —
다 지금 당장 설계할 필요는 없음(`CLAUDE.md` "지금 할 일" 1번 —
구현 착수가 최우선). 사용자 판단이 필요한 것:
- 이걸 `.claude/question.md`/`CLAUDE.md`에 정식 백로그 항목으로 올릴지,
아니면 이 파일 하나로 충분한지.

View file

@ -27,6 +27,12 @@ Default를 넣으면 더 어려워짐 — Default로 쓰다가 실제 쓰인 값
복사(clone) 모드를 켜야 하나 — 근데 그러면 너무 복잡해질 것 같다는 우려까지만
기록. 결론 없음.
관련: Modifier flatten 자체의 clone 정책(변환마다 `table.clone`, immutable
확정)은 이미 `base/modifier-plan.md`의 "3. Immutable 값 + clone 기반 체이닝"
절에서 다뤄짐 — 단 그건 "Modifier 값 자체의 clone"이고, 여기서 미정인 건
"이미 마운트된 Instance에 대한 재바인드 시점의 Default→실값 flatten"이라 층위가
다름, 새로 설계하지 않고 참고 링크만.
## 사용자 확인 결과: 진짜로 모르겠음 — 열린 가능성으로 유지
**사용자 확인 완료, 그러나 결론은 "미정 유지".** 실제로 이 기능을 원한다고

View file

@ -6,8 +6,9 @@
정도. 원본:
`.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은
항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이
반응 그래프 안에 있는 설계는 명시적 반면교사 — `base/comparison-fusion-vide.md`
"Animation(Tween/Spring) integration — 경고 신호" 절 참고.
반응 그래프 안에 있는 설계는 명시적 반면교사 — [정정: 절 제목이 실제와
달랐음] `base/comparison-fusion-vide.md`의 "Fusion" 절 마지막 불릿
("Tween/Spring이 State그래프 안의 1급 노드") 참고.
## 확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다

105
CLAUDE.md
View file

@ -120,9 +120,10 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
`research/debug-tooling-plan.md`)도 2026-08-06 세션에서 설계 수렴 —
채널 실현 가능성(Studio 플러그인↔Play 중 게임 간 BindableEvent/Function
통신)까지 실측 검증 완료, 세부 API 이름만 남음. 착수 시점은 여전히
"quad 개발 상당 부분 끝난 뒤"로 사용자가 못박음. 같은 세션에서 파생된
문서화 전략 뼈대(`research/documentation-plan.md`, UI 네이밍 컨벤션 +
Store 부작용 게임 시스템 활용 패턴)도 후순위 백로그로 같이 남김.
"quad 개발 상당 부분 끝난 뒤"로 사용자가 못박음. 같은 날 파생된 문서화
전략 뼈대(`research/documentation-plan.md` — UI 네이밍 컨벤션, Store
부작용 게임 시스템 활용 패턴, 권장 이벤트 핸들링 패턴 3종)도 후순위
백로그로 같이 남김.
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목).
@ -234,11 +235,8 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
UI 네이밍 컨벤션 문서, 스킬/쿨타임/재화 같은 게임 시스템에서 Store의
부작용 허용을 깔끔한 패턴으로 쓰는 법 문서 — 를 `research/
documentation-plan.md`에 뼈대만 분리해서 남김(위 "지금 할 일" 4번).
- **아직 확인 안 된 것 하나**: 사용자가 "이벤트 함수들이 실제 instance를
읽을 수 있게 self를 건네받는 게 quad의 관습"이라고 언급했으나 본인도
"문서화됐는지 모르겠다"고 함 — 어떤 함수(이벤트 핸들러? Compute?
`describe` 훅?)에 정확히 어떤 시그니처로 적용되는지 다음 세션에서 확인
필요(`debug-tooling-plan.md` "열린 질문" 참고), 추측성 반영은 안 해둠.
- **이벤트 self 관습 확인 필요했던 항목 — 같은 날 후속 세션에서 해소됨.**
아래 "2026-08-06 후속 세션" 절 참고.
**같은 세션 후반, 별개 주제 두 개 추가**(quad-debug와 무관, 사용자가
"적어는 뒀는데 안 줬는건가" 하며 새로 떠올린 것들):
@ -275,3 +273,94 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
상호작용면(자기 트리 뷰/리프 클릭→상세 패널/실제 Explorer 선택과 연동,
Explorer와 플러그인 트리는 별도 도킹 위젯)으로 구성된다는 것도 사용자
질문에 확인 응답 — `debug-tooling-plan.md` "핵심 설계 방향" 9번.
## 2026-08-06 후속 세션 — 이벤트 self 관습 결정, rbvm GC 참고, 문서 코퍼스 정리
같은 날 이어진 세션에서 세 가지를 처리함. **다음 세션이 새로 알아야 할 것은
없음** — 아래 전부 `base/`/`research/`/`question.md`에 실제로 반영 완료.
**1. 이벤트 핸들러 self(Instance) 관습 — 채택하지 않기로 확정.** 위 절에서
"확인 필요"로 남겨뒀던 것의 결론: v1의 `func(self or this, ...)` 관습은
실존함을 확인했지만(`.claude/initreq/quad/src/event.lua` 82행, 튜토리얼
문서화까지 있음), quad 재설계에서는 채택하지 않음. 근거 네 가지 —
(1) Ref가 이미 "생성 직후/마운트 후 Instance 접근"을 콜백으로 커버해서
중복 채널이 됨, (2) self로 재바인드 가능한 thin wrapper를 준다면 Modifier의
정적 flatten과 경쟁하는 두 번째 쓰기 경로가 생겨 KV 핸들러가 매번
"flatten된 값이냐 wrapper냐"를 분기해야 하는 오버엔지니어링, (3)
quad-debug가 추적하는 반응형 그래프 밖의 mutate 경로가 공식 API로
생기는 셈이라 `purity-and-effects-plan.md`의 이식성 원칙과 충돌, (4)
self를 넘기려면 원본 콜백을 클로저로 한 번 더 감싸야 해서 불필요한 할당
비용 — quad는 어차피 라이프사이클 끝까지 바인딩을 들고 있어 Destroy 시
Connection도 자연히 정리되므로(`lifecycle-pattern.md`, GC-native) 감쌀
이유가 없음. 상세 결정문은 `base/bind-system-plan.md`의 "이벤트 핸들러는
self(Instance)를 받지 않는다" 절. `research/debug-tooling-plan.md`/
`.claude/question.md`의 관련 항목은 "해소됨"으로 갱신 완료, 이 결정을
설명하는 문서화 숙제("왜 thin wrapper를 안 주는가", "권장 이벤트 핸들링
패턴")는 `research/documentation-plan.md` 3번으로 신설(다른 두 항목과
동일하게 아직 백로그 뼈대만).
**2. rbvm GC 패턴 — "실물 검증됨" 근거 보강.** 사용자가 "GC 처리를 봐야
한다면 rbvm을 확인하라, 실제 프로덕션에서 잘 돌아가는 걸 직접 확인한
모듈"이라고 언급 — 실제로 rbvm의 GC 패턴(weak table 4종, `Instance.
Destroying` 기반 gcHold 클로저, 네임스페이스 Dispose 훅 등)은 이미
`base/lifecycle-pattern.md`에 파일:라인까지 인용하며 상세 반영돼 있었지만
"사용자가 직접 실행해서 확인했다"는 신뢰도 근거는 빠져있어서 그 문단을
추가함(사람이 짠 코드라 100% 무결 보장은 아님 — 이미 발견된 버그 2건도
근거로 같이 인용, 규범이 아니라 참고용 비교 대상이라는 톤 유지).
**3. `.claude/` 코퍼스 전체 정리 패스.** 이전 세션들에서 쌓인 stale
참조/모순을 서브에이전트로 전수 감사 후 수정 — `modifier-plan.md`/
`architecture.md``research/component-composition-plan.md` 참조를
승격된 `base/` 경로로 갱신, `comparison-fusion-vide.md`의 낡은 "Vide식
암묵적 추적 vs Fusion식 명시적 축, quad는 미정" 서술을 실제 확정 사실
(`bind-system-plan.md`의 `:With`+`:Compute` 명시적 모델 채택)로 정정,
`tween-plan.md`의 끊긴 절 참조 수정, `documentation-plan.md`의 인용
오류 정정. `module-lifecycle-plan.md`가 스스로 "question.md에도 취합"
표시해뒀지만 누락돼 있던 "프로바이더" 이름(provider/processor/plug)
미정 항목도 `question.md`에 추가함. 여러 문서에 흩어져 있던 진짜 열린
설계 질문들(Slot 형제 순서 보장, Attribute 타입 파라미터화, UI shorthand
이름 등)은 전부 `.claude/question.md`에 이미 반영되어 있음을 재확인만
하고 임의로 결정하지 않음 — **이 파일이 여전히 "지금 열려있는 것"의
단일 소스.**
**4. Store `:Emit`, `:Compute``previous` 인자, `state:Observer(fn)`,
Ref 일반화 — 네 가지 다 확정, 실제 base 문서에 반영 완료.** 같은 세션에서
더 이어진 Store/Ref 설계 논의, 전부 `base/store-semantics.md`
`base/bind-system-plan.md`에 반영됨:
- **`Store:Emit(key)`** — Source 원천에 한해서만 허용(중간/파생 State엔
없음). 존재 이유는 clone 불가능한 userdata/엔진 객체가 우선(편의성은
부차적). `Get()`이 라이브 레퍼런스를 주므로 캐시해서 비교/diff하면
안 된다는 캐비엇 명시. Modifier는 정적 flatten이라 Store/State 경로에
아예 안 걸치므로 Emit과 충돌할 지점 자체가 없음(따름정리:
`Store<T>``T`는 Modifier가 될 수 없음) — `store-semantics.md`.
- **`:Compute(fn)`의 선택적 두 번째 인자 `previous`** — Compute 결과
자체가 무거운 userdata인 경우(예: 큰 locale 테이블 → Roblox
`LocalizationTable` 변환) 재생성 대신 이전 결과를 재사용/patch하는
용도, opt-in. `previous`는 "정확히 한 단계 전"이 보장 안 되므로 반드시
full diff로 다뤄야 함(React reconciler와 같은 모양). **핵심 캐비엇**:
이 패턴은 결과 State가 계속 능동적으로 관측(정상 prop 바인딩 또는
`state:Observer(fn)`+명시적 `Get()`)되지 않으면 mutate 로직 자체가
다시 실행 안 되어 조용히 영구 정지함 — `bind-system-plan.md`.
- **`state:Observer(fn)`** — 무효화 신호만 주고 값은 안 줌, `fn` 안에서
명시적으로 `Get()` 해야 실제 값을 얻음(기존 "emit은 저렴한 무효화
신호" 원칙 재사용). 반환값 자체가 `CreatedRef`처럼 children 배열에
바로 놓는 leaf 값(별도 `ObserverHolder` 래퍼 불필요, 사용자가 직접
단순화) — 그 leaf가 살아있는 동안만 구독 유지, `canExecute`로 게이팅.
`fn` 생략 시 "이 State를 그냥 계속 능동 관측 상태로 유지"하는 유틸로
씀(위 `previous` 캐비엇의 해결 도구). 구현은 값 내부가 아니라 외부
weak table로 살아있는 Observer를 추적하는 방식 권장(rbvm
`getNamespaceOf`류 선례) — `bind-system-plan.md`.
- **Ref 일반화** — "quad가 만든 instance 전용"에서 "아무 사용자 값이나
담는 범용 값 박스"로 확장(object-ref/function-ref 안 나눔, React
`useRef`가 선례). `.Value` + `:Wait()`(coroutine 컨텍스트용) + 콜백
등록(복수 허용, 이미 채워져 있으면 즉시 1회 호출) — 이걸로 "코루틴
기반 대기 지원 미정"이던 항목 해소. `CreatedRef`는 이 위에 얹힌 특수
편의 패턴으로 재정리, 상충 없음. **one-shot 여부도 해소됨 — 반복
재설정 가능으로 확정**(React의 자식 재생성 시 ref 재사용 패턴이 선례,
라벨 컨테이너 재사용 예시로 확인). 콜백은 발화 후에도 안 소진되고
`:Set()`마다 다시 불림 — 소진되는 건 `:Wait()`의 개별 대기자뿐.
**Ref는 의도적으로 lazy가 아니고 `:Compute` 파생도 지원 안 함**
State와의 이 차이가 중요(예전에 Store가 Ref 역할도 겸했다가 lazy
모델과 섞여서 안 좋았던 경험에서 나온 의도적 분리). Ref 정의 자체가
넓어졌으니 용어 정리 때 이름도 같이 재검토 대상. `question.md`
관련 항목은 해소됨으로 갱신.