diff --git a/.claude/README.md b/.claude/README.md index 87bc2d9..a7b72cb 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -14,7 +14,7 @@ | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | -| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | +| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | `research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시 `qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만 @@ -26,20 +26,22 @@ |---|---| | `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서) | | `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 | -| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료 | +| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료(전파 모델 등 일부 서술은 이후 라운드에서 뒤집혔으니 `bind-system-plan.md` 쪽을 최신으로 볼 것) | | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 | | `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` | -| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드) | — | -| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | — | -| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 | — | +| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드) | +| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | +| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 | +| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정, getter 이름만 남음 | +| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | ## `research/` — 아직 착수 전, 상의 필요 | 문서 | 내용 | 우선순위 | |---|---|---| | `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 | -| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | 하 — 문서화 성격, 급하지 않음 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | +| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, `StoreSource` 프록시 — 핵심 골격 수렴, modifier/Ref가 컴포넌트 경계를 어떻게 통과하는지만 남음 | 상 — 사용자가 "가장 문제되는 부분"으로 직접 지목 | ## 참고 diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 56f2eb4..61fffbb 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -31,17 +31,36 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo 내장되어 store 컴퓨티드 바인드도 가능해야 함. 5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/ `Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유. - 네임스페이싱 문제는 있지만(`.claude/question.md` 참고) 별도 네임스페이스 - 개념을 추가하면 라이브러리 복잡도가 너무 올라간다고 판단 — 당장은 - TagService 그대로 사용. **대신 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가 - 아니라 "외부에서 이미 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/ - 래핑하기 위해 직접 참조를 얻는 것"(`base/bind-system-plan.md`의 Ref 절 - 참고) — 둘을 혼동하지 말 것. + 네임스페이싱 문제는 있지만 별도 네임스페이스 개념을 추가하면 라이브러리 + 복잡도가 너무 올라간다고 판단 — 당장은 TagService 그대로 사용. **대신 + Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가 아니라 "외부에서 이미 + 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/래핑하기 위해 + 직접 참조를 얻는 것"(`base/bind-system-plan.md`의 Ref 절 참고) — 둘을 + 혼동하지 말 것. + - **2026-08-04 6차: 네임스페이싱 충돌을 심각하게 안 보는 이유 확정.** + 충돌을 피해야 하는 단위는 보통 컴포넌트 단위로 나오고, 그 경우는 Ref로 + 직접 참조를 얻으면 되므로 태그 자체의 전역 네임스페이스가 굳이 필요 + 없음. 태그는 원래 주로 스타일링(스타일시트 셀렉터) 용도인데, 스타일시트는 + 적용 위치가 트리 상위에 존재해야 하고 사용자가 직접 그 위치에 심어야 + 하는 등 스크립팅으로 구성하기 어려워 quad 같은 UI 라이브러리에서는 잘 + 안 쓰는 접근 — 그래서 스타일시트 대신 modifier kit을 제공하는 것(아래 + 7번 항목의 modifier 우선순위 규칙 참고). 6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말 편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양. -7. **Style(Default) 시스템 폐기.** Roblox 자체 스타일시트를 쓰는 게 낫다고 판단. - 대신 modifier(spread되는 값, `...`으로 풀리는 것)를 지향 — 함수형 modifier가 - store 바인드를 받을 수도 있음. +7. **Style(Default) 시스템 폐기.** 대신 modifier(spread되는 값, `...`으로 + 풀리는 것)를 지향 — 함수형 modifier가 store 바인드를 받을 수도 있음. + (초기 근거였던 "Roblox 자체 스타일시트를 쓰는 게 낫다"는 6차 라운드에서 + 갱신됨 — 위 5번 항목의 6차 추가분 참고: 스타일시트는 적용 위치 제약과 + 스크립팅 난이도 때문에 오히려 안 쓰기로 하고 modifier kit으로 대체함.) + - **2026-08-04 세션: modifier 메커니즘 전체 확정, 상세는 + `research/modifier-plan.md`로 분리.** 요지만: 런타임 pluggable 핸들러가 + 아니라 디스패치 이전에 정적으로 flatten되는 값(핸들러 레지스트리 미참여, + CSS cascade 문제 회피). Merge 우선순위는 "배열 순서상 나중 modifier가 + 우선"과 "인라인 키는 modifier보다 무조건 우선"이라는 독립된 두 규칙(Lua + 테이블 리터럴이 배열/해시 파트 간 소스 순서를 보존 안 하므로 하나로 합칠 + 수 없음). 값은 immutable — 체이닝 메소드(`:FontSize(...)`류)는 항상 + `table.clone` 후 반환, 원본 mutate 금지(형제 서브트리 오염/재렌더 드리프트 + 방지, 비용은 무시 가능한 수준으로 확인됨). 8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""` 같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로 구현(`base/bind-system-plan.md`). @@ -59,9 +78,10 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo 확정으로 재확인 — 더 이상 열린 질문 아님.) 12. **멀티 타겟(pluggable 백엔드) — 특히 GTK 지원까지 염두.** Roblox 전용 렌더 기술(react.lua, Fusion)은 결국 외부 개발자 유인이 없어 발전이 더딜 거라는 - 문제의식. 결과적으로 `plug/roblox`, `plug/base` 정도로 나뉠 전망 — base가 - 가상돔 없이도 프로바이더 패턴으로 백엔드를 받는 인터페이스만 정의하고, - 실제 Roblox 구현은 `quad-roblox` 격 서브패키지가 담당. + 문제의식. 결과적으로 `quad-base`/`quad-roblox`로 나뉨(5차 라운드에서 확정된 + 정확한 패키지 이름, 아래 "구현 착수" 절 참고) — base가 가상돔 없이도 + 프로바이더 패턴으로 백엔드를 받는 인터페이스만 정의하고, 실제 Roblox 구현은 + `quad-roblox`가 담당. 13. **모듈은 기본 싱글톤, `New()`는 나중에.** 한 Lua 스레드에서 Roblox/비-Roblox 프로바이더를 동시에 쓸 일이 거의 없을 거라 판단 — 필요해지면 그때 `New()` 추가. @@ -137,21 +157,22 @@ quad/ Tween/purity/existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 확정을 막지 않음. -## 아직 미정 (research/로 분리됨) +## Store/State/Source 온톨로지 — 확정됨 (요약) -Tween 플러깅, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 — -`.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`. 바인드 -디스패치/Slot/모듈 라이프사이클은 위 "구현 착수" 섹션대로 확정되어 -`.claude/base/`로 승격됨(`bind-system-plan.md`/`module-lifecycle-plan.md`/ -`slot-plan.md`). - -**Store/State/Source 온톨로지(2026-08-04 두 라운드에 걸쳐 확정)**: Store는 -source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할 때마다 -그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를 +Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할 +때마다 그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를 반환한다. 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고(값 쓰기는 항상 Store의 `__newindex`), 값 하나만 다룰 땐 -Store와 별개인 가벼운 `Source` 프리미티브를 씀. 남은 건 정확한 API 이름과 -"`store.key` dot-access를 타입 추론 1급 경로로 삼는다"는 제안의 정식 확인 -뿐 — `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절, -`.claude/question.md` 참고. +Store와 별개인 가벼운 `Source` 프리미티브를 씀. `store.key` dot-access를 타입 +추론 1급 경로로 삼는 것도 3차 라운드에서 정식 확정됨 — **더 이상 열린 질문 +아님**, 남은 건 정확한 API 이름뿐. 상세는 `base/bind-system-plan.md`의 +"Store/State/Source 온톨로지" 절 참고. + +## 아직 미정 (research/로 분리됨) + +Tween 플러깅, 이미 생성된 인스턴스에 대한 바인드, 컴포넌트가 modifier/Ref를 +경계 너머로 어떻게 전달하는지 — `.claude/research/` 각 문서 참고, 전체 색인은 +`.claude/README.md`. 바인드 디스패치/Slot/모듈 라이프사이클은 위 "구현 착수" +섹션대로 확정되어 `.claude/base/`로 승격됨(`bind-system-plan.md`/ +`module-lifecycle-plan.md`/`slot-plan.md`). diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 0235c8b..e5358d1 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -122,10 +122,14 @@ Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract` 아니면 아예 다른 값으로 둬야 하는가? table/number 같은 프리미티브 타입이나 ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플러그를 만드는 걸로." -이 문서의 제안: "Store 안의 값이 Store"인 경우도 그냥 하나의 (key,value) 쌍일 -뿐이고, 그 값 타입(Store)을 인식하는 핸들러가 pluggable 레지스트리에 등록되어 -있으면 됨 — 위 "재실행 래핑" 방식과 동일한 메커니즘으로 커버됨. 별도 특수 -케이스 코드 불필요. +**2026-08-04 6차 확정: 그런 경우는 없다고 본다.** 위에 적힌 "재실행 래핑으로 +기계적으로는 커버 가능하다"는 제안은 메커니즘상 틀리지 않지만, 실제 설계 +의도와 안 맞음 — Store는 Source에 준하는 존재로 모든 반응형 값의 "시작점" +역할만 함. 시작점은 다른 변화하는 무언가에 연결되는 것을 제공하고자 하지 +않음(= Store가 다른 Store/State를 값으로 담아 자동으로 따라가게 하는 용도로 +쓰지 않음). Store에서 값을 꺼내 State를 옵저빙하다가 콜백으로 다른 Store 값을 +바꾸는 식의 수동 연결은 있을 수 있지만, 잘 짜인 UI에서 실사용 사례를 거의 +보지 못했다는 게 사용자 판단 — 그래서 이 케이스를 위해 별도로 신경 쓰지 않음. ## Ref — 도입 확정, 단 용도는 재정의됨 @@ -167,29 +171,29 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플 **채택 방향**: `:With(...)`로 필요한 의존성을 모으고, 그 뒤 `:Compute(function() ... end)`에서 **`with`한 값을 포지셔널 인자로 받지 않고 클로저로 직접 읽는다** -(정확히 어떤 방식으로 "직접 읽는지"는 아래 열린 질문 — `:fromState` 후보 참고). +(정확히 어떤 방식으로 "직접 읽는지"는 2차 라운드에서 확정 — self/with 값 둘 다 +lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/ +`:Compute`" 부분 참고). -## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 기술적 난이도 미확정 +## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨 -**중요한 배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐. +**배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐. 이상적으로는 store에서 한 값을 추적(track)하면 "State"가 나오고, 거기에 `compute`를 적용하면 또 다른 "State"가 나오는 식 — Unix의 `(cat a; cat b) | while read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최종 목표. `:With`의 두 번째 인자(`b`)도 다른 `:Compute`의 결과물(State)을 그대로 받을 수 있어야 이상적. -**미해결 긴장 관계**: 이걸 구현하는 두 갈래 방식이 있고 어느 쪽이 맞는지 아직 -결정 안 됨: -1. **Compute 체인이 항상 자기 자신을 mutable하게 바꾼다** — 엔지니어링 비용은 - 낮지만, 다른 코드가 나중에 그 체인 뒤에 새 compute를 붙이는(다른 소비자가 - 동일 State에 독립적으로 파생값을 추가하는) 것이 불가능해짐 — 공유/합성이 - 깨짐. -2. **명시적 `State:fromState(state)`류의 비-mutating 생성자** — 합성은 - 안전해지지만 엔지니어링 비용이 더 큼(정확히 얼마나 큰지 미확정). - -이건 `base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 -같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가 -아직 검증 안 됨. +**해소됨(2차 라운드) — 두 갈래 방식 중 실질적으로 옵션 2 방향으로 정리됨**: +당시엔 (1) Compute 체인이 자기 자신을 mutable하게 바꾸는 방식(엔지니어링 비용 +낮지만 공유/합성이 깨짐) vs (2) 명시적 `State:fromState(state)`류 비-mutating +생성자(합성은 안전, 비용 미확정) 둘로 긴장이 있었으나, 실제 확정된 모델은 +아래 "Store/State/Source 온톨로지" 절의 **`state(state)`로 기존 state의 +결과를 받아 새 state를 만드는 조합**임 — 매번 새 State를 만든다는 점에서 +옵션 2와 같은 축(비-mutating)이고, 별도 `fromState`/`Pipe` 콤비네이터 타입 +없이도 `state(state)` 하나로 충분하다는 게 최종 결론(`Pipe` 후보는 폐기). +`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 같은 +축의 해법. ## Store/State/Source 온톨로지 — 핵심 메커니즘 확정 (2026-08-04 2차 라운드) @@ -228,6 +232,22 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦** - `emit`은 이 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함 ("emit 필요 여부" 열린 질문은 이걸로 해소). +**전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)** + +위 pull-recompute 규칙을 State 하나의 재계산 메커니즘으로만 읽지 말고, +프로젝트 전역에 적용되는 원칙으로 명시함: **어떤 파생값도 `.value`/`Get()`로 +직접 읽히기(관측) 전까지는 계산되지 않는다.** 이 원칙은 State 자체뿐 아니라, +State를 필드 값으로 담고 있는 다른 구조(예: `base/modifier-plan.md`의 +Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 읽으면 그 +순간이 바로 관측이고, 그 순간 계산이 확정됨. + +**주의 — 구조적 복사는 관측이 아님.** `table.clone`처럼 테이블 레퍼런스만 +복사하는 연산은 안에 담긴 State 핸들을 그대로 옮길 뿐 `.value`/`Get()`을 +호출하지 않으므로 관측이 아니고, 계산을 트리거하지 않음. Modifier 체이닝 +메소드가 `table.clone` 후 필드를 덮어쓰는 것(위 "Immutable 값 + clone 기반 +체이닝")과 이 원칙이 충돌하지 않는 이유가 바로 이것 — clone은 그저 참조 +복사라 State 필드는 클론 이후에도 여전히 살아있는 lazy 핸들로 남음. + **`:With`/`:Compute` — self 인자도 lazy 핸들로 통일** - 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제 @@ -278,9 +298,10 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦** 레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열 리터럴 narrowing 문제 자체가 안 생김. `store "key"` 문자열 커링은 동적 키가 필요할 때 쓰는 미타입(`State`) 폴백으로 격하. -- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성(`DI.Frame`)/이벤트 - (`On.EventName`)까지 관통하는 프로젝트 전역 관습으로 확정**됨 — 아래 - "인스턴스 생성 / 이벤트 네이밍 인체공학" 절 참고. +- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성까지 관통하는 프로젝트 + 전역 관습으로 확정**됨 — 단 이벤트는 이후 4차 라운드에서 이 관습의 + **유일한 예외**로 빠졌음(PA님 방식인 문자열 키+런타임 리플렉션으로 전환). + 아래 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절이 최신 확정 내용. **`Pipe`(quad2-try 후보)는 폐기 확정** — 별도 `Pipe` 타입에 소유권/버전 가드를 넣어 재설계하는 대신, State 자체가 파이핑 결합체이고 @@ -359,15 +380,16 @@ State/스트림)를 다뤘던 이전 시도가 있었음. 조사 결과 요약: **건질 만한 것 (인체공학/아이디어만, 코드는 아님):** - **`store:Pipe(key):Compute(fn)` 같은 왼쪽에서 오른쪽으로 읽히는 파이프 문법 자체**는 목표로 유지할 가치가 있음. -- **`Pipe`가 mutate-vs-`fromState` 긴장 관계에 제시한 절충안** — "체이닝된 +- **`Pipe`가 mutate-vs-`fromState` 긴장 관계에 제시했던 절충안** — "체이닝된 `Compute`/`Add`/... 호출은 자신이 액션 리스트의 유일한 '끝(tip)'일 때만 공유 배열에 그대로 append(뮤테이션), 이미 다른 코드가 그 지점 이후로 체인을 확장해버렸다면 배열을 복사한 뒤 새 `Pipe` 객체를 반환"하는 **copy-on-write - 방식** — 이건 이 문서의 "mutate-in-place vs `fromState`" 긴장을 실제로 - 풀어보려 한 유일한 시도라 **quad-v2에서 제대로 다시 설계해볼 만한 후보**. - 단, 원본은 "내가 지금 유일한 tip인가" 체크에 소유권/버전 관리가 전혀 없어서 - 경쟁 상황에 취약했고 테스트/실사용 검증도 없었음 — **그대로 베끼지 말고, - 같은 아이디어를 소유권 가드를 제대로 넣어 재설계할 것.** + 방식** — 한때는 이 문서의 "mutate-in-place vs `fromState`" 긴장을 풀어보려 + 한 유일한 시도로서 다시 설계해볼 후보였으나, **아래 "종합"에서 최종적으로 + 폐기됨** — `state(state)` 조합 모델이 소유권/버전 가드 없이도 같은 문제를 + 더 간단히 풀어서 이 절충안 자체가 불필요해짐. 원본이 갖고 있던 진짜 결함 + (소유권/버전 관리 없이 경쟁 상황에 취약, 테스트/실사용 검증도 없었음)은 + 기록으로만 남김. - **`Depend(...)` 액션** — 계산값에는 관여하지 않고 오직 "이 소스가 바뀌면 다시 계산하라"는 추가 의존성만 등록하는 값-투명(value-transparent) no-op 액션. 작지만 깔끔한 아이디어라 이름 그대로 채택할 만함. @@ -472,7 +494,7 @@ Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근 인덱스라 지금 quad-v2 스코프 밖 — Instance가 아닌 데이터에 태깅이 필요해질 미래 시나리오를 위한 참고 자료로만 기록. - **Store/State 전파 모델, 라이프사이클 — 둘 다 재검토 후 기존 확정 유지** - (아래 "Store/State/Source 온톨로지" 절의 "PA님 코드와의 교차검증" 참고). + (위 "Store/State/Source 온톨로지" 절의 "PA님 코드와의 교차검증" 참고). ## 남은 열린 질문 (`.claude/question.md`에도 취합) @@ -493,9 +515,12 @@ Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근 검증 대상). **해소된 것**: "Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 -필요한가"는 재검토 결과 질문 자체가 성립 안 함으로 결론 — State/Source -그래프 구독이 전부 weak-keyed GC-native(명시적 `dispose()` 호출이 아예 없음, -`base/lifecycle-pattern.md`의 GC 위임 원칙 재사용)라 "같은 걸 두 번 해제"할 -행위 자체가 존재하지 않음(GC는 멱등). "`:Compute`가 with한 값을 어떻게 +필요한가"는 재검토 결과 질문 자체가 성립 안 함으로 결론 — 두 가지 독립적인 +이유로 이중 해소됨. (1) 애초에 그런 경우를 만들지 않기로 확정(위 "Store가 +Store를 저장 가능한가" 절, 2026-08-04 6차 — Store는 Source에 준하는 "시작점" +이라 다른 반응형 값을 담아 자동 연결되는 용도로 안 씀). (2) 설령 발생해도 +State/Source 그래프 구독이 전부 weak-keyed GC-native(명시적 `dispose()` 호출이 +아예 없음, `base/lifecycle-pattern.md`의 GC 위임 원칙 재사용)라 "같은 걸 두 번 +해제"할 행위 자체가 존재하지 않음(GC는 멱등). "`:Compute`가 with한 값을 어떻게 읽는가"/"emit 필요 여부"도 전파 모델 확정으로 해소, `RobloxFactory` 중복 호출/충돌 시나리오·인스턴스 생성/이벤트 네이밍도 위 절에서 전부 확정. diff --git a/.claude/base/comparison-fusion-vide.md b/.claude/base/comparison-fusion-vide.md index 5a90494..bbac9a6 100644 --- a/.claude/base/comparison-fusion-vide.md +++ b/.claude/base/comparison-fusion-vide.md @@ -49,7 +49,7 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자 | 축 | Fusion | Vide | quad-v2 시사점 | |---|---|---|---| -| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | "Store는 값 자체에 항상 eager 발화, cleanup이 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-cleanup 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. | +| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | ⚠️ **[정정] 아래 서술은 리서치 당시(2026-08-03 이전) 검토 방향이며 이후 뒤집힘 — 최종 확정은 `base/bind-system-plan.md`의 "전파 모델 확정" 절 참고**(push-invalidate는 신호만 쏘고 값은 안 실음, 재계산은 `Get()` 시점 pull-recompute로만, Fusion식 eager 노드·생성순 정렬은 아예 채택 안 함 — quad엔 그런 다단계 즉시 재계산이 필요한 소비자가 없다는 판단). 당시 스냅샷 원문: "Store는 값 자체에 항상 eager 발화, retract(구 cleanup)가 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-retract 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. | | 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). | | 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`base/bind-system-plan.md`). | diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index 882c996..962a2ab 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -1,6 +1,6 @@ # 라이프사이클 패턴 — rbvm의 `Connected` + GC 관용구 채택 -**상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(cleanup) 전략의 원본. +**상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(`retract`) 전략의 원본. 완료 개념 없음, 구현하면서 세부 조정 있을 수 있음. ## 배경 @@ -42,7 +42,7 @@ rbvm은 실제 Roblox Instance의 파괴를 감지하는 지점을 단 하나로 플래그를 그 콜백에서만 true로 뒤집음. `AncestryChanged`나 폴링 방식은 안 씀. quad-v2도 동일: 인스턴스 라이프사이클 훅 지점은 `Destroying` 하나로 통일. -### 3. 정리(cleanup)는 기본적으로 GC에 위임, 예외적으로만 즉시(eager) +### 3. 정리(`retract`)는 기본적으로 GC에 위임, 예외적으로만 즉시(eager) rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private 데이터를 저장 — 홀더 객체를 아무도 안 들고 있으면 그 private 레코드도 자동으로 사라짐. @@ -51,13 +51,17 @@ rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private 전체가 통째로 죽을 때의 순서 있는 dispose 훅. **quad-v2 원칙: 기본은 GC 위임, 즉시 정리는 "안 끊으면 죽은 참조를 순회하게 되는 작은 포인터"류에만 국한.** -### 4. Signal 자체는 커스텀 구현체를 그대로 재사용 가능 +### 4. (참고 기록) rbvm의 Signal 자체는 재사용 가능한 범용 emitter였음 — 실제로는 채택 안 함 `signal.luau`의 `Signal`/`Connection` 클래스는 rbvm 프록시 시스템에 의존하지 않는 범용 이벤트 emitter임 (`Connect`/`Once`/`Wait`/`Fire`/`Destroy`, -`IsInited`/`OnInit`/`OnUninit` 지연 활성화 훅 포함). **단, 사용자 원 메모에는 -"시그널 자체 구현은 아닌듯... 콜백 정도로도 충분"이라고 되어 있어 서로 상충함** -— 아래 열린 질문 참고. +`IsInited`/`OnInit`/`OnUninit` 지연 활성화 훅 포함). 사용자 원 메모에는 +"시그널 자체 구현은 아닌듯... 콜백 정도로도 충분"이라는 언급이 있어 한때 +이 문서 초안 단계에서 상충하는 것처럼 보였으나, **이 질문은 2026-08-04 +검증 라운드에서 최종 확정으로 재확인됨 — 더 이상 열린 질문 아님** +(`base/architecture.md` 11번 항목도 동일하게 명시). 결론은 아래 "확정: Signal +클래스는 안 만든다" 절 참고 — 커스텀 `Signal`/`Connection` 클래스는 만들지 +않고, 콜백 + `Connected` 계산 속성만 채택한다. ### 5. rbvm에서 그대로 가져오면 안 되는 것 (버그 발견됨) @@ -97,8 +101,9 @@ Destroy되면 그 대상에 묶인 것들(Tween 등)도 자연히 죽은 상태 이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전 소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/ -tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 전부 `retract`로 -갱신됨(이름 변경 근거는 아래). +tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 대부분 `retract`로 +갱신됨(이름 변경 근거는 아래) — 잔여 표기 확인은 진행 중, 해당 문서들은 +각자 별도로 정리될 예정. ## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요 @@ -180,4 +185,5 @@ Roblox 엔진 자체가 Destroy 시 Tag/Attribute/실행 중인 Tween을 전부 쉬움) — 실제 의미는 "이전에 적용한 처리를 무른다/멈춘다"이므로 **`retract`** 로 통일. (`revert`, `rescind`도 검토했으나 `retract`가 "이전에 취한 조치를 철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를 -이룸.) 모든 문서에서 이 이름으로 갱신. +이룸.) 대부분의 문서에서 이 이름으로 갱신됨 — 잔여 "cleanup" 표기가 남은 +문서가 있을 수 있으며, 그 확인/정리는 진행 중. diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md new file mode 100644 index 0000000..c1273bf --- /dev/null +++ b/.claude/base/modifier-plan.md @@ -0,0 +1,145 @@ +# Modifier 설계 (정적 merge, immutable 체이닝) + +**상태**: base — 핵심 메커니즘(런타임 plug 아님/정적 merge, immutable +값+clone 기반 체이닝, 이중 setter)은 2026-08-04 세션 채팅 논의로 확정. 남은 +건 getter 정확한 이름뿐(구현 단계). Modifier가 컴포넌트 경계를 어떻게 +통과하는지(다중 루트, 상속 방식)는 별개 문제로 +`research/component-composition-plan.md`의 열린 질문에 남음 — 이 문서는 +"Modifier 값 자체가 어떻게 동작하는가"만 다룸. + +## 문제 + +`base/architecture.md` 7번 항목("Style(Default) 시스템 폐기, modifier +지향")이 방향만 정하고, 실제 메커니즘은 미정이었음: 핸들러 레지스트리에 +넣을 것인가, 여러 modifier가 같은 키를 건드리면 어떻게 되는가, 트리를 +타고 내려가며 조금씩 변형되는 modifier(예: 문서 뷰어의 TextStyle 상속)를 +어떻게 안전하게 다룰 것인가. + +## 확정된 결론 + +### 1. 런타임 pluggable 핸들러 아님 — 정적 merge + +Modifier는 `isHandlable`/`priority`/`process`/`retract` 핸들러 레지스트리에 +안 들어감. 그냥 평범한 테이블(데이터)을 보유하는 값이고, 디스패치 들어가기 +전에 한 번 평탄화(flatten)돼서 최종 props 테이블에 합쳐짐. 이유: 런타임 +pluggable로 만들면 여러 modifier가 반응형으로 같은 키를 계속 다투는 CSS +cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드 변경은 전체 +교체, 부분 오버레이 없음"(`base/architecture.md` 3번) 원칙과 충돌함. + +### 2. Merge 우선순위: 배열 순서와 인라인은 독립된 두 규칙 + +`Frame { modifier1, modifier2, Name = ... }` 평탄화 시: +(a) 배열에 나열된 modifier들끼리는 순서상 나중 것이 우선. +(b) 명시적 키(인라인)는 modifier가 뭘 하든 무조건 우선. +Lua 테이블 리터럴은 배열 파트/해시 파트 사이에 소스 텍스트 순서를 보존하지 +않으므로 "순서상 나중이 이긴다"는 단일 규칙만으로는 구현 불가 — 반드시 두 +규칙으로 쪼개야 함. + +### 3. Immutable 값 + clone 기반 체이닝 + +컴포지션 트리를 타고 내려가며 조금씩 변형되는 modifier(문서 뷰어에서 상위 +TextStyle을 상속해 타이틀만 1.2배 키우는 경우 — Jetpack Compose의 +`TextStyle.merge()`/`CompositionLocal`과 동일한 use case)는 특히 위험함 — +mutable하게 구현하면 같은 modifier 레퍼런스를 공유하는 형제 서브트리가 +오염되거나(한쪽이 mutate하면 다른 쪽도 영향받음), 재렌더 시 값이 누적 +드리프트하는 버그가 생김(`.claude/question.md` 초기 논의의 "원본 테이블 +덮어쓰기/루프 깨짐" 우려와 동일 클래스). + +**해결**: 모든 변환 메소드(`:FontSize(...)`류 체이닝)는 내부에서 +`table.clone(self)`로 새 테이블을 만든 뒤 필드만 덮어써 반환 — 원본은 +절대 mutate하지 않음. 별도의 제네릭 clone 콤비네이터 타입 +(`modifier<>(modifier):Set` 류 아이디어)은 기각 — 그런 타입을 만들면 +`base/architecture.md` 3번의 "복사 구현 지양, 필요한 곳만 팩토리 함수로 +명시적 복사" 원칙을 다시 재작업하는 셈이라, 각 변환 메소드 자체가 그 원칙을 +따라 알아서 최소한만 복사하면 충분. + +**성능**: Luau `table.clone`은 native shallow-copy라 modifier 크기(보통 +한 자리~여남은 개 필드) 기준 비용 무시 가능, 렌더/컴포지션 타임에만 +발생(프레임마다 도는 게 아님). State가 이미 `:With`/`:Compute`마다 새 +노드를 할당하는 것과 같은 급의 비용이라 일관되고, mutable+문서화 경고보다 +오염 버그를 원천 차단하는 쪽이 라이브러리 복잡도/사용자 편의 양쪽에서 +낫다고 판단 — **immutable 기본으로 확정**. + +### 4. Setter는 리터럴 값과 변환 함수 둘 다 받음 + +`:FontSize(value)`(리터럴) / `:FontSize(function(current) return +current*1.2 end)`(변환 함수) 둘 다 지원 — 한 줄로 끝내고 싶을 때는 콜백, +여러 줄로 풀어쓰고 싶을 때는 현재 값을 getter로 꺼내 계산 후 리터럴로 +다시 넣는 스타일 둘 다 인체공학상 필요하다고 판단. + +변환 함수는 State의 `:Compute`처럼 lazy State 핸들을 넘길 필요가 없음(*필드가 +순수 데이터인 일반적인 경우에 한해* — 필드가 State일 때의 예외는 아래 참고). +계산 비용 자체가 없는 순수 데이터라면 콜백엔 그냥 raw 현재 값을 즉시 넘기면 +충분(State의 self-lazy-핸들 문제와는 다른 카테고리). + +**내부 구현**: `__real` 같은 별도 래퍼는 불필요해 보임 — 데이터를 테이블에 +직접 두고 메소드는 공유 메타테이블 `__index`로 붙이면, `table.clone`이 +메타테이블까지 그대로 복사해주는 Luau 동작 덕분에 클론해도 체이닝이 안 +끊김. flatten도 그 테이블 필드를 직접 읽으면 됨. + +**Getter 정확한 모양은 미정** — `mod:Get("FontSize")` 같은 전용 메소드로 +할지, 아니면 Store/DI 관습처럼 dot-access(`mod.fontSize`) 자체가 읽기 +경로를 겸하게 해서 별도 `:Get()`이 아예 불필요하게 할지는 구현 단계에서 +확정. **다만 getter의 동작 자체은 확정**: 필드가 State면 getter 호출이 +곧 관측이라 그 순간 계산되어 확정된(더 이상 반응하지 않는) 값이 반환됨 — +`base/bind-system-plan.md`의 "관측해야 실체화된다" 전역 원칙 그대로 적용 +(아래 참고). + +### 4-1. 필드가 State일 수도 있음 — Setter가 State/plain 여부로 분기 + +`architecture.md` 7번 항목이 "함수형 modifier가 store 바인드를 받을 수도 +있음"이라고 이미 언급한 대로, Modifier 필드는 plain 값뿐 아니라 State일 +수도 있음(예: 상위에서 내려온 테마 색상이 Store에 바인드된 반응형 값). +이 경우 위 4번의 setter가 그대로 통하려면, **현재 저장된 필드 값이 State냐 +plain이냐에 따라 setter 내부 동작이 갈려야 함** — 새 개념이 아니라 State에 +이미 있는 lazy/`:Compute` 체이닝을 그대로 재사용하는 것뿐: + +| 현재 필드 | 인자 | 동작 | +|---|---|---| +| plain | 리터럴 | clone 후 그 값으로 덮어씀 | +| plain | 함수 | clone 후 즉시 호출해 나온 값으로 덮어씀(현재 값이 raw로 넘어감) | +| **State** | **리터럴** | clone 후 **State를 통째로 리터럴로 덮어씀 — 의도적으로 반응성이 끊김**(Store의 "부분 오버레이 없음, 전체 교체" 원칙과 같은 결) | +| **State** | **함수** | clone 후 `field:Compute(fn)`으로 **새 파생 State**를 만들어 대입 — 반응성 유지, State의 기존 `:Compute` 메커니즘에 그대로 위임 | + +즉 함수형 셋터는 필드가 State일 때 반응성을 보존하고, 리터럴 셋터(혹은 +getter로 꺼내 계산 후 리터럴로 다시 넣는 멀티라인 스타일)는 그 순간 값을 +확정시켜 반응성을 끊음 — 이 차이는 사용자가 인지하고 골라 쓰는 것으로 +문서화. + +### 4-2. Modifier는 소유권/유일성 제약이 없음 + +Modifier는 자식(child)을 담지 않음 — 마운트 정체성이 없는 순수 값. 그래서 +어떤 컴포넌트가 특정 modifier를 실제로 적용하든 안 하든, 또 같은 modifier를 +트리 여러 곳에 반복 적용하든 에러가 나지 않고 상관없음(Ref나 Slot 자식처럼 +"정확히 한 곳에만 마운트돼야 한다"는 소유권 제약이 이들에게는 있지만 +Modifier에는 없음). + +### 5. 타입 출처는 이미 확정된 dot-access 관습 재사용 + +"누가 modifier에 타입을 붙여주냐"는 새 문제가 아니라, Store/인스턴스 생성에 +이미 적용한 "정적으로 알려진 건 dot-access, 동적인 건 문자열 폴백" 프로젝트 +전역 관습(`base/bind-system-plan.md` "타입 추론 문제" 절)을 그대로 적용하면 +됨 — `Modifier.Rounded(8)`/`Modifier.FontSize(...)`처럼 DI 쪽 "제네릭 +생성자 함수 하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용. +(주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은 +PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/bind-system-plan.md` +"이벤트 바인딩 정정" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스 +생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.) + +### 6. State/Pipe 쪽엔 영향 없음 — 이미 있던 결정의 재확인일 뿐 + +Modifier가 immutable해야 하는 이유(변환마다 clone)와 State가 이미 +"`:With`/`:Compute`마다 새 노드를 만든다"(`base/bind-system-plan.md` 2차 +라운드 확정)로 확정해둔 이유는 같은 클래스의 문제(공유 mutable 상태로 인한 +오염 방지)임을 이번 논의에서 재확인했을 뿐 — State/Source 온톨로지 자체엔 +변경 사항 없음. 파이프 분기(`:With(...):Compute(fn)`)는 이미 코드에 +명시적으로 쓰는 구조라 "암묵적 분기"가 애초에 존재하지 않음 — 새로 결정할 +것 없음. + +## 열린 질문 (`.claude/question.md`에도 취합) + +- Getter 정확한 이름/모양(`:Get(key)` vs dot-access 겸용) — 후순위, 구현 + 단계에서 다른 세부 API 이름들과 같이 확정 가능. +- Modifier가 컴포넌트 경계를 어떻게 통과하는지(다중 루트, 상속 방식)는 + `research/component-composition-plan.md`에서 계속 다룸 — 이 문서가 다루는 + "값 자체의 동작"과는 별개 문제. diff --git a/.claude/base/module-lifecycle-plan.md b/.claude/base/module-lifecycle-plan.md index 927b691..4ed563e 100644 --- a/.claude/base/module-lifecycle-plan.md +++ b/.claude/base/module-lifecycle-plan.md @@ -53,9 +53,15 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류 - **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은 이름 붙은 체이닝 연산(named modifier)은 명시적으로 안 만들기로 확정, 대신 일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한 - 연산들은 오히려 일관성을 해친다"는 게 이유. + 연산들은 오히려 일관성을 해친다"는 게 이유. (주의: 아래의 v2 `:With(...)`는 + 이름만 같을 뿐 여기서 안 만들기로 한 v1의 `:With`와는 다른 연산임 — v1은 + "함수/테이블에서 값을 가져오는" 가공 연산이었고, v2는 그냥 "여러 State를 + 의존성으로 모으는" 수집 연산.) - **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency - array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `base/bind-system-plan.md`의 남은 열린 질문 참고. + array)은 있으면 좋겠다는 요청이었고 — **API 시그니처도 확정됨**: + `:With(...)`로 의존성을 모으고 `:Compute(fn)`으로 파생 State를 만드는 + 형태, 상세는 `base/store-semantics.md`의 "여러 스토어 값을 묶어 처리하는 + 것" 절 참고. - `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는 잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무 처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이 @@ -93,6 +99,6 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. **중복 호출 가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로 재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 — - 바로 아래 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()`가 + 바로 위 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()`가 생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로 스코핑됨, 별도 재설계 불필요. diff --git a/.claude/research/purity-and-effects-plan.md b/.claude/base/purity-and-effects-plan.md similarity index 90% rename from .claude/research/purity-and-effects-plan.md rename to .claude/base/purity-and-effects-plan.md index b8d4e5d..0653768 100644 --- a/.claude/research/purity-and-effects-plan.md +++ b/.claude/base/purity-and-effects-plan.md @@ -1,8 +1,9 @@ # 컴포넌트 순수성이 아니라 "이식성" 문제 (재정의됨) -**상태**: research — 사용자 확인 완료로 문제 자체는 명확해짐, 남은 건 문서화 -강도 정도. 원본: `.claude/initreq/raw-userinput.md` "순수함수에 대한 범위를 -정할 필요가 있음" / "진짜 부작용은 외부에 만들어버린다" 절. +**상태**: base — 확정됨(2026-08-04 세션에 `research/`에서 승격). 남은 건 +가이드 문서 내 배치 위치 정도로 기술적 결정 사항은 없음. 원본: +`.claude/initreq/raw-userinput.md` "순수함수에 대한 범위를 정할 필요가 있음" / +"진짜 부작용은 외부에 만들어버린다" 절. ## 정정: "순수함수 여부"가 아니라 "이식성(portability)" 문제였다 diff --git a/.claude/base/quad-v1-architecture.md b/.claude/base/quad-v1-architecture.md index 322e1c1..bd14cb8 100644 --- a/.claude/base/quad-v1-architecture.md +++ b/.claude/base/quad-v1-architecture.md @@ -24,7 +24,8 @@ Mount(ScreenGui, Frame {...}) `Class.Extend()`로 재사용 컴포넌트(`Init/Render/AfterRender/Getter/Setter/ UpdateTriggers/Unload`) 정의 가능. `Store.GetObject(id)`류 id 기반 전역 조회는 -v2에서 태그 시스템으로 대체 예정(`base/store-and-tags.md` 참고). +v2에서 대체될 예정 — Ref 도입과 네임스페이싱 판단까지 포함해 최신 상세는 +`base/architecture.md` 5번 항목 참고. ## 핵심 내부 동작 요약 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index ec266e6..8a143b7 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -65,14 +65,24 @@ Slot은 하나의 instance 안에 여럿 존재할 수 있다. 전부 하나의 편하다는 방향. **기울어진 결론**: 별도 "Named Slot" 개념 없이, store나 파라미터로 넘기고 그게 그냥 ref처럼 바인드되는 모양. +**상태 확인(2026-08-04 문서 정리 시점)**: 이 방향은 아래 "열린 질문" 절이나 +`.claude/question.md`의 확정 목록 어디에도 명시적으로 흡수된 흔적이 없음 — +아직 정식 확정 절차(`AskUserQuestion` 등)를 거치지 않은 것으로 보임. **열린 +질문으로 유지**, `.claude/question.md`에도 반영 필요. + ## Slot과 Store 바인드의 관계 (`retract` 순서) Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract`(구 cleanup, `base/lifecycle-pattern.md` 참고) 핸들러가 필요함 — 한번 넘어간 slot 요소가 나중에 `retract`되면 삭제되는지, 아니면 "부모의 소유이니 부모가 처리"해야 -하는지 검토 필요. **기울어진 결론**: 부모가 정리 정도만 미리 수행하고 다시 -`process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 스스로 정리를 -실행하는 게 아니라). +하는지 검토 필요. **기울어진 결론(잠정안, 이후 정정됨)**: 부모가 정리 정도만 +미리 수행하고 다시 `process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 +스스로 정리를 실행하는 게 아니라). + +> **정정(2026-08-04 검증 라운드)**: 위 "부모 위임" 잠정안은 이후 **폐기** +> 쪽으로 정정됨 — 아래 "확정" 절과 `.claude/question.md`("Slot의 `retract` +> 동작이 '부모 위임' 잠정안에서 '폐기(옮기지 않음)'로 확정") 참고. 이 문단은 +> 검토 과정의 히스토리로만 남겨둠, 현재 유효한 동작 아님. 이건 `base/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정 모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index 63b3b42..c48e758 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -1,7 +1,8 @@ # Store 의미론 — 부작용 허용, State는 Store 위의 조합 가능한 캐시 레이어 -**상태**: base — 부작용 허용/Store 문법 부분은 확정. State/Source 온톨로지는 -2026-08-04 검증 라운드에서 새로 열린 진행 중인 설계 스레드(`base/bind-system-plan.md` 참고). 원본: `.claude/initreq/raw-userinput.md` +**상태**: base — 전부 확정. State/Source 온톨로지는 2026-08-04 검증 +라운드에서 새로 열려 같은 세션 2~4차 라운드에 걸쳐 확정까지 마침 — 최신 +상세는 `base/bind-system-plan.md` 참고. 원본: `.claude/initreq/raw-userinput.md` "store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절. ## Store는 부작용을 허용하는 게 기본 디자인 @@ -12,7 +13,7 @@ 다만 한 가지는 명확히 구분: **렌더 리턴 위에서 무언가를 observe하는 것은 그냥 부작용**이다 (`useEffect`와 유사한 것으로 문서화). 이건 "허용되는 부작용"이 아니라 -"당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`research/ +"당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`base/ purity-and-effects-plan.md`와 연결됨). **보강(2026-08-04 검증 라운드): 부작용은 심각도가 다른 두 갈래로 나뉜다.** @@ -23,7 +24,7 @@ purity-and-effects-plan.md`와 연결됨). 2. **경계를 넘는 부작용** — globalStore처럼 컴포넌트 바깥의 전역 상태를 다루는 경우. 게임 UI 특성상(스킬/주변 환경에 영향받는 UI 등) 완전히 막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을 - 가지면 이식성이 떨어짐(`research/purity-and-effects-plan.md`와 연결). + 가지면 이식성이 떨어짐(`base/purity-and-effects-plan.md`와 연결). **해소됨(2026-08-04 2차 라운드)**: state를 옵저빙해서 나온 결과로 slot에 `clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면 @@ -60,14 +61,14 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립 방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신 State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임 (`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `base/bind-system-plan.md`의 - "Store/State/Source 온톨로지" 절 참고 — **아직 완전히 결론난 설계는 아니고, - 구현 단계에서 더 다뤄야 할 진행 중인 스레드.** + "Store/State/Source 온톨로지" 절 참고 — **이 절 이후 2~4차 라운드에 걸쳐 + 전부 확정됨, 더 이상 진행 중인 스레드 아님.** -미해결로 남은 것: `:Compute`의 캐싱/무효화 전략(값이 바뀌었는데 듣는 소비자가 -없으면 연산을 미루는 dirty-flag 방식 등), Luau 타입 시스템에서 `store "key"` -같은 커링 호출이 오버로드 함수 타입으로 `state`를 정확히 추론하기 어려운 -문제(문자열 리터럴이 as-const로 좁혀지지 않는 문제) — 둘 다 열린 채로 -`base/bind-system-plan.md`에서 계속 다룰 것. +과거 "미해결로 남은 것"으로 적었던 두 항목도 모두 해소됨: `:Compute`의 +캐싱/무효화 전략은 push-invalidate(신호만)/pull-recompute(`Get()` 시점)로 +확정(`base/bind-system-plan.md` "전파 모델" 절), `store "key"` 커링의 타입 +추론 문제는 `store.key`(dot-access)를 1급 경로로 확정하며 해소(같은 문서 +"타입 추론 문제" 절, 3차 라운드). ## Store 값 설정 문법 — v1 인체공학 유지 (확정) @@ -92,8 +93,11 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립 `useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는 요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 — `base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의 -`:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않음** — -대신 일반 함수를 받아 처리. 최종 형태는 `:With(...)`로 의존성을 모으고 +`:Add`/`:With`/`:Tween`처럼 값을 직접 가공하는 이름 붙은(named) 체이닝 연산은 +만들지 않음** — 대신 일반 함수를 받아 처리. (주의: 아래의 v2 `:With(...)`는 +이름만 같을 뿐 v1의 `:With`와는 다른 연산임 — v1은 "함수/테이블에서 값을 +가져오는" 가공 연산이었고, v2는 그냥 "여러 State를 의존성으로 모으는" 수집 +연산.) 최종 형태는 `:With(...)`로 의존성을 모으고 `:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b}, fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. diff --git a/.claude/question.md b/.claude/question.md index bd2dee3..1cd4770 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -1,194 +1,89 @@ -# 확인/결정 필요 목록 (전체 취합) +# 확인/결정 필요 목록 -각 plan 문서에 흩어진 "사용자 확인 필요" 절의 취합본. **막고 있는 항목은 -거의 없음** — 대부분 합리적 기본값/방향을 잡아두고 research 단계에 머물러 -있음. 사용자가 Lua/Roblox 엔진에 대해 깊이 아는 사람이라는 전제로, 우선순위 -높은 것부터 정렬. +**2026-08-04 세션 말미에 전체 재정리함.** 예전엔 라운드(1차~6차)별로 문서가 +계속 쌓이면서 순서가 시간순도 우선순위순도 아니게 됐고, 이미 해소된 라운드 +기록이 새로 열린 질문보다 위에 있는 등 혼동을 유발했음(문서 감사에서 발견). +그 상세 히스토리는 지우지 않았음 — git log로 이 파일의 이전 버전을 보거나, +각 `base/`/`research/` 문서 안의 라운드 표시("2026-08-04 3차 라운드" 등)를 +따라가면 그대로 남아있음. 이 문서는 이제 **"지금 열려있는 것" 우선으로만** +구성. -## 2026-08-04 5차 라운드 완료 — 소스 트리 구조 확정 +## 지금 열려있는 것 (우선순위순) -`.claude/base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고. -`base/bind-system-plan.md`/`base/module-lifecycle-plan.md`/`base/slot-plan.md`가 -이 라운드에서 `research/`에서 승격됨. +### 1. [최우선] 컴포넌트 경계에서 modifier/Ref가 어떻게 전달되는가 -- **패키징**: 최종 목표는 다중 wally 패키지지만, 지금 Luau 툴링(wally 타입 - 단절, `luau-lsp` 심볼릭 링크 해석 문제)이 불안정해서 당장은 모놀리식 — - `Sleitnick/RbxUtil` 패턴(루트 통합 개발/테스트, 서브폴더마다 자체 - `wally.toml`) 채택. `.luaurc` alias는 런타임 require에서 아직 엔진 미지원 — - 편집기 경험용으로만 사용, 런타임 require는 상대경로. -- **패키지 경계**: `quad-base` = Store/State/Source 온톨로지+전파 **+** - pluggable 디스패치 엔진(`process`/`retract`, 핸들러 계약, `LifetimeHandle`/ - `PerInstanceState` 인터페이스, Ref, Slot 코어 재조정 로직) — 전부 - "인터페이스"로, 다른 엔진(GTK 등)에서도 재사용 가능해야 한다는 전제. - `quad-roblox` = 위 인터페이스의 실제 구현체(`RobloxFactory`, Property/Event/ - Attribute/Tag/Tween/Slot 적용 핸들러, `DI` 인스턴스 생성자) — 이유: 엔진마다 - 큰 구현을 중복하지 않기 위함(rbvm의 relation 통합 시도와 같은 동기). -- **Slot 패키지 경계**: 재조정 로직(add/remove/clear)은 base, 실제 Instance - `Parent`/`Destroy` 조작은 roblox의 핸들러가 담당 — `base/slot-plan.md` - "base/roblox 패키지 경계" 절. -- **새 핸들러 필요성 확인**: `k:number, v:Instance`(중첩 인스턴스를 직접 - 자식으로 넣는 경우, `Frame { Frame {} }`)를 위한 `InstanceChild` 핸들러가 - Slot과 별개로 필요 — `quad-roblox/src/Handlers/InstanceChild.luau`. +사용자가 "지금 quad에서 가장 문제되는 부분"으로 직접 지목. 컴포넌트가 +플레인 함수이고 반환하는 루트가 여러 개(혹은 Slot으로 갈라지는 구조)일 때, +호출부가 넘긴 modifier/Ref가 "어느 루트로 가야 하는지" 모호해지는 케이스가 +있음. Jetpack Compose는 언어 강제가 아니라 "컴포저블은 `modifier` 파라미터를 +받아 루트에 적용해야 한다"는 순수 관례(+린트)로 풂 — quad도 비슷한 관례 +기반으로 갈 수 있어 보이나 다중 루트 케이스는 미정. -## 2026-08-04 검증 라운드 완료 +**주의**: modifier "값 자체"가 어떻게 동작하는지(정적 merge, immutable+clone +체이닝, State 필드 지원)는 이미 완전히 확정됨(`base/modifier-plan.md`) — 이 +질문은 그것과 별개로 "경계를 어떻게 통과하느냐"만 다룸, 혼동하지 말 것. -아래 "확정됨" 절 전체(architecture.md 14개 항목, lifecycle-pattern.md, -store-semantics.md, bind-system-plan.md, module-lifecycle-plan.md, -slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오 재검증 -완료 — 대부분 그대로 확인됐지만, 아래는 검증 과정에서 실제로 문서가 수정된 -항목: +→ 상세/배경: `research/component-composition-plan.md`. -- **`State` 프리미티브는 "안 만든다"가 아니라 실제로 필요함** — 정정 완료, - `base/store-semantics.md` 참고. **이 결과로 Store/State/Source 온톨로지 - 전체가 새로운 열린 설계 스레드로 떠올랐음** — 아래 "최우선 새 열린 질문" - 참고. -- Slot의 `retract` 동작이 "부모 위임" 잠정안에서 "폐기(옮기지 않음)"로 확정 - — `base/slot-plan.md`. -- quad2-try의 `Pipe` copy-on-write 후보는 사실상 폐기, `state(state)` 조합 - 모델로 대체 — `base/bind-system-plan.md`. -- `Connected` 체크/GC 위임/`Destroying` 훅 관련 뉘앙스 보강(엔진별 인터페이스 - 주입, quad는 rbvm보다 즉시정리 필요성이 낮음) — `base/lifecycle-pattern.md`. -- base 유틸(per-instance 저장소, 생명 바인드)은 인터페이스만, 실제 구현은 - `RobloxFactory(BaseModule)`류 백엔드 팩토리가 주입 — `base/bind-system-plan.md`. +### 2. 용어 정리 (사용자 요청, 진행 중) -## 최우선 새 열린 질문 (검증 라운드에서 새로 터져나옴) +사용자 원 메모: "quad는 register라던가 좀 부정확하거나 느낌이 바로 와닿지 +않던 용어들이 많음 — 전체적 용어를 보고 생각해볼래? 제안을 줘, 나도 같이 +볼게." 1차 제안 완료, 아래는 우선순위순 요약 — 최종 판단은 사용자와 계속 +논의 필요: -**전부 확정됨** — 아래 "2026-08-04 3차 라운드" 절 참고. 이 섹션에 새 항목이 -생기면 여기 추가. +- **`State`(1순위, 위험도 높음)**: 지금 정의는 "읽기 전용, 파생/캐시 뷰"인데 + React/Vue 등 업계 전반에서 "state"는 거의 항상 "쓸 수 있는 로컬 슬롯"을 + 뜻함 — 처음 보는 사람이 정반대로 오해할 위험이 큼. `Computed`/`Derived` + (Vue `computed()`, Svelte 5 `$derived`가 정확히 같은 의미로 씀)가 실제 + 의미에 더 맞아 보임. 단, v1의 "register"를 이미 한 번 "State"로 리네임한 + 지 얼마 안 됐다는 점 고려 필요. +- **`DI`(Declarative Instance, 1순위)**: "Dependency Injection"의 업계 + 표준 축약어와 완전히 겹침 — 4차 라운드에서 이미 한 번 실제로 오해가 + 있었던 전례(`base/bind-system-plan.md`의 "인스턴스 생성" 절 참고). +- **`PerInstanceState`(2순위)**: 핵심 프리미티브 `State`와 이름이 겹쳐서 + 실제로는 완전히 무관한 유틸(인스턴스별 weak-keyed 저장소)인데 혼동 + 유발 가능 — `PerInstanceStorage`/`InstanceData` 등 대안. +- **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가 + 다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이 + 헷갈릴 수 있음. +- **`CreatedRef`/`canExecute`(3순위, 사소함)**: `CreatedRef`는 과거분사형이라 + 생성자처럼 안 읽힘. `canExecute`는 실제로 "이 핸들이 아직 살아있나" + 확인인데 이름이 범용 권한 체크처럼 들림 — `isAlive` 쪽이 더 직접적. +- **이미 지나간 사례로 참고**: `register`(v1) → `State`(v2) 리네임은 + "모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든 + 셈 — 이번 정리에서 같은 패턴을 조심할 것. +- `Store`/`Source`/`Modifier`/`Ref`/`process`/`retract`/`isHandlable`은 + 업계 선례와 잘 맞거나 이미 신중하게 결정된 이름들이라 특별한 문제 없음. -## 2026-08-04 4차 라운드 완료 — PA님 실 코드(`initreq/artworks`) 교차검증 +### 3. 낮은 우선순위 -사용자가 실제 참고 코드를 공유(`.claude/initreq/artworks/`, PA님 작성) — -아래 두 항목이 3차 라운드 잠정안에서 정정됨, 나머지는 재검토 후 기존 확정 -유지: +- `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현 + 착수를 막지 않음. +- **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요** — + `base/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션 + 가이드 작성 시점에 필요. 지금은 그냥 백로그로만 기록. -- **"DI" = Declarative Instance**(Dependency Injection 아님) — 3차 라운드의 - 오해 정정. -- **이벤트 바인딩 정정**: `On.EventName` 도트액세스 안 씀 — PA님 방식(평범한 - 문자열 키 + `ReflectionService` 기반 자동 판별, `Frame { MouseButton1Click - = fn }`)으로 전환. Store의 `store.key`는 실질적 타입 이득이 있어 dot-access - 유지, 이벤트만 예외. -- **인스턴스 생성**: 2트랙(`DI.Frame`/`DI.New<>`) 대신 PA님 코드처럼 - 제네릭 생성자 함수 하나 + 자주 쓰는 클래스만 정적 필드로 미리 바인딩하는 - 더 단순한 모양으로 정정. -- **전파 모델(push-invalidate/pull-recompute)·라이프사이클(GC-native)은 - 재검토 후 기존 확정 유지** — PA님 코드가 반례처럼 보였으나(전자는 push-값 - 단순 pub-sub, 후자는 전부 수동 해제) 대등한 비교가 아니었거나(파생/합성 - 개념 자체가 없음) 지금 필요성이 없다는 게 사용자 판단. 라이프사이클은 - 나중에 하이브리드로 확장 가능한 여지만 기록. -- OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 보일러플레이트를 - 보여 오히려 보강됨. Instance 태그는 CollectionService 직접 사용 유지. +## 참고: 지금까지 확정된 것 (요약) -→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 -인체공학" 절, "Store/State/Source 온톨로지"의 "PA님 코드와의 교차검증" 절, -`base/lifecycle-pattern.md`의 "교차검증" 절. +전부 `base/`에 문서화되어 더 이상 열려있지 않음 — 상세 근거/논의 과정이 +필요하면 아래 문서를 열어볼 것(라운드별 세부 히스토리는 각 문서 안에 +"2026-08-04 O차 라운드" 식으로 표시돼 있음): -## 2026-08-04 3차 라운드 완료 — dot-access 관습 확정, RobloxFactory 가드 확정 (일부 4차 라운드에서 정정됨) - -- **dot-access를 프로젝트 전역 관습으로 확정**: "정적으로 알려진 것=필드 - 접근, 동적인 것=문자열 호출 폴백"이 Store(`store.key`/`store "key"`)와 - 인스턴스 생성에 적용됨 — **이벤트는 4차 라운드에서 예외로 정정**(위 참고). -- **`RobloxFactory` 재호출 가드 확정**: 같은 팩토리로 재호출 시 무시 - (no-op, hot-reload 안전), 다른 팩토리로 재호출 시 에러(유일 슬롯 충돌). - `New()`와는 인스턴스별 테이블 분리로 자연히 공존 — 재설계 불필요. (4차 - 라운드에서 변경 없음) - -→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 -인체공학" 절, "base 유틸은 인터페이스..." 절의 재호출 가드 부분. - -## 2026-08-04 2차 라운드 완료 — Store/State/Source 온톨로지 핵심 메커니즘 - -위 최우선 질문 중 "Store/State/Source 온톨로지 전체"와 "부작용이 slot 생존 -여부와 어떻게 연관되는가"는 `AskUserQuestion`으로 확인 완료, 더 이상 열려있지 -않음: - -- **전파 모델**: push-invalidate(신호만, 값 안 실음) / pull-recompute(`Get()` - 시점에만 재계산) — Fusion식 eager 노드·생성순 정렬은 안 만듦. -- **`:Compute`의 self 인자**: raw 값이 아니라 State 핸들 자체를 넘겨서 self도 - with한 값과 동일하게 lazy하게(`.value`를 실제로 읽을 때만 계산) 처리 — - 별도 `ComputeWithout` 불필요. -- **State는 쓰기 대상이 아님**: `.value`는 읽기 전용, 값 쓰기는 항상 Store의 - `__newindex`로만. `Source`는 Store 내부 디테일이 아니라 값 하나만 다룰 때 - 쓰는 별도의 가벼운 공개 프리미티브로 격상. -- **Slot 생존 확인**: 별도 메커니즘 없이 기존 "생명 바인드 유틸"의 - `canExecute`로 통일 게이트. -- **`store.key` dot-access 타입 추론 제안**: 3차 라운드에서 정식 확정됨(위 - "2026-08-04 3차 라운드 완료" 절 참고). - -→ 상세: `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 — -핵심 메커니즘 확정" 절, `base/store-semantics.md`, `base/lifecycle-pattern.md`. - -## 확정됨 (2026-08-03 질의응답 라운드, 더 이상 열려있지 않음) - -- **Store 책임 분리**: base가 `LifetimeHandle` 추상화 + store-bind의 재실행 - 로직(`process(inst,k,realv)` 재귀)을 소유, provider는 "언제 죽었다고 - 판단할지"(Roblox `Destroying` 등)만 결정. → `base/module-lifecycle-plan.md`, - `base/bind-system-plan.md` -- **Signal 클래스**: 안 만듦 — 콜백 + `Connected` 계산 속성만. → `base/ - lifecycle-pattern.md` -- **핸들러 계약**: `isHandlable`+`priority`+`process`+`retract` 4종 유지, - tbox식 세분화는 지금 안 함. → `base/bind-system-plan.md` -- **Ref**: 도입하되 용도는 "id 조회 대체"가 아니라 "외부 관리 instance를 - 점진적으로 마이그레이션/래핑하기 위한 직접 참조 획득". Tween 등 어떤 - 핸들러도 대상 획득에 Ref가 필요하지 않음(항상 `inst`를 직접 받음). - → `base/bind-system-plan.md` -- **`retract`(구 cleanup) 호출 시점**: 값 교체 시에만 호출, Destroy 시엔 - 호출 안 함(quad는 자신이 만든 instance의 생명주기 중간에 있지 않으므로 - destroy-time 정리 자체가 불필요/불가능). → `base/lifecycle-pattern.md` -- **핸들러 내부 상태 저장**: base가 범용 weak-keyed per-instance 저장 유틸 - 제공(모든 핸들러 재사용). → `base/bind-system-plan.md`, - `base/lifecycle-pattern.md` -- **Store 값 설정 문법**: `__newindex`(`myStore.key = v`) 유지, 괄호 생략 - 커링/`:` 체이닝 인체공학도 유지 — 바뀌는 건 내부 구현(팩토리 함수)뿐. - → `base/store-semantics.md` -- **Store의 named modifier(`:Add`/`:Mul` 등)**: 안 만듦 — 일반 함수를 받는 - 형태로 통일. → `base/store-semantics.md` - -## 추가 확정됨 (2번째 라운드) - -- **트윈 오버라이드 기본값**: 멈춤(Cancel), 새 트윈은 현재 보간된 값에서 시작. - 나머지 세 동작(오버라이드/삭제후재시작/끝점이동후재시작)은 옵션으로 선택 - 가능. → `research/tween-plan.md` -- **Slot 재마운트 에러**: 즉시 throw. → `base/slot-plan.md` -- **`CreatedRef` 콜백 타이밍**: 생성 시점/마운트 시점 둘 다 옵션으로 지원. - → `base/bind-system-plan.md` -- **여러 store 값 묶기**: `Store.Combine`류 포지셔널 인자 방식과 Vide식 암묵적 - 추적 둘 다 기각 — `:With(...)` + `:Compute(fn)`(fn은 with한 값을 포지셔널 - 인자가 아니라 클로저로 읽음) 방식으로 확정. Unix 파이프에서 영감받은 완전 - 합성 가능한 State 스트림이 이상향이나 기술적 난이도 미확정 — 과거 시도 - (`quad2-try/quad-core`) 리서치 진행 중. → `base/bind-system-plan.md` - -## quad2-try(이전 폐기된 시도) 리서치 완료 — 추가 확정 - -- **OOP 상속/`--&` 커스텀 파서/Slot 스텁은 확인대로 죽은 접근** — 절대 반복 - 금지, Slot은 from-scratch 설계 그대로 진행(재조사 불필요). -- **mutate-vs-`fromState` 긴장 관계**: quad2-try의 `Pipe` copy-on-write - 절충안(유일한 tip일 때만 뮤테이션, 아니면 복사)이 한때 유력 후보였으나 - **2026-08-04 검증 라운드에서 사실상 폐기로 재평가됨** — 별도 `Pipe` 타입 - 대신 State 자체가 파이핑 결합체이고 `state(state)`로 분기하는 쪽이 더 - 간단하다는 판단(위 "최우선 새 열린 질문"의 Store/State/Source 온톨로지 - 절로 흡수됨). -- **`Depend(...)` 액션, `:With` 네이밍**은 이전 시도에서도 지향했던 것과 일치 - — 그대로 채택. → `base/bind-system-plan.md` - -## 순수성/이식성, 기존 인스턴스 바인드 — 확인 완료, 낮은 우선순위로 유지 - -- **"순수함수" 문제는 실제로는 "이식성" 문제였음** — 재사용 의도 컴포넌트가 - 전역 store를 직접 참조하면 이식성이 깨짐(단일 페이지용 컴포넌트나 라이브러리 - 내부 전용 공유 상태는 문제 없음). 기술적 강제 안 함, 문서 경고 수준으로 - 확정. → `research/purity-and-effects-plan.md` -- **이미 생성된 인스턴스 재바인드**: 실제 요청한 사용자를 본 적 없지만 - `retract` 인프라가 이미 있어 미래에 자연스럽게 가능해질 여지가 있음 — - "미지원" 확정도, 착수도 안 함, 진짜 열린 가능성으로만 유지. → `research/ - existing-instance-bind-plan.md` - -## 급하지 않음, 여유 있을 때만 - -- 태그 시스템의 네임스페이싱 부재(라이브러리 간 충돌 가능성)를 얼마나 - 심각하게 볼지 — 지금은 "별도 네임스페이스 개념은 복잡도 대비 이득이 적다"는 - 판단으로 보류 중. → `base/architecture.md` 5번 항목. -- Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 실제로 필요한 - 상황이 있는지 — 구현 단계에서 실사례로 재검증. → `base/bind-system-plan.md` +| 주제 | 문서 | +|---|---| +| 전체 아키텍처 결정(디스패치 모델, DOMless, 태그/Ref, Signal 미채택 등) | `base/architecture.md` | +| Store/State/Source 온톨로지, 인스턴스 생성/이벤트 인체공학, Ref, 남은 API 이름 | `base/bind-system-plan.md` | +| Store 부작용 허용, `:With`+`:Compute`, dot-access 문법 | `base/store-semantics.md` | +| 프로바이더 패턴, bind/store 구현 책임 분리 | `base/module-lifecycle-plan.md` | +| Slot 재조정, 재마운트 시 throw, retract=폐기 | `base/slot-plan.md` | +| `Connected`+GC 라이프사이클 패턴 | `base/lifecycle-pattern.md` | +| Modifier(정적 merge, immutable 체이닝, State 필드 지원) | `base/modifier-plan.md` | +| 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` | +| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `base/comparison-fusion-vide.md` | +| v1 내부 동작 스냅샷 | `base/quad-v1-architecture.md` | +| 트윈 오버라이드(기본값 Cancel), 세부 옵션만 남음 | `research/tween-plan.md` | +| quad2-try(폐기된 이전 시도) 리서치 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW 전부 죽은 접근으로 확인, 반복 조사 금지 | `base/bind-system-plan.md` | --- 전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이 diff --git a/.claude/research/component-composition-plan.md b/.claude/research/component-composition-plan.md new file mode 100644 index 0000000..77f4185 --- /dev/null +++ b/.claude/research/component-composition-plan.md @@ -0,0 +1,110 @@ +# 컴포넌트화 (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/research/existing-instance-bind-plan.md b/.claude/research/existing-instance-bind-plan.md index c4ad3f7..d0b45f8 100644 --- a/.claude/research/existing-instance-bind-plan.md +++ b/.claude/research/existing-instance-bind-plan.md @@ -6,12 +6,12 @@ ## 문제 이미 생성된 Roblox Instance에 새로운 `{k=v}` 프롭 테이블을 나중에 바인드하는 -걸 허용할지. 허용하려면 이전 바인드를 끊는 처리가 필요한데, cleanup이 +걸 허용할지. 허용하려면 이전 바인드를 끊는 처리가 필요한데, retract가 구현되어 있어도 바로 지원하는 건 엔지니어링 비용이 높음. ## 기울어진 방향 -**UB로 두거나, 마일스톤(추후 구현)으로 미룬다.** cleanup이 이미 있고 store +**UB로 두거나, 마일스톤(추후 구현)으로 미룬다.** retract가 이미 있고 store 바인드도 우선순위 높은 플러그라면 이론적으로는 가능해 보이지만(핸들러 레지스트리가 이미 "우선순위 스캔 후 bind" 구조라 재바인드도 같은 경로를 타면 됨), 초기 구현에서 **우선순위를 낮게** 잡아야 함 — 문제 유무가 많을 수 있어서. diff --git a/.claude/research/tween-plan.md b/.claude/research/tween-plan.md index 7071a2c..591a512 100644 --- a/.claude/research/tween-plan.md +++ b/.claude/research/tween-plan.md @@ -1,7 +1,9 @@ -# Tween / 애니메이션 플러깅 (착수 전, 사용자와 상의 필요) +# Tween / 애니메이션 플러깅 (기본값 확정, 옵션 키 이름만 남음) -**상태**: research — 방향은 뚜렷하게 잡혀 있으나(라이브러리가 트윈을 직접 -구현하지 않는다) cleanup 순서/오버라이드 시맨틱은 미확정. 원본: +**상태**: research — 방향은 뚜렷하게 잡혀 있고(라이브러리가 트윈을 직접 +구현하지 않는다), `retract` 순서/오버라이드 기본값(Cancel)도 확정됨. 남은 건 +기본값 외 나머지 오버라이드 동작을 고르는 옵션 키의 정확한 이름/시그니처 +정도. 원본: `.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은 항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이 반응 그래프 안에 있는 설계는 명시적 반면교사 — `base/comparison-fusion-vide.md` @@ -79,7 +81,7 @@ Destroy 시점엔 아무 것도 안 함(라이프타임 `Connected` 체크로 ## 네임스페이스드 객체 (성능상 이유로 보류) 트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 — -TagService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페이스가 따로 있을 +CollectionService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페이스가 따로 있을 필요가 있는지는 미정(단, 위 정정으로 이 절 자체의 필요성이 낮아짐 — 핸들러가 이미 대상을 직접 받으므로 "나중에 이름으로 찾아서 트윈"할 필요 자체가 잘 없을 수 있음). diff --git a/CLAUDE.md b/CLAUDE.md index 86676d9..9c38224 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,48 +20,33 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터 길게 잡음. **지금은 설계/계획 단계이고 구현은 아직 시작 전** — 저장소 루트에 실제 소스 -코드(`src/` 등)가 없음. 2026-08-03에 확정됐던 핵심 아키텍처 결정들(Store -책임 분리, `process`/`retract` 디스패치 모델, Signal 미채택, Ref 역할, Store -문법 인체공학, 트윈 기본 오버라이드, Slot 재마운트 에러 처리, 순수성→이식성 -재정의 등)은 2026-08-04에 `AskUserQuestion`으로 하나씩 재검증까지 마쳐서 -확정 상태 — `.claude/question.md`의 "확정됨" 절 참고. 이전에 시도했다 폐기한 -v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치 완료 — OOP 상속/커스텀 -파서/Slot 스텁은 확인된 죽은 접근이라 반복 금지, `Pipe`의 copy-on-write -절충안은 한때 살려볼 후보였으나 **2026-08-04에 사실상 폐기로 재평가**됨(State -자체가 `state(state)`로 분기하는 쪽으로 대체). +코드(`src/` 등)가 없음. 핵심 아키텍처(Store 책임 분리, `process`/`retract` +디스패치 모델, Store/State/Source 온톨로지, 소스 트리 구조, Modifier 메커니즘, +컴포넌트=플레인 함수)는 전부 `.claude/base/`에 문서로 확정돼 있음 — 먼저 +`.claude/base/architecture.md`를 읽을 것. **단, "핵심 설계 질문이 더 이상 +없다"는 뜻은 아님** — 컴포넌트화(특히 modifier/Ref가 컴포넌트 경계를 어떻게 +통과하는지)는 사용자가 직접 "지금 quad에서 가장 문제되는 부분"으로 지목한 +채 아직 열려있음, 아래 "지금 할 일" 참고. -**Store/State/Source 온톨로지 및 관련 인체공학 질문은 2026-08-04 네 라운드에 -걸쳐 전부 확정됨**(사용자가 공유해준 실제 참고 코드 `.claude/initreq/ -artworks/`, PA님 작성, 로 4차 교차검증까지 마침) — push-invalidate/ -pull-recompute 전파 모델, `:Compute` self/with 인자를 둘 다 lazy State -핸들로 통일, State는 쓰기 불가(값 쓰기는 항상 Store의 `__newindex`), -`Source`는 Store와 별개인 독립 프리미티브로 격상, Slot 생존 확인은 기존 -canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추론 1급 -경로로(인스턴스 생성도 같은 관습, 단 이벤트는 PA님 방식인 평범한 문자열 -키+런타임 리플렉션으로 예외), `RobloxFactory` 재호출 가드(같은 팩토리=무시, -다른 팩토리=에러)까지 확정. 남은 건 정확한 API 표면 이름뿐 — -`.claude/base/bind-system-plan.md` 전체, `.claude/question.md`의 -"2026-08-04" 절들 참고. - -**소스 트리 구조도 확정됨(2026-08-04 5차 라운드)**: `bind-system-plan.md`/ -`module-lifecycle-plan.md`/`slot-plan.md` 모두 `research/`에서 `base/`로 -승격 완료. 모노레포(`quad-base`/`quad-roblox` 서브폴더, RbxUtil 패턴)로 -당장은 모놀리식 진행, 패키지 경계(디스패치 엔진까지 base가 인터페이스로 -소유)까지 확정 — `base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" -절 참고. 아래 "지금 할 일" 1번이 다음 단계(실제 스캐폴딩)를 명시. +이전에 시도했다 폐기한 v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치 +완료 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` copy-on-write 절충안은 확인된 +죽은 접근이라 반복 조사 금지(`base/bind-system-plan.md` 참고). ## 계획 문서 구조 `.claude/README.md`가 색인. 요약: - `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저 `.claude/base/architecture.md`를 읽을 것. -- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. +- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. 지금은 + `tween-plan.md`(세부 옵션만 남음), `existing-instance-bind-plan.md`(급하지 + 않음), `component-composition-plan.md`(**사용자가 최우선으로 지목한 열린 + 주제**) 세 개뿐. - `.claude/qa-request/`, `.claude/archive/`, `.claude/feedback/` — 구현 시작되면 쓰기 시작함, 지금은 비어있음. - `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox, - code-docker) + 원본 요청. **읽기 전용, `.gitignore`로 커밋 제외됨** — 내용을 - 다른 곳으로 옮기지 말고 항상 원본 그대로 둘 것. 리서치가 더 필요하면 이 - 폴더를 다시 파고들 것. + code-docker) + PA님 실 코드(`artworks/`) + 원본 요청. **읽기 전용, + `.gitignore`로 커밋 제외됨** — 내용을 다른 곳으로 옮기지 말고 항상 원본 + 그대로 둘 것. 리서치가 더 필요하면 이 폴더를 다시 파고들 것. - `.claude/question.md` — 사용자가 답해야 할 질문 전체 취합(우선순위순). - 루트 `HUMAN_TODO.md` — 사람만 할 수 있는 일(로컬 GUI 조작, 스케줄/루프 설정 등). @@ -72,7 +57,9 @@ canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추 컨텍스트 보호. 이미 완료된 v1/rbvm/tbox/Fusion/Vide 리서치 결과는 `.claude/base/`에 정리되어 있으니 중복 조사하지 말고 먼저 그걸 볼 것. - **병렬화 가능한 작업은 Agent 여러 개를 한 메시지에 동시 호출.** 서로 독립적인 - 파일/주제를 다루는 리서치나 구현 조사가 여기 해당. + 파일/주제를 다루는 리서치나 구현 조사, 또는 서로 다른 문서 파일을 고치는 + 문서 정리 작업이 여기 해당(단, 같은 파일을 동시에 고치는 에이전트를 병렬로 + 띄우지 말 것 — 충돌함). - **크리티컬한 설계 결정은 구현으로 밀어붙이지 말고 plan을 research/에 남긴 채 연기.** 사용자는 Lua/Roblox 엔진을 깊이 아는 사람 — 근거와 선택지를 문서에 정리해두면 사용자가 깨어있을 때 훑어보고 답해줄 것. `.claude/question.md`에 @@ -81,6 +68,12 @@ canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추 조사하게 되는 재작업을 막기 위함. `.claude/base/`로 승격, `.claude/qa-request/`로 이동, 또는 문서 자체를 갱신. code-docker/webmanager의 `.claude/` 관리 방식이 좋은 예시(`.claude/initreq/code-docker/webmanager/.claude/README.md` 참고). +- **문서가 쌓이면서 모순/중복/stale 마커가 생기기 쉬움 — 주기적으로 감사할 + 것.** 2026-08-04 세션에 실제로 전체 `.claude/` 코퍼스에서 이런 문제가 + 다수 발견되어 정리함(아래 "최근 세션 요약" 참고) — 여러 라운드에 걸쳐 + 같은 문서를 계속 고치다 보면 "정정됨" 표시가 원래 문장에 안 반영되고 + 방치되는 패턴이 반복되니, 큰 방향 전환이 있을 때마다 관련 문서 전체를 + 훑어 확인할 것. - **Roblox Studio MCP 연결 시 주의**: Studio는 잘 죽는 편 — 죽었을 때 살리려고 위험한 명령을 반복 시도하지 말 것. 그런 상황이면 MCP 없이 할 수 있는 작업만 하거나 대기. 연결 방법은 `HUMAN_TODO.md` 1번 항목 참고(사용자가 Studio에서 @@ -94,77 +87,59 @@ canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추 ## 지금 할 일 (우선순위순) -1. **[다음 세션 최우선] 실제 스캐폴딩.** 소스 트리 구조는 문서로 이미 확정됨 - (`base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절) — 다음 - 세션에서 실제로 `quad-base/`, `quad-roblox/` 폴더, 각각의 `wally.toml`, - 루트 `default.project.json`, `.luaurc`를 만들 것. 이 시점부터 `qa-request/`/ - `archive/` 폴더가 실제로 쓰이기 시작함. -2. 남은 세부 시그니처(`CreatedRef`/`state()`/`Source()`/`DI`류 정확한 - 이름)는 위 항목과 자연스럽게 같이 확정 가능 — PA님 실 코드(`.claude/ - initreq/artworks/`)를 이미 받아서 교차검증 완료(아래 인수인계 메모 - 참고), `On` 모듈은 이벤트 바인딩 방식이 바뀌며 아예 불필요해짐. -3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만 +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차 제안 완료(우선순위 + 순: `State`가 React/Vue식 "쓸 수 있는 로컬 상태"라는 통상 의미와 반대라 + 가장 위험, `DI`가 Dependency Injection 축약어와 충돌, `PerInstanceState`가 + 핵심 프리미티브 `State`와 이름 충돌 — 세부는 `.claude/question.md` 참고), + 사용자와 같이 계속 논의 필요. +4. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만 필요, 구현 착수를 막지 않음. -4. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 +5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 (`HUMAN_TODO.md` 2번 항목). -## 인수인계 메모 (2026-08-04 세션 종료 시점, 5차 라운드까지 반영) +## 최근 세션 요약 (2026-08-04, 6차 라운드 이후) -**5차 라운드(소스 구조 확정)**: 4차 라운드 종료 시점에 서브에이전트로 먼저 -계획 문서 전체의 정합성을 점검(차질 없음 확인) 후 진행. 패키징 방식은 -서브에이전트 웹 리서치로 확인(`.luaurc` alias 런타임 미지원, wally 심볼릭 -링크/타입 문제, `Sleitnick/RbxUtil`의 모노레포+개별 wally.toml 선례, -`pesde`는 아직 이름) — 모노레포로 당장 진행, 나중에 실제 분리 결정. -패키지 경계는 사용자가 "base=인터페이스, roblox=구현"이라는 원칙을 명확히 -해서 확정 — Store/State/Source 온톨로지뿐 아니라 `process`/`retract` -디스패치 엔진, `LifetimeHandle`/`PerInstanceState` 인터페이스, Ref, Slot -코어 재조정 로직까지 전부 `quad-base`가 소유(다른 엔진에서도 재사용 -가능해야 한다는 전제, 엔진마다 큰 구현 중복 방지가 목적). Slot도 같은 -원칙 적용 확정, 그 과정에서 `k:number,v:Instance` 중첩 인스턴스 자식용 -`InstanceChild` 핸들러가 추가로 필요하다는 게 밝혀짐. `bind-system-plan.md`/ -`module-lifecycle-plan.md`/`slot-plan.md` 세 문서 모두 `research/`에서 -`base/`로 승격 완료, `base/architecture.md`에 전체 소스 트리가 문서화됨 — -실제 폴더/파일 스캐폴딩은 다음 세션(위 "지금 할 일" 1번). +**6차 라운드**: 남아있던 "급하지 않음" 질문 두 개 해소 — 태그 네임스페이싱 +충돌은 컴포넌트 단위로는 Ref가 대신 해결해줘서 심각하게 안 봄(`architecture.md` +5번), Store가 Store를 담는 경우는 없음으로 확정(Store는 Source에 준하는 +"시작점"이라 다른 반응형 값에 자동 연결되지 않음, `bind-system-plan.md`). -## 인수인계 메모 (2026-08-04 세션 종료 시점, 4차 라운드까지 반영) +**그 이후 채팅에서 세 가지 큰 스레드가 새로 열림/정리됨**: +- **Modifier 메커니즘 전체 확정** — 런타임 pluggable 핸들러가 아니라 정적 + merge, immutable+`table.clone` 기반 체이닝, 필드가 State일 수도 있는 + 경우의 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번). +- **문서 전체 감사 및 정리** — `.claude/` 코퍼스 전체(약 15개 문서)를 + 서브에이전트로 감사해 여러 라운드에 걸쳐 쌓인 모순/중복/stale 마커를 + 대거 발견하고 수정(예: 이벤트 dot-access 확정 여부가 문서 내에서 서로 + 모순, 이미 해소된 질문이 "미해결"로 방치, 존재하지 않는 문서/섹션을 + 가리키는 끊긴 참조 다수, `TagService`/`CollectionService` 혼용 등). + `research/purity-and-effects-plan.md`도 내용이 이미 확정 상태라 `base/`로 + 승격. **이 CLAUDE.md 자체도 이번에 오래된 라운드별 인수인계 메모 3개를 + 이 요약 하나로 통합하며 정리함** — 라운드별 상세 히스토리가 필요하면 + git log와 각 `base/`/`research/` 문서 안의 라운드 표시(예: "2026-08-04 + 3차 라운드")를 참고할 것, 여기서 전부 반복하지 않음. -2026-08-03에 확정됐다고 표시된 결정 전체(architecture.md 14개 + lifecycle- -pattern/store-semantics/bind-system-plan/module-lifecycle-plan/slot-plan/ -tween-plan)를 `AskUserQuestion`으로 하나씩 예/아니오 검증 완료 — 상세는 -`.claude/question.md`의 "2026-08-04 검증 라운드 완료" 절. 검증 과정에서 -사용자가 실시간으로 설계를 더 전개하면서 **"State 프리미티브는 안 만든다"는 -기존 결정이 틀렸다는 게 밝혀짐** — Store/State/Source 온톨로지 전체가 이 -세션에서 새로 열린 가장 중요한 설계 스레드로 떠올랐음. - -**같은 날 이어진 2차/3차 라운드에서 그 온톨로지와 인체공학 질문 전부를 -확정함**: push-invalidate/pull-recompute 전파 모델(Fusion식 eager 노드/생성순 -정렬 불필요), `:Compute`의 self/with 인자를 둘 다 lazy State 핸들로 통일 -(별도 `ComputeWithout` 불필요), State는 쓰기 불가(값 쓰기는 Store의 -`__newindex`로만) 확정, `Source`는 Store 내부 디테일이 아니라 값 하나만 -다룰 때 쓰는 독립 공개 프리미티브로 격상, Slot 생존 확인 문제는 새 메커니즘 -없이 기존 canExecute 유틸 재사용으로 해소(부수 효과로 "Store가 Store를 담을 -때 이중 해제 방지 필요한가" 백로그 항목도 "명시적 dispose가 없어 질문 자체가 -성립 안 함"으로 닫힘), `store.key` dot-access를 타입 추론 1급 경로로 삼는 -관습을 인스턴스 생성까지 프로젝트 전역으로 확정, `RobloxFactory` 재호출 -가드(같은 팩토리=무시, 다른 팩토리=에러, `New()`와는 인스턴스별 테이블 -분리로 자연히 공존)까지 확정. - -**4차 라운드에서 사용자가 실제 참고 코드(`.claude/initreq/artworks/`, PA님 -작성 — UI 포함 전반적 설계 패턴을 시범 적용한 데모 모듈)를 공유해줘서 -교차검증**: "DI"는 Dependency Injection이 아니라 Declarative Instance였음 -(정정). 인스턴스 생성은 2트랙 구상보다 단순한 "제네릭 생성자 함수 하나 + -자주 쓰는 클래스만 정적 필드로 미리 바인딩" 모양으로 정정. **이벤트 -바인딩은 `On.EventName` 도트액세스를 접고 PA님 방식(평범한 문자열 키 + -`ReflectionService` 기반 자동 판별)으로 전환** — Store의 dot-access는 실질적 -타입 이득이 있어 그대로 유지, 이벤트만 예외. 전파 모델(push-invalidate/ -pull-recompute)과 라이프사이클(GC-native)은 PA님 코드가 반례처럼 보였으나 -(각각 파생 개념이 없는 단순 pub-sub, 전부 수동 해제) 재검토 후 **기존 -확정 유지** — 라이프사이클은 나중에 하이브리드로 확장 가능한 여지만 기록. -OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 문제를 보여 오히려 -보강됨. **더 이상 열려있는 핵심 설계 질문은 없음** — 남은 건 API 표면 이름뿐 -(위 "지금 할 일" 참고). 그 외 자잘한 정정들(Slot retract=폐기 확정, Pipe COW -후보 폐기 등)은 각 문서에 바로 반영해둠 — 재조사 불필요. - -이전 세션(2026-08-03) 종료 시점 메모: `.claude/` 전체 스캐폴드 + 대부분의 -핵심 아키텍처 결정을 완료, 로컬 git 저장소 초기화+첫 커밋(원격 없음, -`SAFETY.md` 참고 — 원격은 사용자가 제한 계정을 마련해줘야 추가 가능). +용어 정리 제안 진행 중인 점은 위 "지금 할 일" 3번 참고.