로드맵 인수인계 라운드(2026-08-04) 결과 반영 — ROADMAP.md 신설, 설계 단계 종료

component-composition-plan을 research/에서 base/로 승격하고, 구현 착수 전
리스크 감사에서 나온 M0 스파이크 항목(Store/State propagation, dispatch,
컴포넌트 경계 named-parameter 전달)을 문서에 반영. quad-base 테스트 mock
방향(Vide 선례 채택) 확정.
This commit is contained in:
qwreey 2026-08-04 17:11:23 +09:00
parent c19e82f661
commit 470f188d0f
Signed by: qwreey
GPG key ID: D28DB79297A214BD
9 changed files with 532 additions and 167 deletions

View file

@ -157,6 +157,36 @@ quad/
Tween/purity/existing-instance-bind는 여전히 `research/`에 남아있고 이
구조 확정을 막지 않음.
## 테스트 전략: quad-base용 최소 mock (2026-08-04)
**결정**: quad-base 테스트는 Vide 선례(`initreq/vide/test/mock.luau`, 약
300줄)를 따라 최소한의 mock으로 감 — parent/children 트리 + 타입 검증 없는
property bag + property별 변경 시그널 정도만 흉내내고, `IsA()`/클래스별
프로퍼티 스키마/`WaitForChild`/`DataModel` 같은 건 안 만듦. 순수 `luau` CLI로
Studio/엔진 없이 테스트(Vide가 실제로 이렇게 CI에 물려놓음) — Fusion처럼
Studio 안에서만 도는 방식은 채택 안 함. 근거: quad-base 코어(Store/State/
Source/Modifier/Slot, 디스패치 엔진)는 이미 `inst``any`로 취급하고
Instance 특정 동작을 전혀 참조하지 않도록 설계돼 있어(`bind-system-plan.md`
"inst가 항상 Roblox Instance일 필요는 없음" 절), mock이 실제 Roblox 충실도를
가질 이유가 없음.
**스코프는 "정적 디버깅"으로 한정** — **사용자 확정**: mock으로 확인하려는
건 한 시점의 렌더 결과(정적 스냅샷)지, 시간에 따라 변하는 동적 동작(Tween
애니메이션, 타이밍 등)이 아님. 그래서 지금 단계 mock엔 시간 기반 핸들러를
흉내낼 계획이 없음.
**"quad-roblox로 작성한 컴포넌트가 mock에서도 그대로 돌아가야 한다"는 요구는
없음** — **사용자 확정**("이건 꼭 지켜질 필요까지 있진 않아, 단순하게 가도
됨"). mock은 quad-roblox의 실제 핸들러(ReflectionService 기반 이벤트 판별,
CollectionService 태그 등)를 흉내낼 필요가 없고, quad-base 자체 로직(디스패치
엔진, Store/State/Source, Modifier, Slot)만 검증하면 충분 — quad-roblox
개발과는 무관해도 됨.
**백로그**: 나중에 범용 렌더 결과 디버깅 도구로 키우고 싶어지면(정적
스냅샷을 넘어 Tween mock 같은 동적 동작까지 포함) 그때 스코프를 넓히는
걸로 — 지금은 quad-base 테스트 전용 최소 mock까지만(`CLAUDE.md` 백로그
참고).
## Store/State/Source 온톨로지 — 확정됨 (요약)
Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할
@ -171,8 +201,9 @@ Store와 별개인 가벼운 `Source` 프리미티브를 씀. `store.key` dot-ac
## 아직 미정 (research/로 분리됨)
Tween 플러깅, 이미 생성된 인스턴스에 대한 바인드, 컴포넌트가 modifier/Ref를
경계 너머로 어떻게 전달하는지 — `.claude/research/` 각 문서 참고, 전체 색인은
`.claude/README.md`. 바인드 디스패치/Slot/모듈 라이프사이클은 위 "구현 착수"
섹션대로 확정되어 `.claude/base/`로 승격됨(`bind-system-plan.md`/
`module-lifecycle-plan.md`/`slot-plan.md`).
Tween 플러깅, 이미 생성된 인스턴스에 대한 바인드 — `.claude/research/`
문서 참고, 전체 색인은 `.claude/README.md`. 바인드 디스패치/Slot/모듈
라이프사이클/Modifier/컴포넌트화(컴포넌트 경계 modifier/Ref 전달 포함)는
위 "구현 착수" 섹션대로 확정되어 `.claude/base/`로 승격됨
(`bind-system-plan.md`/`module-lifecycle-plan.md`/`slot-plan.md`/
`modifier-plan.md`/`component-composition-plan.md`).

View file

@ -99,6 +99,16 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
- **다른 값 변경을 추적하는 것도 process 함수의 정상 범위**: 예를 들어 Slot
핸들러는 자기가 감시하는 값(배열/스토어)이 바뀌면 그에 따라 child를
갱신해야 함 — `retract` 시점엔 그 추적(구독)만 풀면 됨.
- **일반적인 무한루프 방어(사이클 감지 등)는 하지 않기로 확정(2026-08-04,
로드맵 인수인계 라운드)**: 우선순위 스캔+재귀 `process` 구조 자체는 핸들러가
규율을 안 지키면(예: 값을 좁히지/변형하지 않고 같은 값을 그대로 다시
`process`에 넘김) 무한루프에 빠질 수 있음 — 하지만 이건 base가 방어 로직을
둬야 할 문제가 아니라 오작동하는 handler/provider(`quad-roblox` 등) 쪽
버그로 간주 — **사용자 확정**("입력된 값이 다시 입력되면 무한루프
빠지겠지만, 그건 막기 힘들고 유저가 내기도 힘들어. 아예 quad-roblox나
프로바이더가 잘못 짠 코드일테니까"). Tween의 store-bind 재귀 케이스(위
78-79행)처럼 자연히 좁혀지는 경우가 일반적이고, 일반 사용자가 만들어낼 수
있는 상황이 아니라고 판단해 별도 가드 없이 진행.
## Store 바인드는 특수 경우인가, 아니면 pluggable 바인드를 재실행하는 래핑인가

View file

@ -0,0 +1,259 @@
# 컴포넌트화 (Roblox 기본 오브젝트 이외의 사용자 정의 컴포넌트)
**상태**: base — 2026-08-04 세션(6차 라운드 이후) 채팅 논의로 핵심 골격 +
modifier/Ref 컴포넌트 경계 통과 문제까지 전부 확정. 사용자가 "지금 quad에서
가장 문제되는 부분"으로 직접 지목했던 주제였으나 이번 라운드에서 수렴 완료.
남은 건 API 이름뿐(아래 "남은 열린 질문" 참고). `base/bind-system-plan.md`
Store/State/Source 온톨로지가 먼저 확정된 뒤에야 이 논의가 열림 — 그
문서가 선행 컨텍스트.
## 문제
v1의 `Class.Extend()`(Init/Render/AfterRender/Getter/Setter/UpdateTriggers,
`base/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기
방향이지만, 그게 제공하던 실제 편의 기능(컴포넌트가 자기 store를 자동으로
가짐, props로 넘어온 State를 자동 흡수, `self:Default`/`self "key"`로
기본값·바인딩)까지 같이 버려도 되는지가 미결이었음. `MyComp {...}` 형태로
호출되는 사용자 정의 컴포넌트를 v2에서 어떤 모양으로 작성하게 할지가 핵심
질문.
## v1 실제 메커니즘 (조사 완료, `quad.qwreey.kr` 튜토리얼 + `initreq/quad/src/` 소스로 교차검증)
- `myStore "key"` → register(현재 State에 해당) 반환. `:Default(v)`/
`:With(fn)`/`:Add(v)`/`:Tween(opts)` 체이닝 가능(`store.lua:433-457`).
- `Class.Extend()``:Init(props)`에서 `props:Default("Size", v)`로 기본값
설정, props 테이블 자체가 store 인스턴스로 변신(`class.lua:365-379`,
`storeNew(prop,nil)`).
- props로 넘어온 값이 State(`quad_register`)면 `initStoreRegisterBinding`
(`store.lua:394-431`)이 자동으로 감지해 컴포넌트 자신의 store 키에 재귀
연결 — **자동 흡수 매직**이 실제로 존재했음.
- `self(name)` linker가 **두 가지 역할**을 겸함: (1) `self "_button"`
자식 자리에 넣으면 렌더링된 인스턴스를 `self._button`에 즉시 잡아둠(Ref
역할) (2) `[Event.Prop "Text"] = self "Text"`로 인스턴스 프로퍼티 변경을
다시 컴포넌트 store로 역방향 전파(양방향 바인딩, `EmitPropertyChangedSignal`
자동 연결과 동일) — quad.qwreey.kr 튜토리얼 `11_extend/` 문서 원문 확인.
이 두 역할이 v2 온톨로지에서는 이미 갈라져 있음: (1)은 확정된 **Ref**가
대체, (2)는 이번 논의에서 다루는 Source 양방향 프록시가 대체.
## 수렴된 결론
### 1. 컴포넌트 = 그냥 함수, "자기 store 자동 소유" 매직은 폐기
`MyComp = function(props) return Frame {...} end`, 호출 규약은
`Frame{...}`와 동일(`MyComp{...}` → `MyComp(propsTable)`). v1의 Extend
자동-store-생성+자동-흡수 매직은 재현하지 않음 — 대신 React식으로 호출부가
State/raw/Source/콜백 중 뭘 넘길지 명시적으로 고름. 이유: 자동 흡수는
매 컴포넌트 호출마다 "이 prop이 State인가?" 타입 분기를 프레임워크가
암묵적으로 수행해야 하는 매직이고, 명시적 전달이 더 단순·예측 가능(React가
Vue/Svelte 대비 내세우는 강점과 동일 논리) — **사용자 확정**("마법 안쓴다
그것도 동의함").
### 2. State/Source 경계 규칙: 파생이면 읽기전용, 원본이면 쓰기 가능
State는 `:With`/`:Compute`로 만들어진 파생값일 수 있어 쓰기가 정의 자체가
안 됨. Source(독립이든 Store 소속 `StoreSource` 프록시든)는 파생이 아니라
항상 원본 슬롯 하나를 직접 가리키므로 쓰기가 의미 있음 — **사용자 확정**
("맞음. 확실해").
### 3. `StoreSource`: Source를 인터페이스+구현체로 두고, Store 키에서 그 인터페이스를 구현하는 얇은 프록시를 받음
- **Source = 인터페이스이자 구현체**: 독립 생성자 `Source(initial)`가 기본
구현체, `store:GetSource("key")`(가칭)류 접근자가 반환하는 값은 같은
인터페이스를 구현하는 별도의 얇은 프록시(`StoreSource`) — 읽기는
`store.key`로, 쓰기는 `store.key = v`로 위임. **내부 Source 객체를 그대로
노출하지 않음** — 그러면 "쓰기는 오직 Store의 `__newindex`뿐"이라는 기존
확정과 새 쓰기 경로가 충돌하게 됨.
- **캐시 안 함**: State가 이미 "매번 새로 만듦, store에 캐시 안 됨"으로
확정돼 있어 일관성 + 엔지니어링 비용 둘 다 이쪽이 쌈 — **사용자 확정**
("그냥 엔지니어링적으로 비용이 싼거 택해").
### 4. Source 직접 전달(양방향)은 핸들러 계약 확장 없이 타입 유니온으로 처리 — 단, 실사용 범위는 좁음
- 핸들러가 값을 받을 때 `Source<T> | State<T>` 유니온으로 받고, 내부에서
타입 체크만 하면 됨(Source면 인스턴스 변경 이벤트에 걸어 역방향 쓰기까지
처리, State면 읽기만) — `isHandlable`/`priority`/`process`/`retract` 4종
계약에 5번째 항목을 추가할 필요 없음. Source 자체가 계산이 없는 원천이라
가능한 단순화 — **사용자 확정**("그냥 타입 상 source를 받거나 state를
받거나 하면 됨. source 자체는 원천이라 컴퓨팅 같은거 없어").
- **하지만 실사용은 좁을 것으로 예상**: `isEnabled`처럼 여러 조건에 영향
받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못 넘김.
즉 Source 직접 전달이 통하는 건 진짜 단순한 1:1 원본-토글 케이스뿐이고,
일반적인 경우엔 React식 `value(State) + onChange(callback)` 패턴이 기본
**사용자 확정**("isenabled가 여러 조건에 영향 받으면 바로 문제가
생기는거지. 따라서 실제 사용은 제한적일듯. callback을 쓰는게
일반적이여 보이긴 해. 타입으로도 편하기도 하고 디버깅도 편함").
### 5. 리프(Roblox 프로퍼티) 바인딩엔 원칙적으로 State만
계산된 최종값만 실제로 인스턴스에 반영되어야 하므로, 리프 바인딩은 State가
일반 경로. Source는 리프 바인딩용 프리미티브가 아니라, 아주 단순한 구조에서
콜백 보일러플레이트를 줄이기 위한 좁은 용도의 예외 — **사용자 확정**
("리프 바인딩엔 state만 쓰이지 않을까... source는 그냥 아주 단순한
구조에서 콜백을 넣고 하는 복잡함을 줄이기 위함일 뿐임").
## 프레임워크 사례 조사 (2026-08-04, modifier/Ref 경계 통과 문제 관련)
병렬 리서치로 4개 소스(Compose 공식 문서, Fusion/Vide 소스, quad v1 +
PA artworks)를 확인. **결론: 조사한 어떤 선례도 "컴포넌트 경계에서 modifier/Ref
전달" 문제를 완전히 풀어놓지 않음** — 심지어 quad가 이미 많이 참고한 Fusion도
multi-root를 지원은 하지만 그 상태에서 외부 ref/props를 특정 root에 연결하는
관례는 자체 문서에도 없음.
### Compose의 Modifier는 애초에 flat property bag이 아님 — 순서 의존적 wrapper 체인
`modifier-plan.md`가 이미 확정한 "필드 단위 flatten, 나중 게 이김" 모델과
Compose의 실제 메커니즘은 근본적으로 다른 종류임. Compose `Modifier`
`CombinedModifier`(2-노드 연결 리스트)로 순서대로 이어붙는 wrapper 체인 —
`Modifier.padding(16.dp).clickable(onClick)` vs
`Modifier.clickable(onClick).padding(16.dp)`가 실제로 다르게 동작함(패딩
영역이 클릭 가능한지 여부가 순서에 따라 갈림, 공식 문서 예시).
`.then()`/`+`는 "같은 프로퍼티면 덮어쓰기"가 아니라 순수 **연결(concatenation)**.
→ **quad의 "필드명 기준 last-wins" 모델은 Compose를 그대로 벤치마킹한 게
아니라 독자 설계임을 확인** — Compose와의 유사성은 "관례로 경계를 넘긴다"는
아이디어 수준에서만 성립, merge 의미론까지 가져올 근거는 아님.
공식 API 가이드라인(`compose-api-guidelines.md`,
`compose-component-api-guidelines.md`, `mrmans0n/compose-rules` 린트)이
명시하는 규칙:
- `modifier` 파라미터는 이름 고정, 타입 `Modifier`, 기본값 `Modifier`, 첫 번째
optional 파라미터여야 함.
- 받은 modifier는 컴포저블이 만드는 루트 레이아웃 노드에 **체인의 맨 앞**에
적용, 필요하면 뒤쪽에 이어붙이는 것만 허용(앞에 붙이는 것 금지).
- 같은 modifier 인스턴스를 여러 노드에 나눠 쓰지 말 것(단일 소비 전제).
**Multi-root(루트가 여럿인 컴포저블)에 대한 공식 답은 없음** — 오히려
가이드라인은 `CheckboxRow(rowModifier, checkboxModifier)`처럼 파트별
modifier 파라미터를 두는 패턴을 명시적으로 **반례(DON'T)**로 제시하며
"modifier는 컴포넌트 자체의 외부 동작을 위한 것이지 하위 파츠용이 아님,
대신 슬롯(자식 컴포저블 람다)으로 만들어라"라고 함. 즉 Compose는 이 문제를
**풀지 않고 애초에 안 생기게 architecture로 피함**(multi-root 자체를 권장하지
않고 slot 패턴으로 유도).
### Fusion — modifier 개념 자체가 없음, multi-root는 있지만 ref 전달 관례 없음
- `merge.luau`(`src/Utility/merge.luau:13-33`)는 scope 메소드 테이블 병합용이지
props 병합용이 아님 — quad Modifier에 대응하는 게 Fusion엔 없음.
- prop 전달 관례는 전부 **named table**(`props.Layout.Size`,
`props[Children]`) — 배열 아이템으로 뭔가를 넘기는 관례 자체가 없음.
Children도 예약된 `[Children]` 키로 감, 포지셔널 아님.
- `New()`/`Hydrate()`는 raw Instance 리턴(quad와 동일 지점).
- **Multi-root 컴포넌트는 실제로 지원**(`docs/tutorials/best-practices/
instance-handling.md:17-61` — "Instance 배열 리턴, 여러 값 리턴 대신 배열로
감싸라"), 하지만 **외부에서 넘어온 ref/props를 그중 특정 root에 연결하는
예시나 관례는 문서에 없음** — quad가 지금 맞닥뜨린 것과 완전히 같은 질문이
Fusion 자체 문서에서도 답이 안 나가 있음.
### Vide — modifier도, 배열 기반 전달 관례도, multi-root 사례도 전무
`src/`/`docs/` 전체에 `modifier`/`merge`/`combine`/`spread` 매칭 0건. 모든
컴포넌트 예제가 named+typed `props` 테이블을 필드별로 직접 옮겨씀. 인스턴스
생성자는 raw Instance 리턴. multi-root 예제/개념 자체가 문서에 존재하지 않음.
### quad v1 — 배열 아이템 구분은 항상 런타임 `__type` 태그로 함
`ProcessQuadProperty`(`class.lua:134-213`)는 배열 위치의 모든 아이템을
`__type`으로 검사(`quad_linker`/`quad_register`/`quad_style` 중 하나면 그
용도로, 아니면 무조건 자식으로 마운트) — v2의 "리프 레벨에서 타입으로
Modifier/Ref/자식을 구분"이 이미 v1의 유일한 해법이었던 패턴 그대로임을
확인. 단 v1도 multi-root 사례가 전혀 없어서, "컴포넌트가 여러 루트를 반환할
때"는 v1도 답을 준 적이 없음. PA artworks에도 컴포넌트 추상화/multi-root
사례 없음(재사용 가능한 컴포넌트 함수 자체가 아직 코드로 존재하지 않음).
### 종합
| | Modifier-equiv 있음? | 전달 관례 | multi-root 지원 | multi-root 시 ref/modifier 전달 관례 |
|---|---|---|---|---|
| Compose | O(순서의존 체인) | named 파라미터 강제(린트) | 사실상 비권장, slot으로 유도 | 없음(애초에 안 만듦) |
| Fusion | X | named table | O(배열 리턴) | **없음(미해결로 확인)** |
| Vide | X | named table | 사례 없음 | 해당 없음 |
| quad v1 | X(런타임 태그로 대체) | 태그 기반 배열 아이템 | 없음 | 없음 |
시사점: (1) "배열 아이템을 타입으로 구분"은 quad v1 고유 패턴이자 quad-v2
리프 레벨이 이미 계승한 것 — 그런데 이 문서 위쪽에서 지적했듯 컴포넌트
함수 경계에서는 타입 스니핑을 자동으로 해줄 디스패처가 없어서 저작자가 직접
루프를 돌려야 함(v1도 이 경계에서 실제로 쓰인 적이 없어 검증 안 된 채로
남음). (2) named-key 전달(Fusion/Vide/Compose 공통)이 "함수 호출만으로
경계를 넘는" 상황에서 유일하게 실제로 쓰이고 있는 관례. (3) multi-root +
외부 ref/modifier 전달은 **조사한 4개 선례 중 어느 것도 실제로 풀어놓지
않음** — Compose는 회피, Fusion은 미해결로 방치, Vide/v1은 애초에 안 함.
즉 이 지점은 quad가 진짜 새로 설계해야 하는 부분이지, 어딘가에 있는 답을
못 찾은 게 아님.
## 최종 결론: 컴포넌트 경계 modifier/Ref 전달 (2026-08-04, 확정)
### 1. Named parameter로 경계를 넘김 — 리프 레벨과는 다른 계약
컴포넌트 함수(`function(props) return Frame{...} end`)는 `Frame{...}`처럼
배열 아이템 + 런타임 타입 스니핑으로 modifier/Ref를 받지 않음 — 함수 호출로
경계를 넘는 순간부터는 자동으로 타입을 스니핑해줄 디스패처가 없기 때문(리프
레벨의 `ProcessQuadProperty`류 디스패치는 `Frame{...}` 호출 내부에서만
동작하고 컴포넌트 함수 몸통엔 적용되지 않음). 대신 caller는 named key(가칭
`props.Modifier`/`props.Ref`)로 넘기고, 컴포넌트 저작자가 자기 코드 안에서
명시적으로 원하는 내부 `Frame{...}` 호출의 배열 자리에 다시 꽂아넣음
(`return Frame { props.Modifier, props.Ref, ... }`) — **사용자 확정**
("결과적으로 함수 구현에선 타입을 멀쩡히 지정하는게 더 중요하니 네임드가
맞는듯"). Compose(named `modifier` 파라미터 강제, 린트로 감시)와
Fusion/Vide(named prop 전달, `[Children]`류 예약 키)가 서로 다른 이유로 전부
같은 결론에 도달한 유일한 실용적 패턴 — quad가 발명한 게 아니라 선례가
수렴하는 지점(위 "프레임워크 사례 조사" 절 참고).
### 2. "다중 루트로 반환" 자체를 컴포넌트 개념에서 제거
기존에 "컴포넌트가 여러 루트를 반환하면 모호해짐"이라던 프레이밍이 서로
다른 두 가지를 하나로 섞은 것이었음이 드러나 재정리:
- **정적으로 고정된 여러 형제 Instance를 한 함수 호출이 그대로 반환**(React
Fragment류) — **불필요로 폐기**. 근거 셋: (1) Luau가 tail position 밖에서
다중 리턴을 지원 안 함 — `return a, b``Frame{ MyComp{...}, other }`처럼
배열 중간에 놓이는 순간 첫 값만 살아남으므로, 언어 차원에서 이 패턴이
애초에 자연스럽게 지원되지 않음(**사용자 확인**). (2) 필요하면 호출부에서
그냥 여러 컴포넌트를 나란히 쓰면 됨(`Frame{ IconA{...}, LabelB{...} }`) —
한 컴포넌트 호출이 몰래 여러 형제를 뿜어낼 이유가 없음. (3) 프레임워크
조사에서도 진짜 수요가 있어 제대로 지원된 사례가 없음(Fusion은 "된다"고만
하고 ref 연결 관례는 미해결로 방치, Compose는 아예 안 만들도록 가이드) —
어려워서 방치된 게 아니라 실제 수요가 없어서 아무도 안 만든 것 —
**사용자 확정**("그럴 필요가 있나 싶네... 애초에 다중 리턴이 될 이유가
없는듯").
- **컴포넌트가 Slot을 반환**(개수가 가변적인 자식 묶음을 부모의 형제 레벨에
래퍼 없이 그대로 펼침 — 예: `ItemList{items=state}``UIListLayout` 밑에서
래퍼 Frame 없이 `Header{}`/`Footer{}`와 같은 레벨로 항목들을 끼워넣는 경우)
**이미 있는 별개 메커니즘**(`base/slot-plan.md`), 새 설계 불필요. Slot은
단일 Instance 정체성이 없으므로, 이런 컴포넌트는 애초에 `Modifier`/`Ref`
파라미터를 선언하지 않으면 그만 — 타입 시그니처 자체가 "나는 단일 대상에게
적용할 modifier/Ref가 없다"를 표현. 별도 조율 메커니즘 불필요 — **사용자
확정**("불가능하진 않고 기술적으로도 충분히 되는 일... 엄청 집중해야할
일은 아니지 않을까").
이 정리로 원래의 "모호해지는 케이스"는 사라짐: 컴포넌트가 단일 root를 갖는
한 named parameter로 명확히 전달되고, 단일 root가 없는 컴포넌트(Slot 반환)는
애초에 그 파라미터를 안 받으므로 모호함이 생길 지점 자체가 없음. 반환값에
"사후적으로" 뭔가를 꽂아넣는다는 그림 자체가 틀렸던 것 — forwarding은 항상
컴포넌트가 반환하기 *전에*, 저작자 코드 안에서 일어나는 일이라 어느 root로
가야 하는지는 저작자가 자기 코드에 뭐라고 쓰느냐로 완전히 결정됨(자동 전파가
없기 때문에 성립하는 단순함).
### 3. 여러 modifier를 하나로 합치는 공개 유틸 필요 — `Modifier.Merge`(가칭)
caller가 named parameter 하나에 여러 modifier를 몰아넣고 싶을 때
(`Frame{modifier1, modifier2}`의 컴포넌트판)를 위해, 기존 flatten 규칙(배열
순서상 나중 것이 필드 단위로 이김, `modifier-plan.md` 2번)을 그대로 재사용하는
결합 함수를 공개 API로 노출: `Modifier.Merge(mod1, mod2, ...) -> Modifier`.
새 병합 규칙이 아니라 이미 확정된 flatten을 함수로 한 번 더 꺼내 쓸 수 있게
하는 것뿐 — **사용자 요청**("modifier를 합칠 방법도 존재한다면 좋을것
같아"). `MyComp { Modifier = Modifier.Merge(theme, override) }` → 컴포넌트
내부는 항상 이미 합쳐진 단일 값만 받으므로 컴포넌트 저작자가 배열 처리를
신경 쓸 필요 없음. Ref는 필드 충돌 개념이 없어 이 문제 자체가 없음(여러
Ref를 받으면 그냥 전부 실행하면 됨, `modifier-plan.md` §4-2) — 별도 결합
유틸 불필요.
## 남은 열린 질문 (`.claude/question.md`에도 취합, 전부 후순위 — 이름만 남음)
- **정확한 API 이름**: `Component`(플레인 함수 규약이라 별도 래퍼가 필요한지
자체도 불확실 — 아마 불필요), `GetSource` 계열 접근자 이름, `Source`
독립 생성자 이름, 컴포넌트 경계용 `props.Modifier`/`props.Ref` 필드명,
`Modifier.Merge` 함수명은 전부 가칭. `base/bind-system-plan.md`의 "남은
열린 질문" 절(정확한 함수/생성자 이름 미정)과 같은 급의 후순위 항목 —
구현 단계에서 다른 이름들과 함께 확정.
- **`quad2-try`는 확인 불필요로 재확인** — 진행이 중단된 상태라 이 논의와
무관.

View file

@ -136,6 +136,18 @@ Modifier가 immutable해야 하는 이유(변환마다 clone)와 State가 이미
명시적으로 쓰는 구조라 "암묵적 분기"가 애초에 존재하지 않음 — 새로 결정할
것 없음.
### 7. State가 Modifier를 값으로 담는 것은 UB — 타입으로 막을 것 (2026-08-04, 로드맵 인수인계 라운드)
Modifier "필드"가 State일 수 있는 것(4-1번)과는 별개로, **State 자체의
value가 Modifier인 경우**(예: `someState:With(fn)`이 Modifier를 반환)는
지원 대상이 아님 — Modifier는 "flatten해서 한 번 적용"이 전제인 정적 값인데,
State에 담기면 그 값이 반응형으로 바뀔 수 있다는 뜻이 되어 매번 재-flatten이
필요해지고, 이는 "정적 merge" 확정(1번)과 정면으로 충돌함 — **사용자
확정**("state 안에 modifier가 있으면 그건 끔찍히 힘들꺼야... 타입 상 받지
못하게 만들어야 할 수도 있고"). **UB로 확정, 가능하면 타입 시스템으로
아예 못 넣게 막을 것**(`State<Modifier>` 같은 조합을 타입 정의 단계에서
거부) — 런타임 가드가 아니라 타입 차단을 우선 검토.
## 열린 질문 (`.claude/question.md`에도 취합)
- Getter 정확한 이름/모양(`:Get(key)` vs dot-access 겸용) — 후순위, 구현

View file

@ -65,10 +65,27 @@ Slot은 하나의 instance 안에 여럿 존재할 수 있다. 전부 하나의
편하다는 방향. **기울어진 결론**: 별도 "Named Slot" 개념 없이, store나
파라미터로 넘기고 그게 그냥 ref처럼 바인드되는 모양.
**상태 확인(2026-08-04 문서 정리 시점)**: 이 방향은 아래 "열린 질문" 절이나
`.claude/question.md`의 확정 목록 어디에도 명시적으로 흡수된 흔적이 없음 —
아직 정식 확정 절차(`AskUserQuestion` 등)를 거치지 않은 것으로 보임. **열린
질문으로 유지**, `.claude/question.md`에도 반영 필요.
**확정(2026-08-04, 로드맵 인수인계 라운드)**: 위 방향 그대로 확정 — 별도
"Named Slot" 개념 없음, 슬롯 바인드 테이블을 store나 파라미터로 그냥 넘기면
ref처럼 바인드됨 — **사용자 확정**("A. 맞음. 리프노드에선 그렇게 마운트됨").
단, 이 확정은 "리프 레벨에서 슬롯 하나가 마운트되는 방식"에 한정 — 여러
Slot이 형제로 섞이는 경우의 순서 보장 문제는 별도로 열려있음, 바로 아래
참고.
### 여러 Slot이 섞일 때 순서 보장 — 열린 질문 (2026-08-04 신규)
`Frame { Slot1, 일반자식, Slot2 }`처럼 Slot과 Slot 사이에 다른 요소가 끼거나
Slot이 여럿 형제로 존재할 때, 최종 자식 순서가 저작 순서(위쪽 Slot의 요소가
항상 아래쪽 Slot의 요소보다 앞)를 안정적으로 지키는지는 아직 설계 안 됨 —
**사용자 확정**("순서도 따름, 만약 웹이라면 순서에 맞춰서 마운트 되어야 하고,
아래쪽 slot은 위쪽 slot의 요소보다 무조건 아래 있어야함... 이게 가능하냐는
생각해봐야할 이야기임"). Roblox 자체는 z-순서를 주로 `LayoutOrder`/`ZIndex`로
푸는 편이라 당장 급하게 막히는 지점은 아니지만, `architecture.md`가 명시한
"quad-base는 다른 렌더 백엔드(GTK, 웹 DOM 등)에서도 재사용 가능해야 한다"는
전제와 직결되는 문제 — DOM류 백엔드는 형제 순서 자체가 렌더 순서라 이 보장이
없으면 못 씀. **후순위 열린 질문으로 유지**, Slot 코어 로직 구현 시점에
다시 볼 것 — M0/M1 단계를 막지는 않음(Roblox 단일 백엔드로는 당장 문제
없음).
## Slot과 Store 바인드의 관계 (`retract` 순서)
@ -112,3 +129,8 @@ Slot은 바인딩되는 순간 그 안의 요소를 전부 own해버리는 데
- 재마운트 에러 처리(throw), retract 시 폐기(옮기지 않음) 둘 다 확정. 남은 건
실제 구현 단계에서 이 "폐기" 동작이 실사용에서 불편하지 않은지 재검증하는
정도 — 설계 방향 자체는 더 이상 열려있지 않음.
- "클래스가 슬롯을 받는 방법"(Named Slot 없음)도 확정됨(위 "클래스가 슬롯을
받는 방법" 절 참고).
- **여러 Slot이 형제로 섞일 때 순서 보장**은 아직 열려있음(위 "여러 Slot이
섞일 때 순서 보장" 절 참고) — Roblox 단일 백엔드로는 급하지 않음, Slot
코어 로직 구현 시점에 재검토.

View file

@ -10,22 +10,7 @@
## 지금 열려있는 것 (우선순위순)
### 1. [최우선] 컴포넌트 경계에서 modifier/Ref가 어떻게 전달되는가
사용자가 "지금 quad에서 가장 문제되는 부분"으로 직접 지목. 컴포넌트가
플레인 함수이고 반환하는 루트가 여러 개(혹은 Slot으로 갈라지는 구조)일 때,
호출부가 넘긴 modifier/Ref가 "어느 루트로 가야 하는지" 모호해지는 케이스가
있음. Jetpack Compose는 언어 강제가 아니라 "컴포저블은 `modifier` 파라미터를
받아 루트에 적용해야 한다"는 순수 관례(+린트)로 풂 — quad도 비슷한 관례
기반으로 갈 수 있어 보이나 다중 루트 케이스는 미정.
**주의**: modifier "값 자체"가 어떻게 동작하는지(정적 merge, immutable+clone
체이닝, State 필드 지원)는 이미 완전히 확정됨(`base/modifier-plan.md`) — 이
질문은 그것과 별개로 "경계를 어떻게 통과하느냐"만 다룸, 혼동하지 말 것.
→ 상세/배경: `research/component-composition-plan.md`.
### 2. 용어 정리 (사용자 요청, 진행 중)
### 1. 용어 정리 (사용자 요청, 진행 중)
사용자 원 메모: "quad는 register라던가 좀 부정확하거나 느낌이 바로 와닿지
않던 용어들이 많음 — 전체적 용어를 보고 생각해볼래? 제안을 줘, 나도 같이
@ -56,13 +41,17 @@
- `Store`/`Source`/`Modifier`/`Ref`/`process`/`retract`/`isHandlable`은
업계 선례와 잘 맞거나 이미 신중하게 결정된 이름들이라 특별한 문제 없음.
### 3. 낮은 우선순위
### 2. 낮은 우선순위
- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
착수를 막지 않음.
- **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요**
`base/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션
가이드 작성 시점에 필요. 지금은 그냥 백로그로만 기록.
- **여러 Slot이 형제로 섞일 때 순서 보장**`base/slot-plan.md`의 "여러
Slot이 섞일 때 순서 보장" 절 참고. Roblox 단일 백엔드로는 급하지 않음
(LayoutOrder/ZIndex로 대부분 해결), 다른 백엔드(웹 DOM 등) 재사용성과
직결되는 문제라 Slot 코어 로직 구현 시점에 재검토.
## 참고: 지금까지 확정된 것 (요약)
@ -79,6 +68,7 @@
| Slot 재조정, 재마운트 시 throw, retract=폐기 | `base/slot-plan.md` |
| `Connected`+GC 라이프사이클 패턴 | `base/lifecycle-pattern.md` |
| Modifier(정적 merge, immutable 체이닝, State 필드 지원) | `base/modifier-plan.md` |
| 컴포넌트화(플레인 함수, State/Source 경계, 컴포넌트 경계 modifier/Ref는 named parameter로 전달, multi-root 개념 폐기, `Modifier.Merge`) | `base/component-composition-plan.md` |
| 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` |
| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `base/comparison-fusion-vide.md` |
| v1 내부 동작 스냅샷 | `base/quad-v1-architecture.md` |

View file

@ -1,110 +0,0 @@
# 컴포넌트화 (Roblox 기본 오브젝트 이외의 사용자 정의 컴포넌트)
**상태**: research — 2026-08-04 세션 채팅 논의에서 핵심 골격 수렴, 세부
API 이름/modifier·Ref passthrough는 미정. 사용자가 "지금 quad에서 가장
문제되는 부분"으로 직접 지목한 주제. `base/bind-system-plan.md`
Store/State/Source 온톨로지가 먼저 확정된 뒤에야 이 논의가 열림 — 그
문서가 선행 컨텍스트.
## 문제
v1의 `Class.Extend()`(Init/Render/AfterRender/Getter/Setter/UpdateTriggers,
`base/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기
방향이지만, 그게 제공하던 실제 편의 기능(컴포넌트가 자기 store를 자동으로
가짐, props로 넘어온 State를 자동 흡수, `self:Default`/`self "key"`로
기본값·바인딩)까지 같이 버려도 되는지가 미결이었음. `MyComp {...}` 형태로
호출되는 사용자 정의 컴포넌트를 v2에서 어떤 모양으로 작성하게 할지가 핵심
질문.
## v1 실제 메커니즘 (조사 완료, `quad.qwreey.kr` 튜토리얼 + `initreq/quad/src/` 소스로 교차검증)
- `myStore "key"` → register(현재 State에 해당) 반환. `:Default(v)`/
`:With(fn)`/`:Add(v)`/`:Tween(opts)` 체이닝 가능(`store.lua:433-457`).
- `Class.Extend()``:Init(props)`에서 `props:Default("Size", v)`로 기본값
설정, props 테이블 자체가 store 인스턴스로 변신(`class.lua:365-379`,
`storeNew(prop,nil)`).
- props로 넘어온 값이 State(`quad_register`)면 `initStoreRegisterBinding`
(`store.lua:394-431`)이 자동으로 감지해 컴포넌트 자신의 store 키에 재귀
연결 — **자동 흡수 매직**이 실제로 존재했음.
- `self(name)` linker가 **두 가지 역할**을 겸함: (1) `self "_button"`
자식 자리에 넣으면 렌더링된 인스턴스를 `self._button`에 즉시 잡아둠(Ref
역할) (2) `[Event.Prop "Text"] = self "Text"`로 인스턴스 프로퍼티 변경을
다시 컴포넌트 store로 역방향 전파(양방향 바인딩, `EmitPropertyChangedSignal`
자동 연결과 동일) — quad.qwreey.kr 튜토리얼 `11_extend/` 문서 원문 확인.
이 두 역할이 v2 온톨로지에서는 이미 갈라져 있음: (1)은 확정된 **Ref**가
대체, (2)는 이번 논의에서 다루는 Source 양방향 프록시가 대체.
## 수렴된 결론
### 1. 컴포넌트 = 그냥 함수, "자기 store 자동 소유" 매직은 폐기
`MyComp = function(props) return Frame {...} end`, 호출 규약은
`Frame{...}`와 동일(`MyComp{...}` → `MyComp(propsTable)`). v1의 Extend
자동-store-생성+자동-흡수 매직은 재현하지 않음 — 대신 React식으로 호출부가
State/raw/Source/콜백 중 뭘 넘길지 명시적으로 고름. 이유: 자동 흡수는
매 컴포넌트 호출마다 "이 prop이 State인가?" 타입 분기를 프레임워크가
암묵적으로 수행해야 하는 매직이고, 명시적 전달이 더 단순·예측 가능(React가
Vue/Svelte 대비 내세우는 강점과 동일 논리) — **사용자 확정**("마법 안쓴다
그것도 동의함").
### 2. State/Source 경계 규칙: 파생이면 읽기전용, 원본이면 쓰기 가능
State는 `:With`/`:Compute`로 만들어진 파생값일 수 있어 쓰기가 정의 자체가
안 됨. Source(독립이든 Store 소속 `StoreSource` 프록시든)는 파생이 아니라
항상 원본 슬롯 하나를 직접 가리키므로 쓰기가 의미 있음 — **사용자 확정**
("맞음. 확실해").
### 3. `StoreSource`: Source를 인터페이스+구현체로 두고, Store 키에서 그 인터페이스를 구현하는 얇은 프록시를 받음
- **Source = 인터페이스이자 구현체**: 독립 생성자 `Source(initial)`가 기본
구현체, `store:GetSource("key")`(가칭)류 접근자가 반환하는 값은 같은
인터페이스를 구현하는 별도의 얇은 프록시(`StoreSource`) — 읽기는
`store.key`로, 쓰기는 `store.key = v`로 위임. **내부 Source 객체를 그대로
노출하지 않음** — 그러면 "쓰기는 오직 Store의 `__newindex`뿐"이라는 기존
확정과 새 쓰기 경로가 충돌하게 됨.
- **캐시 안 함**: State가 이미 "매번 새로 만듦, store에 캐시 안 됨"으로
확정돼 있어 일관성 + 엔지니어링 비용 둘 다 이쪽이 쌈 — **사용자 확정**
("그냥 엔지니어링적으로 비용이 싼거 택해").
### 4. Source 직접 전달(양방향)은 핸들러 계약 확장 없이 타입 유니온으로 처리 — 단, 실사용 범위는 좁음
- 핸들러가 값을 받을 때 `Source<T> | State<T>` 유니온으로 받고, 내부에서
타입 체크만 하면 됨(Source면 인스턴스 변경 이벤트에 걸어 역방향 쓰기까지
처리, State면 읽기만) — `isHandlable`/`priority`/`process`/`retract` 4종
계약에 5번째 항목을 추가할 필요 없음. Source 자체가 계산이 없는 원천이라
가능한 단순화 — **사용자 확정**("그냥 타입 상 source를 받거나 state를
받거나 하면 됨. source 자체는 원천이라 컴퓨팅 같은거 없어").
- **하지만 실사용은 좁을 것으로 예상**: `isEnabled`처럼 여러 조건에 영향
받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못 넘김.
즉 Source 직접 전달이 통하는 건 진짜 단순한 1:1 원본-토글 케이스뿐이고,
일반적인 경우엔 React식 `value(State) + onChange(callback)` 패턴이 기본
**사용자 확정**("isenabled가 여러 조건에 영향 받으면 바로 문제가
생기는거지. 따라서 실제 사용은 제한적일듯. callback을 쓰는게
일반적이여 보이긴 해. 타입으로도 편하기도 하고 디버깅도 편함").
### 5. 리프(Roblox 프로퍼티) 바인딩엔 원칙적으로 State만
계산된 최종값만 실제로 인스턴스에 반영되어야 하므로, 리프 바인딩은 State가
일반 경로. Source는 리프 바인딩용 프리미티브가 아니라, 아주 단순한 구조에서
콜백 보일러플레이트를 줄이기 위한 좁은 용도의 예외 — **사용자 확정**
("리프 바인딩엔 state만 쓰이지 않을까... source는 그냥 아주 단순한
구조에서 콜백을 넣고 하는 복잡함을 줄이기 위함일 뿐임").
## 아직 열린 질문 (`.claude/question.md`에도 취합)
- **modifier/Ref가 컴포넌트 경계를 어떻게 통과하는가**: 컴포넌트가 플레인
함수이고 반환하는 루트가 여러 개(혹은 Slot으로 갈라지는 구조)일 때, 호출부가
넘긴 modifier/Ref가 "어느 루트로 가야 하는지" 모호해지는 케이스가 있음.
Jetpack Compose는 언어 강제가 아니라 "컴포저블은 `modifier` 파라미터를
받아 루트에 적용해야 한다"는 순수 관례(+린트)로 풂 — quad도 비슷한 관례
기반으로 갈 수 있어 보이나 다중 루트 케이스는 미정. **주의: 이건 "경계를
어떻게 통과하는가"의 문제이고, "modifier 값 자체가 어떻게 동작하는가"(정적
merge, immutable 체이닝)는 `research/modifier-plan.md`로 이미 별도 확정됨
— 둘을 혼동하지 말 것.**
- **정확한 API 이름**: `Component`(플레인 함수 규약이라 별도 래퍼가 필요한지
자체도 불확실 — 아마 불필요), `GetSource` 계열 접근자 이름, `Source`
독립 생성자 이름은 전부 가칭. `base/bind-system-plan.md`의 "남은 열린
질문" 절(정확한 함수/생성자 이름 미정)과 같은 급의 후순위 항목.
- **`quad2-try`는 확인 불필요로 재확인** — 진행이 중단된 상태라 이 논의와
무관.

View file

@ -22,11 +22,12 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
**지금은 설계/계획 단계이고 구현은 아직 시작 전** — 저장소 루트에 실제 소스
코드(`src/` 등)가 없음. 핵심 아키텍처(Store 책임 분리, `process`/`retract`
디스패치 모델, Store/State/Source 온톨로지, 소스 트리 구조, Modifier 메커니즘,
컴포넌트=플레인 함수)는 전부 `.claude/base/`에 문서로 확정돼 있음 — 먼저
`.claude/base/architecture.md`를 읽을 것. **단, "핵심 설계 질문이 더 이상
없다"는 뜻은 아님** — 컴포넌트화(특히 modifier/Ref가 컴포넌트 경계를 어떻게
통과하는지)는 사용자가 직접 "지금 quad에서 가장 문제되는 부분"으로 지목한
채 아직 열려있음, 아래 "지금 할 일" 참고.
컴포넌트=플레인 함수, 컴포넌트 경계 modifier/Ref 전달)는 전부 `.claude/base/`
문서로 확정돼 있음 — 먼저 `.claude/base/architecture.md`를 읽을 것. 사용자가
직접 "지금 quad에서 가장 문제되는 부분"으로 지목했던 컴포넌트화(특히
modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션에서 수렴
완료(`base/component-composition-plan.md`) — 남은 핵심 설계 질문은 없고,
용어 정리(진행 중)와 실제 스캐폴딩만 남음, 아래 "지금 할 일" 참고.
이전에 시도했다 폐기한 v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치
완료 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` copy-on-write 절충안은 확인된
@ -39,8 +40,8 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
`.claude/base/architecture.md`를 읽을 것.
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. 지금은
`tween-plan.md`(세부 옵션만 남음), `existing-instance-bind-plan.md`(급하지
않음), `component-composition-plan.md`(**사용자가 최우선으로 지목한 열린
주제**) 세 개뿐.
않음) 두 개뿐 — `component-composition-plan.md`는 2026-08-04 세션에 수렴
완료돼 `base/`로 승격됨.
- `.claude/qa-request/`, `.claude/archive/`, `.claude/feedback/` — 구현
시작되면 쓰기 시작함, 지금은 비어있음.
- `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox,
@ -48,6 +49,9 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
`.gitignore`로 커밋 제외됨** — 내용을 다른 곳으로 옮기지 말고 항상 원본
그대로 둘 것. 리서치가 더 필요하면 이 폴더를 다시 파고들 것.
- `.claude/question.md` — 사용자가 답해야 할 질문 전체 취합(우선순위순).
- 루트 `ROADMAP.md` — 설계 단계 종료 후 실제 구현 순서(M0, M1, ... 마일스톤 +
todo 체크박스). "무엇을 확정했는가"는 `.claude/base/`가 소스, "어떤 순서로
만드는가"는 이 문서가 소스 — 헷갈리지 말 것.
- 루트 `HUMAN_TODO.md` — 사람만 할 수 있는 일(로컬 GUI 조작, 스케줄/루프
설정 등).
@ -87,28 +91,31 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
## 지금 할 일 (우선순위순)
1. **컴포넌트화 논의 계속 — 사용자가 직접 "가장 문제되는 부분"으로 지목.**
`research/component-composition-plan.md` 참고. 핵심 골격(컴포넌트=플레인
함수, State/Source 읽기·쓰기 경계, `StoreSource` 프록시)과 Modifier
메커니즘 자체(`base/modifier-plan.md`, 완전 확정)는 수렴됨 — 남은 건
**modifier/Ref가 컴포넌트 경계(특히 다중 루트)를 어떻게 통과하는가**라는
진짜 설계 질문(이름 문제가 아님). 다음 세션에서 이걸 이어서 파고들 것.
2. **실제 스캐폴딩.** 소스 트리 구조는 문서로 이미 확정됨(`base/
architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절) — `quad-base/`,
`quad-roblox/` 폴더, 각각의 `wally.toml`, 루트 `default.project.json`,
`.luaurc`를 만들 것. 1번의 컴포넌트 경계 논의가 `DI`/`Modifier` 모듈
설계에 영향을 주므로, 그 결론이 안 나온 상태에서도 나머지 구조(Store/
State/Source, 디스패치 엔진, Slot)는 그대로 스캐폴딩 가능 — 막을 필요
없음(`architecture.md`에도 명시). 이 시점부터 `qa-request/`/`archive/`
폴더가 실제로 쓰이기 시작함.
3. **용어 정리 — 사용자가 별도로 요청, 진행 중.** "register"(v1) 같이
1. **구현 시작 — 루트 `ROADMAP.md`의 M0부터.** 설계 단계는 2026-08-04
로드맵 인수인계 라운드로 종료, 다음 세션은 바로 `ROADMAP.md` M0(스켈레톤+
기술검증 스파이크)부터 시작. M0는 "진짜" 마일스톤이 아니라, 지금까지
추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 세 가지(Store/State
propagation, 재귀 process/retract 디스패치, 컴포넌트 경계 named-parameter
전달)를 던지는 코드로 검증하는 단계 — 여기서 걸리면 `base/` 문서를 그
자리에서 고치는 게 정상. M0 통과 후 M1(실제 스캐폴딩: `quad-base/`,
`quad-roblox/` 폴더 + `wally.toml`/`default.project.json`/`.luaurc` +
quad-base용 최소 mock 테스트 하네스)으로 진행 — 소스 트리 자체는 이미
확정됨(`base/architecture.md` "구현 착수" 절). 이 단계부터
`qa-request/`/`archive/` 폴더가 실제로 쓰이기 시작함. **세부 순서/todo는
`ROADMAP.md`가 소스** — 여기서 반복 안 함.
2. **용어 정리 — 사용자가 별도로 요청, 진행 중.** "register"(v1) 같이
부정확한 이름들을 전체적으로 재검토하자는 요청 — 1차 제안 완료(우선순위
순: `State`가 React/Vue식 "쓸 수 있는 로컬 상태"라는 통상 의미와 반대라
가장 위험, `DI`가 Dependency Injection 축약어와 충돌, `PerInstanceState`
핵심 프리미티브 `State`와 이름 충돌 — 세부는 `.claude/question.md` 참고),
사용자와 같이 계속 논의 필요.
4. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만
사용자와 같이 계속 논의 필요. 컴포넌트 경계용 `props.Modifier`/`props.Ref`/
`Modifier.Merge` 같은 새 가칭들도 이 정리에 합류 대상.
3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만
필요, 구현 착수를 막지 않음.
4. **[백로그] 범용 렌더 디버깅 도구로서의 quad-mock.** 1번의 quad-base 테스트용
mock과는 별개 — 정적 스냅샷을 넘어 Tween mock 같은 동적 동작까지 지원하는
더 큰 스코프의 디버깅 도구(`architecture.md` "테스트 전략" 절 백로그 참고).
효용성 봐가며 나중에 검토, 지금 당장 설계할 필요 없음.
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목).
@ -125,12 +132,19 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
경우의 setter/getter 동작까지 전부 확정(`base/modifier-plan.md`, 새로
base 승격). 이 논의에서 "관측해야 실체화된다"는 프로젝트 전역 원칙도
명문화(`bind-system-plan.md`).
- **컴포넌트화 논의 시작, 아직 미완** — v1의 `Class.Extend()` 자동-store
매직은 폐기하고 React식으로 값을 명시적으로 전달하는 방향으로 수렴,
`StoreSource`(Source를 인터페이스+구현체로 보고 Store 키에서 얇은
프록시로 얻는 것) 아이디어까지 나왔지만 modifier/Ref의 컴포넌트 경계
통과 방식은 미해결(`research/component-composition-plan.md`, 위 "지금
할 일" 1번).
- **컴포넌트화 논의, 같은 날 후속 세션에서 완결** — v1의 `Class.Extend()`
자동-store 매직은 폐기하고 React식으로 값을 명시적으로 전달하는 방향으로
수렴, `StoreSource`(Source를 인터페이스+구현체로 보고 Store 키에서 얇은
프록시로 얻는 것) 아이디어 확정. 마지막 미결이던 "modifier/Ref의 컴포넌트
경계 통과"도 후속 세션에서 풀림: Compose/Fusion/Vide/v1 4개 선례를
서브에이전트로 병렬 조사한 결과 전부 named parameter로 경계를 넘기는
패턴에 수렴한다는 게 확인됐고, "컴포넌트가 여러 루트를 반환한다"는
프레이밍 자체가 (a) Luau가 tail position 밖 다중 리턴을 지원 안 해서
불필요한 개념과 (b) 이미 있는 Slot 메커니즘을 섞은 것이었음이 드러나
정리됨 — 결론: 경계는 named parameter(`props.Modifier`/`props.Ref`
가칭), "다중 루트"라는 별도 개념은 폐기, 여러 modifier를 하나로 합치는
`Modifier.Merge`(가칭) 유틸 추가. `research/component-composition-plan.md`
`base/component-composition-plan.md`로 승격 완료.
- **문서 전체 감사 및 정리**`.claude/` 코퍼스 전체(약 15개 문서)를
서브에이전트로 감사해 여러 라운드에 걸쳐 쌓인 모순/중복/stale 마커를
대거 발견하고 수정(예: 이벤트 dot-access 확정 여부가 문서 내에서 서로
@ -142,4 +156,25 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
git log와 각 `base/`/`research/` 문서 안의 라운드 표시(예: "2026-08-04
3차 라운드")를 참고할 것, 여기서 전부 반복하지 않음.
용어 정리 제안 진행 중인 점은 위 "지금 할 일" 3번 참고.
**같은 날 로드맵 인수인계 라운드 — 설계 단계 마무리, 구현 준비 완료**:
- **quad-base 테스트 mock 방향 확정**: Vide 선례(`test/mock.luau`, ~300줄,
순수 `luau` CLI, Studio 불필요) 그대로 채택, 스코프는 정적 디버깅 한정(Tween
같은 동적 동작 제외), quad-roblox로 작성한 컴포넌트가 mock에서도 그대로
돌아가야 한다는 요구 없음(단순하게 감) — `architecture.md` "테스트 전략"
절. 범용 렌더 디버깅 도구(Tween mock 포함)는 별개로 백로그.
- **구현 전 리스크 감사**: `.claude/base/` 전체 + 남은 `research/`
서브에이전트로 감사해 "실제 Luau 접촉 없이 추론만으로 확정된 것" 3개
(Store/State 반응형 코어, 디스패치 엔진, 컴포넌트 경계 modifier/Ref)를
식별 — 이것들은 M0 스파이크로 검증하기로 함(아래). 감사 중 `slot-plan.md`
스스로 "정식 확정 안 됨"이라 표시해뒀던 "클래스가 슬롯을 받는 방법"(Named
Slot 없음)도 이번에 정식 확정, 대신 "여러 Slot이 형제로 섞일 때 순서 보장"
이라는 새 하위 질문이 열림(다중 백엔드 관점, Roblox만이면 급하지 않음).
`State<Modifier>` 조합은 UB로 확정해 타입으로 막기로 함(`modifier-plan.md`
7번), 디스패치 엔진의 일반적 무한루프는 방어 로직 없이 provider 버그로
간주하기로 확정(`bind-system-plan.md`).
- **루트 `ROADMAP.md` 신설** — M0(스켈레톤+기술검증 스파이크, "진짜"
마일스톤 아님)부터 M11(Tween)까지 + 병행 가능 항목 + 백로그로 구성된 실행
계획, todo 체크박스 포함. 오늘은 문서 준비만 — **다음 세션이 M0부터 실제
시작**.
용어 정리 제안 진행 중인 점은 위 "지금 할 일" 2번 참고.

116
ROADMAP.md Normal file
View file

@ -0,0 +1,116 @@
# ROADMAP.md
quad-v2 구현 단계 실행 계획. 설계 근거/아키텍처 자체는 여기 안 옮겨적음 —
`.claude/base/`가 소스, 여긴 **순서와 진행 상황**만. 마일스톤 시작할 때
체크박스를 세분화해서 늘려도 되고, 끝나면 체크만 하면 됨 — 살아있는 문서.
**2026-08-04 세션에 준비만 해둔 상태 — 아직 M0도 시작 안 함.** 다음 세션은
바로 M0부터.
## M0 — 스켈레톤 + 기술검증 (스파이크, "진짜" 마일스톤 아님)
최종 소스 트리를 그대로 만들기 전에, 지금까지 **추론만으로 확정하고 실제
Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검증하는 단계 —
`.claude/base/` 감사에서 나온 결론(2026-08-04). 여기서 뭔가 어긋나면
`architecture.md`/`bind-system-plan.md` 등을 이 시점에 고치는 게 정상 —
실패가 아니라 이 단계의 목적.
- [ ] Store/State push-invalidate → pull-recompute propagation을 실제로
짜보기(다이아몬드 의존성 케이스 포함 — 이미 invalid면 전파 중단되는지)
- [ ] `process`/`retract` 재귀 재-process 디스패치를 실제로 짜보기(store-bind
핸들러 하나 + `isHandlable` 우선순위 스캔 포함)
- [ ] `props.Modifier`/`props.Ref` named-parameter로 받는 컴포넌트 하나 작성,
`export type Params = {...}`로 타입 체크되는지 확인
(`component-composition-plan.md` 최종 결론 1번)
- [ ] 위 과정에서 소스 트리/메커니즘 문서에 고칠 부분이 생기면 그 자리에서
`.claude/base/` 갱신
**통과 기준**: 세 개 다 Luau에서 자연스럽게 짜이는 게 확인되면 M1 진행.
안 되면 여기서 관련 `base/` 문서부터 고치고 재시도.
## M1 — 실제 스캐폴딩
- [ ] `quad-base/`, `quad-roblox/` 폴더 + 각 `wally.toml`
- [ ] 루트 `default.project.json`, `.luaurc`(`architecture.md` "구현 착수:
소스 트리 구조 확정" 절 그대로)
- [ ] quad-base용 최소 mock 테스트 하네스(Vide `test/mock.luau` 선례, 순수
`luau` CLI, `architecture.md` "테스트 전략" 절 참고)
- [ ] 이 시점부터 `.claude/qa-request/`/`.claude/archive/` 폴더 실사용 시작
## M2 — 디스패치 엔진
- [ ] `Dispatch/init.luau`(`process`/`retract` 엔진, `isHandlable` 우선순위 스캔)
- [ ] `Handler.luau`(핸들러 계약 타입)
- [ ] mock 대상 테스트
## M3 — Store/State/Source
- [ ] `Source.luau`/`State.luau`/`Store.luau`
- [ ] `store.key` dot-access 타입 추론 확인
- [ ] mock 대상 테스트
## M4 — 첫 end-to-end 반응형 업데이트
- [ ] `Dispatch/StoreBind.luau`(재귀 재실행 로직, 엔진 무관)
- [ ] mock 대상으로 "store 값 바꾸면 `process`가 다시 호출된다" 확인
## M5 — quad-roblox 최소 프로바이더
- [ ] `RobloxFactory.luau`(BaseModule 뮤테이션, 재호출 가드)
- [ ] `DI/init.luau`(제네릭 생성자 + ~25개 정적 필드)
- [ ] `Handlers/Property.luau`, `Handlers/InstanceChild.luau`
- [ ] 실제 Roblox에서 첫 `Frame{...}` 렌더 확인 — **Studio 작업이라
`HUMAN_TODO.md` 1번(계정 분리) 먼저 되어야 진행 가능, `SAFETY.md` 준수**
## M6 — Slot
- [ ] "여러 Slot이 형제로 섞일 때 순서 보장" 열린 질문 확인(`slot-plan.md`) —
Roblox 단일 백엔드로는 급하지 않으면 스킵하고 진행 가능
- [ ] base `Dispatch/Slot.luau`(추상 재조정) + quad-roblox `Handlers/Slot.luau`
(실제 Parent 조작)
## M7 — Modifier
- [ ] flatten-before-dispatch, immutable `table.clone` 체이닝
- [ ] `Modifier.Merge`(가칭)
- [ ] `State<Modifier>` 조합 타입 차단 확인(`modifier-plan.md` 7번, UB 확정)
## M8 — Ref
- [ ] `CreatedRef` 메커니즘(숫자 슬롯 참가자)
- [ ] `LifetimeHandle` 인터페이스 + quad-roblox 실제 구현(Instance 생존 확인)
- [ ] `PerInstanceState` 인터페이스 + quad-roblox 실제 구현(weak-keyed table)
## M9 — 컴포넌트 합성 레이어
- [ ] 플레인 함수 컴포넌트 관례 문서화/예제
- [ ] `props.Modifier`/`props.Ref` 전달 관례를 정식 컴포넌트로 검증(M0
스파이크를 정식화)
## M10 — Event / Attribute / Tag
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
- [ ] `Handlers/Attribute.luau`
- [ ] `Handlers/Tag.luau`(`CollectionService`)
## M11 — Tween
- [ ] `research/tween-plan.md` 남은 옵션 이름 확정(구조는 이미 확정)
- [ ] `Handlers/Tween.luau`(높은 우선순위 store-bind 핸들러, 기본 오버라이드
Cancel)
## 특정 마일스톤에 안 묶이고 병행 가능
- [ ] 용어 정리 스윕 — `State`/`DI`/`PerInstanceState`/`Slot` 등
(`.claude/question.md` 1번), 최종 이름 확정되는 대로 아무 시점에나
- [ ] 각 마일스톤 완료 시 `.claude/qa-request/`/`.claude/archive/`에 기록,
필요하면 `CLAUDE.md` "최근 세션 요약"도 갱신
## 백로그 (스코프 밖 — 필요성이 실제로 드러나면 그때 설계)
- [ ] `research/existing-instance-bind-plan.md` — Modifier 정적 flatten과
긴장 관계 있음, 재검토 시 그 문서부터 다시 볼 것
- [ ] 범용 렌더 디버깅 도구로서의 quad-mock(Tween mock 등 동적 동작 포함,
M1의 quad-base 테스트용 mock과는 별개)
- [ ] v1 마이그레이션 가이드 + `objectListClass.__newIndex` 오타 기능 재현 테스트
- [ ] Slot 형제 순서 보장(다중 백엔드 관점) — Roblox만이면 급하지 않음