이벤트 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:
parent
bc0a8b9f5f
commit
936e0766b7
12 changed files with 440 additions and 34 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지
|
||||
|
||||
|
|
|
|||
|
|
@ -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의
|
||||
차별점이라는 근거로 재사용 가능.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
**상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(`retract`) 전략의 원본.
|
||||
완료 개념 없음, 구현하면서 세부 조정 있을 수 있음.
|
||||
|
||||
**참고(2026-08-06)**: rbvm은 사용자가 직접 실행해 실제로 잘 동작하는 것을 확인한
|
||||
코드베이스(프로덕션에서 검증됨) — 사람이 짠 코드라 무결성이 100% 보장되는 건
|
||||
아니지만(실제로 아래 5번 항목에서 `Connected` 체크 방향 반전 버그, `__mode` 오타
|
||||
버그가 발견됨), 다른 리서치 참고 레포보다 "실물로 돌아간다"는 근거가 확실한
|
||||
비교 대상. 단 이건 참고용 비교 대상일 뿐 quad-v2가 반드시 따라야 할 규범은
|
||||
아님 — 실제 채택 여부는 이 문서 각 절의 확정 내용(특히 아래 "확정" 두 절)을
|
||||
따를 것, rbvm 코드를 그대로 베끼라는 뜻이 아님.
|
||||
|
||||
## 배경
|
||||
|
||||
`raw-userinput.md`(이하 사용자 원 메모): "레지스터의 실행은 내 생각엔 그냥 Destroy
|
||||
|
|
|
|||
|
|
@ -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로
|
||||
전달, "다중 루트" 개념 자체는 폐기) — 더 이상 열린 질문 아님, 이 문서가
|
||||
다루는 "값 자체의 동작"과는 별개 문제였다는 점만 참고로 남김.
|
||||
|
|
|
|||
|
|
@ -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 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
|
||||
|
|
|
|||
|
|
@ -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`(뼈대만). 정식 백로그 항목으로
|
||||
올릴지, 착수 시점을 언제로 볼지 사용자 판단 필요.
|
||||
|
|
|
|||
|
|
@ -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 이름 (후순위, 구현 착수 시점에 자연히 정리)**
|
||||
|
||||
|
|
|
|||
|
|
@ -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`에 정식 백로그 항목으로 올릴지,
|
||||
아니면 이 파일 하나로 충분한지.
|
||||
|
|
|
|||
|
|
@ -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"이라 층위가
|
||||
다름, 새로 설계하지 않고 참고 링크만.
|
||||
|
||||
## 사용자 확인 결과: 진짜로 모르겠음 — 열린 가능성으로 유지
|
||||
|
||||
**사용자 확인 완료, 그러나 결론은 "미정 유지".** 실제로 이 기능을 원한다고
|
||||
|
|
|
|||
|
|
@ -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
105
CLAUDE.md
|
|
@ -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`의
|
||||
관련 항목은 해소됨으로 갱신.
|
||||
|
|
|
|||
Loading…
Reference in a new issue