diff --git a/.claude/README.md b/.claude/README.md index b37aa38..5e23b71 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -12,7 +12,7 @@ | `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식 | | `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 | | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | -| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 | +| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용 — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음 | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | | `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`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | @@ -34,6 +34,7 @@ | `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 | | `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정, getter 이름만 남음 | | `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | +| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족(`StoreSource`/`RefSource` 중간안은 전부 폐기됨) — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐 [정정: 2026-08-04 승격됐으나 이 표에 반영이 안 돼있던 걸 2026-08-06 뒤늦게 수정] | ## `research/` — 아직 착수 전, 상의 필요 @@ -41,11 +42,18 @@ |---|---|---| | `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | -| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, `StoreSource` 프록시 — 핵심 골격 수렴, modifier/Ref가 컴포넌트 경계를 어떻게 통과하는지만 남음 | 상 — 사용자가 "가장 문제되는 부분"으로 직접 지목 | | `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 | -| `documentation-plan.md` | UI 네이밍 컨벤션 문서 + Store 부작용을 게임 시스템에서 깔끔하게 쓰는 패턴 문서 — 뼈대만, `debug-tooling-plan.md` 논의에서 파생 | 하 — 착수 시점 미정, 뼈대만 기록해둔 상태 | +| `documentation-plan.md` | 문서 사이트 구조(초심자/api/심화/`quadnomicon` 4축, 백엔드별 트랙 분리) + UI 네이밍 컨벤션·Store 부작용 패턴·권장 이벤트 핸들링 3개 세부 문서 뼈대 | 하 — 착수 시점 미정, 구조/스코프만 합의된 상태 | +| `documentation-content-map.md` | 위 4축에 실제로 뭘 채울지 `base/` 전체를 초심자/api/심화/skip으로 서베이한 콘텐츠 맵 — 초심자 core loop 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 | +| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 진짜 불리한 점 중 고칠 만한 것 3개, 못 고치는 트레이드오프 정리 | 하 — 사용자 검토 후 반영 여부 결정 대기 | | `ui-shorthand-plan.md` | UICorner/UIPadding/UIScale 인라인 편의 키(v1 `Corner`/`PaddingAll`/`Scale` 선례) — 여전히 필요한 기능으로 재확정(RoundSize만 네이티브 UICorner로 대체돼 불필요), 메커니즘(Handler)·패키지 배치(quad-roblox 코어) 확정 | 하 — 결론 남, M10 전후 구현하면 됨 | +## `archive/` — 완료됐거나 완전히 뒤집힌 것, 능동 참고 불필요 + +| 문서 | 내용 | +|---|---| +| `store-source-proxy-reversed.md` | 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 | + ## 참고 - **저장소 소유자가 답해야 할 질문 전체 취합**: `.claude/question.md` diff --git a/.claude/archive/store-source-proxy-reversed.md b/.claude/archive/store-source-proxy-reversed.md new file mode 100644 index 0000000..41ca8d4 --- /dev/null +++ b/.claude/archive/store-source-proxy-reversed.md @@ -0,0 +1,73 @@ +# [역전됨] `StoreSource` 프록시 설계 — Source가 State를 만족하는 서브타입으로 대체됨 + +**역전 일시**: 2026-08-06 (세 번째 세션). **원 확정 일시**: 2026-08-04 +(`component-composition-plan.md` 컴포넌트화 논의 3~4번 항목). +**현재 유효한 설계**: `base/store-semantics.md`의 "Source가 State를 +만족함 — 구조적 서브타입" 절이 최종 소스. 이 파일은 더 이상 능동적으로 +참고할 필요 없음(구현에 안 씀) — 왜 이 경로를 갔다가 되돌아왔는지가 +`quadnomicon`(프레임워크 설계자용 심화 콘텐츠, `research/documentation-plan.md` +0번 항목) 소재로 가치 있어서 사유·원문을 통째로 보존해둔 것. + +## 역전된 사례 — 원래 무엇을 확정했었나 (2026-08-04 원문) + +`store.key`로 값을 얻을 때, Store가 내부 Source 객체를 **직접 노출하지 +않고** 별도의 얇은 프록시 타입 `StoreSource`를 만들어 반환하는 설계였음: + +> **Source = 인터페이스이자 구현체**: 독립 생성자 `Source(initial)`가 기본 +> 구현체, `store:GetSource("key")`(가칭)류 접근자가 반환하는 값은 같은 +> 인터페이스를 구현하는 별도의 얇은 프록시(`StoreSource`) — 읽기는 +> `store.key`로, 쓰기는 `store.key = v`로 위임. **내부 Source 객체를 그대로 +> 노출하지 않음** — 그러면 "쓰기는 오직 Store의 `__newindex`뿐"이라는 기존 +> 확정과 새 쓰기 경로가 충돌하게 됨. +> +> **캐시 안 함**: State가 이미 "매번 새로 만듦, store에 캐시 안 됨"으로 +> 확정돼 있어 일관성 + 엔지니어링 비용 둘 다 이쪽이 쌈 — **사용자 확정** +> ("그냥 엔지니어링적으로 비용이 싼거 택해"). + +같은 논의에서 파생된 핸들러 계약 쪽 결정도 같이 뒤집힘 — Source를 핸들러가 +직접 받을 때는 별도 유니온 타입으로 처리하기로 했었음: + +> 핸들러가 값을 받을 때 `Source | State` 유니온으로 받고, 내부에서 +> 타입 체크만 하면 됨(Source면 인스턴스 변경 이벤트에 걸어 역방향 쓰기까지 +> 처리, State면 읽기만) — `isHandlable`/`priority`/`process`/`retract` 4종 +> 계약에 5번째 항목을 추가할 필요 없음. + +## 역전된 이유 + +`store.key`의 타입 문제를 다시 들여다보다가 드러남: Store의 정적 타입을 +`{key: State}`류 평범한 레코드 타입으로 지으면(2026-08-04 3차 +라운드에서 확정했던 방식) Luau 구조적 타이핑상 그 필드의 읽기/쓰기 타입이 +같아야 하는데, 실제 쓰기(`store.key = value`, raw `number`)와 읽기 +(`State`)가 서로 다른 타입이라 애초부터 정합적이지 않았음 — +`StoreSource` 프록시 설계 시점엔 이 비대칭을 못 잡았던 것. + +이걸 풀려고 대안(store를 `store.key`/`store.state.key`로 네임스페이스 +분리하는 안, `RefSource`라는 store 전용 타입을 새로 만드는 안)을 +검토하다가, 더 근본적인 재구성으로 수렴: **Source 자체가 구조적으로 +State를 만족**(Svelte `Writable extends Readable`와 같은 모양) +하게 만들면,애초에 "Source를 감추고 별도 프록시로 감쌀" 이유 자체가 +없어짐 — Store가 내부에 갖고 있는 진짜 Source 객체를 그대로 돌려줘도 +안전하고, 오히려 프록시 객체를 매번 만들거나 캐싱하는 계층 하나가 통째로 +사라져서 더 쌈. + +## 이전 것과 지금 것의 차이 + +| | `StoreSource`(역전됨) | Source가 State를 만족(현재) | +|---|---|---| +| `store.key`가 반환하는 것 | 별도 프록시 `StoreSource`(Source 인터페이스를 구현한 wrapper) | 진짜 `Source` 객체 그대로 | +| 쓰기 문법 | `store.key = value`(`__newindex`) | `store.key:Set(value)` | +| 캐싱 | "매번 새로 만듦"(State와 같은 정책) | Store 생성 시 이미 만들어둔 Source를 그대로 반환 — 별도 캐싱 메커니즘 자체가 불필요 | +| 핸들러가 Source를 받는 방법 | `Source \| State` 명시적 유니온 | `State` 하나만 받아도 서브타입 호환으로 자동 통과, 런타임에 구분하고 싶으면 `isSource`류 판별자 | +| 타입 정합성 | 레코드 필드 읽기/쓰기 타입 비대칭 문제가 잠재해 있었음(발견 안 된 채로 확정됐었음) | 필드 타입이 항상 `Source`로 대칭 — 쓰기가 메소드 호출로 옮겨가며 문제 자체가 해소됨 | + +## 왜 완전히 헛수고는 아니었나 + +`StoreSource`가 짚었던 문제의식(Store 내부 표현을 그대로 노출하면 안 +될 수 있다, Source와 State는 다른 쓰기 권한을 가져야 한다)은 여전히 +유효함 — "State/Source 경계 규칙: 파생이면 읽기전용, 원본이면 쓰기 가능" +원칙(`base/component-composition-plan.md` 2번)은 살아남았고, 결론만 +"별도 프록시 타입을 만든다"에서 "Source 자체를 서브타입으로 승격한다"로 +바뀐 것. 이 반전 자체가 "타입이 없던 v1 습관을 재검토 없이 typed 재작성에 +그대로 가져오면 안 된다"는 더 큰 교훈의 구체적 사례이기도 함(사용자 회고, +`base/store-semantics.md` 참고) — `quadnomicon`에서 "설계가 왜 이렇게 +반전됐는가" 사례로 쓰기 좋음. diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 791b021..c21205f 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -190,15 +190,20 @@ CollectionService 태그 등)를 흉내낼 필요가 없고, quad-base 자체 ## Store/State/Source 온톨로지 — 확정됨 (요약) -Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할 -때마다 그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를 -반환한다. 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) — -Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는 -쓰기 대상이 아니고(값 쓰기는 항상 Store의 `__newindex`), 값 하나만 다룰 땐 -Store와 별개인 가벼운 `Source` 프리미티브를 씀. `store.key` dot-access를 타입 -추론 1급 경로로 삼는 것도 3차 라운드에서 정식 확정됨 — **더 이상 열린 질문 -아님**, 남은 건 정확한 API 이름뿐. 상세는 `base/bind-system-plan.md`의 -"Store/State/Source 온톨로지" 절 참고. +Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 +접근하면 Store 생성 시 이미 만들어둔 그 Source 객체를 그대로 반환한다 +(별도 wrapper 없음 — **[2026-08-06 후속 세션 정정]** 원래 "매번 새 State를 +감싸 반환"이었으나, `Source`가 구조적으로 `State`를 만족하도록 재구성되며 +wrapper 계층 자체가 불필요해짐). 전파는 push-invalidate(신호만)/ +pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 +의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는 +경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 — +`.value`/`:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가). 값 하나만 +다룰 땐 Store와 별개인 가벼운 `Source` 프리미티브를 독립적으로도 씀. +`store.key` dot-access를 타입 추론 1급 경로로 삼는 것도 3차 라운드에서 +정식 확정됨 — **더 이상 열린 질문 아님**, 남은 건 정확한 API 이름뿐. +상세는 `base/store-semantics.md`의 "Source가 State를 만족함" 절과 +`base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. ## 아직 미정 (research/로 분리됨) diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index b7c280a..300076f 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -528,11 +528,16 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최 생성자 이름뿐(구현 단계). `base/store-semantics.md`의 "State 프리미티브는 실제로 필요하다" 정정에서 이어짐. -**핵심 온톨로지** (변경 없음): +**핵심 온톨로지** (2026-08-06 후속 세션에서 Store/Source 부분 정정 — +아래 "State는 쓰기 대상이 아님" 절 이후 내용 및 `base/store-semantics.md`의 +"Source가 State를 만족함" 절 참고): - **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원"). -- **Store** — source들의 집합체. `store.a`처럼 키로 접근하면 그 source를 - 감싼 **새 State**를 매번 만들어 반환(state가 store에 캐시되어 재사용되는 - 게 아님 — source만 store에 귀속된 유일한 실체). + **구조적으로 State를 만족(단방향 호환)** — `.value`/`:Get()`/`:With`/`:Compute` + 전부 지원 위에 `:Set(value)`/`:Emit()` 추가. +- **Store** — Source들의 이름 붙은 모음, 그 이상 아님. `store.a`처럼 키로 + 접근하면 Store 생성 시 이미 만들어둔 **그 Source를 그대로 반환**(더 이상 + 별도 State wrapper를 매번 만들거나 따로 캐싱하지 않음 — Source 자체가 + 이미 State를 만족하므로 wrapper 계층 자체가 불필요해짐). - **State** — source(또는 다른 state)의 결과를 캐싱만 하는 존재, 자기 고유의 독립적 value 개념이 없음. `state(state)`로 기존 state의 결과를 받아 새 state를 만들어 분기 가능 — 이게 사실상 Unix 파이프 영감의 "State끼리 @@ -574,6 +579,41 @@ Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 체이닝")과 이 원칙이 충돌하지 않는 이유가 바로 이것 — clone은 그저 참조 복사라 State 필드는 클론 이후에도 여전히 살아있는 lazy 핸들로 남음. +**왜 State 체인을 Modifier처럼 플래튼하지 않는가 (2026-08-06 후속 세션)** + +**문제 제기(사용자)**: State가 `a → b → c`처럼 계속 연결되는 구조면, 이전 +노드가 다음 노드에 대한 emit 연결/값 연결을 항상 들고 있어야 함(weak +table로 GC는 되지만 별도 데이터스트럭처 관리 부담). 대안으로, 각 State가 +자기 Compute 함수 목록을 통째로 누적해서 갖고(Modifier의 clone-then-return +체이닝처럼) 매번 클론+append하면 링크드 그래프 자체가 필요 없어지지 +않는가? + +**기각 이유 — State의 정의 자체가 "캐싱하는 존재"임.** 위 온톨로지에 +"State — source(또는 다른 state)의 결과를 **캐싱만 하는** 존재"라고 +확정돼 있고, `previous` 두 번째 인자 메커니즘(무거운 파생 엔진 객체 +재생성 비용 절감)도 이 캐싱 전제 위에서만 의미가 있음. 만약 Compute +체인을 매번 통째로 클론해 각 leaf가 독립된 함수 목록을 갖게 하면, 중간 +State를 여러 갈래가 공유하는 다이아몬드 형태(`b`에서 `c1 = b:Compute(g1)`, +`c2 = b:Compute(g2)`로 분기)에서 `b`까지의 계산이 캐시 공유 없이 소비자 +수만큼 중복 실행됨 — `previous` 메커니즘이 막으려던 문제를 반대로 다시 +만들어내는 셈이라 방향이 안 맞음. + +**"별도 데이터스트럭처 관리" 부담은 실제로는 작음.** "관측해야 +실체화된다" 원칙 때문에 살아있는 노드-대-노드 구독 엣지가 필요한 건 +실제로 관측되는(`Get()`되는) State뿐 — 중간에 만들어놓고 아무도 안 보는 +State는 구독 등록 자체가 안 일어남. 다이아몬드에서 중복 워크를 막는 +`invalid` 플래그 dedup 장치도 체인 전체가 링크드일 것을 요구하지 않고 +각 노드가 자기 구독자 목록만 가지면 되는 것이라, 이 결정과 무관하게 +그대로 유지됨. 구현은 Observer와 동일한 패턴(외부 weak table, +`{[child] = true}` 류)으로 충분 — 새 메커니즘 발명 아님. + +**결론**: 노드별 캐시 유지(현재 모델) 유지, 플래튼 기각. Modifier가 +플래튼+클론을 쓰는 건 애초에 캐싱이 필요 없는 정적 데이터라 성립하는 +것이고, State는 존재 이유 자체(캐싱)가 달라 같은 패턴을 적용할 수 없음. +`research/documentation-plan.md`의 심화 문서 후보로 남겨둠 — "왜 State는 +Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사용자를 위한 +좋은 심화 콘텐츠 소재. + **`:With`/`:Compute` — self 인자도 lazy 핸들로 통일** - 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제 @@ -595,11 +635,17 @@ Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 **State는 쓰기 대상이 아님 — 확정, Source는 독립 공개 프리미티브로 격상** -- `.value`는 항상 읽기 전용. 값을 쓰는 경로는 오직 Store의 `__newindex` - (`store.key = value`, 이미 확정된 문법)뿐 — State에는 대응하는 쓰기 API가 - 아예 없음. "State에 `.value = x`를 허용하면 다른 source에서 파생된 - state에 직접 쓰기가 가능해져 버린다"는 이전 우려는 이걸로 근본적으로 - 해소(그런 API 자체가 없음). +- `.value`는 항상 읽기 전용. State에는 쓰기 API가 아예 없음. "State에 + `.value = x`를 허용하면 다른 source에서 파생된 state에 직접 쓰기가 + 가능해져 버린다"는 이전 우려는 이걸로 근본적으로 해소(그런 API 자체가 + 없음). +- **[정정, 2026-08-06 후속 세션] 값을 쓰는 경로는 `store.key = value` + (`__newindex`)가 아니라 `store.key:Set(value)`로 전환됨** — 이유와 + 상세는 `base/store-semantics.md`의 "Store 값 설정 문법" 절 참고(요지: + Source가 State를 만족하는 구조로 바뀌며 레코드 타입 읽기/쓰기 대칭을 + 맞추려면 대입 문법을 포기해야 함 + `=`가 암시하는 "즉시 커밋"이 실제 + lazy 동작과 정서적으로 안 맞는다는 논거). 같은 문서의 "Source가 State를 + 만족함" 절에 Source/State 서브타입 구조 전체가 정리돼 있음. - **`Source`는 Store의 내부 구현 디테일이 아니라 별도의 가벼운 공개 프리미티브로 노출** — Store는 다수의 source를 등록/관리하는 무거운 구조라, 값 하나만 반응형으로 다루고 싶을 때 Store를 통째로 만드는 건 @@ -632,10 +678,15 @@ Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 - `store "key"`(문자열 커링)로 `state`를 오버로드 함수 타입으로 정확히 추론하려는 시도는 포기하고, **`store.key`(dot-access)를 1급 경로로 확정** - — Store 타입을 `{key: State, other: State}`류 평범한 + — Store 타입을 `{key: Source, other: Source}`류 평범한 레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열 - 리터럴 narrowing 문제 자체가 안 생김. `store "key"` 문자열 커링은 동적 - 키가 필요할 때 쓰는 미타입(`State`) 폴백으로 격하. + 리터럴 narrowing 문제 자체가 안 생김([정정, 2026-08-06] 원래 `State` + 필드로 적혀있었으나 Source가 State를 만족하는 구조로 바뀌며 `Source`로 + 갱신 — `store.key = value` 쓰기 문법이 `:Set()`으로 옮겨가 이 필드가 + 더 이상 `__newindex`로 쓰이지 않으므로 읽기/쓰기 타입 대칭 문제도 같이 + 해소됨, `base/store-semantics.md` "Source가 State를 만족함" 절 참고). + `store "key"` 문자열 커링은 동적 키가 필요할 때 쓰는 미타입(`Source`) + 폴백으로 격하. - 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성까지 관통하는 프로젝트 전역 관습으로 확정**됨 — 단 이벤트는 이후 4차 라운드에서 이 관습의 **유일한 예외**로 빠졌음(PA님 방식인 문자열 키+런타임 리플렉션으로 전환). @@ -821,9 +872,11 @@ Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근 **Store 쪽 dot-access는 그대로 유지**: `store.key`(1급 타입 경로)/ `store "key"`(문자열 커링, 동적 키 폴백)는 이벤트와 달리 실질적으로 Luau가 -타입을 좁혀주는 이득이 있어서(Store 자체가 `{key: State, ...}`류 -평범한 레코드 타입으로 지어짐) 그대로 유지 — 이벤트만 예외였을 뿐, "정적으로 -알려진 것=필드 접근" 원칙 자체가 깨진 건 아님. +타입을 좁혀주는 이득이 있어서(Store 자체가 `{key: Source, ...}`류 +평범한 레코드 타입으로 지어짐[정정: 2026-08-06 후속 세션에서 필드 타입이 +`State`→`Source`로 갱신, "Source가 State를 만족함" 절 참고]) 그대로 +유지 — 이벤트만 예외였을 뿐, "정적으로 알려진 것=필드 접근" 원칙 자체가 +깨진 건 아님. **PA님 코드와 대조해서 재확인한 것(변경 없음)**: - **OOP 회피 결정은 오히려 보강됨** — PA님의 `ObjectOrientedProgramming/ diff --git a/.claude/base/component-composition-plan.md b/.claude/base/component-composition-plan.md index 4690c10..e9d1c2c 100644 --- a/.claude/base/component-composition-plan.md +++ b/.claude/base/component-composition-plan.md @@ -52,33 +52,50 @@ Vue/Svelte 대비 내세우는 강점과 동일 논리) — **사용자 확정** ### 2. State/Source 경계 규칙: 파생이면 읽기전용, 원본이면 쓰기 가능 State는 `:With`/`:Compute`로 만들어진 파생값일 수 있어 쓰기가 정의 자체가 -안 됨. Source(독립이든 Store 소속 `StoreSource` 프록시든)는 파생이 아니라 -항상 원본 슬롯 하나를 직접 가리키므로 쓰기가 의미 있음 — **사용자 확정** -("맞음. 확실해"). +안 됨. Source는 파생이 아니라 항상 원본 슬롯 하나를 직접 가리키므로 쓰기가 +의미 있음 — **사용자 확정**("맞음. 확실해"). 이 원칙 자체는 그대로 유지되고, +아래 3번의 구체적 메커니즘만 2026-08-06 후속 세션에서 더 단순하게 갱신됨. -### 3. `StoreSource`: Source를 인터페이스+구현체로 두고, Store 키에서 그 인터페이스를 구현하는 얇은 프록시를 받음 +### 3. [정정, 2026-08-06 후속 세션] `StoreSource` 프록시 개념 폐기 — Source가 State를 구조적으로 만족하므로 Store가 내부 Source를 그대로 반환 -- **Source = 인터페이스이자 구현체**: 독립 생성자 `Source(initial)`가 기본 - 구현체, `store:GetSource("key")`(가칭)류 접근자가 반환하는 값은 같은 - 인터페이스를 구현하는 별도의 얇은 프록시(`StoreSource`) — 읽기는 - `store.key`로, 쓰기는 `store.key = v`로 위임. **내부 Source 객체를 그대로 - 노출하지 않음** — 그러면 "쓰기는 오직 Store의 `__newindex`뿐"이라는 기존 - 확정과 새 쓰기 경로가 충돌하게 됨. -- **캐시 안 함**: State가 이미 "매번 새로 만듦, store에 캐시 안 됨"으로 - 확정돼 있어 일관성 + 엔지니어링 비용 둘 다 이쪽이 쌈 — **사용자 확정** - ("그냥 엔지니어링적으로 비용이 싼거 택해"). +**원래 이 절은 "Source를 인터페이스+구현체로 두고 Store 키에서 얇은 프록시 +(`StoreSource`)를 받는다"는 방향이었음 — 지금은 폐기됨.** 이후 세션에서 +Store/Source dot-access 타입 문제(레코드 타입의 읽기/쓰기 비대칭)를 +다루다가 더 근본적인 재구성으로 수렴: **`Source`가 구조적으로 +`State`를 만족**(단방향 호환, Svelte `Writable extends Readable`와 +같은 모양)하도록 만들면, Store가 "내부 Source를 감추고 별도 프록시를 +새로 만들어 노출"할 이유 자체가 없어짐 — `store.key`가 Store 생성 시 +이미 만들어둔 진짜 Source 객체를 그대로 돌려줘도 안전함(Source 자체가 +이미 State의 읽기 계약을 전부 만족하고, 거기에 `:Set(value)`/`:Emit()`이 +추가로 있을 뿐이라 "원본이라 쓰기 가능"이라는 위 2번 규칙과도 자연히 +맞아떨어짐). 상세 근거·타입 설계·Luau 솔버 검증 필요 항목은 +`base/store-semantics.md`의 "Source가 State를 만족함" 절이 최종 소스 — +이 문서는 배경만 유지. -### 4. Source 직접 전달(양방향)은 핸들러 계약 확장 없이 타입 유니온으로 처리 — 단, 실사용 범위는 좁음 +- **쓰기 문법도 같이 바뀜**: `store.key = v`가 아니라 `store.key:Set(v)` + (레코드 타입 읽기/쓰기 대칭 + lazy 동작에 `=`가 암시하는 "즉시 커밋"이 + 안 맞는다는 논거, 같은 절 참고). +- **캐시 문제도 이걸로 자연히 해소**: State를 "매번 새로 만듦"이던 이전 + 모델과 달리, 이제 Store는 생성 시 만들어둔 Source를 그대로 갖고 있다가 + 돌려주기만 하므로 별도 캐싱 메커니즘 자체가 불필요(래퍼 생성 단계가 + 아예 없어짐 — 이전보다 더 쌈). -- 핸들러가 값을 받을 때 `Source | State` 유니온으로 받고, 내부에서 - 타입 체크만 하면 됨(Source면 인스턴스 변경 이벤트에 걸어 역방향 쓰기까지 - 처리, State면 읽기만) — `isHandlable`/`priority`/`process`/`retract` 4종 - 계약에 5번째 항목을 추가할 필요 없음. Source 자체가 계산이 없는 원천이라 - 가능한 단순화 — **사용자 확정**("그냥 타입 상 source를 받거나 state를 - 받거나 하면 됨. source 자체는 원천이라 컴퓨팅 같은거 없어"). -- **하지만 실사용은 좁을 것으로 예상**: `isEnabled`처럼 여러 조건에 영향 - 받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못 넘김. - 즉 Source 직접 전달이 통하는 건 진짜 단순한 1:1 원본-토글 케이스뿐이고, +### 4. [정정, 2026-08-06 후속 세션] Source 직접 전달 — 타입 유니온도 불필요해짐 + +원래 "핸들러가 `Source | State` 유니온으로 받는다"는 방향이었으나, +Source가 State를 구조적으로 만족하는 지금은 **유니온 자체가 필요 없음** — +핸들러는 그냥 `State` 하나만 받아도 Source 인스턴스가 자동으로 그 +자리에 들어감(서브타입 호환). `isHandlable`/`priority`/`process`/`retract` +4종 계약에 5번째 항목을 추가할 필요 없다는 결론은 그대로 유지, 다만 근거가 +"타입 유니온으로 처리"에서 "서브타입이라 유니온 자체가 불필요"로 더 +단순해짐. 단, 핸들러가 "이거 Source면 역방향 쓰기까지 걸고 싶다"처럼 +**런타임에** Source인지 구분하고 싶은 경우는 여전히 있을 수 있음 — +그건 타입 유니온이 아니라 런타임 판별자(`isSource`류, `isObserver` +패턴과 동일한 결)로 처리하면 됨. + +- **실사용 범위가 좁다는 판단은 그대로 유지**: `isEnabled`처럼 여러 조건에 + 영향받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못 + 넘김. Source 직접 전달이 통하는 건 진짜 단순한 1:1 원본-토글 케이스뿐이고, 일반적인 경우엔 React식 `value(State) + onChange(callback)` 패턴이 기본 — **사용자 확정**("isenabled가 여러 조건에 영향 받으면 바로 문제가 생기는거지. 따라서 실제 사용은 제한적일듯. callback을 쓰는게 @@ -250,10 +267,12 @@ Ref를 받으면 그냥 전부 실행하면 됨, `modifier-plan.md` §4-2) — ## 남은 열린 질문 (`.claude/question.md`에도 취합, 전부 후순위 — 이름만 남음) - **정확한 API 이름**: `Component`(플레인 함수 규약이라 별도 래퍼가 필요한지 - 자체도 불확실 — 아마 불필요), `GetSource` 계열 접근자 이름, `Source` - 독립 생성자 이름, 컴포넌트 경계용 `props.Modifier`/`props.Ref` 필드명, - `Modifier.Merge` 함수명은 전부 가칭. `base/bind-system-plan.md`의 "남은 - 열린 질문" 절(정확한 함수/생성자 이름 미정)과 같은 급의 후순위 항목 — - 구현 단계에서 다른 이름들과 함께 확정. + 자체도 불확실 — 아마 불필요), `Source`/`State` 독립 생성자·타입 이름, + 컴포넌트 경계용 `props.Modifier`/`props.Ref` 필드명, `Modifier.Merge` + 함수명은 전부 가칭. (`GetSource` 계열 접근자는 위 3번 정정으로 아예 + 불필요해짐 — `store.key`가 직접 Source를 반환하므로 별도 접근자 자체가 + 없음.) `base/bind-system-plan.md`의 "남은 열린 질문" 절(정확한 함수/ + 생성자 이름 미정)과 같은 급의 후순위 항목 — 구현 단계에서 다른 + 이름들과 함께 확정. - **`quad2-try`는 확인 불필요로 재확인** — 진행이 중단된 상태라 이 논의와 무관. diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index f76f7b5..3ff65d9 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -54,9 +54,11 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립 해주지만, 그 결과를 다시 분기하고 싶으면(하나의 파생 스트림에서 여러 소비자가 각자 다른 추가 compute를 얹고 싶은 경우) `state(state)`처럼 기존 state의 결과를 받아 새 state를 만드는 조합이 필요. -- **store에서 state를 얻는 연산(예: `store "key"`)은 항상 새 state 인스턴스를 - 반환한다** — state 자체가 캐시되어 재사용되는 게 아니라, source만 store에 - 귀속된 유일한 실체이고 그 위의 state는 매번 새로 생성됨. +- **[정정, 2026-08-06 후속 세션] store에서 값을 얻는 연산(`store.key`)은 + Source를 직접 반환한다 — 더 이상 별도 State 인스턴스를 감싸서 반환하지 + 않음.** 상세는 아래 "Source가 State를 만족함" 절 참고. 이 항목의 원래 + 버전("항상 새 state 인스턴스를 반환")은 틀림 — Store가 별도 wrapper + 없이 자기 안에 만들어둔 Source를 그대로 돌려주는 쪽으로 재정리됨. - 이건 quad2-try(폐기된 이전 시도)의 `Pipe` copy-on-write 절충안을 대체하는 방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신 State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임 @@ -93,29 +95,115 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립 추론 문제는 `store.key`(dot-access)를 1급 경로로 확정하며 해소(같은 문서 "타입 추론 문제" 절, 3차 라운드). -## Store 값 설정 문법 — v1 인체공학 유지 (확정) +## Source가 State를 만족함 — 구조적 서브타입, RefSource 개념 폐기 (2026-08-06 후속 세션) -**사용자 확인 완료**: Store 값 설정은 `__newindex` 기반(`myStore.key = value`)을 -그대로 유지 — ProfileService 등 Roblox 생태계에서 이미 익숙한 관용구라 바꿀 -이유 없음. 마찬가지로 다음 두 인체공학도 유지: +**배경**: `store.key`가 매번 새 State를 감싸 반환하던 이전 모델의 타입 +문제(레코드 타입 `{key: State}`가 읽기/쓰기 비대칭이라 Luau +타이핑이 안 맞음, 위 "Source가 State를 만족함" 논의에서 도출)를 풀다가 +사용자가 제안한 더 근본적인 재구성. `RefSource`(store 슬롯을 가리키는 +전용 타입)를 따로 만드는 중간안도 검토했으나, 최종적으로 **Source 자체가 +State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하는 쪽으로 +수렴. -- **괄호 생략(paren-less) 구조** — 필요 시 커링(`myStore "key"`처럼 문자열 - 하나로 register를 얻는 v1 스타일)을 계속 허용. -- **`:` 체이닝** — 값을 바꾸는 연산에 한해 체이닝 문법 허용(`base/ - architecture.md`의 "함수지향 디폴트, `:`는 예외적으로만" 원칙과 일치 — 체이닝이 - 자연스러운 곳 중 하나가 바로 이 store 값 변경). +**확정 방향**: +- **`Source`가 구조적으로 `State`를 만족(단방향 호환)** — State + 자리엔 Source를 넣을 수 있지만 역은 안 됨(Svelte의 `Writable extends + Readable`와 같은 모양). Source는 State가 주는 모든 것(`.value`, + `:Get()`, `:With(...)`, `:Compute(fn)`) 위에 `:Set(value)`/`:Emit()`을 + 추가로 가짐. +- **`:With`/`:Compute`는 Source에서도 항상 `State`를 반환** — Source + 자신을 변형하는 게 아니라, "Source의 State 뷰를 뽑아 그 위에 파이핑"하는 + 것과 동치. 구현은 metatable `__index` 델리게이션(Source의 메소드 + 테이블이 State의 메소드 테이블로 폴백)으로 충분 — `Modifier`의 제네릭 + `__index` 트릭(`base/modifier-plan.md`)과 같은 패턴이라 로직 중복이 + 생기지 않음. +- **`RefSource` 같은 별도 타입은 불필요, `Store({defaults})`가 + 내부적으로 `{[key] = Source(default), ...}`나 다름없게 됨** — `store.key`는 + Store 생성 시 이미 만들어둔 Source를 그대로 돌려줄 뿐, 매번 새로 만들거나 + 별도로 캐싱할 wrapper 객체 자체가 없음. 이전에 검토했던 "State를 + weak table로 캐싱" 절충안보다 더 싸다(래퍼 생성/캐싱 단계 자체가 + 사라짐). v1이 모든 값을 Store 하나에 몰아넣던 습관은 "당시 정적 타입이 + 없어 단순하게 쓰는 게 편해서"였다는 게 사용자의 회고적 재평가 — 지금은 + 타입이 핵심 제약이라 그 전제 자체가 더 이상 안 맞고, 이번 정리로 Store는 + "이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐. +- **이 서브타입 관계는 `quad2-try`에서 기각한 컴포넌트/클래스 OOP 상속과는 + 다른 층위.** 그때 금지한 건 사용자가 짜는 컴포넌트 계층 구조(`Class:Extend()`류 + 매직)였고, 지금은 두 프리미티브 타입 사이의 구조적 서브타이핑(런타임 + 구현 델리게이션 포함)이라 그 금지와 충돌하지 않음. +- **동적 키 폴백(`store "key"`)은 이제 `State`가 아니라 `Source`를 + 반환**하는 것으로 자연히 갱신됨(위 "타입 추론 문제" 절과 연동). -`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 함께 읽을 -것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며 "불변 빌더"를 -흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가 아니었음. v2는 문법 -인체공학(사용자가 좋아하는 부분)은 유지하되 내부 구현(체이닝이 아니라 팩토리 -함수)만 바꾼다. +**검증 필요(확정 아님, M0 스파이크 대상)**: `Source`의 `:Compute` +시그니처가 자기 자신(`Source`)과 `State`를 동시에 참조하는 제네릭 +메소드라, Luau 솔버가 재귀 타입 조합에서 막히지 않는지 실제로 검증 +필요(사용자 우려: "솔버가 종종 죽는다"). 구분해서 볼 것: +- **자기 자신을 가리키는 self 타이핑**(`{ Compute: (self: Source, ...) -> State }` + 같은 패턴)은 Luau에서 극히 흔하고 대체로 안전 — 모든 메소드 테이블 + 클래스가 쓰는 패턴이라 이것 자체가 위험 신호는 아님. +- **진짜 위험한 건 두 제네릭 타입 별칭이 서로를 참조하는 상호 재귀** + (`Source` 정의가 `State`를 참조하고, `State`도 거꾸로 + `Source`를 참조하는 경우) — 이게 Luau 솔버가 알려진 대로 취약한 + 패턴. **`State`가 `Source`를 전혀 참조하지 않도록 먼저 독립적으로 + 정의하고, `Source`만 `State`를 참조하는 단방향 의존으로 두면** + 이 위험한 패턴 자체를 피할 수 있어 보임 — 다만 이것도 추론이라 실제 + Luau로 확인 전엔 확정 아님. +- 사용자는 `&`(교차 타입) 조합보다 **타입을 손으로 펼쳐 쓰는(flatten) + 쪽을 선호**(엔지니어링 비용을 감수하더라도 솔버 안정성 우선) — 이건 + 런타임 구현의 델리게이션(위 항목)과는 별개 축이라 서로 충돌 안 함: + 타입은 펼쳐 쓰고 구현은 공유하는 조합이 가능함. +- `ROADMAP.md` M0의 "Store/State propagation" 스파이크 항목에 이 구체적 + 케이스(Source가 State를 만족하는 제네릭 메소드 체이닝)를 포함해서 + 검증할 것. -## Store 값을 직접 mutate한 뒤 전파 — `:Emit(key)` (2026-08-06 후속 세션) +**이름 주의**: `Source`/`State`라는 이름 자체가 `CLAUDE.md` "지금 할 일" +2번의 용어 정리 대상(특히 `State`)과 겹침 — 구조(서브타입 관계, RefSource +폐기)는 지금 확정해도 정확한 이름은 용어 정리 라운드까지 가칭으로 남김. + +## Store 값 설정 문법 — `myStore.key = value` 폐기, `source:Set(value)`로 전환 (2026-08-06 후속 세션, 정정) + +**이전 버전("v1 인체공학 유지, `__newindex` 기반 `myStore.key = value` +그대로")은 폐기됨.** 아래 "Source가 State를 만족함" 절의 타입 설계와 +맞물려 재검토된 결과: + +1. **타입 대칭성**: `store.key`가 이제 `Source`를 직접 반환하는 + 평범한 레코드 필드(`{key: Source}`)로 타이핑되는데, 레코드 + 필드는 읽기/쓰기 타입이 같아야 Luau 구조적 타이핑이 깨끗하게 성립함. + `store.key = value`(raw `T` 대입)를 유지하면 읽기(`Source`)/쓰기(`T`) + 타입이 갈려 mismatch가 남음 — `store.key:Set(value)`로 통일하면 필드 + 타입이 항상 `Source`로 대칭적이라 문제 자체가 안 생김(사용자 지적). +2. **의미론적 정직성**: `=` 대입 문법은 관례상 "그 자리에서 즉시 확정되는 + 부작용 없는 값 쓰기"를 암시하는데, quad의 실제 동작은 **lazy** — + `Set`은 무효화 신호만 쏘고, 실제 재계산은 나중에 누군가 관측(`Get()`)할 + 때만 일어남("Emit으로 필요한 사람 있어? 하고 물어보고, 있어야 진짜 + 계산 시작"). 이건 `=`가 암시하는 "즉시 커밋"과 정서가 안 맞고, 메소드 + 호출(`:Set()`)이 "이건 프로세스를 트리거하는 연산"이라는 걸 더 정직하게 + 신호함(사용자 확정 논거). +3. `:Set()`은 이미 확정된 "값을 바꾸는 연산엔 `:` 체이닝 허용" 원칙(`base/ + architecture.md`)에도 자연스럽게 들어맞음 — 문법 자체가 새로 생기는 게 + 아니라 기존 원칙의 정상적인 적용. + +**남는 것**: `myStore "key"`(문자열 커링)는 이미 3차 라운드에서 동적 키 +전용 미타입 폴백으로 격하돼 있었으므로 이번 정정과 무관하게 그대로 유지. +`:` 체이닝 원칙도 `:Set()` 자체가 그 사례라 유지. + +`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 함께 +읽을 것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며 +"불변 빌더"를 흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가 +아니었음. + +## Source 값을 직접 mutate한 뒤 전파 — `:Emit()` (2026-08-06 후속 세션, 호출부 정정) **결정**: Source가 들고 있는 값을 새 값으로 교체하지 않고 제자리에서 -mutate한 뒤, `Store:Emit(key)`로 그 key의 무효화 신호만 별도로 쏘는 -것을 **Source 원천(store가 직접 들고 있는 값)에 한해 허용**한다. +mutate한 뒤, `:Emit()`으로 무효화 신호만 별도로 쏘는 것을 **Source +원천(store가 직접 들고 있는 값)에 한해 허용**한다. + +**[정정, 같은 세션 후반]** 원래 `Store:Emit(key)`(Store에 key를 넘겨 +호출)로 적혀있었으나, 아래 "Source가 State를 만족함" 절에서 `store.key` +자체가 Source를 직접 반환하는 것으로 바뀌면서 `Emit`도 Source의 평범한 +메소드로 이동 — `store.key:Emit()`(key 인자 불필요, 이미 손에 든 Source +핸들에 바로 호출). `Store:Emit(key)`라는 별도 경로는 유지할 이유가 +없어져 폐기(같은 걸 하는 두 번째 경로를 남기지 않는다는 이번 세션 전반의 +원칙과 일치 — `store.key = value` → `store.key:Set(value)` 정리와 같은 결). **존재 이유(우선순위순)**: 1. **clone이 아예 불가능한 값이 있음.** userdata나 외부 라이브러리 @@ -153,8 +241,13 @@ mutation에 정식 신호를 붙여주는 것뿐. 정적 flatten으로 dispatch와 완전히 별개인 단계에서 처리되고 (`base/modifier-plan.md`), `State`가 UB로 확정된 것도 같은 이유(`modifier-plan.md` 7번) — Store/State/dispatch 경로엔 애초에 -Modifier용 processor가 없음. 위 "하드 경계"와 같은 이유로, `Emit`이 -Modifier의 정적 flatten과 충돌할 걱정 자체가 성립하지 않음(둘이 만날 +Modifier용 processor가 없음. **[2026-08-06 후속 세션 추가]** Source가 +State를 구조적으로 만족하게 되면서 이 UB는 `Source`(Store를 +거치지 않는 독립 `Source(someModifier)`)에도 동일하게 적용됨을 명시 — +Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processor +부재)가 그대로 적용되고, 별도로 다시 논증할 필요 없음. 위 "하드 경계"와 +같은 이유로, `Emit`이 Modifier의 정적 flatten과 충돌할 걱정 자체가 +성립하지 않음(둘이 만날 지점이 없음). ## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 확정 diff --git a/.claude/research/documentation-content-map.md b/.claude/research/documentation-content-map.md new file mode 100644 index 0000000..e2975d7 --- /dev/null +++ b/.claude/research/documentation-content-map.md @@ -0,0 +1,174 @@ +# 문서 콘텐츠 분류 맵 (초심자/api/심화/skip) + +**상태**: research — `documentation-plan.md` 0번 항목(3축 구조: 초심자/api/심화 ++ 백엔드별 트랙 분리)이 확정된 뒤, 실제로 각 축에 뭘 채울지 `.claude/base/*.md` +전체 + 관련 `research/*.md`(tween-plan, ui-shorthand-plan)를 2026-08-06 세션에 +6개 에이전트로 병렬 서베이해 분류함. **아직 문서를 쓰라는 뜻 아님** — 착수 +시점은 여전히 구현 우선(`CLAUDE.md` "지금 할 일" 1번). 나중에 실제 문서화를 +시작할 때 이 맵을 목차/우선순위표로 쓰면 됨. + +**api↔심화 연결 원칙(사용자 확정)**: api 문서는 항목마다 설명을 간략하게 +유지하고, 근거·내부 동작까지 파고드는 내용은 심화 섹션으로 링크("더 알아보기 +→ 심화")하는 방식으로 연결. 아래 [api] 항목 중 "→심화"가 붙은 것들이 이 +패턴 대상. + +**분류 기준**: [초심자] core loop에 필수(백엔드 구체적, quad-roblox 기준) / +[api] 레퍼런스, 빠른 룩업용 짧은 설명 / [심화] 왜 이렇게 설계했는지, 최적화· +대규모 코드베이스 관리 관심자용 / [skip] 내부 설계 과정 기록, 최종 사용자 +문서엔 안 들어감(세션 날짜, 정정 이력, 조사 원자료 등). + +--- + +## 1. 초심자(getting-started) core loop — 취합된 목차 초안 + +전체 서베이에서 나온 [초심자] 항목을 실제 학습 순서로 재배열한 것. 이대로 +목차를 잡으면 좋아 보임(그대로 확정은 아니고 초안): + +1. **초기화** — `RobloxFactory(QuadBase)`로 base+backend 조립 (`module-lifecycle-plan.md`, `bind-system-plan.md`) +2. **Instance 만들기** — DOMless 즉시 생성 모델, 제네릭 `new` + 자주 쓰는 ~25개 클래스 정적 필드(`Frame`, `TextButton` 등) (`architecture.md`, `bind-system-plan.md`) +3. **속성 채우기** — `[Attribute "Name"]`, `[Tag ""] = true` 특수 바인드 키 (`architecture.md`) +4. **반응형 기초** — `Source`/`Store` 생성, `store.key`(dot-access)로 Source 읽기(Source는 State를 만족), `store.key:Set(value)`로 쓰기, State는 항상 읽기 전용 (`bind-system-plan.md`, `store-semantics.md`; 2026-08-06 후속 세션에서 dot-access가 Source를 직접 반환하고 쓰기가 `:Set()`으로 바뀜) +5. **스타일링** — Modifier 기본 체이닝(`:FontSize(14)`), 배열/인라인 merge 우선순위 규칙 (`modifier-plan.md`) +6. **자식 전달** — Slot 기본 개념(children 배열, add/remove/clear), 마운트된 slot 재마운트 시 throw (`slot-plan.md`) +7. **컴포넌트 작성** — 컴포넌트 = 순수 함수, 리프 프로퍼티엔 State만 바인딩, 전역 store 직접 참조 금지(이식성) (`component-composition-plan.md`, `purity-and-effects-plan.md`) +8. **컴포넌트 경계 넘기기** — `props.Modifier`/`props.Ref` named parameter 패턴 (`component-composition-plan.md`) +9. **이벤트** — self(Instance) 안 받음, 문자열 키(`Frame { MouseButton1Click = fn }`) (`bind-system-plan.md`) +10. **생명주기** — GC 위임(수동 정리 불필요), Destroy 이후 대상 재사용 금지 (`lifecycle-pattern.md`) +11. **Ref 기초** — 외부 관리 Instance 참조/마이그레이션용, `CreatedRef(fn, {phase=...})` (`architecture.md`, `bind-system-plan.md`) +12. **파생값 최소 예시** — `:With(...)` + `:Compute(fn)` 기본형 (`bind-system-plan.md`, `store-semantics.md`) +13. **Tween 기초** — `[Tween(key, ...)] = storeValue`, 취소 시 현재 보간값에서 자연스럽게 이어짐 (`research/tween-plan.md`) +14. **UI 숏핸드(quad-roblox 한정)** — `Corner`/`PaddingAllOffset`/`Scale` 인라인 키 (`research/ui-shorthand-plan.md`) + +--- + +## 2. 파일별 상세 분류 + +### architecture.md +- 초심자: DOMless 즉시 Instance 생성 모델 / 특수 바인드 키 / Ref 기본 개념 / modifier 기본 사용법(스타일링) / Store·State·Source 온톨로지 핵심 동작 / quad-base·quad-roblox 패키지 구조 존재 사실 +- api: Class 함수형+`:` 체이닝 예외 규칙 / store 바인드=전체 교체 의미론 / Tag/retract(TagService 기반) / modifier 병합 우선순위 규칙(→심화: CSS cascade 회피 근거) / PropertyChangedSignal이 pluggable 핸들러로 구현 / Source·State·Store 타입 정의 +- 심화: Class가 OOP 아닌 함수형인 이유 / metatable 체이닝 폐기 이유(v1 clone 문제) / id 기반 전역 조회 폐지 이유 / Style(Default) 시스템 폐기→modifier 대체 근거 / 멀티 백엔드(GTK 등) 지향 이유 / push-invalidate·pull-recompute 전파 모델 상세, 다이아몬드 의존성 해결 근거 +- skip: Tracker 미구현, lang 모듈 분리, Signal 클래스 미구현 판단 과정, 소스 트리·모노레포 구조, 테스트 전략(mock 설계) + +### comparison-fusion-vide.md — 대부분 skip(내부 리서치 스냅샷) +- **quadnomicon**으로 재작성 가치 있는 것 두 개(2026-08-06 재분류 — 원래 + 심화 후보였다가, 독자층이 "quad 사용자"가 아니라 "프레임워크 설계 자체에 + 관심 있는 엔지니어"라 quadnomicon으로 이동): **Slot 단일 마운트 소유권**이 + Fusion/Vide 둘 다에 없는 quad만의 차별점(Fusion/Vide 경험자 대상 "왜 이중 + mount를 막는가" 비교 소재) / **`:With`+`:Compute` 명시적 파생값**이 Vide의 + 암묵적 ambient stack 대신 채택된 이유(Vide 경험자 대상 비교 설명, 원문 + 재작성 필요) — 단, "왜 Slot은 단일 마운트를 강제하는가" 자체(다른 프레임워크 + 비교 없이 quad 논리만으로 설명 가능한 부분)는 여전히 심화에 남음(아래 4번 + 9번 항목). +- 나머지(Fusion 반응 그래프 BFS 분석, Scope 정리 모델, Vide 디스패치 분석, 비교표 전체)는 전부 내부 설계 근거 수집용, skip — `.claude/` 내부 설계사로만 남고 publish 대상 아님 + +### quad-v1-architecture.md — 전체 skip +v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 회고. 단, **v1에서 +넘어오는 기존 사용자용 마이그레이션 가이드**가 나중에 별도 문서로 계획된다면 +그때만 재사용 가치 있음 — 지금 3축 어디에도 해당 없음. + +### bind-system-plan.md (943줄, 최대 문서) +- 초심자: Source/Store/State 기본 정의+생성자, State 읽기 전용 규칙 / dot-access가 값 읽기 1급 경로 / `:With`+`:Compute` 최소 사용법 / Ref 기본 개념+`CreatedRef` / 이벤트 self 미채택 기본 규칙+문자열 키 / 인스턴스 생성(제네릭+정적 필드) / 라이브러리 초기화 3줄(`RobloxFactory(QuadBase)`) +- api: `state:Observer(fn)` 사용법(→심화: weak-table 내부 인덱싱) / `:Subscribe()`/`:Unsubscribe()` 시그니처(→심화: 강참조 레지스트리 구조) / Ref 일반화 표면 API(→심화: "왜 값이 아니라 콜백인가") / 이벤트 store-bind 존재+권장 안 함 가이드(→심화: 엔지니어링 비용 근거) / 핸들러 4종 계약(`isHandlable`/`priority`/`process`/`retract`) / `Attribute` 특수 키 후보(미확정 명시 필요) +- 심화: push-invalidate/pull-recompute 전파 모델+"관측해야 실체화된다" 원칙+`previous` 캐비엇 / **왜 State를 Modifier처럼 플래튼하지 않는가**(이미 문서화 완료, 아래 3번 참고) / Store가 Store를 못 담는 이유 / 이벤트 self 미채택 4가지 근거 / store-bind 재귀 래핑 내부 메커니즘, retract가 Destroy 시 호출 안 되는 이유 / 같은 팩토리 재호출 no-op·다른 팩토리 충돌 에러 내부 안전장치 +- skip: quad2-try 리서치 결과 섹션 전체(OOP 상속/커스텀 파서/Slot 스텁/`Pipe` 폐기 이력) / PA님 코드 교차검증 절(역사적 검증 기록) / "남은 열린 질문"/"확정된 것" 메타 요약 + +### component-composition-plan.md / module-lifecycle-plan.md +- 초심자: 컴포넌트=순수 함수 / 리프 프로퍼티엔 State만 바인딩 / `props.Modifier`/`props.Ref` named parameter 경계 전달 / `InitRoblox(Module)` 팩토리 초기화 +- api: State(파생, 읽기전용) vs Source(원본, 쓰기가능) 경계 요약(→심화) / Slot 반환 컴포넌트는 Modifier/Ref 파라미터 미선언 / `Modifier.Merge(mod1, mod2, ...)` 유틸 / Bind는 유일 슬롯(재호출 no-op, 충돌 에러, →심화) / `:With`/`:Compute`로 파생 State 생성 시그니처 / 모듈 싱글톤 스코프 +- 심화: v1 `Extend` 자동 store 소유 폐지 이유(React 벤치마킹) / Source가 State를 구조적으로 만족하는 서브타입 설계(2026-08-06 후속 세션 — `StoreSource` 프록시 중간안은 폐기되고 이걸로 대체됨, `store-semantics.md` 참고) / named-parameter 경계 방식 채택 이유(Compose/Fusion/Vide/v1 선례 수렴) / 다중 루트 반환 개념 제거 근거 / 팩토리 초기화 패턴 채택 이유(RBVM `InitNamespace` 반례) / Store 책임 분리(base가 `LifetimeHandle` 소유) / v1 named 체이닝 연산 폐기 +- skip: Compose/Fusion/Vide/v1 프레임워크 비교 원자료 / provider/processor 네이밍 미정 등 열린 질문 메모 + +### lifecycle-pattern.md / purity-and-effects-plan.md +- 초심자: 수동 정리 불필요(GC 위임) / Destroy 이후 재사용 금지 / 컴포넌트는 파라미터로 받은 store만 사용(전역 store 직접 참조 금지) +- api: `Connected`/canExecute 인터페이스(→심화) / 생명 바인드 유틸 시그니처(→심화) / 이식성 규칙이 린트 강제가 아니라 컨벤션이라는 사실 +- 심화: `Connected`가 계산된 속성인 이유(rbvm 근거) / `Instance.Destroying` 훅 단일화 이유 / weak-table GC-native 원칙+eager 정리 예외 / Signal 클래스 미채택 이유 / "quad는 생명주기 중간 계층이 아니다" 소유권 모델 / `retract` 네이밍 배경 / "순수함수 아니라 이식성 문제"로 재정의된 배경(vdom 없음 전제) +- skip: rbvm 조사 세션 메타, EventDrivenProgramming 교차검증 일화(결론만 심화에 남음) + +### modifier-plan.md / slot-plan.md +- 초심자: Modifier 기본 체이닝+merge 우선순위 규칙 실제 예시 / Slot 기본 개념(children 배열)+클래스가 슬롯 받는 방법(Named Slot 없음) / 마운트된 slot 재마운트 시 즉시 throw +- api: Setter가 리터럴/변환 함수 둘 다 받음(→심화: getter 없는 이유) / 필드가 State일 수 있는 4가지 조합 표(→심화: 반응성 유지/끊김 이유) / `Modifier.Rounded(8)` dot-access 생성자 관습 / Slot은 인스턴스당 여럿 가능 / 중첩 인스턴스 자식 처리 / retract 시 slot 내용 폐기(→심화: portal 없는 이유) +- 심화: 정적 merge vs 런타임 pluggable 기각 이유(CSS cascade) / immutable+clone 체이닝 이유(형제 오염 방지) / getter 미채택 이유 / `__index` 런타임 구현 통찰 / Modifier가 핸들러 계층을 모르는 이유 / base/roblox 패키지 경계(Dispatch/Slot vs Handlers/Slot) / Slot 단일 마운트 소유권이 v1/Fusion/Vide 대비 개선인 이유 / retract=폐기 확정 히스토리(portal 검토 후 기각) +- 열린 질문(문서화 보류): 여러 Slot이 형제로 섞일 때 순서 보장 — 미확정 +- skip: 세션 날짜/확정 이력, 문서 승격/정정 안내 + +### store-semantics.md / tween-plan.md / ui-shorthand-plan.md +- 초심자: Store 생성+`myStore.key = value` 문법 / `store.key`로 State 얻기 개념 / Tween 기본 바인드 키+취소 기본 동작 / UI 숏핸드 인라인 키 기본 예시(`Frame { PaddingAllOffset = 50 }`) +- api: `:With`+`:Compute` 시그니처(→심화) / `source:Emit()` 존재+"Get() 결과 캐시 금지" 캐비엇(버그 유발 포인트라 api에도 명시 가치 있음, →심화; 2026-08-06 후속 세션에서 `Store:Emit(key)`→`source:Emit()`로 호출부 변경, `store-semantics.md` 참고) / Tween 핸들러가 Instance 직접 받음(Ref 불필요) / retract는 Destroy 시 호출 안 됨(→심화) / UI 숏핸드 키 목록 레퍼런스 표 / Modifier와 순수 인라인 키 동등성 +- 심화: Source·Store·State·Observer 온톨로지(독립 프리미티브 vs 파생 데이터 원칙, 생성자 모양 근거) / `Emit`이 Source 전용인 이유(디버깅 그래프 무결성) / `Store`의 T가 Modifier 불가인 이유 / Tween을 반응 그래프 밖 특수 bind key로 둔 이유(Fusion 반면교사) / RoundSize 포팅 불필요 vs Corner/PaddingAll/Scale 필요 이유 / "작고 opt-in 아닌 편의 기능은 코어 포함" 원칙 +- 열린 질문(문서화 보류): tween-plan.md의 오버라이드/삭제후재시작/끝점이동 옵션 키 이름 미정 / ui-shorthand의 RoundSize 완전 드롭 여부 +- skip: 세션 정정 이력, v1 소스 조사 경위 + +--- + +## 3. 이미 작성 완료된 심화 콘텐츠 + +- **왜 State 체인을 Modifier처럼 플래튼하지 않는가** — `base/bind-system-plan.md` 해당 절에 결정문 있음(2026-08-06 세션에서 이 대화 중 확정). + +## 4. 심화 전용 신설 콘텐츠 후보 (반복 테마 정리, 에세이 단위) + +위 표에서 반복 등장하는 "왜" 주제들을 에세이 단위로 묶으면: + +1. 왜 함수형 컴포넌트인가(OOP 상속 대신) — `architecture.md`, `component-composition-plan.md` +2. 왜 Modifier는 런타임 pluggable이 아니라 정적 flatten인가 — `modifier-plan.md` +3. 왜 push-invalidate/pull-recompute인가(Fusion eager 노드 미채택) — `bind-system-plan.md` +4. 왜 State는 플래튼하지 않는가 — 작성 완료(위 3번) +5. 왜 GC-native 생명주기인가(Signal 클래스 없음) — `lifecycle-pattern.md` +6. 왜 이벤트 핸들러는 self를 안 받는가 — `bind-system-plan.md`, `research/documentation-plan.md` 3번과 통합 가능 +7. 왜 컴포넌트 경계는 named parameter인가(Compose/Fusion/Vide/v1 수렴) — `component-composition-plan.md` +8. 왜 "다중 루트 반환" 개념을 없앴는가 — `component-composition-plan.md` +9. 왜 Slot은 단일 마운트 소유권을 강제하는가(v1/Fusion/Vide 대비) — `slot-plan.md`, `comparison-fusion-vide.md` +10. 왜 Tween은 반응 그래프 밖에 있는가 — `research/tween-plan.md` +11. 왜 `:Emit()`은 Source 전용이고 파생 State엔 없는가(호출부는 `source:Emit()`, 2026-08-06 후속 세션에서 `Store:Emit(key)`→이 형태로 정리) — `store-semantics.md` +12. 독립 프리미티브 vs 파생 데이터 — 생성자 모양을 결정하는 원칙 — `store-semantics.md` +14. 왜 컴포넌트는 전역 store를 직접 참조하면 안 되는가(이식성) — `purity-and-effects-plan.md` +15. 왜 Source가 State를 구조적으로 만족하는가(Svelte Writable/Readable과 같은 서브타입 모양, `RefSource`/`StoreSource` 중간안이 왜 기각됐는가) — `store-semantics.md`, `component-composition-plan.md` + +(13번이었던 "Fusion/Vide 경험자용 비교 섹션"은 2026-08-06 재분류로 아래 6번 +`quadnomicon`으로 이동) + +## 6. `quadnomicon` — 4번째 축, 프레임워크 설계자용 (2026-08-06 신설) + +**독자층이 다름**: 심화(1~5번)는 "quad를 깊게 이해해 최적화하거나 왜 +이런지 이해하고 싶은 quad 사용자"용. `quadnomicon`은 "비슷한 반응형 UI +프레임워크를 직접 설계/포크하려는 엔지니어"용 — quad를 그냥 쓰기만 +한다면 평생 안 읽어도 무방한 콘텐츠. Rustonomicon 패러디로 이름 확정 +(사용자 선택). + +**현재 후보(둘 다 `comparison-fusion-vide.md`에서 재작성 필요, 원문 +그대로 쓰면 안 됨 — 지금은 우리 내부 리서치 원자료 톤)**: +1. Slot 단일 마운트 소유권이 Fusion/Vide 둘 다에 없는 quad만의 차별점 — + "왜 이중 mount를 막는가"를 Fusion/Vide 내부 동작과 나란히 비교 +2. `:With`+`:Compute` 명시적 파생값이 Vide의 암묵적 ambient stack 대신 + 채택된 이유 — Vide 경험자 대상 비교 + +**publish 안 하는 것과의 경계**: 세션별 정정 이력, 조사 원자료(Fusion +반응 그래프 BFS 분석, quad2-try 죽은 코드 조사 등)는 quadnomicon에도 +안 들어감 — 그건 새 티어가 필요한 게 아니라 애초에 `.claude/` 내부 +설계사로만 남고 절대 publish 안 하는 것(RFC 논의 저장소 같은 성격, +위 각 파일 섹션의 skip 참고). quadnomicon은 잘 다듬은 소수의 큐레이션된 +에세이 공간이지, 내부 연구 기록을 그대로 옮기는 곳이 아님. + +**배경지식 자체가 깊은 주제(예: GC) 처리 방침**: 새 티어를 만들지 않음. +"quad가 GC를 어떻게 활용하는가"(quad 고유)는 심화에 그대로 두되, "GC란 +무엇인가" 자체를 가르치는 자체 튜토리얼은 안 쓰고 외부 좋은 자료로 +링크 처리 — 안 그러면 문서 프로젝트가 일반 프로그래밍 교육 쪽으로 +스코프 크리프될 위험이 있음(사용자 판단). + +## 5. 문서화 아직 보류(미확정 설계라 쓰면 안 됨) + +- Slot 형제 순서 보장 (`slot-plan.md`) +- Tween 오버라이드/삭제후재시작/끝점이동 세부 옵션 키 이름 (`research/tween-plan.md`) +- UI 숏핸드 `RoundSize` 드롭 여부 (`research/ui-shorthand-plan.md`) +- `Attribute` 제네릭 vs 타입별 정적 생성자 (`bind-system-plan.md`) +- provider/processor 네이밍 (`module-lifecycle-plan.md`) + +이 항목들은 `.claude/question.md`에도 이미 열린 질문으로 잡혀있음 — 여기선 +"확정 전엔 문서화 대상 아님"이라는 표시만 겸함. + +## 다음 단계 + +이 맵 자체를 지금 실행할 필요는 없음(구현 착수가 여전히 최우선, +`documentation-plan.md` "다음 단계" 참고). 나중에 실제로 문서 사이트 +작업을 시작할 때: (1) 위 1번 목차 초안으로 초심자 트랙 스캐폴딩, (2) 파일별 +[api] 항목으로 레퍼런스 페이지 스캐폴딩, (3) 4번 리스트를 심화 섹션 +에세이 백로그로 사용. diff --git a/.claude/research/documentation-plan.md b/.claude/research/documentation-plan.md index e940e7b..f65f406 100644 --- a/.claude/research/documentation-plan.md +++ b/.claude/research/documentation-plan.md @@ -5,7 +5,80 @@ 판단해 뼈대만 기록해둠. 세부 설계는 없음, 착수 시점도 미정 — 사용자가 "간단히 plan 작성은 하는 게 맞을 듯, 아주 뼈대만 있는 것이라 할지라도 적어줄 필요는 있겠다"고 판단해서 만든 문서. 같은 날 후속 세션에서 -이벤트 핸들링 self 관습 논의가 확정되며 3번 항목이 추가됨. +이벤트 핸들링 self 관습 논의가 확정되며 3번 항목이 추가됨. 별도 세션 +(날짜 미상, 다음 세션이 채워 넣을 것)에서 전체 문서 사이트 구조 논의가 +추가돼 0번 항목으로 들어감. + +## 0. 전체 문서 사이트 구조 — 3축 + 백엔드별 트랙 분리 (확정, 착수는 여전히 후순위) + +**축 구조 확정**: 초심자(getting started, 전반적 패턴을 실행 가능한 +예제로) / api(레퍼런스, 노출 표면 전부를 빠르게 훑어보기 용) / 심화(왜 +quad가 그렇게 설계됐는지 기술적 근거, 최적화·대규모 코드베이스 관리 +관심자용) 3축으로 관리. Diátaxis 프레임워크(tutorial/reference/ +explanation)와 사실상 동형 — 검증된 패턴이라 그대로 채택. 원래 4분면인 +Diátaxis의 "how-to guides"(태스크 지향 레시피)에 해당하는 콘텐츠는 +별도 축을 새로 만들지 않고 초심자 트랙 안에 녹이기로 함(아래 "초심자 +스코프" 참고, 2번 항목의 "Store 부작용 패턴" 문서가 대표 사례). + +**초심자 문서 스코프 — "core loop 완주에 필요한 최소 집합만"**: +프리미티브(Source/State/Modifier 등)와 그 주요 파생 데이터 입력값 +정도는 처음부터 가르치기 쉬움(quad엔 가상 DOM이 없어서 개념 자체가 +단순함). 반면 `Compute`의 `previous` 인자, Observer, Store `:Emit`, +이벤트 store-bind, Slot 형제 순서 같은 건 "필요해지면 찾아보는" 것으로 +api/심화에 위임 — 처음부터 노출하면 core loop이 흐려짐. 로블록스 +스크립팅 유저 간 실력 편차가 큰 문제는, 초심자 트랙 자체를 짧고 선형으로 +유지하면서 각 개념 옆에 api/심화로 새는 링크를 촘촘히 박아두는 방식으로 +대응 — "쉬운 버전/어려운 버전"을 따로 쓰지 않고 한 문서 안에 탈출구를 +많이 두는 쪽. + +**초심자 문서에 quad-base/quad-roblox 분리를 노출하지 않기로 확정**: +지금은 백엔드가 quad-roblox 하나뿐이라, 초심자에게 "이건 base 프리미티브, +이건 roblox 전용"이라고 구분해서 가르치면 실행도 안 해본 추상 개념부터 +마주치게 됨 — 대신 `Source`/`State`/`Modifier` 등을 전부 구체적인 +Roblox 코드(Frame 만들기 등) 안에서 자연스럽게 등장시키고, 엔진 무관하다는 +사실은 각주 정도로만 언급. 이 구분이 실제로 값어치를 하는 곳은 (a) api +문서 — 패키지별로 quad-base 페이지/quad-roblox 페이지를 나눠 "이 함수가 +어디서 왔는지" 룩업 가능하게, (b) 심화 문서 — base/roblox를 왜 쪼갰는지 +설계 근거 자체가 콘텐츠. + +**백엔드별 튜토리얼은 필요해질 때 추가 — 지금 설계 불필요**: 초심자 +문서는 메이저 버전업이나 개명 수준이 아니면 거의 안 바뀌는 성격이라, +나중에 quad-web/love2d 등 실제로 다른 백엔드가 분리되어 나올 때 그 +사용자들을 위한 튜토리얼을 그때 가서 추가하면 됨 — 이벤트 핸들링 모델도 +다르고 Tween처럼 아예 없는 백엔드도 있을 수 있어 지금 추상 개념을 +먼저 가르치려 하면 없는 공통분모를 억지로 만드는 셈이라 오히려 나쁨. +즉 초심자 트랙은 **백엔드별로 완전히 분리된 트랙**(지금은 quad-roblox +하나)으로 구조화하고, "quad 코어가 뭔지"는 심화에서만 다룸. + +**착수 시점**: 위 1~3번 항목과 동일하게 지금 당장 설계/작성할 필요는 +없음(`CLAUDE.md` "지금 할 일" 1번 — 구현 착수가 최우선). 이 0번 항목은 +"만약 시작한다면 이런 구조로"에 대한 합의이지, 착수 신호는 아님. + +**콘텐츠 분류 완료(2026-08-06)**: 위 3축을 실제로 뭘로 채울지 +`.claude/base/*.md` 전체 + 관련 `research/*.md`(tween-plan, ui-shorthand-plan)를 +초심자/api/심화/skip으로 서베이한 결과는 `research/documentation-content-map.md` +참고 — 초심자 core loop 목차 초안, 파일별 상세 분류, 심화 전용 신설 콘텐츠 +후보(14개 에세이 소재), 아직 미확정이라 문서화 보류해야 할 항목까지 정리됨. +api 항목은 간략 설명 + 심화로 "더 알아보기" 링크를 다는 패턴(사용자 확정)도 +그 문서에 반영. + +**4번째 축 신설 — `quadnomicon`(2026-08-06, 같은 세션 후속)**: 콘텐츠 분류 +중 "심화"로 분류하기엔 성격이 다른 두 갈래가 섞여 있는 게 드러남 — +(a) Fusion/Vide 내부 구현 비교, 다른 프레임워크 선례 조사처럼 "quad를 깊게 +이해해 최적화하려는 사용자"가 아니라 "비슷한 프레임워크를 직접 설계/포크 +하려는 엔지니어"를 위한 콘텐츠, (b) 세션별 정정 이력/조사 원자료처럼 애초에 +공개 문서화 대상이 아닌 내부 설계사. **(b)는 새 티어도 필요 없이 그냥 +`.claude/` 내부 설계 기록으로 남고 절대 publish 안 함**(Rust로 치면 +rust-lang/rfcs 저장소처럼 raw 상태로만 존재). **(a) 중 잘 다듬은 소수만** +Rustonomicon 패러디 이름 `quadnomicon`으로 새 4번째 축에 publish — +독자층은 quad 사용자가 아니라 프레임워크 설계 자체에 관심 있는 엔지니어. +이름은 사용자가 여러 후보(quadnomicon/심해/해구/흑마법서) 중 직접 확정. + +**GC처럼 quad 고유 내용은 아니지만 배경지식 자체가 깊은 주제 처리 방침**: +새 티어를 안 만들고, "quad가 GC를 어떻게 활용하는가"(quad 고유)는 심화에 +그대로 남기되 "GC란 무엇인가" 자체는 자체 튜토리얼을 쓰지 않고 외부 좋은 +자료로 링크 처리하는 쪽으로 방침 확정 — 안 그러면 문서 프로젝트가 일반 +프로그래밍 교육 쪽으로 스코프 크리프될 위험이 있다는 판단. ## 1. UI 요소 네이밍 컨벤션 문서 @@ -85,6 +158,19 @@ UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_`나 `QUAD_` 같 - 어느 문서에 넣을지 — 퀵스타트? 위 1번 네이밍 컨벤션 문서와 마찬가지로 아직 미정. +## 4. 심화 문서 소재 후보 (누적, 2026-08-06 추가) + +설계 논의 중 "이건 왜 이렇게 안 했는가/이렇게 했는가" 식으로 나온 세세한 +근거들 중, 나중에 0번 항목의 "심화" 축에 넣을 만한 것들을 누적 기록. +지금 당장 문서를 쓰라는 뜻 아님 — 원 결정문은 각 `base/` 문서에 이미 +있으므로 여기선 소재 제목과 위치만 남김: + +- **왜 State 체인을 Modifier처럼 플래튼하지 않는가** — 결정문은 + `base/bind-system-plan.md` "왜 State 체인을 Modifier처럼 플래튼하지 + 않는가" 절. State의 정의가 "캐싱하는 존재"라 다이아몬드 파생에서 + 캐시 공유를 잃으면 `previous` 메커니즘이 막으려던 중복 계산 문제가 + 재발한다는 게 핵심 논거. + ## 다음 단계 셋 다 지금 당장 설계할 필요는 없음(`CLAUDE.md` "지금 할 일" 1번 — diff --git a/.claude/research/framework-comparison-findings.md b/.claude/research/framework-comparison-findings.md new file mode 100644 index 0000000..50a9441 --- /dev/null +++ b/.claude/research/framework-comparison-findings.md @@ -0,0 +1,110 @@ +# quad vs Fusion/Vide/react-lua — 정직한 비교 (2026-08-06) + +**상태**: research — 3개 에이전트가 각각 Fusion(`.claude/initreq/fusion` 실 +소스), Vide(`.claude/initreq/vide` 실 소스), react-lua(로컬 클론 없어 +웹 리서치)를 직접 읽고 quad의 확정 설계와 대조. 목적은 마케팅이 아니라 +정직한 자가점검 — "quad가 진짜 나은 부분"(나중에 초심자/quadnomicon 문서의 +"왜 quad인가" 소재)과 "quad가 진짜 불리한 부분, 그중 고칠 수 있는 것"을 +사용자가 직접 검토하기 위함. **quad는 구현 0줄** 상태라 모든 비교가 +"검증된 프로덕션 코드 vs 종이 설계"라는 근본적 비대칭을 안고 있음 — 아래 +모든 강점/약점은 이 전제하에 읽을 것. + +## 1. quad가 실제로 나은 점 (소스 근거 있음, 향후 "왜 quad인가" 문서 소재) + +- **Slot 단일 마운트 가드 — Fusion·Vide 둘 다 없음, 실재하는 버그 클래스를 막음.** + Fusion `Children.luau`엔 `-- TODO: check for ancestry conflicts here` + 주석이 그대로 남아있고 이미 마운트된 인스턴스를 조건 없이 재부모화함(조용한 + 이중 마운트). Vide `mount.luau`도 중복 마운트 체크가 전혀 없음. quad의 + "이미 마운트된 Slot 재마운트 시 즉시 throw"는 둘 다에 없는 실질적 안전장치. +- **열린 우선순위 축 — Fusion의 하드코딩된 4단계보다 확장성 좋음.** Fusion + `applyInstanceProps.luau`는 `{self, descendants, ancestor, observer}` + 정확히 4개 버킷만 갖고 5번째를 쓰면 에러남. quad의 열린 숫자 우선순위 + 레지스트리는 커스텀 bind key를 라이브러리 수정 없이 임의 우선순위에 + 끼워넣을 수 있음. +- **명시적 의존성이 여러 버그 클래스를 원천 차단.** Vide는 전역 `scopes` + 스택 기반 암묵 추적이라 리액티브 스코프 안 yield가 그래프를 깨는 걸 막기 + 위해 별도 `ycall` 장치까지 둠(`graph.luau`). quad의 명시적 `:With` + 의존성 전달은 이 버그 클래스 자체가 발생하지 않음. +- **다이아몬드 의존성 재계산 dedup — Vide가 스스로 미해결로 남긴 문제를 + 더 구조적으로 해결.** Vide `todo.md`가 diamond 그래프 중복 재평가 방지를 + 미해결로 인정했고, 실제로 `test/tests.luau`의 "recursive queue flush + diamond" 테스트가 이상적 2회 대신 3회 실행됨을 재현함. quad의 `invalid` + 플래그 dedup은 이걸 원시 레벨에서 막도록 설계됨. +- **fine-grained라 vdom 특유의 버그 클래스가 통째로 없음(vs react-lua).** + react-lua는 리스트 diffing을 위해 key 관리가 필요하고(불안정하면 자식 + 상태 유실), hooks 호출 순서 규칙이 있으며(위반 시 "Rendered fewer/more + hooks" 에러), 고빈도 갱신엔 리렌더를 우회하는 별도 API(`Bindings`)를 + 공식적으로 추가해야 했음(react-lua 스스로 "vdom 재조정만으론 부족하다"고 + 인정한 셈). quad는 모든 값이 동일한 push-invalidate/pull-recompute + 모델이라 이 세 문제 자체가 없음. +- **Tween을 그래프 밖에 둬서 구조적 복잡도를 회피.** Fusion `Animation/ + Tween.luau`는 `Stopwatch`+`ExternalTime` 그래프 노드와 + `checkLifetime.bOutlivesA` 교차 lifetime 검증까지 필요한 3중 장치. + quad엔 이 장치 자체가 없음(단, 반대급부는 아래 3번 참고). + +## 2. quad가 불리한 점 중 — 고칠 만한 것(fixable, 검토 가치 있음) + +- **use-after-destroy 검증 안전망 부재.** Fusion `Memory/checkLifetime.luau`는 + "짧게 사는 스코프가 오래 사는 대상에 바인딩됐다" 같은 실수를 사람이 읽을 + 수 있는 에러 메시지로 즉시 잡아줌. quad `base/lifecycle-pattern.md`엔 + 이런 사전 검증 개념이 없음. GC-native 프로덕션 동작 자체를 바꿀 필요는 + 없고, **개발/Studio 모드 한정 옵트인 검증 레이어**(quad-debug류와 결합 + 가능)로 추가하는 정도는 GC-native 철학과 안 부딪히고 고려해볼 만함. +- **`:With(...)` 정적 의존성 목록 — Fusion의 동적 재평가보다 약함.** Fusion + `evaluate.luau`는 매 평가마다 실제 `use()`된 의존성만 다시 구독해 특정 + 라운드엔 조건부로 일부 의존성을 아예 구독 안 할 수 있음. quad는 `:With` + 에 나열한 목록이 Compute 시점에 고정돼, lazy handle로 재계산 트리거는 + 피해도 무효화 신호 자체는 계속 도착해 불필요한 재-Get이 누적될 수 있음. + **동적 With 등록/해제 API** 정도로 완화 가능해 보임 — 우연한 갭에 가까움. +- ~~Store dot-access가 매 접근마다 새 State를 할당~~ — **[해소됨, + 2026-08-06 세 번째 세션]** 이 항목이 직접 트리거가 되어 Source/State + 관계 자체를 재구성(`store-semantics.md` "Source가 State를 만족함" 절) — + Store가 이제 생성 시 만들어둔 Source를 그대로 반환해 wrapper 할당 자체가 + 없어짐, 구현 단계 최적화가 아니라 설계로 완전히 없앰(캐싱/풀링보다도 쌈). + +## 3. quad가 불리한 점 중 — 못 고치는 것(의도된 트레이드오프, "고친다" 개념 자체가 안 맞음) + +- **암묵적 추적의 인체공학적 우위(vs Vide)** — `derive()` 안에서 그냥 + 호출하면 의존성이 잡히는 Vide 대비, quad는 전부 `:With`에 나열해야 해 + 보일러플레이트가 늘어남. quad가 "Lua에서 암묵 추적은 부작용 관찰이 + 필요해 지저분하다"는 이유로 의도적으로 거부한 결과라, 명시성을 유지하는 + 한 고칠 개념 자체가 아님(경감책은 있을 수 있음 — 아래 4번 참고). +- **Tween이 그래프 밖이라 다른 Compute의 입력으로 자유롭게 합성 불가(vs + Fusion/Vide)** — Fusion Tween/Spring, Vide `spring()`은 그래프 노드라 + 다른 파생값의 입력으로 얽어 쓸 수 있음. quad는 Fusion을 반면교사 삼아 + 의도적으로 이 경로를 포기한 것이라 원 설계 취지와 충돌. 필요해지면 + 옵트인 브릿지 추가가 현실적 타협(지금 급한 건 아님). +- **GC-native 라이프사이클 자체가 안고 있는 리스크** — Vide는 GC와 + `Instance.Destroying` 순서가 비결정적이라는 알려진 함정 때문에 의도적으로 + eager·수동 cleanup을 택함. quad의 "수동 dispose 불필요"는 GC 의존을 + 없애려면 결국 Vide식 수동 owner 트리로 돌아가야 해서 철학과 충돌 — + 다만 `base/lifecycle-pattern.md`의 rbvm 실물 검증 근거로 리스크는 이미 + 어느 정도 완화돼 있음(기존 base 문서 참고). +- **DOMless+컴포넌트 1회 실행 때문에 "지금 트리가 어떻게 생겼는가"를 + 한눈에 재구성하기 어려움(vs react-lua)** — react-lua는 렌더마다 전체 + 서브트리를 선언적으로 다시 기술해 현재 상태가 코드 한 곳에 드러남. quad는 + 변화가 개별 leaf bind에 흩어져 처리돼 복잡한 조건부 트리 추론이 어려움. + 근본 선택에서 필연적으로 따라오는 트레이드오프라 설계 변경으론 해소 안 + 되고, quad-debug 같은 관측 도구로만 보완 가능(이미 백로그에 있음 — + `research/debug-tooling-plan.md`). + +## 4. 성숙도 격차 — 설계 결함 아니지만 지금 시점 비교에선 정직하게 명시해야 함 + +Fusion(~5000줄+테스트+수년 실사용), Vide(2800줄+테스트+0.1.0→0.4.1 하드닝 +이력), react-lua(Roblox 사내 실사용+전용 벤치마크 레포)는 전부 실전에서 +발견되고 고쳐진 문제들의 산물. quad는 구현이 0줄이라 이 비교의 강점 항목도 +전부 M0 스파이크 이후 실제 Luau로 검증돼야 신뢰할 수 있고, 구현이 진행되면 +유사한 이유로 비슷한 안전장치를 뒤늦게 추가하게 될 가능성이 있음(1번의 +use-after-destroy 검증처럼). "hooks 없는 quad의 `:With`/`:Compute`가 +React 커스텀 훅만큼의 합성성을 실사용 규모에서 주는가"도 지금은 데이터 +없음 — 고칠 문제인지조차 판단 이를 정도로 이름. + +## 다음 단계 + +이 문서 자체는 지금 당장 뭘 바꾸라는 결정문이 아님 — 사용자가 직접 검토 +후 판단할 항목: +- 2번의 남은 두 가지(use-after-destroy 검증, 동적 With)를 실제로 설계에 + 반영할지, 반영한다면 언제(M0 스파이크 때 같이 검증할지, 나중 최적화 + 패스로 미룰지). 세 번째(Store dot-access 할당)는 위에서 이미 해소됨. +- 1번 강점 목록은 `research/documentation-content-map.md`의 "왜 quad를 + 쓰는가" 초심자/quadnomicon 콘텐츠 소재로 재사용 가능. diff --git a/CLAUDE.md b/CLAUDE.md index 11ef18b..695e0fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,12 +38,19 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 `.claude/README.md`가 색인. 요약: - `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저 `.claude/base/architecture.md`를 읽을 것. -- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. 지금은 - `tween-plan.md`(세부 옵션만 남음), `existing-instance-bind-plan.md`(급하지 - 않음) 두 개뿐 — `component-composition-plan.md`는 2026-08-04 세션에 수렴 - 완료돼 `base/`로 승격됨. -- `.claude/qa-request/`, `.claude/archive/`, `.claude/feedback/` — 구현 - 시작되면 쓰기 시작함, 지금은 비어있음. +- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. + `tween-plan.md`/`existing-instance-bind-plan.md`/`debug-tooling-plan.md`/ + `ui-shorthand-plan.md`/`documentation-plan.md`/`documentation-content-map.md`/ + `framework-comparison-findings.md` — 전부 후순위(급한 건 `tween-plan.md` + 세부 옵션 정도). 최신 목록·우선순위는 `.claude/README.md`가 소스, 여기서 + 개수 반복 안 함(과거에 "두 개뿐"이라 적어놨다가 새 문서 추가될 때마다 + 안 갱신되는 패턴이 반복돼서 아예 안 세기로 함). +- `.claude/qa-request/`, `.claude/feedback/` — 구현 시작되면 쓰기 시작함, + 지금은 비어있음. `.claude/archive/`는 원래 같은 취급이었으나 + 2026-08-06 세 번째 세션부터 **완전히 뒤집힌 설계 결정을 원문+역전 + 이유+diff와 함께 보존하는 용도로도 사용 시작**(구현 완료 대상만이 + 아님) — `archive/store-source-proxy-reversed.md`가 첫 사례, 나중 + `quadnomicon` 콘텐츠 소재로 재사용 예정. - `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`) + 원본 요청. **읽기 전용, `.gitignore`로 커밋 제외됨** — 내용을 다른 곳으로 옮기지 말고 항상 원본 @@ -121,9 +128,13 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 채널 실현 가능성(Studio 플러그인↔Play 중 게임 간 BindableEvent/Function 통신)까지 실측 검증 완료, 세부 API 이름만 남음. 착수 시점은 여전히 "quad 개발 상당 부분 끝난 뒤"로 사용자가 못박음. 같은 날 파생된 문서화 - 전략 뼈대(`research/documentation-plan.md` — UI 네이밍 컨벤션, Store - 부작용 게임 시스템 활용 패턴, 권장 이벤트 핸들링 패턴 3종)도 후순위 - 백로그로 같이 남김. + 전략(`research/documentation-plan.md` — UI 네이밍 컨벤션, Store 부작용 + 게임 시스템 활용 패턴, 권장 이벤트 핸들링 패턴 3종)도 후순위 백로그로 + 같이 남김. **[2026-08-06 세 번째 세션에서 크게 확장됨]** 문서 사이트 + 전체 구조(초심자/api/심화/`quadnomicon` 4축)와 실제 콘텐츠 분류맵 + (`research/documentation-content-map.md`), quad vs Fusion/Vide/react-lua + 정직 비교(`research/framework-comparison-findings.md`)까지 늘어남 — + 착수 우선순위 자체는 안 바뀜(여전히 후순위), 아래 최신 세션 요약 참고. 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 (`HUMAN_TODO.md` 2번 항목). @@ -411,3 +422,83 @@ Ref 일반화 — 네 가지 다 확정, 실제 base 문서에 반영 완료.** 쌈). 내부 레지스트리는 자동 케이스의 weak table과 별개로 강참조 (weak면 "살려둔다"는 목적이 무의미해짐). 둘 다 idempotent, `:Unsubscribe()`는 자동 케이스 조기 해제에도 재사용. + +## 2026-08-06 세 번째 세션 — 문서 사이트 구조, 프레임워크 정직 비교, Source가 State를 만족하는 서브타입 재구성 + +같은 날 이어진 세 번째 세션. 셋으로 갈리는 주제라 순서대로 요약 — **다음 +세션이 새로 알아야 할 건 4번(Source/State 재구성)뿐**, 1~3번은 배경/참고용. + +**1. 문서 사이트 구조 확정 — 초심자/api/심화 3축 + `quadnomicon` 4번째 축.** +`research/documentation-plan.md` 0번 항목에 전부 반영. 초심자는 "core loop +완주에 필요한 최소 집합만, 백엔드 구체적(quad-roblox), quad-base/roblox +분리 노출 안 함, 다른 백엔드 생기면 그때 별도 트랙 추가"로 스코프 확정. +api는 간략 설명 + 심화로 "더 알아보기" 링크 패턴. `quadnomicon`(Rustonomicon +패러디, 사용자 확정 이름)은 quad 사용자가 아니라 "비슷한 프레임워크를 +설계/포크하려는 엔지니어"용 4번째 축 — Fusion/Vide 내부 비교 같은 콘텐츠가 +여기 해당, 세션 정정 이력 같은 순수 내부 리서치 원자료는 이 축에도 안 +들어가고 그냥 `.claude/` 내부에만 영구히 남음(RFC 저장소 성격). GC처럼 +quad 밖 배경지식이 깊은 주제는 새 티어 없이 "quad 활용법만 심화에, 일반 +개념은 외부 링크"로 처리. 실제 콘텐츠 분류(초심자 core loop 목차 초안, +파일별 분류, 심화 에세이 후보 15개)는 `research/documentation-content-map.md`. + +**2. quad vs Fusion/Vide/react-lua 정직 비교 — 3개 에이전트가 실제 +소스/웹 리서치로 검증.** `research/framework-comparison-findings.md`. +요지: quad의 Slot 단일 마운트 가드·열린 우선순위 축·명시적 의존성·다이아몬드 +dedup은 실 소스 근거로 확인된 진짜 강점(Fusion `Children.luau`의 TODO +주석, Vide `mount.luau`의 중복 체크 부재, Vide 자신이 `todo.md`에 미해결로 +남긴 diamond 문제 등). 반대로 use-after-destroy 검증 안전망 부재·`:With` +정적 의존성·Store dot-access 할당 비용 3가지는 고칠 만한 약점으로 식별(3번은 +이후 4번 논의로 이미 해소됨). GC-native 리스크·암묵 추적 대비 보일러플레이트· +Tween 비합성성·"지금 트리 상태" 파악 어려움은 의도된 트레이드오프로 "고친다" +개념 자체가 안 맞음. 성숙도 격차(quad 구현 0줄)는 정직하게 명시. + +**3. 위 1·2번에서 파생된 실행 항목**: 아직 결정 아님, `research/ +documentation-plan.md`/`framework-comparison-findings.md`의 "다음 단계"에 +남겨둔 사용자 판단 대기 항목들(문서화 착수 시점, 프레임워크 비교에서 나온 +개선안 반영 여부/시점) 그대로 참고. + +**4. Source가 State를 구조적으로 만족 — Store/State/Source 핵심 메커니즘 +재구성, base 문서 전부 반영 완료.** `store.key`의 타입 문제(레코드 타입 +`{key: State}`가 읽기/쓰기 비대칭이라 Luau 타이핑이 안 맞음)를 +풀다가 나온 더 근본적인 재구성: +- **`Source`가 구조적으로 `State`를 만족**(단방향 호환, Svelte + `Writable extends Readable`와 같은 모양) — `.value`/`:Get()`/ + `:With`/`:Compute` 전부 지원 위에 `:Set(value)`/`:Emit()` 추가. `:With`/ + `:Compute`는 Source에서도 항상 `State` 반환(구현은 metatable `__index` + 델리게이션, `Modifier`의 제네릭 `__index` 트릭과 같은 패턴이라 로직 + 중복 없음). 이 서브타입 관계는 `quad2-try`에서 기각한 컴포넌트/클래스 + OOP 상속과 다른 층위(프리미티브 타입 간 구조적 서브타이핑일 뿐, 사용자가 + 짜는 클래스 계층 구조가 아님)라 그 금지와 안 부딪힘. +- **`RefSource`(store 슬롯 전용 타입 중간안)와 그 전신인 `StoreSource` + 프록시(2026-08-04 세션에서 confirmed였던 것)는 전부 폐기.** Store는 + 이제 "이름 붙은 Source 모음, 그 이상 아님" — `store.key`는 Store 생성 + 시 이미 만들어둔 진짜 Source 객체를 그대로 반환(별도 wrapper 생성/캐싱 + 단계 자체가 사라짐, 이전에 검토한 "State를 weak table로 캐싱"보다도 + 쌈). v1이 타입 없던 시절 습관으로 모든 값을 Store에 몰아넣은 건 "당시엔 + 편해서"였지 지금 그대로 가져올 이유가 아니라는 게 사용자의 회고적 + 재평가 — 그 재검토가 이번 단순화로 이어짐. +- **`store.key = value`(`__newindex`) 폐기, `store.key:Set(value)`로 + 전환** — 이유 둘: (a) 레코드 타입 `{key: Source}`가 읽기/쓰기 + 둘 다 같은 타입이어야 Luau 타이핑이 깨끗한데 대입 문법을 유지하면 + 비대칭이 남음, (b) `=`는 관례상 "즉시 커밋되는 부작용 없는 쓰기"를 + 암시하는데 quad는 실제로 lazy(무효화 신호만 쏘고 재계산은 관측 시점에)라 + 대입 문법이 실제 동작과 정서적으로 안 맞음(사용자 논거). `Store:Emit(key)`도 + 같은 이유로 `source:Emit()`(key 인자 불필요)로 이동 — 같은 일 하는 + 두 번째 경로를 안 남긴다는 원칙과 일치. +- **검증 필요, M0 스파이크에 항목 추가됨(`ROADMAP.md`)**: Source의 + `:Compute` 시그니처가 자기 자신과 `State`를 동시 참조하는 제네릭 + 메소드라 Luau 솔버가 재귀 타입 조합에서 안 막히는지 확인 필요. 자기 + 참조 self 타이핑 자체는 흔하고 안전하나, `State`가 거꾸로 `Source`를 + 참조하는 **상호 재귀**는 Luau의 알려진 취약 패턴이라 피해야 함 — + `State`를 `Source` 참조 없이 독립적으로 먼저 정의하고 `Source`만 + 단방향으로 `State`를 참조하게 두면 이 위험을 피할 수 있어 보이나 + 확정 아님. 타입은 `&`(교차) 조합 대신 손으로 펼쳐 쓰는 쪽으로(사용자 + 선호, 솔버 안정성 우선) — 이건 런타임 구현 델리게이션과 다른 축이라 + 서로 안 부딪힘(타입은 펼치고 구현은 공유 가능). +- **반영된 파일**: `base/store-semantics.md`(신규 "Source가 State를 + 만족함" 절이 최종 소스), `base/bind-system-plan.md`(온톨로지·타입 추론 + 절 정정), `base/component-composition-plan.md`(`StoreSource`/타입 + 유니온 절 재작성), `ROADMAP.md`(M0 항목 추가), `research/ + documentation-content-map.md`/`.claude/README.md`(참조 갱신). 이름 + 자체(`Source`/`State`)는 여느 때처럼 "지금 할 일" 2번 용어 정리 + 라운드까지 가칭. diff --git a/ROADMAP.md b/ROADMAP.md index f4c9f03..a794898 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,6 +17,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [ ] Store/State push-invalidate → pull-recompute propagation을 실제로 짜보기(다이아몬드 의존성 케이스 포함 — 이미 invalid면 전파 중단되는지) +- [ ] Source가 State를 구조적으로 만족하는 제네릭 타입(`:Compute(self: + Source, ...) -> State`류, self 타이핑 + State 참조 혼합)이 + Luau 솔버에서 안전하게 추론되는지 확인(2026-08-06 후속 세션, + `base/store-semantics.md` "Source가 State를 만족함" 절 — `State`가 + `Source`를 참조하지 않는 단방향 의존으로 두면 위험한 상호 재귀는 + 피할 수 있어 보이나 실제 검증 전엔 확정 아님) - [ ] `process`/`retract` 재귀 재-process 디스패치를 실제로 짜보기(store-bind 핸들러 하나 + `isHandlable` 우선순위 스캔 포함) - [ ] `props.Modifier`/`props.Ref` named-parameter로 받는 컴포넌트 하나 작성,