From 470f188d0fc7d03db9ce179c86b23cc6f1f80086 Mon Sep 17 00:00:00 2001 From: qwreey Date: Tue, 4 Aug 2026 17:11:23 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EB=93=9C=EB=A7=B5=20=EC=9D=B8?= =?UTF-8?q?=EC=88=98=EC=9D=B8=EA=B3=84=20=EB=9D=BC=EC=9A=B4=EB=93=9C(2026-?= =?UTF-8?q?08-04)=20=EA=B2=B0=EA=B3=BC=20=EB=B0=98=EC=98=81=20=E2=80=94=20?= =?UTF-8?q?ROADMAP.md=20=EC=8B=A0=EC=84=A4,=20=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=20=EC=A2=85=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit component-composition-plan을 research/에서 base/로 승격하고, 구현 착수 전 리스크 감사에서 나온 M0 스파이크 항목(Store/State propagation, dispatch, 컴포넌트 경계 named-parameter 전달)을 문서에 반영. quad-base 테스트 mock 방향(Vide 선례 채택) 확정. --- .claude/base/architecture.md | 41 ++- .claude/base/bind-system-plan.md | 10 + .claude/base/component-composition-plan.md | 259 ++++++++++++++++++ .claude/base/modifier-plan.md | 12 + .claude/base/slot-plan.md | 30 +- .claude/question.md | 24 +- .../research/component-composition-plan.md | 110 -------- CLAUDE.md | 97 ++++--- ROADMAP.md | 116 ++++++++ 9 files changed, 532 insertions(+), 167 deletions(-) create mode 100644 .claude/base/component-composition-plan.md delete mode 100644 .claude/research/component-composition-plan.md create mode 100644 ROADMAP.md diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 61fffbb..dd45855 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -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`). diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index e5358d1..7c31475 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -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 바인드를 재실행하는 래핑인가 diff --git a/.claude/base/component-composition-plan.md b/.claude/base/component-composition-plan.md new file mode 100644 index 0000000..4690c10 --- /dev/null +++ b/.claude/base/component-composition-plan.md @@ -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 | State` 유니온으로 받고, 내부에서 + 타입 체크만 하면 됨(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`는 확인 불필요로 재확인** — 진행이 중단된 상태라 이 논의와 + 무관. diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md index c1273bf..e906af7 100644 --- a/.claude/base/modifier-plan.md +++ b/.claude/base/modifier-plan.md @@ -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` 같은 조합을 타입 정의 단계에서 +거부) — 런타임 가드가 아니라 타입 차단을 우선 검토. + ## 열린 질문 (`.claude/question.md`에도 취합) - Getter 정확한 이름/모양(`:Get(key)` vs dot-access 겸용) — 후순위, 구현 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 8a143b7..56679c0 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -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 + 코어 로직 구현 시점에 재검토. diff --git a/.claude/question.md b/.claude/question.md index 1cd4770..3fc79b1 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -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` | diff --git a/.claude/research/component-composition-plan.md b/.claude/research/component-composition-plan.md deleted file mode 100644 index 77f4185..0000000 --- a/.claude/research/component-composition-plan.md +++ /dev/null @@ -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 | State` 유니온으로 받고, 내부에서 - 타입 체크만 하면 됨(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`는 확인 불필요로 재확인** — 진행이 중단된 상태라 이 논의와 - 무관. diff --git a/CLAUDE.md b/CLAUDE.md index 9c38224..2b66eb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` 조합은 UB로 확정해 타입으로 막기로 함(`modifier-plan.md` + 7번), 디스패치 엔진의 일반적 무한루프는 방어 로직 없이 provider 버그로 + 간주하기로 확정(`bind-system-plan.md`). +- **루트 `ROADMAP.md` 신설** — M0(스켈레톤+기술검증 스파이크, "진짜" + 마일스톤 아님)부터 M11(Tween)까지 + 병행 가능 항목 + 백로그로 구성된 실행 + 계획, todo 체크박스 포함. 오늘은 문서 준비만 — **다음 세션이 M0부터 실제 + 시작**. + +용어 정리 제안 진행 중인 점은 위 "지금 할 일" 2번 참고. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..8b48962 --- /dev/null +++ b/ROADMAP.md @@ -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-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만이면 급하지 않음