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

View file

@ -167,6 +167,115 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
- **콜백 호출 시점 확정**: 생성 직후(construction 시점)와 트리 마운트 후(Parent - **콜백 호출 시점 확정**: 생성 직후(construction 시점)와 트리 마운트 후(Parent
세팅 완료) 둘 다 지원 — 옵션으로 선택(`CreatedRef(fn, {phase="created"| 세팅 완료) 둘 다 지원 — 옵션으로 선택(`CreatedRef(fn, {phase="created"|
"mounted"})`류, 정확한 API 이름은 구현 단계에서 확정). "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`, 포지셔널 인자 지양 ## 여러 Store 값을 묶어 파생값 만들기 — `:With` + `:Compute`, 포지셔널 인자 지양
@ -185,6 +294,97 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/ lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/
`:Compute`" 부분 참고). `: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 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨 ## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨
**배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐. **배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
@ -353,7 +553,9 @@ Observable/Observer)을 조사한 결과, 두 지점에서 기존 확정과 실
방식이면, 나중에 GC만으로 정말 부족한 케이스가 생겨도 그 connection을 얻어 방식이면, 나중에 GC만으로 정말 부족한 케이스가 생겨도 그 connection을 얻어
`disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게 가능한 디자인 — `disconnect()`하는 명시적 dispose 경로를 추가로 얹는 게 가능한 디자인 —
지금 마일스톤에서는 필요 없어서 안 함(사용자: "필요하다면 dispose 핸들러를 지금 마일스톤에서는 필요 없어서 안 함(사용자: "필요하다면 dispose 핸들러를
만들어주는 것도 가능한 디자인, 다만 지금까지 요구가 없었음"). 만들어주는 것도 가능한 디자인, 다만 지금까지 요구가 없었음"). (rbvm의
GC-native 패턴이 실물에서 검증됐다는 근거는 `base/lifecycle-pattern.md` 상단
참고 메모 참고.)
## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지 ## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,8 @@
(2026-08-06) 도중 파생된 두 아이디어를 별도로 트래킹할 가치가 있다고 (2026-08-06) 도중 파생된 두 아이디어를 별도로 트래킹할 가치가 있다고
판단해 뼈대만 기록해둠. 세부 설계는 없음, 착수 시점도 미정 — 사용자가 판단해 뼈대만 기록해둠. 세부 설계는 없음, 착수 시점도 미정 — 사용자가
"간단히 plan 작성은 하는 게 맞을 듯, 아주 뼈대만 있는 것이라 할지라도 "간단히 plan 작성은 하는 게 맞을 듯, 아주 뼈대만 있는 것이라 할지라도
적어줄 필요는 있겠다"고 판단해서 만든 문서. 적어줄 필요는 있겠다"고 판단해서 만든 문서. 같은 날 후속 세션에서
이벤트 핸들링 self 관습 논의가 확정되며 3번 항목이 추가됨.
## 1. UI 요소 네이밍 컨벤션 문서 ## 1. UI 요소 네이밍 컨벤션 문서
@ -18,8 +19,12 @@ Roblox가 Play 중 라이브 UI 편집 도구를 꺼버려서 실제 화면의 U
**뼈대(아직 설계 아님, 물음표만)**: **뼈대(아직 설계 아님, 물음표만)**:
- 어떤 단위에 이름을 붙이게 유도할까 — 모든 Frame? 컴포넌트 루트만? - 어떤 단위에 이름을 붙이게 유도할까 — 모든 Frame? 컴포넌트 루트만?
- 자동 이름 부여 옵션이 있으면 좋을까(예: 컴포넌트 함수 이름을 기본 - 자동 이름 부여 옵션이 있으면 좋을까(예: 컴포넌트 함수 이름을 기본
`Name`으로 쓰는 관례 — 있으면 편하지만 매직에 가까워 `architecture.md` `Name`으로 쓰는 관례 — 있으면 편하지만 매직에 가까워 "마법 안 쓴다" 원칙과
2번의 "마법 안 쓴다" 원칙과 긴장 있을 수 있음, 검토 필요). 긴장 있을 수 있음, 검토 필요).[정정: 인용 위치 정정 —
`architecture.md` 2번(함수형 vs OOP 논의)이 아니라
`base/component-composition-plan.md` §1("컴포넌트 = 그냥 함수, '자기
store 자동 소유' 매직은 폐기")의 **사용자 확정** "마법 안쓴다 그것도
동의함" 발언이 근거임]
- 어디에 문서화할까 — 퀵스타트 튜토리얼? 별도 스타일 가이드? 린트 규칙으로 - 어디에 문서화할까 — 퀵스타트 튜토리얼? 별도 스타일 가이드? 린트 규칙으로
강제할까(과한 선택지, 참고만)? 강제할까(과한 선택지, 참고만)?
@ -50,9 +55,32 @@ UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_`나 `QUAD_` 같
문서에 합칠지 분리할지는 미정. 문서에 합칠지 분리할지는 미정.
- 안티패턴 vs 권장 패턴 대조 형식이 적당할지, 다른 형식이 나을지도 미정. - 안티패턴 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`에 정식 백로그 항목으로 올릴지, - 이걸 `.claude/question.md`/`CLAUDE.md`에 정식 백로그 항목으로 올릴지,
아니면 이 파일 하나로 충분한지. 아니면 이 파일 하나로 충분한지.

View file

@ -27,6 +27,12 @@ Default를 넣으면 더 어려워짐 — Default로 쓰다가 실제 쓰인 값
복사(clone) 모드를 켜야 하나 — 근데 그러면 너무 복잡해질 것 같다는 우려까지만 복사(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` "트윈은 어떻게 할 것이냐" / "스토어 값은 `.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은
항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이 항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이
반응 그래프 안에 있는 설계는 명시적 반면교사 — `base/comparison-fusion-vide.md` 반응 그래프 안에 있는 설계는 명시적 반면교사 — [정정: 절 제목이 실제와
"Animation(Tween/Spring) integration — 경고 신호" 절 참고. 달랐음] `base/comparison-fusion-vide.md`의 "Fusion" 절 마지막 불릿
("Tween/Spring이 State그래프 안의 1급 노드") 참고.
## 확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다 ## 확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다

105
CLAUDE.md
View file

@ -120,9 +120,10 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
`research/debug-tooling-plan.md`)도 2026-08-06 세션에서 설계 수렴 — `research/debug-tooling-plan.md`)도 2026-08-06 세션에서 설계 수렴 —
채널 실현 가능성(Studio 플러그인↔Play 중 게임 간 BindableEvent/Function 채널 실현 가능성(Studio 플러그인↔Play 중 게임 간 BindableEvent/Function
통신)까지 실측 검증 완료, 세부 API 이름만 남음. 착수 시점은 여전히 통신)까지 실측 검증 완료, 세부 API 이름만 남음. 착수 시점은 여전히
"quad 개발 상당 부분 끝난 뒤"로 사용자가 못박음. 같은 세션에서 파생된 "quad 개발 상당 부분 끝난 뒤"로 사용자가 못박음. 같은 날 파생된 문서화
문서화 전략 뼈대(`research/documentation-plan.md`, UI 네이밍 컨벤션 + 전략 뼈대(`research/documentation-plan.md` — UI 네이밍 컨벤션, Store
Store 부작용 게임 시스템 활용 패턴)도 후순위 백로그로 같이 남김. 부작용 게임 시스템 활용 패턴, 권장 이벤트 핸들링 패턴 3종)도 후순위
백로그로 같이 남김.
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목). (`HUMAN_TODO.md` 2번 항목).
@ -234,11 +235,8 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
UI 네이밍 컨벤션 문서, 스킬/쿨타임/재화 같은 게임 시스템에서 Store의 UI 네이밍 컨벤션 문서, 스킬/쿨타임/재화 같은 게임 시스템에서 Store의
부작용 허용을 깔끔한 패턴으로 쓰는 법 문서 — 를 `research/ 부작용 허용을 깔끔한 패턴으로 쓰는 법 문서 — 를 `research/
documentation-plan.md`에 뼈대만 분리해서 남김(위 "지금 할 일" 4번). documentation-plan.md`에 뼈대만 분리해서 남김(위 "지금 할 일" 4번).
- **아직 확인 안 된 것 하나**: 사용자가 "이벤트 함수들이 실제 instance를 - **이벤트 self 관습 확인 필요했던 항목 — 같은 날 후속 세션에서 해소됨.**
읽을 수 있게 self를 건네받는 게 quad의 관습"이라고 언급했으나 본인도 아래 "2026-08-06 후속 세션" 절 참고.
"문서화됐는지 모르겠다"고 함 — 어떤 함수(이벤트 핸들러? Compute?
`describe` 훅?)에 정확히 어떤 시그니처로 적용되는지 다음 세션에서 확인
필요(`debug-tooling-plan.md` "열린 질문" 참고), 추측성 반영은 안 해둠.
**같은 세션 후반, 별개 주제 두 개 추가**(quad-debug와 무관, 사용자가 **같은 세션 후반, 별개 주제 두 개 추가**(quad-debug와 무관, 사용자가
"적어는 뒀는데 안 줬는건가" 하며 새로 떠올린 것들): "적어는 뒀는데 안 줬는건가" 하며 새로 떠올린 것들):
@ -275,3 +273,94 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
상호작용면(자기 트리 뷰/리프 클릭→상세 패널/실제 Explorer 선택과 연동, 상호작용면(자기 트리 뷰/리프 클릭→상세 패널/실제 Explorer 선택과 연동,
Explorer와 플러그인 트리는 별도 도킹 위젯)으로 구성된다는 것도 사용자 Explorer와 플러그인 트리는 별도 도킹 위젯)으로 구성된다는 것도 사용자
질문에 확인 응답 — `debug-tooling-plan.md` "핵심 설계 방향" 9번. 질문에 확인 응답 — `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`
관련 항목은 해소됨으로 갱신.