diff --git a/.claude/README.md b/.claude/README.md index d1d461b..99659a1 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -9,10 +9,11 @@ | 폴더 | 기준 | |---|---| -| `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식 | +| `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식. **항상 읽어야 하는** 배경지식만 여기 둠(다른 문서를 이해하는 데 전제되는 것) | +| `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음 | | `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 | | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | -| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용 — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음 | +| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것 | | `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`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | @@ -25,8 +26,6 @@ | 문서 | 내용 | |---|---| | `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서) | -| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 | -| `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차 라운드) | @@ -34,20 +33,28 @@ | `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 뒤늦게 수정] | +| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 | +| `additional-primitives.md` | **[2026-08-07 신설]** `Blocker`(여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤과 함께 개발)와 `Effect`(leaf 죽음에 확정 정리) — Blocker는 메커니즘+이름 확정, Effect는 Observer와의 관계가 아직 미해결(문서 내 "미해결" 절, `question.md` 0번) | +| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고 | + +## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설) + +| 문서 | 내용 | +|---|---| +| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선. **[2026-08-07 `base/`→`reference/` 이동]** v2의 결정 자체가 아니라 다른 문서가 인용하는 온디맨드 자료라 항상 읽을 필요는 없음 | +| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료(전파 모델 등 일부 서술은 이후 라운드에서 뒤집혔으니 `bind-system-plan.md` 쪽을 최신으로 볼 것). **[2026-08-07 `base/`→`reference/` 이동]**, `quadnomicon` 소재 후보 | ## `research/` — 아직 착수 전, 상의 필요 | 문서 | 내용 | 우선순위 | |---|---|---| -| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 | +| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel, 트윈 옵션 값 모양(TweenInfo vs 편의 필드)은 신규 열린 논의 | 중 — 세부 옵션만 남음 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | | `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 | | `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 전후 구현하면 됨 | -| `additional-primitives-plan.md` | 확정 프리미티브(Source/State/Store/Ref/Observer/Modifier/Slot/DI)만으로 충분한지 웹 프레임워크·Fusion/Vide/v1 소스 근거로 조사 — 키 기반 동적 컬렉션 재조정(Fusion `ForPairs`/Vide `indexes()`류)이 가장 명확한 빈 자리로 확인, Effect/Batch/Context는 부차적 후보 | 상 — 사용자 판단 대기, 착수 전 M0/M1 스코프에 영향 줄 수 있음 | +| `additional-primitives-plan.md` | **[2026-08-07 범위 축소]** 확정/기각된 Effect·Blocker·Batch·Context는 `base/additional-primitives.md`·`archive/`로 분리됨 — 이제 **키 기반 동적 컬렉션 재조정**(Fusion `ForPairs`/Vide `indexes()`류에 대응하는 프리미티브가 quad엔 없음) 하나만 다룸 | 상 — 사용자 판단 대기, 착수 전 M0/M1 스코프에 영향 줄 수 있음 | | `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1(M0~M4 착수 전 확인 권장) + 11개 우선순위2 + 2개 단순화후보 | 상 — M0 착수 전 최소 우선순위1 항목 확인 권장 | | `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 | @@ -55,7 +62,11 @@ | 문서 | 내용 | |---|---| -| `store-source-proxy-reversed.md` | 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 | +| `store-source-proxy-reversed.md` | [역전됨] 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 | +| `ref-phase-option-reversed.md` | [역전됨] `CreatedRef`의 `phase` 옵션 — 위치 기반 순서 + `PreRef` 신설로 대체됨 | +| `ui-shorthand-roundsize-dropped.md` | **[기각됨, 2026-08-07 신설]** v1 `RoundSize`(이미지 9-slice 라운드 트릭) — 네이티브 `UICorner`로 대체되어 포팅 불필요. 이 판단이 한 차례 "Corner/PaddingAll/Scale 숏핸드 전체가 불필요하다"로 과잉일반화됐다가 정정된 이력 포함 | +| `batch-rejected.md` | **[기각됨, 2026-08-07 신설]** lexical `Batch(fn)` — 코루틴 yield 위에서 구조적으로 위험해 기각, 값 기반 `Blocker`(`base/additional-primitives.md`)로 대체 | +| `context-rejected.md` | **[기각됨, 2026-08-07 신설]** `Context`(트리 하위 암묵 전파) + 대안이던 레이어드 Store 둘 다 기각 — 명시적 타입 강제 Store 전달로 충분하다는 판단 | ## 참고 diff --git a/.claude/archive/batch-rejected.md b/.claude/archive/batch-rejected.md new file mode 100644 index 0000000..52a52f5 --- /dev/null +++ b/.claude/archive/batch-rejected.md @@ -0,0 +1,63 @@ +# [기각됨] `Batch(fn)` — lexical block 기반 지연/합치기, `Blocker`로 대체됨 + +**기각 일시**: 2026-08-06~07. **현재 유효한 설계**: `base/ +additional-primitives.md`의 "Blocker" 절 — 이 문서가 다루는 것과 같은 +문제("여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게")를 +값 기반으로 풀어 대체함. 이 파일은 더 이상 능동적으로 참고할 필요 없음 +(구현에 안 씀) — "왜 lexical Batch를 기각하고 값 기반 Blocker를 택했는가"가 +`quadnomicon`(프레임워크 설계자용 심화 콘텐츠) 소재로 가치 있어서 사유를 +통째로 보존해둔 것. + +**중요**: 이건 "값 기반 지연/합치기"라는 문제 자체를 기각한 게 아니라, +**그 문제를 lexical block(Solid `batch()`/MobX `runInAction()`류)으로 +풀려는 접근만** 기각한 것 — 실제 해법은 완전히 다른 별개 primitive인 +`Blocker`로 채택됨. + +## 무엇을 검토했었나 + +`Batch(fn)`을 "플래그 세우고 `fn` 실행, 끝나면 flush"로 구현하는 안 — +`fn` 안에서 여러 `Set()`을 몰아서 호출해도 소비자에게 전파는 `fn`이 끝난 +뒤 딱 한 번만 되게 하는, 함수/코루틴 스코프 lexical transaction 블록. + +### "즉시 pull"이 뭔지 (참고용 예시) + +store-bind 핸들러(예: `Frame { BackgroundColor3 = total }`)는 lazy가 +아니다 — 화면에 실제로 반영하는 "누군가"가 바로 이 핸들러 자신이라, +무효화 신호를 받자마자 스스로 `Get()`하고 바로 대입한다: + +```lua +local total = a:With(b):Compute(function(av, bv) return av + bv end) +Frame { BackgroundColor3 = total } + +a:Set(1) -- total 무효화 → 핸들러가 즉시 (av=1, bv=이전b)로 재계산+대입 +b:Set(2) -- total 무효화 → 핸들러가 즉시 (av=1, bv=2)로 다시 재계산+대입 +-- BackgroundColor3가 중간값을 한 번 거쳐가고, 두 번 대입됨 +``` + +## 기각 이유 — 코루틴 yield 위에서 구조적으로 위험 + +`Batch(fn)`을 "플래그 세우고 `fn` 실행, 끝나면 flush"로 구현하면 **`fn`이 +yield하는 순간 위험해진다**(사용자 지적, 정확함): + +1. 플래그가 전역이면, yield 중 스케줄러가 돌리는 무관한 코루틴의 `Set()`이 + 이 Batch에 잘못 휘말릴 수 있음. +2. 플래그를 코루틴 스코프로 만들어도(Fusion `Contextual`류 코루틴 키 weak + table), `fn` 안에서 새 코루틴을 스폰하는 API(Promise, `task.spawn`)를 + 부르면 그 새 코루틴은 배치 스코프를 상속 못 받아 일부 Set이 새어나감. +3. `fn`이 영원히 재개 안 되면(장시간 대기, 리크된 코루틴) flush가 영영 안 + 일어나 store-bind 핸들러들이 화면을 무기한 stale 상태로 방치 — 즉시 + pull보다 더 나쁜 실패 모드. + +이건 구현을 잘 짜서 피할 수 있는 버그가 아니라 **lexical block 모델 +자체가 협조적 스케줄링 환경과 구조적으로 안 맞는 것**으로 판단, 기각. + +## 왜 완전히 헛수고는 아니었나 + +"지연 구간을 표현하고 싶다"는 문제의식 자체는 정확했고, `Blocker`가 +정확히 그 문제를 콜스택/코루틴이 아니라 **값**(`Blocker` 객체의 +`On()`/`Off()`)으로 표현해 풀었다 — Batch가 무너뜨렸던 세 가지 실패 +모드(전역 플래그 오염, 새 코루틴 미상속, 영구 yield로 인한 무기한 방치)가 +Blocker에선 구조적으로 전부 해당 안 됨(`On()`/`Off()` 사이에 얼마나 많은 +yield/코루틴 전환이 끼어도, 심지어 완전히 다른 코루틴에서 `Off()`를 +불러도 문제없음). `quadnomicon`에서 "콜스택/코루틴 스코프로 상태를 +표현하려던 시도가 왜 항상 위험한가"의 구체 사례로 쓰기 좋음. diff --git a/.claude/archive/context-rejected.md b/.claude/archive/context-rejected.md new file mode 100644 index 0000000..e39ece6 --- /dev/null +++ b/.claude/archive/context-rejected.md @@ -0,0 +1,61 @@ +# [기각됨] `Context`(트리 하위 암묵 전파) + 대안이었던 "레이어드 Store" + +**기각 일시**: 2026-08-06~07. **현재 유효한 설계**: 명시적 타입 강제 +Store 전달(`props.Theme: Store`처럼 컴포넌트가 필요한 걸 named +parameter로 명시적으로 요구) + 오버라이드가 필요한 지점에서 +`Store({...부모값, 변경필드=새값})`을 한 번 명시적으로 만들어 그 지점부터 +평소처럼 prop으로 넘기는 것 — 새 primitive 없이 이미 있는 Modifier의 +"merge, 나중 게 이김" 패턴 재사용. 이 파일은 더 이상 능동적으로 참고할 +필요 없음(구현에 안 씀) — "왜 Context가 없는가"가 `quadnomicon`(프레임워크 +설계자용 심화 콘텐츠) 소재로 가치 있어서 사유를 통째로 보존해둔 것. + +## 무엇을 검토했었나 + +React `Context`/Vue `provide`-`inject`류, 트리 상위에서 값을 하나 심어두면 +중간 컴포넌트가 명시적으로 전달하지 않아도 하위 어디서든 그 값을 읽을 수 +있는 암묵적 전파 메커니즘. + +### 난이도 판정 요약 + +서브에이전트 조사 결과: 동기적 저작 트리에 한정한 얕은 버전(Fusion +`Contextual`류 코루틴 키 weak table push-pop)은 구현 난이도 **낮음**이지만, +quad가 정상 패턴으로 확정한 "Slot에 이벤트 핸들러/코루틴에서 비동기로 +자식이 추가되는 경우"엔 **에러 없이 조용히 기본값으로 폴백**하는 함정이 +있음. 완전 자동(비동기 추가까지 자동 전파)은 Roblox Luau에 +thread-local/async-context-propagation 훅이 없어 **사실상 불가**(플랫폼 +한계, Node `AsyncLocalStorage`/Python `contextvars`도 "자동"이 아니라 +"async 경계마다 명시적 캡처+재진입"으로 같은 문제를 품). + +## 기각 이유 + +얕은 버전조차: (1) `base/slot-plan.md`가 정상 패턴으로 명시한 Slot 비동기 +추가에서 가장 먼저, 가장 조용히 깨짐. (2) `research/debug-tooling-plan.md`의 +"모든 연결은 선언된 그래프여야 한다"는 quad-debug 철학과 충돌하는 안 보이는 +채널을 만듦. + +## 대안이었던 "레이어드 Store"도 철회 (사용자 반박 수용) + +Context 대신 권고했던 대안 — "레이어드 Store"(자식 Source 모음이 없는 +키는 부모로 `__index` 폴백)도 사용자 반박으로 철회됨: + +- quad는 이미 **타입으로 강제되는 명시적 Store 전달**을 갖고 있고, 이건 + Context의 "Provider 안 넣으면 조용히 기본값/에러"보다 **더 안전**하다 + (컴파일타임에 걸림 vs 런타임에 조용히 새는 값) — Context보다 나은 지점. +- "필드 일부만 오버라이드, 나머지는 부모 값 그대로"가 필요하면, 오버라이드 + 지점에서 `Store({...부모값, 변경필드=새값})`을 **한 번 명시적으로** + 만들어서 그 지점부터 평소처럼 prop으로 넘기면 끝 — Modifier가 이미 쓰는 + "merge, 나중 게 이김" 패턴 재사용 가능, 새 primitive 불필요. 레이어드 + Store(읽는 시점에 몇 단계를 거슬러 올라가는지 안 보이는 자동 폴백)는 + 정확히 Context와 같은 이유(디버깅 어려움 — "이 값이 왜 이거지?"를 + 추적하려면 부모 체인을 다 훑어야 함)로 얻는 것보다 잃는 게 크다. +- 서드파티 라이브러리가 뭔가 필요하면, 타입이 강제하는 명시적 요구 + (`props.Theme: Store`)가 "몰래 안 줘서 죽는다"보다 나은 실패 + 모드 — Slot 기반 저작 모델(부모가 자손을 직접 구성)에서도 이런 요청은 + 자연스럽게 props로 흐른다. + +## 결론 + +Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Context가 +없는가"(명시적 Store 전달이 이미 그 역할을 하고, 타입 강제가 Context의 +실패 모드보다 안전하다는 논증)는 `quadnomicon` 에세이 후보로 등록 +(`research/documentation-content-map.md` 참고). diff --git a/.claude/archive/ui-shorthand-roundsize-dropped.md b/.claude/archive/ui-shorthand-roundsize-dropped.md new file mode 100644 index 0000000..cc87560 --- /dev/null +++ b/.claude/archive/ui-shorthand-roundsize-dropped.md @@ -0,0 +1,44 @@ +# [기각됨] `RoundSize`(이미지 9-slice 라운드 트릭) 포팅 — 네이티브 `UICorner`로 대체되어 불필요 + +**기각 일시**: 2026-08-06. **현재 유효한 설계**: `base/ui-shorthand-plan.md` — +이 문서는 v1의 `RoundSize`가 왜 포팅 대상에서 빠졌는지, 그리고 그 판단이 +한 차례 잘못 일반화됐다가 정정된 이력을 보존해둔 것. 능동적으로 참고할 +필요 없음(구현에 안 씀) — `RoundSize`류 "네이티브 Instance가 나중에 생겨 +워크어라운드가 필요 없어진 사례"는 `quadnomicon` 소재로 가치 있음. + +## 무엇이었나 + +v1 `class.lua`가 지원하던 특수 키 `RoundSize = 16`(`ImageLabel`/ +`ImageButton` 전용) — `UICorner`가 아니라 이미지 자체를 9-slice로 잘라 +둥글게 보이게 만드는 트릭(`round.SetRound()`). `UICorner`/`UIPadding`/ +`UIScale` 자동 생성 숏핸드(`Corner`/`PaddingAll`/`Scale`, 현재 +`base/ui-shorthand-plan.md`가 이어받은 기능)와 겉보기엔 "인라인 리터럴 값 +하나로 GUI를 꾸민다"는 카테고리가 비슷해 보이지만, **메커니즘 자체가 +완전히 다름**(하나는 별도 Instance 생성, 하나는 이미지 처리) — 이 문서가 +쓰인 이유가 바로 이 둘을 혼동하지 않기 위함. + +## 기각 이유 + +`RoundSize`는 **당시 Roblox에 `UICorner` 같은 네이티브 구현체가 없었기 +때문에** 존재하던 워크어라운드였음. 지금은 `UICorner`가 안정적인 네이티브 +Instance라 이미지 대상에도 그냥 실제 `UICorner`를 붙이면 되므로, 이미지를 +9-slice로 잘라 둥글게 "보이게" 만드는 트릭 자체를 그대로 포팅할 이유가 +없음 — **포팅 안 함으로 확정**. + +## 왜 archive에 남기나 — 한 차례 과잉일반화됐다가 정정된 이력 + +`RoundSize` 하나를 드롭하기로 한 판단이, 초안 작성 과정에서 실수로 +**"UICorner가 네이티브가 됐으니 Corner/PaddingAll/Scale 숏핸드 자체가 +불필요하다"는 훨씬 넓은 결론으로 잘못 일반화된 적이 있었음**("이전 정리 +('포팅 불필요')는 오해였고 정정함"). 사용자가 직접 반박해 정정됨: +`UICorner`가 네이티브 Instance가 됐다는 사실은 "이미지를 트릭으로 둥글게 +보이게 할 필요가 없어졌다"는 것만 의미할 뿐 — `UIScale`/`UIPadding`류가 +**여전히 부모에 Parent해야 하는 별도 Instance**라는 구조적 사실 자체는 +전혀 안 바뀌었으므로, `Corner`/`PaddingAll`/`Scale` 숏핸드(현재 +`UICorner`/`UIPadding`/`UIScale`)의 존재 이유는 그대로 유효. + +**교훈(재사용 가능)**: "네이티브 Instance가 생겼다"는 사실 하나로부터 +"관련 숏핸드 전체가 불필요해졌다"를 성급히 일반화하지 말 것 — 워크어라운드가 +드롭되는 이유(네이티브 대체재 등장)와 편의 숏핸드가 필요한 이유(별도 +Instance를 만들어 Parent해야 하는 구조적 번거로움)는 서로 다른 축이라, +하나가 해소됐다고 다른 하나도 자동으로 해소되는 게 아님. diff --git a/.claude/base/additional-primitives.md b/.claude/base/additional-primitives.md new file mode 100644 index 0000000..4c0d60d --- /dev/null +++ b/.claude/base/additional-primitives.md @@ -0,0 +1,157 @@ +# 추가 확정 프리미티브 — Blocker / Effect + +**상태**: base — `research/additional-primitives-plan.md`(다른 프레임워크 대비 +갭 분석)에서 갈라져 나온 두 확정 프리미티브. Batch(lexical block)/Context는 +기각되어 각각 `archive/batch-rejected.md`/`archive/context-rejected.md`로, +아직 미확정인 키 기반 동적 컬렉션 재조정은 `research/additional-primitives-plan.md`에 +그대로 남아있음 — 이 문서는 **확정된 것만** 다룬다. + +## Blocker — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게 + +**왜 필요한가**: `state1, state2 -> state3`처럼 여러 소스가 한 파생값에 +합류할 때, 둘을 한 번에 바꾸면 소비자에게 두 번 전파(재계산+재대입)되는 +문제. lexical `Batch(fn)`(Solid `batch()`/MobX `runInAction()`류)으로 +풀려던 접근은 코루틴 yield 위에서 구조적으로 위험해 기각됨 — 상세 근거는 +`archive/batch-rejected.md` 참고, 여기서 반복하지 않음. **Blocker는 그 +문제를 콜스택/코루틴이 아니라 사용자가 들고 있는 "값"으로 표현**해서 이 +위험을 구조적으로 우회한다. + +**store 개발(M3)과 밀접하게 연관됨** — `state:Block(blocker)`가 State +위에 얹히는 메소드이므로 `base/store-semantics.md`의 Store/State/Source +온톨로지, 특히 push-invalidate/pull-recompute 전파 모델(`base/ +bind-system-plan.md` "전파 모델 확정" 절)을 전제로 함. 별도 파일로 두되 +State와 같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것. + +### 메커니즘 (확정) + +``` +Blocker() -> blocker -- 생성자 +blocker:On() -> self -- IsBlocked = true로만 설정, 그 외 아무것도 안 함 +blocker:Off() -> self -- IsBlocked = false로 먼저 설정, 그 다음 등록된 + -- onunblock 핸들 전부 실행(순서 무관, idempotent) + +state:Block(blocker) -> state -- 새 gated state 반환. **호출되는 즉시**(나중에 + -- 처음 블록될 때가 아니라) onunblock 핸들을 + -- blocker의 weak 배열에 등록. +``` + +gated state의 동작: +- 원본 state가 emit(무효화)될 때, 이 gated state로 전파를 시도. +- `blocker.IsBlocked`이면: 전파 안 하고 `HasBlockedEmit = true`만 세팅. +- `blocker.IsBlocked`가 아니면: 평소처럼 그냥 전파(투명하게 통과). +- `blocker:Off()`가 실행하는 onunblock 핸들은: `HasBlockedEmit`을 확인해 + true면 그제서야 정확히 1회 전파(emit)하고 플래그를 리셋. 이미 false면 + 아무 것도 안 함(idempotent). + +**`:Get()`엔 영향 없음** — 블록은 emit **전파**만 지연시킨다. 블록 중이라도 +누군가 명시적으로 `:Get()`하면 그 순간의 실제 값을 정상적으로 계산해서 +준다 — `store-semantics.md`의 "`Get()`은 라이브 레퍼런스를 준다" 원칙과 일치. + +### 사용 예시 + +`state1`/`state2` 각각이 아니라 **결합된 결과(`state3`) 하나에만** `:Block`을 +건다: + +```lua +local blocker = Blocker() +local gated3 = state3:Block(blocker) -- 소비자는 gated3를 구독 + +blocker:On() +state1:Set(1) -- state3 무효화 → gated3로 전파 시도 → 블록됨 → HasBlockedEmit=true +state2:Set(2) -- state3 무효화 → gated3로 전파 시도 → 이미 true, 그대로 +blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한 번 emit +``` + +**일반 사용 가이드(확정, 문서화 필수)**: Block은 **파이프라인의 최종 연산 +지점**(실제로 무거운 계산이 일어나는 derived state, eager 소비자에 가장 +가까운 지점)에 거는 게 원칙 — 소스가 여러 개든, 하나가 한 주기에 여러 번 +바뀌든 상관없이 이 지점 하나만 지키면 됨. 소스 쪽에 각각 거는 게 아니다. + +### 이름 확정 + +- 클래스: `Blocker` — `Observer`/`Modifier`/`Ref`와 같은 명사-행위자 + 네이밍 관례와 일치. +- Blocker 자신의 토글: **`On()`/`Off()` -> self** (`Block()`/`Unblock()` + 아님) — `state:Block(blocker)`가 이미 "배선(wiring)" 동작의 동사로 + "Block"을 쓰고 있어서, Blocker 자신의 토글까지 같은 단어를 쓰면 + `blocker:Block()`(블로커를 켠다)과 `state:Block(blocker)`(state를 이 + 블로커에 배선한다)가 같은 단어로 다른 두 동작을 가리키게 됨. +- 필드: **`IsBlocked`**(Blocker 자신의 On/Off 상태), **`HasBlockedEmit`** + (gated state의 대기 플래그, `Is`/`Has` 접두어로 불리언임을 바로 알려줌). +- 메소드: `state:Block(blocker) -> state`. + +### 재진입(네스팅) — 의도적으로 미지원, 강한 문서화 필수 + +`IsBlocked`는 카운터가 아니라 단순 불리언이고, **의도적으로 그렇게 둔다.** +레퍼런스 카운팅으로 네스팅을 지원할 수도 있었지만, 그러면 "`On()` 여러 +번, `Off()` 실수로 적게" 같은 버그가 **영구 블록으로 조용히 새는** 더 +위험한 실패 모드를 만든다("poisoned mutex" 트래킹류 해키함도 만들지 +않기로 함). + +**대신 확정된 규칙**: 겹치는 배치가 필요하면 **각자 새 `Blocker` 인스턴스를 +만들 것** — 하나의 Blocker를 여러 컨텍스트에서 재사용/중첩하지 않는다. +`Off()`는 스태킹 없이 즉시 그 자리에서 꺼진다. **이 제약은 반드시 사용자 +문서(API 레퍼런스 수준)에 명시적으로 강조할 것** — 네스팅을 시도하면 +조용히 잘못된 시점에 조기 해제되는, 원인 추적이 어려운 버그로 이어짐. + +### 상태: 핵심 메커니즘+이름 확정. 남은 건 문서화뿐 + +`quadnomicon`에서 "Batch를 기각하고 왜 Blocker로 갔는가"를 비교 설명하는 +게 좋은 소재(`archive/batch-rejected.md`와 나란히 인용). + +--- + +## Effect — leaf 죽음에 확정 정리, 재실행 개념 없음 + +**Observer와는 별개의, 완전히 새로운 요소로 확정** — Ref/PreRef 같은 +서로 파생된 관계가 아니라 독립적으로 존재하는 primitive. Roblox엔 +`task.spawn`으로 코루틴에 반복문/타이머를 돌리는 패턴이 흔하고, Luau +테이블엔 `__gc` 같은 GC 시점 훅이 없어서 "이게 진짜 사라지는 순간"을 아는 +유일한 방법은 `Instance.Destroying`류 명시적 신호뿐 — 이런 케이스(타이머 +시작 → leaf가 죽을 때 반드시 정지)를 위한 별도 primitive로 합의됨. + +``` +Effect(fn) -> EffectHandle -- fn을 즉시 1회 실행, 리턴값(nil | () -> ())은 + -- 이 Effect가 바인드된 leaf가 죽을 때 정확히 1회 호출 +``` + +**재실행 개념이 없다** — 값 변화에 반응해 다시 도는 건 Observer(+클로저로 +직접 짠 cleanup)의 영역이고, Effect는 순수하게 "설치 + 확정 정리" 페어 +하나만 담당한다. children 배열에 leaf로 놓는 기존 Observer 바인딩 패턴을 +그대로 재사용(그 leaf가 살아있는 동안만 유효, leaf가 죽으면 정리 콜백 +호출). 비용은 leaf당 실제 Destroying 바인딩 하나(공유 weak table로 되는 +Observer보다 비쌈) — 필요할 때만 쓰는 걸로 충분. + +**Observer에 cleanup 반환 계약을 추가하는 안은 기각됨** — React `useEffect`류로 +`fn`이 `nil | () -> ()`를 반환하면 다음 재실행 직전에 그걸 불러주는 안을 +검토했으나, 클로저 업밸류로 이미 쉽게 되고 잘 작동해서(`local lastConn; +state:Observer(function() if lastConn then lastConn:Disconnect() end; +lastConn = ... end)`) 프레임워크가 이걸 대신해줄 이유가 약하다는 판단 — +`state:Observer(fn)` 자체는 여전히 재실행 계약만 갖고, Effect가 별도로 +"1회 설치 + 확정 정리"를 담당하는 이 분리 구조가 유지됨. + +### ⚠️ 미해결 — Effect와 Observer의 관계, 사용자 확인 필요 + +**임의로 결론내지 않고 열어둠(2026-08-07 문서 정리 세션)**: 위 스펙은 +`Effect(fn)`를 State에 종속되지 않는 완전한 자유 함수로 서술하지만, 아래 +두 가지가 문서상 명확히 확인되지 않음: + +1. **`Effect`가 실제로는 `state:Effect(fn)`처럼 State의 메소드(=Observer의 + 변형, "재실행 없음 + 확정 정리 추가"만 다른 버전)로 구현/노출되어야 + 하는 것 아닌가?** — 그렇다면 "독립 존재 가능한 프리미티브 vs 원천에 + 종속된 파생 데이터"(`base/store-semantics.md`) 분류상 Effect도 + Observer처럼 후자(자유 함수 생성자 없음, 항상 `:` 메소드)로 재분류해야 + 함. 지금 이 문서는 이전 조사(`research/additional-primitives-plan.md`)를 + 따라 자유 함수로 서술했지만, 이게 최종 확정인지는 불명확. +2. **`state:Observer(fn)`가 생성 시점에 `fn`을 즉시 1회 실행하는지가 문서 + 어디에도 명시돼 있지 않음** — `base/bind-system-plan.md`의 Observer + 절은 "값을 안 실어줌, `fn` 본문에서 `Get()`을 다시 읽어야 함"만 + 명시할 뿐 "생성 즉시 1회 호출되는지"는 다루지 않는다. Effect는 + "즉시 1회 실행"이 스펙에 명시돼 있어 이 부분만 보면 둘이 겹쳐 + 보인다. + +이 두 질문이 풀리면 Effect가 (a) 완전히 별개인 자유 함수 primitive로 +남는지, (b) `state:Effect(fn)`로 Observer 계열에 합류하는지가 갈린다. +**확인 전까지는 위에 적은 자유 함수 스펙을 잠정 스펙으로 두되, 구현 +착수(M3~M4 전후) 전에 반드시 사용자와 다시 확인할 것** — `.claude/ +question.md`에 같은 항목 등재됨. diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 8e27a9d..5d6ba00 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -3,8 +3,8 @@ **상태**: base — 횡단 결정의 최종 상태 요약. 특정 기능 plan이 아니라 프로젝트 전체에 걸친 결정이라 완료 개념 없음. 근거가 된 원본 브레인스토밍은 `.claude/initreq/raw-userinput.md`(안 옮기고 그대로 둠 — 이 문서들로 나누기 전의 -raw chain-of-thought 백업 역할). 현재 v1 구조는 `base/quad-v1-architecture.md`, -비교 리서치는 `base/comparison-fusion-vide.md`, `base/lifecycle-pattern.md` 참고. +raw chain-of-thought 백업 역할). 현재 v1 구조는 `reference/quad-v1-architecture.md`, +비교 리서치는 `reference/comparison-fusion-vide.md`, `base/lifecycle-pattern.md` 참고. ## 한 줄 요약 @@ -23,7 +23,7 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo 바인드처럼 정말 체인이 자연스러운 곳에만(`:` 문법) 남김. 타입 작성 난이도가 OOP 스타일에서 너무 커진다는 게 이유. 3. **복사(clone) 구현 지양, 팩토리 함수로 대체.** v1의 metatable 체이닝(1-필드 - 테이블을 계속 쌓는 방식, `base/quad-v1-architecture.md` 참고)은 폐기. + 테이블을 계속 쌓는 방식, `reference/quad-v1-architecture.md` 참고)은 폐기. store 바인드에 대한 변경은 "전체 변경"으로 간주(UB 아님, 문서화된 의미론) — 부분 복사/오버레이가 필요하면 팩토리 함수로 필요한 곳만 명시적으로 복사. 4. **PA님 스타일 DI 키 계속 지원**: `[Attribute "Name"]`, `[Tag ""] = true` 같은 @@ -67,12 +67,12 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo 구현(`base/bind-system-plan.md`). 9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher, 실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua`에 - 연결 안 된 죽은 코드였음, `base/quad-v1-architecture.md` 참고)은 렌더 + 연결 안 된 죽은 코드였음, `reference/quad-v1-architecture.md` 참고)은 렌더 라이브러리 범위 밖으로 판단. 스토리북 구현체(https://ui-labs.luau.page/docs/getstarted)가 이미 존재하므로 대체. 10. **lang 모듈 미구현, 분리.** 로케일/문자열 처리는 리액터블 라이브러리와 별개 라이브러리로 존재해야 함(v1 `lang.lua`의 전역 스코프 버그 등은 - `base/quad-v1-architecture.md` 참고 — 애초에 반면교사). + `reference/quad-v1-architecture.md` 참고 — 애초에 반면교사). 11. **커스텀 Signal 클래스 미구현.** 콜백 정도로 이벤트 바인드 뒤에 함수를 넣는 것만으로 충분하다고 판단. (이전 초안엔 "rbvm의 Signal이 재사용 가능해 보여 상충한다"는 메모가 있었으나 2026-08-04 검증 라운드에서 최종 diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 59e30d8..1b524d1 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -8,9 +8,9 @@ Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치 array API, `CreatedRef` 모양) 뿐 — 구현 단계에서 자연히 정리됨. 원본: `.claude/initreq/raw-userinput.md` "key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를 -저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `base/quad-v1-architecture.md` +저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `reference/quad-v1-architecture.md` ("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 `.claude/initreq/tbox` -(레지스트리)와 Fusion/Vide 비교는 `base/comparison-fusion-vide.md` 참고. +(레지스트리)와 Fusion/Vide 비교는 `reference/comparison-fusion-vide.md` 참고. ## 문제 @@ -352,7 +352,7 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store ## 이벤트 핸들러는 self(Instance)를 받지 않는다 — 확정 (2026-08-06) **결정**: v1의 `function(self, ...)` 관습(`self`/`this`로 이벤트 대상 -Instance를 넘겨주는 것, `.claude/base/quad-v1-architecture.md` 참고 — +Instance를 넘겨주는 것, `.claude/reference/quad-v1-architecture.md` 참고 — 실제로 `event.lua`의 `Bind`가 `func(self or this, ...)`로 넘겨줌)은 **채택하지 않는다.** quad-roblox의 이벤트 핸들러는 엔진이 네이티브로 주는 이벤트 인자만 받는다(React의 `onXxx`가 DOM 노드가 아니라 diff --git a/.claude/base/component-composition-plan.md b/.claude/base/component-composition-plan.md index e9d1c2c..bada990 100644 --- a/.claude/base/component-composition-plan.md +++ b/.claude/base/component-composition-plan.md @@ -10,7 +10,7 @@ Store/State/Source 온톨로지가 먼저 확정된 뒤에야 이 논의가 열 ## 문제 v1의 `Class.Extend()`(Init/Render/AfterRender/Getter/Setter/UpdateTriggers, -`base/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기 +`reference/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기 방향이지만, 그게 제공하던 실제 편의 기능(컴포넌트가 자기 store를 자동으로 가짐, props로 넘어온 State를 자동 흡수, `self:Default`/`self "key"`로 기본값·바인딩)까지 같이 버려도 되는지가 미결이었음. `MyComp {...}` 형태로 @@ -56,42 +56,30 @@ State는 `:With`/`:Compute`로 만들어진 파생값일 수 있어 쓰기가 의미 있음 — **사용자 확정**("맞음. 확실해"). 이 원칙 자체는 그대로 유지되고, 아래 3번의 구체적 메커니즘만 2026-08-06 후속 세션에서 더 단순하게 갱신됨. -### 3. [정정, 2026-08-06 후속 세션] `StoreSource` 프록시 개념 폐기 — Source가 State를 구조적으로 만족하므로 Store가 내부 Source를 그대로 반환 +### 3. Store는 내부 Source를 그대로 반환 — Source가 State를 구조적으로 만족 -**원래 이 절은 "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를 만족함" 절이 최종 소스 — -이 문서는 배경만 유지. +**확정**: `Source`가 구조적으로 `State`를 만족하므로(단방향 호환, +Svelte `Writable extends Readable`와 같은 모양), `store.key`는 Store +생성 시 이미 만들어둔 진짜 Source 객체를 그대로 반환한다 — 별도 프록시 +타입도, 별도 캐싱 계층도 없음(Source 자체가 이미 State의 읽기 계약을 +전부 만족하고 거기에 `:Set(value)`/`:Emit()`이 추가로 있을 뿐이라 "원본이라 +쓰기 가능"이라는 위 2번 규칙과도 자연히 맞아떨어짐). 쓰기 문법도 같이 +바뀜: `store.key = v`가 아니라 `store.key:Set(v)`(레코드 타입 읽기/쓰기 +대칭 + lazy 동작에 `=`가 암시하는 "즉시 커밋"이 안 맞는다는 논거). 상세 +근거·타입 설계·Luau 솔버 검증 필요 항목은 `base/store-semantics.md`의 +"Source가 State를 만족함" 절이 최종 소스. -- **쓰기 문법도 같이 바뀜**: `store.key = v`가 아니라 `store.key:Set(v)` - (레코드 타입 읽기/쓰기 대칭 + lazy 동작에 `=`가 암시하는 "즉시 커밋"이 - 안 맞는다는 논거, 같은 절 참고). -- **캐시 문제도 이걸로 자연히 해소**: State를 "매번 새로 만듦"이던 이전 - 모델과 달리, 이제 Store는 생성 시 만들어둔 Source를 그대로 갖고 있다가 - 돌려주기만 하므로 별도 캐싱 메커니즘 자체가 불필요(래퍼 생성 단계가 - 아예 없어짐 — 이전보다 더 쌈). +**[이전에 확정했다가 폐기된 `StoreSource` 프록시 설계는 이 결론으로 +완전히 대체됨 — 원문·역전 이유·신구 비교표는 +`archive/store-source-proxy-reversed.md` 참고, 여기서는 반복하지 않음.]** -### 4. [정정, 2026-08-06 후속 세션] Source 직접 전달 — 타입 유니온도 불필요해짐 +### 4. Source 직접 전달 — 타입 유니온 불필요, 서브타입 호환으로 자동 통과 -원래 "핸들러가 `Source | State` 유니온으로 받는다"는 방향이었으나, -Source가 State를 구조적으로 만족하는 지금은 **유니온 자체가 필요 없음** — -핸들러는 그냥 `State` 하나만 받아도 Source 인스턴스가 자동으로 그 -자리에 들어감(서브타입 호환). `isHandlable`/`priority`/`process`/`retract` -4종 계약에 5번째 항목을 추가할 필요 없다는 결론은 그대로 유지, 다만 근거가 -"타입 유니온으로 처리"에서 "서브타입이라 유니온 자체가 불필요"로 더 -단순해짐. 단, 핸들러가 "이거 Source면 역방향 쓰기까지 걸고 싶다"처럼 -**런타임에** Source인지 구분하고 싶은 경우는 여전히 있을 수 있음 — -그건 타입 유니온이 아니라 런타임 판별자(`isSource`류, `isObserver` -패턴과 동일한 결)로 처리하면 됨. +핸들러는 `State` 하나만 받아도 Source 인스턴스가 서브타입 호환으로 +자동 통과된다(`Source | State` 유니온 불필요, `isHandlable`/ +`priority`/`process`/`retract` 4종 계약에 5번째 항목 추가 불필요). 런타임에 +"이게 Source면 역방향 쓰기까지 걸고 싶다"처럼 구분하고 싶은 경우는 +`isSource`류 판별자로(`isObserver`와 동일한 패턴). - **실사용 범위가 좁다는 판단은 그대로 유지**: `isEnabled`처럼 여러 조건에 영향받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못 diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index 6a5d18d..020495b 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -123,7 +123,7 @@ canExecute 같은 람다 함수 하나 달게 해서 Connected 상태 보게 하 즉, 핸들러가 처리 도중 만든 구독/옵저버 클로저는 그 자체로는 아무것도 자동으로 GC에 묶이지 않음 — v1이 여기저기서 `PropertyChangedSignal`에 연결해 참조를 -붙잡아두던 "GC 방지 핫팩"(`base/quad-v1-architecture.md` 참고)과 같은 문제. +붙잡아두던 "GC 방지 핫팩"(`reference/quad-v1-architecture.md` 참고)과 같은 문제. **base가 범용 유틸로 제공할 것: 임의의 클로저/구독을 실제 Roblox 객체의 생명주기에 바인드하는 도구** — 내부적으로 v1/rbvm이 쓰던 "connect 트릭"(어떤 신호에든 연결해서 참조를 죽을 때까지만 붙잡아두는 것)을 그대로 재사용. 이 diff --git a/.claude/base/modifier-plan.md b/.claude/base/modifier-plan.md index 31d7133..476f8be 100644 --- a/.claude/base/modifier-plan.md +++ b/.claude/base/modifier-plan.md @@ -190,7 +190,7 @@ PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/bind-sys `Modifier.Rounded(8)`가 실제로 어떻게 UICorner 자식을 만들어 붙이는지(v1의 `Corner` 특수 프로퍼티 선례, 핸들러 배치 소견)는 -`research/ui-shorthand-plan.md` 참고 — 이 문서는 Modifier 값 자체의 +`base/ui-shorthand-plan.md` 참고 — 이 문서는 Modifier 값 자체의 동작만 다루므로 분리. ### 6. State/Pipe 쪽엔 영향 없음 — 이미 있던 결정의 재확인일 뿐 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index 56679c0..d3092fc 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -4,7 +4,7 @@ 소스 트리 상 패키지 경계까지 확정되어 `research/`에서 승격됨(`base/ architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고). 원본: `.claude/initreq/raw-userinput.md` "slot을 구현하도록 하기로 했음" 절. Fusion의 -`Children` SpecialKey와 Vide의 mount 무가드 비교는 `base/comparison-fusion-vide.md` +`Children` SpecialKey와 Vide의 mount 무가드 비교는 `reference/comparison-fusion-vide.md` 참고 — 결론: **두 라이브러리 어디에도 이런 엄격한 단일 마운트 가드가 없음, quad의 진짜 개선점.** @@ -34,7 +34,7 @@ InstanceChild.luau`. Slot은 "뮤터블 배열"을 다루고 이 핸들러는 " Slot에 들어간 요소는 **ownership이 귀속**되며 다른 곳에 마운트할 수 없게 된다. `isMounted`를 관리해서, **한 인스턴스에 대한 다중 마운팅이 라이브러리 차원에서 절대 일어나지 않도록 강제**하는 게 v1 대비 핵심 디자인 변화. v1의 `mount()`는 -별다른 강제를 안 했지만(`base/quad-v1-architecture.md`의 mount.lua 분석 참고 — +별다른 강제를 안 했지만(`reference/quad-v1-architecture.md`의 mount.lua 분석 참고 — 실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 mount 함수 자체가 이 강제를 담당. diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index 18372c5..d048320 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -282,7 +282,7 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo `useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는 요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 — -`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의 +`reference/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의 `:Add`/`:With`/`:Tween`처럼 값을 직접 가공하는 이름 붙은(named) 체이닝 연산은 만들지 않음** — 대신 일반 함수를 받아 처리. (주의: 아래의 v2 `:With(...)`는 이름만 같을 뿐 v1의 `:With`와는 다른 연산임 — v1은 "함수/테이블에서 값을 @@ -291,3 +291,12 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo `:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b}, fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 State 핸들로 넘기고 `:Get()`을 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. + +**여러 소스를 한 번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하려면 +`Blocker` 참고.** 위 `:With`+`:Compute`만으로는 "state1, state2를 연달아 +Set하면 결합된 파생값이 두 번 재계산/재대입된다"는 문제(즉시 pull하는 +store-bind 소비자 기준)는 안 풀림 — 이건 별도 확정 프리미티브 +`base/additional-primitives.md`의 "Blocker" 절이 다룸(State 개발과 같은 +마일스톤, `ROADMAP.md` M3에서 함께 구현). lexical `Batch(fn)`으로 풀려던 +초기 시도는 코루틴 yield 위에서 구조적으로 위험해 기각됨 — +`archive/batch-rejected.md` 참고. diff --git a/.claude/base/ui-shorthand-plan.md b/.claude/base/ui-shorthand-plan.md new file mode 100644 index 0000000..390821b --- /dev/null +++ b/.claude/base/ui-shorthand-plan.md @@ -0,0 +1,108 @@ +# UI 편의 숏핸드 (UICorner/UIPadding/UIScale) — 인라인 적용 + +**상태**: base — 기능 필요 여부·이름·메커니즘·패키지 배치·store-bind 가능성까지 +전부 확정(2026-08-07 문서 정리에서 `research/`→`base/` 승격). 남은 건 구현 +단계의 세부 시그니처뿐. + +## 배경 + +사용자 기억: v1을 쓸 때 "UICorner/UIPadding/UIScale 같은 걸 직접 +`Instance.new`로 만들어 Parent하는 귀찮은 작업 없이, Frame 안에 인라인으로 +넣기만 해도 CSS 스타일처럼 적용됐다 — 코드가 줄고 읽기도 편해서 꽤 +괜찮았다"는 것. v1 소스(`.claude/initreq/quad`)와 PA님 코드 +(`.claude/initreq/artworks`)를 서브에이전트로 조사해 확인. + +## v1 실제 메커니즘 (조사 완료) + +`class.lua`의 `SetProperty`/`GetProperty`(38~109행)와 `ProcessQuadProperty` +(134~213행)에 하드코딩된 if/elseif 분기로 특수 문자열 키 5종을 지원했음 — +`Corner = 8` → 숫자 하나, 기존 `UICorner` 자식이 있으면 재사용, 없으면 +`Instance.new("UICorner", item)`으로 생성(`Name = "_quad_round"`), +`CornerRadius = UDim.new(0, value)` 설정. `PaddingAll`/`PaddingAllOffset`, +`Scale`도 동일 패턴(`UIPadding`/`UIScale`, `_quad_padding`/`_quad_scale`). +값 모양은 항상 **리터럴 하나**(숫자/UDim) — 테이블도 `__type` 태그도 아님. +v1엔 이 5종과 별개로 `RoundSize`(이미지 9-slice 라운드 트릭, UICorner와는 +전혀 다른 메커니즘)도 있었으나 **이건 드롭 확정** — 자세한 사유는 +`archive/ui-shorthand-roundsize-dropped.md` 참고, 이 문서에서는 반복하지 +않음. + +**`UIListLayout`/`UIGridLayout`/flex 전용 숏핸드는 v1에 없었음** — +`ProcessQuadProperty`의 범용 자식 나열 분기(배열 인덱스로 놓인 Instance/ +Class 결과를 자동 mount)로 `UIListLayout{...}`을 그냥 직접 나열했을 뿐, +`List = true` 같은 전용 축약 문법은 레포 전체(PA님 코드 포함)에서 찾지 +못했음. quad-v2도 이 부분은 이미 있는 children-array + 인스턴스 생성 +문법으로 그대로 커버됨 — 새로 설계할 것 없음. + +## 결론 — 이름은 UICorner/UIPadding/UIScale로 확정 (프리픽스 필요) + +**기능은 여전히 필요**: `UICorner`가 Roblox 네이티브 Instance가 됐어도 +"별도 Instance를 만들어 부모에 Parent해야 한다"는 구조적 번거로움 자체는 +없어지지 않으므로, 이 숏핸드의 존재 이유는 그대로 유효 — **사용자 +재확정**("UIScale 같은 건 여전히 별도의 Instance고 부모 Frame에 영향을 +주는 구조, 숏핸드는 여전히 필요하다"). + +**이름은 v1의 `Corner`/`PaddingAll`/`Scale`을 그대로 안 가져오고 실제 +Roblox Instance 이름과 맞춘 `UICorner`/`UIPadding`(+`UIPaddingOffset`)/ +`UIScale`로 확정** — v1식 짧은 이름을 그대로 쓰면 Modifier 체이닝 +메소드(`mod:Corner(8)`)가 "진짜 UICorner를 만드는 숏핸드"인지 그냥 우연히 +비슷한 이름의 부가 Modifier 필드인지 구분이 안 됨(사용자 지적). 접두어 +`UI`를 붙이면 실제 대응하는 Roblox Instance 클래스 이름과 1:1로 읽혀서 +이 모호함 자체가 사라짐 — `Frame { UICorner = 8 }`, `mod:UICorner(8)`. + +## 메커니즘 — 새 아키텍처 개념 불필요 + +이미 있는 pluggable Handler로 그대로 커버됨. `UICorner`/`UIPadding`/ +`UIScale` 같은 특수 키를 인식하는 Handler(`isHandlable`이 그 키를 매칭)가 +"이름 붙은 자식을 찾거나 만들고 프로퍼티 세팅"을 `process(inst, k, v)`에 +구현 — v1의 하드코딩 if/elseif 대신 정식 핸들러 계약(`isHandlable`/ +`priority`/`process`/`retract`)을 따르는 것만 다름. `modifier-plan.md`가 +이미 예시로 든 `Modifier.Rounded(8)`은 이 특수 키를 flatten해서 props에 +꽂아넣는 사탕 문법일 뿐, 실제 처리는 이 Handler가 함 — Modifier를 안 거치고 +`Frame { UICorner = 8 }`처럼 순수 인라인 키로 직접 써도(v1처럼) 동일하게 +작동함, `architecture.md`의 `[Attribute "Name"]`류 특수 키와 같은 층위. +자동 생성된 자식은 기존 관례대로 `_`/`QUAD_` 접두어 네이밍 +(`research/debug-tooling-plan.md` 9번, v1의 `_quad_round`류 그대로 재사용). + +**기존 자식과의 매칭 기준(2026-08-06 감사에서 지적된 항목, 여기서 같이 +확정)**: 재사용 대상은 quad가 이전에 만든 고정 이름(`_quad_corner`류) +자식으로 한정 — 타입만 보고(`UICorner`이기만 하면 아무거나) 재사용하지 +않음. 사용자가 직접 만든 `UICorner`를 quad가 멋대로 건드리는 부작용을 +피하기 위함. + +## store-bind — 이 숏핸드도 지원, Tween만큼 무겁게 안 가도 됨 + +v1에서도 `Corner`/`PaddingAll`/`Scale`은 store 값으로 바인드 가능했음 +(`myStore "key"` 체이닝으로 다른 프로퍼티와 동일하게 취급됨) — quad-v2도 +이 능력을 유지한다. 트윈처럼 애니메이션까지 지원할 필요는 없음(API 표면만 +복잡해짐) — 그냥 값이 바뀌면 `CornerRadius`/`Padding`/`Scale` 프로퍼티를 +다시 세팅하는 정도로 충분. 구현 비용도 낮음: 각 Handler가 `process`에서 +"이전에 자기가 찾거나 만든 자식 Instance"를 얻어야 하는데, 이건 이미 +base가 범용 유틸로 제공하기로 확정한 per-instance weak-keyed 저장소 +(`base.perInstanceState(inst)`, `base/bind-system-plan.md` "핸들러 내부 +상태 저장" 절)를 그대로 재사용하면 됨 — Tween 핸들러가 실행 중인 Tween +객체를 기억해두는 것과 정확히 같은 패턴. 새 메커니즘 발명 불필요, 이미 +있는 "store 바인드는 pluggable 바인드를 재실행하는 래핑" 원칙 +(`base/bind-system-plan.md` "확정된 디스패치 모델" 절)이 그대로 적용됨. + +## 패키지 배치 — `quad-roblox` 코어에 직접 포함, 확정 + +"트윈도 인스턴스 생성/제어를 직접 구현 가능한 걸 하나로 묶어 쉽게 쓰게 +합친 것 — 너무 잘게 쪼개 오버엔지니어링하기보다 확실히 하나로 코어에 +넣어도 충분하다, opt-out할 이유가 별로 없다"는 게 사용자 판단 — **작고 +항상 켜져 있어도 비용이 무시할 만한 편의 기능은 별도 opt-out 패키지로 +쪼개지 말고 `quad-roblox` 코어에 직접 포함한다**는 원칙으로 확정(이미 +계획된 Tween 핸들러가 같은 모양이라는 게 근거). 이 원칙은 일반화해서 +재사용 가능 — 앞으로 비슷한 "작은 인스턴스 편의 기능"이 제안되면 +`quad-roblox-util` 같은 걸 새로 만들지 않고 이 선례를 따르면 됨. + +**중요도**: 낮음("이건 나중에도 쉽게 구현됨" — 사용자) — 지금 M0 우선순위를 +바꿀 이유는 없음, M10(Handlers/Attribute 등) 전후로 다른 세부 Handler와 +함께 구현하면 충분. + +## 남은 열린 질문 (단순화 후보, 사소함) + +- Corner/PaddingAll/Scale 3개 거의 동일한 형태의 Handler를 각각 만들지, + `{key -> {ChildClassName, ChildDefaultName, Property, wrap=fn}}` 룩업 + 테이블로 구동되는 단일 `Handlers/InstanceShorthand.luau`로 통합할지 — + `research/pre-implementation-audit.md` 3-2번 참고, 강제 사항 아님, + 구현 시점에 결정할 정도의 사소한 개선 후보. diff --git a/.claude/question.md b/.claude/question.md index 61fbcd8..2c54ff4 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -14,8 +14,11 @@ 사용자 질문: "다른 독립 프리미티브나 종속 파생 데이터는 뭐가 더 필요할 것 같나요. 이것만으로 이 프로젝트는 충분하다 생각해요?" — 여러 서브에이전트 -조사 + 사용자와 라이브 논의로 계속 수렴 중, 상세는 `research/ -additional-primitives-plan.md`. 요지: +조사 + 사용자와 라이브 논의로 계속 수렴 중. **2026-08-07 문서 정리에서 +확정/기각된 항목은 `research/additional-primitives-plan.md`에서 +분리됨**: Effect/Blocker → `base/additional-primitives.md`, Batch → +`archive/batch-rejected.md`, Context(+레이어드 Store) → `archive/ +context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** 남김. - **키 기반 동적 컬렉션 재조정(유일하게 아직 완전히 열려있음, 최우선)**: Fusion `ForPairs`/`ForKeys`/`ForValues`, Vide `indexes()`/`values()`, @@ -26,33 +29,20 @@ additional-primitives-plan.md`. 요지: 못 쓴다는 반례로 철회됨), Slot에 파괴 없이 빼내는 `Extract` 연산 추가 필요 — 최종 이름만 미정(아래 "용어 정리" 절에 후보 추가). **사용자가 "작업 전에 모든 정의를 마치고 싶다"고 명시** — M0 이전 완전 확정 목표. -- **Effect — 거의 수렴**: leaf가 죽을 때 확정적으로 정리 콜백을 부르는 - 단순 primitive(재실행 개념 없음, Observer와 별개)로 합의, 시그니처만 - 남음. Observer에 cleanup 반환 계약을 얹는 안은 기각(클로저 업밸류로 - 이미 충분 — `pre-implementation-audit.md` 3-1과 같은 논리). -- **Blocker — 채택, 핵심 메커니즘+이름 확정(2026-08-07 신설)**: 여러 - Source를 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 - primitive — lexical Batch(아래 항목, 기각)와 달리 콜스택/코루틴이 아니라 - **값**(`Blocker` 객체의 `On()`/`Off()`)으로 지연 구간을 표현해 코루틴 - yield 위험을 구조적으로 우회함. `state:Block(blocker) -> state`가 - gated state를 반환(호출 즉시 onunblock 핸들 등록), `IsBlocked`/ - `HasBlockedEmit` 필드, 재진입(네스팅)은 의도적으로 미지원(Rust - poisoned-mutex류 위험 회피 — 겹치는 배치는 각자 새 `Blocker`를 쓸 것, - **문서화에서 강하게 명시 필요**). `base/`로 승격 가능한 수준, 남은 건 - 문서화뿐. -- **Batch(함수/코루틴 스코프 lexical block) — 기각 확정**: lexical - transaction 블록이 코루틴 yield 위에서 구조적으로 위험(전역/코루틴 - 스코프 플래그 둘 다 새 코루틴 스폰이나 영구 yield에 깨짐) — Blocker로 - 대체됐으므로 더 이상 미해결 문제 아님, quadnomicon "왜 Batch 대신 - Blocker인가" 에세이 대상. -- **Context — 기각 확정, 대안이던 레이어드 Store도 철회**: 완전 자동 - 버전은 Roblox Luau 플랫폼 한계로 사실상 불가, 얕은 버전도 Slot 비동기 - 추가에서 조용히 깨짐 + quad-debug 철학과 충돌. 레이어드 Store 대안은 - 사용자 반박으로 철회 — 이미 있는 타입 강제 명시적 Store 전달 + - 오버라이드 지점에서 새 Store를 만들어 넘기는 것으로 충분하다는 판단. - 둘 다 "왜 없는가" quadnomicon 에세이 대상. + 상세는 `research/additional-primitives-plan.md`(이제 이 주제 전용). +- **Effect가 Observer의 변형(`state:Effect()`)인지, 완전히 독립된 free + function인지 — 신규, 2026-08-07 문서 정리 세션에서 발견.** `base/ + additional-primitives.md`가 지금까지의 조사대로 Effect를 "재실행 없는 + 독립 free function"으로 서술해뒀지만, 사용자가 직접 `state:Effect()` + 형태(=Observer에 "확정 정리" 계약만 추가된 변형)로 기억하고 있어서 + 확인이 필요함. 관련 하위 질문: `state:Observer(fn)`가 생성 시점에 + `fn`을 즉시 1회 실행하는지도 현재 문서 어디에도 명시돼 있지 않음(Effect는 + "즉시 1회 실행"이 스펙에 있음 — 이 부분만 보면 둘이 겹쳐 보이는 이유). + **임의로 결론내지 않고 열어둠** — 구현 착수(M3~M4 전후) 전에 확인 필요, + 상세는 `base/additional-primitives.md`의 "미해결" 절. - Untrack/Suspense/Error Boundary/Readonly는 조사 결과 새 프리미티브 없이 - 기존 설계·Lua 자체 기능으로 이미 충분한 것으로 판단. + 기존 설계·Lua 자체 기능으로 이미 충분한 것으로 판단(`research/ + additional-primitives-plan.md` "빈 자리 아닌 것" 절). ### 1. 용어 정리 (사용자 요청, 진행 중) @@ -95,7 +85,8 @@ additional-primitives-plan.md`. 요지: 사용자 피드백으로 탈락. 후보: `Render`(가장 직접적이지만 "quad엔 렌더 주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만 즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직 - 미정, `research/additional-primitives-plan.md` 1번 절 참고. + 미정, `research/additional-primitives-plan.md`의 "키 기반 동적 컬렉션 + 재조정" 절 참고. - **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`가 "provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로 스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/ @@ -131,9 +122,10 @@ additional-primitives-plan.md`. 요지: - **`canExecute`/`Connected`의 실제 구현 방식(Parent==nil vs Connection. Connected vs Destroying 플래그)이 미확정인데 이미 Slot/Observer/store-bind retract 전역에 재사용 확정됨** — M2/M3 착수 전 실측 필요 — 우선순위1-6. -- **`LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그걸 - 필요로 함(로드맵 순서 역전)** — quad-base 인터페이스 정의를 M2/M3로 - 옮기는 게 자연스러워 보임, `ROADMAP.md` 수정 필요 — 우선순위1-9. +- **~~`LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그걸 + 필요로 함(로드맵 순서 역전)~~ — 반영 완료(2026-08-07 세 번째 세션)**: + `LifetimeHandle`/`PerInstanceState` 인터페이스(타입만)를 `ROADMAP.md` + M2로 옮기고, quad-roblox 실 구현만 M8에 남김 — 우선순위1-9 해소. - 그 외(Slot CRUD 의미론 미정의, retract 시 "이전 핸들러" 추적 책임 소재, 우선순위 스캔 동률/매치실패 처리, `:Compute`의 `previous` 인자가 오버엔지니어링일 수 있음, UI shorthand의 기존 UICorner 매칭 기준 등)는 @@ -141,10 +133,19 @@ additional-primitives-plan.md`. 요지: ### 3. 낮은 우선순위 +- **`None`(가칭) 센티널 프리미티브 — 미확정, 2026-08-07 세 번째 세션 + 신설.** `{ Override = nil, mod }`처럼 인라인 키로 modifier가 주는 값을 + 명시적으로 지우고 싶어도 Lua 테이블 리터럴의 `키 = nil`은 키가 아예 + 없는 것과 구별이 안 돼서 "인라인이 modifier보다 무조건 우선"이라는 + 기존 merge 규칙(`modifier-plan.md` 2번)이 이 케이스에선 실제로 작동을 + 안 함. `false`를 이벤트 disconnect 센티널로 쓴 선례처럼 실재하는 값 + `None`을 도입하는 방향만 나왔고 상세(타입, flatten 내부 표현, State + 필드에도 같은 문제가 적용되는지)는 미정 — `modifier-plan.md` "2-1"절 + 참고, M7(Modifier) 착수 전 확인. - `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현 착수를 막지 않음. - **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요** — - `base/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션 + `reference/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션 가이드 작성 시점에 필요. 지금은 그냥 백로그로만 기록. - **여러 Slot이 형제로 섞일 때 순서 보장** — `base/slot-plan.md`의 "여러 Slot이 섞일 때 순서 보장" 절 참고. Roblox 단일 백엔드로는 급하지 않음 @@ -172,10 +173,6 @@ additional-primitives-plan.md`. 요지: 중 뭘로 갈지 — 소견은 DI 인스턴스 생성 패턴처럼 "제네릭 하나 + 자주 쓰는 타입만 정적 지름길" 절충이지만 확정 아님, M10(Handlers/Attribute) 착수 전 아무 때나 확인해도 됨. -- **UICorner/UIPadding/UIScale 인라인 편의 키 세부** — `research/ - ui-shorthand-plan.md`. 기능 필요 여부(여전히 필요로 재확정)·메커니즘 - (Handler)·패키지 배치(quad-roblox 코어)는 확정, 남은 건 이름 재검토 - (용어 정리 합류)와 `RoundSize`(이미지 라운드) 대체 방식뿐. - **v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat`** — `research/v1-compat-plan.md`(신규, 2026-08-06, 두 차례 후속 논의로 수렴). 방향 확정: v1을 그대로 병행 실행 + 경계에서만 `state:Observer()`(lazy @@ -203,8 +200,11 @@ additional-primitives-plan.md`. 요지: | Modifier(정적 merge, immutable 체이닝, State 필드 지원) | `base/modifier-plan.md` | | 컴포넌트화(플레인 함수, State/Source 경계, 컴포넌트 경계 modifier/Ref는 named parameter로 전달, multi-root 개념 폐기, `Modifier.Merge`) | `base/component-composition-plan.md` | | 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` | -| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `base/comparison-fusion-vide.md` | -| v1 내부 동작 스냅샷 | `base/quad-v1-architecture.md` | +| Blocker(값 기반 emit 지연/합치기), Effect(leaf 죽음에 확정 정리 — 단 Observer와의 관계는 위 0번 열린 질문 참고) | `base/additional-primitives.md` | +| UICorner/UIPadding/UIScale 인라인 편의 키 — 이름·메커니즘·store-bind 가능성까지 확정 | `base/ui-shorthand-plan.md` | +| Batch(lexical) 기각, Context(+레이어드 Store) 기각 | `archive/batch-rejected.md`, `archive/context-rejected.md` | +| Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `reference/comparison-fusion-vide.md` | +| v1 내부 동작 스냅샷 | `reference/quad-v1-architecture.md` | | 트윈 오버라이드(기본값 Cancel), 세부 옵션만 남음 | `research/tween-plan.md` | | quad2-try(폐기된 이전 시도) 리서치 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW 전부 죽은 접근으로 확인, 반복 조사 금지 | `base/bind-system-plan.md` | diff --git a/.claude/base/comparison-fusion-vide.md b/.claude/reference/comparison-fusion-vide.md similarity index 92% rename from .claude/base/comparison-fusion-vide.md rename to .claude/reference/comparison-fusion-vide.md index 613bff5..63248ee 100644 --- a/.claude/base/comparison-fusion-vide.md +++ b/.claude/reference/comparison-fusion-vide.md @@ -1,7 +1,11 @@ # Fusion / Vide 아키텍처 비교 — quad-v2 설계 근거 -**상태**: base — 리서치 스냅샷(참고용 근거 자료), "완료" 개념 없음. quad-v2의 -Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자료. +**상태**: reference — 온디맨드 참고 자료, "완료" 개념 없음. **[2026-08-07 +문서 정리에서 `base/`→`reference/`로 이동]** quad에 관한 결정 자체가 아니라 +Fusion/Vide 리서치 스냅샷이라 항상 읽어야 하는 base 컨텍스트는 아님 — +`quadnomicon`(프레임워크 설계자용 심화 콘텐츠) 소재 후보이기도 함. quad-v2의 +Store/Slot/Tween/bind-dispatch 설계 결정에 근거로 인용될 때만 열어볼 것, +실제 확정 사항은 인용하는 쪽 `base/` 문서가 소스. ## Fusion (`.claude/initreq/fusion/`) diff --git a/.claude/base/quad-v1-architecture.md b/.claude/reference/quad-v1-architecture.md similarity index 88% rename from .claude/base/quad-v1-architecture.md rename to .claude/reference/quad-v1-architecture.md index bd14cb8..249cbdf 100644 --- a/.claude/base/quad-v1-architecture.md +++ b/.claude/reference/quad-v1-architecture.md @@ -1,9 +1,13 @@ # quad v1 내부 구조 (재작성 이전 기준선) -**상태**: base — 참고용 스냅샷, "완료" 개념 없음. v1(`.claude/initreq/quad/`)이 -실제로 어떻게 동작하는지 정리한 문서로, v2 설계 시 "이 문제를 안 반복하려면"의 -기준선으로 계속 참조됨. 아래는 리서치 에이전트가 file:line까지 확인한 내용의 요약 — -정확한 인용이 필요하면 `.claude/initreq/quad/src/*.lua` 원본을 볼 것. +**상태**: reference — 온디맨드 참고 자료, "완료" 개념 없음. **[2026-08-07 +문서 정리에서 `base/`→`reference/`로 이동]** v1 자체에 대한 스냅샷일 뿐 v2의 +결정 사항이 아니라서 항상 읽어야 하는 base 컨텍스트는 아님 — 다른 문서가 +"v1은 이랬는데"를 인용할 때만 열어볼 것. v2 설계 시 "이 문제를 안 반복하려면"의 +기준선으로 근거 인용되는 용도는 그대로 유지(각 인용 지점은 여전히 +`base/`에 있음, 이 문서는 그 인용의 원본 소스). 아래는 리서치 에이전트가 +file:line까지 확인한 내용의 요약 — 정확한 인용이 필요하면 +`.claude/initreq/quad/src/*.lua` 원본을 볼 것. ## 공개 API 개요 diff --git a/.claude/research/additional-primitives-plan.md b/.claude/research/additional-primitives-plan.md index 636b420..0a53e2f 100644 --- a/.claude/research/additional-primitives-plan.md +++ b/.claude/research/additional-primitives-plan.md @@ -1,10 +1,12 @@ # 추가 프리미티브 필요성 조사 — 다른 프레임워크 대비 갭 분석 -**상태**: research — 사용자와 라이브 논의로 수렴(2026-08-06~07). Context/ -Batch(lexical)는 **기각 확정**. **Blocker**(새 primitive, Batch의 대안으로 -채택)는 핵심 메커니즘+이름 확정, 문서화만 남음. 키 기반 동적 컬렉션 -재조정은 설계 진행 중(사용자가 "작업 전에 모든 정의를 마치고 싶다"고 -명시 — M0 전 완전 확정이 목표). Effect는 거의 수렴, 시그니처만 남음. +**상태**: research — 사용자와 라이브 논의로 대부분 수렴(2026-08-06~07), +**2026-08-07 문서 정리에서 확정/기각된 항목을 분리**: Effect/Blocker → +`base/additional-primitives.md`, Batch(lexical) → `archive/ +batch-rejected.md`, Context(+레이어드 Store 대안) → `archive/ +context-rejected.md`. 이 문서에는 **아직 완전히 열려있는 것 하나만** 남음 +— 키 기반 동적 컬렉션 재조정. 사용자가 "작업 전에 모든 정의를 마치고 +싶다"고 명시 — M0 전 완전 확정이 목표. ## 배경 @@ -25,20 +27,20 @@ Vide/v1/artworks 소스 근거 조사, Context 구현 난이도 판정) + 그 ## 결론 요약 -| 후보 | 판정 | 상태 | +| 후보 | 판정 | 현재 위치 | |---|---|---| -| 키 기반 동적 컬렉션 재조정 | **진짜 빈 자리, 최우선** | 설계 진행 중 | -| Effect(leaf 죽음에 확정 정리) | 진짜 빈 자리 | 거의 수렴, 시그니처만 남음 | -| **Blocker(값 기반 emit 지연/합치기)** | **채택** — Batch의 대안 | 핵심 메커니즘+이름 확정, 문서화만 남음 | -| Batch(함수/코루틴 스코프 lexical block) | **기각** | 결정 완료 | -| Context(트리 하위 암묵 전파) | **기각** — 대안(레이어드 Store)도 철회 | 결정 완료 | -| Observer에 cleanup 반환 계약 추가 | **기각** — 클로저 업밸류로 이미 충분 | 결정 완료 | -| Untrack/Peek | 빈 자리 아님 | - | -| Suspense/비동기 경계 | 빈 자리 아님(문서화 문제로 재분류) | - | -| Error Boundary | 빈 자리 아님 | - | -| Readonly wrapper | 빈 자리 아님 | - | +| 키 기반 동적 컬렉션 재조정 | **진짜 빈 자리, 최우선** — 아직 열려있음 | 이 문서(아래) | +| Effect(leaf 죽음에 확정 정리) | 채택, 단 Observer와의 관계는 미해결 | `base/additional-primitives.md` | +| Blocker(값 기반 emit 지연/합치기) | **채택** — Batch의 대안 | `base/additional-primitives.md` | +| Batch(함수/코루틴 스코프 lexical block) | **기각** | `archive/batch-rejected.md` | +| Context(트리 하위 암묵 전파) + 레이어드 Store | **기각** | `archive/context-rejected.md` | +| Observer에 cleanup 반환 계약 추가 | **기각** — 클로저 업밸류로 이미 충분 | `base/additional-primitives.md`(Effect 절에 근거만 인용) | +| Untrack/Peek | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 | +| Suspense/비동기 경계 | 빈 자리 아님(문서화 문제로 재분류) | 아래 "빈 자리 아닌 것" 절 | +| Error Boundary | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 | +| Readonly wrapper | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 | -## 1. 키 기반 동적 컬렉션 재조정 — 최우선, 설계 진행 중 +## 키 기반 동적 컬렉션 재조정 — 최우선, 설계 진행 중 **무엇인가**: 데이터 배열(인벤토리, 리더보드, 채팅로그처럼 삽입/삭제/ 재정렬되는 목록)을 UI로 렌더링할 때, 이전 렌더 결과와 새 데이터를 @@ -157,231 +159,6 @@ Slot:Add(element, index?) -- 기존 그대로, Extract로 뺀 것도 다시 먼저 정하고 나중에 여기를 끼워맞추면 재작업이 날 가능성이 높음. Slot CRUD 시맨틱을 정의할 때 이 프리미티브의 요구사항을 같이 고려할 것. -## 2. Effect — 거의 수렴, 시그니처만 남음 - -### Observer에 cleanup 반환 계약을 추가하는 안 — 기각 - -React `useEffect`류처럼 "`fn`이 `nil | () -> ()`를 반환하면 다음 재실행 -직전에 그걸 불러준다"를 `state:Observer(fn)`에 얹는 안을 검토했으나 -**사용자가 기각** — 클로저 업밸류로 이미 쉽게 되고 잘 작동하는데 -(`local lastConn; state:Observer(function() if lastConn then -lastConn:Disconnect() end; lastConn = ... end)`), 프레임워크가 이걸 -대신해줄 이유가 약하다는 판단. 이건 `pre-implementation-audit.md` -3-1번("`:Compute(fn)`의 `previous` 인자 — 클로저 업밸류로 이미 되는 걸 -별도 API로 만든 것일 수 있음")과 **정확히 같은 논리** — 일관성 있는 -판단으로 보임(3-1 자체도 같은 이유로 재검토 대상일 수 있음, 별도 항목). - -### Effect — 별도의 단순한 primitive로, "leaf 죽음에 확정 정리"만 담당 - -Roblox엔 `task.spawn`으로 코루틴에 반복문/타이머를 돌리는 패턴이 흔하고, -Luau 테이블엔 `__gc` 같은 GC 시점 훅이 없어서 "이게 진짜 사라지는 순간"을 -아는 유일한 방법은 `Instance.Destroying`류 명시적 신호뿐이다. 이런 -케이스(타이머 시작 → leaf가 죽을 때 반드시 정지)를 위한 별도 primitive는 -필요하다는 데 합의: - -``` -Effect(fn) -> EffectHandle -- fn을 즉시 1회 실행, 리턴값(nil | () -> ())은 - -- 이 Effect가 바인드된 leaf가 죽을 때 정확히 1회 호출 -``` - -Observer와 달리 **재실행 개념이 없다** — 값 변화에 반응해 다시 도는 건 -Observer(+클로저로 직접 짠 cleanup)의 영역이고, Effect는 순수하게 "설치 + -확정 정리" 페어 하나만 담당한다. children 배열에 leaf로 놓는 기존 Observer -바인딩 패턴을 그대로 재사용(그 leaf가 살아있는 동안만 유효, leaf가 죽으면 -정리 콜백 호출). 비용은 leaf당 실제 Destroying 바인딩 하나(공유 weak -table로 되는 Observer보다 비쌈) — 사용자가 필요할 때만 쓰는 걸로 충분. - -**상태**: 사실상 수렴 — 시그니처(`fn`의 인자 유무, `EffectHandle`의 모양)만 -다듬으면 `base/`로 승격 가능해 보임. 계속 논의 원하면 이어감. - -## 3. Batch(함수/코루틴 스코프 lexical block) — 기각 확정 - -**중요**: 아래는 "값 기반 지연/합치기"라는 문제 자체를 기각한 게 아니라, -**그 문제를 lexical block(Solid `batch()`/MobX `runInAction()`류)으로 -풀려는 접근만** 기각한 것이다. 실제 해법은 완전히 다른 별개 primitive인 -**Blocker**(아래 3-1번)로 채택됨 — 이 절은 "왜 lexical 접근은 안 되는가"만 -다루는 순수 반면교사 기록으로 남긴다. - -### "즉시 pull"이 뭔지 (참고용 예시) - -store-bind 핸들러(예: `Frame { BackgroundColor3 = total }`)는 lazy가 -아니다 — 화면에 실제로 반영하는 "누군가"가 바로 이 핸들러 자신이라, -무효화 신호를 받자마자 스스로 `Get()`하고 바로 대입한다: - -```lua -local total = a:With(b):Compute(function(av, bv) return av + bv end) -Frame { BackgroundColor3 = total } - -a:Set(1) -- total 무효화 → 핸들러가 즉시 (av=1, bv=이전b)로 재계산+대입 -b:Set(2) -- total 무효화 → 핸들러가 즉시 (av=1, bv=2)로 다시 재계산+대입 --- BackgroundColor3가 중간값을 한 번 거쳐가고, 두 번 대입됨 -``` - -### 왜 lexical block 방식을 안 쓰는가 - -`Batch(fn)`을 "플래그 세우고 fn 실행, 끝나면 flush"로 구현하면 **fn이 -yield하는 순간 위험해진다**(사용자 지적, 정확함): - -1. 플래그가 전역이면, yield 중 스케줄러가 돌리는 무관한 코루틴의 `Set()`이 - 이 Batch에 잘못 휘말릴 수 있음. -2. 플래그를 코루틴 스코프로 만들어도(Fusion `Contextual`류 코루틴 키 weak - table), `fn` 안에서 새 코루틴을 스폰하는 API(Promise, `task.spawn`)를 - 부르면 그 새 코루틴은 배치 스코프를 상속 못 받아 일부 Set이 새어나감. -3. `fn`이 영원히 재개 안 되면(장시간 대기, 리크된 코루틴) flush가 영영 안 - 일어나 store-bind 핸들러들이 화면을 무기한 stale 상태로 방치 — 즉시 - pull보다 더 나쁜 실패 모드. - -이건 구현을 잘 짜서 피할 수 있는 버그가 아니라 **lexical block 모델 -자체가 협조적 스케줄링 환경과 구조적으로 안 맞는 것**으로 판단, 기각. - -## 3-1. Blocker — 채택된 새 primitive (Batch의 대안, 2026-08-06~07 세션) - -### 핵심 아이디어 — 콜스택/코루틴이 아니라 값으로 지연 구간을 표현 - -Batch가 실패하는 근본 이유는 "지연 구간"을 콜스택/코루틴 스코프로 -표현하려 했기 때문이다. `Blocker`는 그 구간을 **그냥 사용자가 들고 있는 -값**으로 표현한다 — `On()`/`Off()`를 부르는 두 시점 사이에 얼마나 많은 -yield/코루틴 전환이 끼어도, 심지어 완전히 다른 코루틴에서 `Off()`를 -불러도 아무 문제가 없다. Batch를 무너뜨렸던 세 가지 실패 모드(전역 플래그 -오염, 새 코루틴 미상속, 영구 yield로 인한 무기한 방치)가 구조적으로 전부 -해당 안 됨. - -### 메커니즘 (확정) - -``` -Blocker() -> blocker -- 생성자 -blocker:On() -> self -- IsBlocked = true로만 설정, 그 외 아무것도 안 함 -blocker:Off() -> self -- IsBlocked = false로 먼저 설정, 그 다음 등록된 - -- onunblock 핸들 전부 실행(순서 무관, idempotent) - -state:Block(blocker) -> state -- 새 gated state 반환. **호출되는 즉시**(나중에 - -- 처음 블록될 때가 아니라) onunblock 핸들을 - -- blocker의 weak 배열에 등록. -``` - -gated state의 동작: -- 원본 state가 emit(무효화)될 때, 이 gated state로 전파를 시도. -- `blocker.IsBlocked`이면: 전파 안 하고 `HasBlockedEmit = true`만 세팅. -- `blocker.IsBlocked`가 아니면: 평소처럼 그냥 전파(투명하게 통과). -- `blocker:Off()`가 실행하는 onunblock 핸들은: `HasBlockedEmit`을 확인해 - true면 그제서야 정확히 1회 전파(emit)하고 플래그를 리셋. 이미 false면 - 아무 것도 안 함(이미 언블록 상태에서 다시 Off를 불러도 안전 — - idempotent). - -**`:Get()`엔 영향 없음** — 블록은 emit **전파**(eager 소비자에게 "바뀌었다" -알리는 신호)만 지연시킨다. 블록 중이라도 누군가 명시적으로 `:Get()`하면 -그 순간의 실제 값을 정상적으로 계산해서 준다 — `store-semantics.md`의 -"Get()은 라이브 레퍼런스를 준다" 원칙과 일치. - -### 사용 예시 — `state1, state2 -> state3` 케이스의 정답 - -처음 문제 제기("state1, state2 -> state3로 갈 때 둘 다 업데이트하면 state3가 -두 번 계산됨, 한번에 할 방법이 없다")에 대한 답: **`state3`(결합된 결과) -하나에만 `:Block`을 걸면 된다** — `state1`/`state2` 각각에 걸 필요 없음: - -```lua -local blocker = Blocker() -local gated3 = state3:Block(blocker) -- 소비자는 gated3를 구독 - -blocker:On() -state1:Set(1) -- state3 무효화 → gated3로 전파 시도 → 블록됨 → HasBlockedEmit=true -state2:Set(2) -- state3 무효화 → gated3로 전파 시도 → 이미 true, 그대로 -blocker:Off() -- onunblock 핸들 실행 → HasBlockedEmit 확인 → 딱 한 번 emit -``` - -**일반 사용 가이드(확정, 문서화 필수)**: Block은 **파이프라인의 최종 -연산 지점**(실제로 무거운 계산이 일어나는 derived state, eager 소비자에 -가장 가까운 지점)에 거는 게 원칙 — 소스가 여러 개든, 하나가 한 주기에 -여러 번 바뀌든 상관없이 이 지점 하나만 지키면 됨. 소스 쪽에 각각 거는 게 -아니다. - -### 이름 확정 - -- 클래스: `Blocker` — `Observer`/`Modifier`/`Ref`와 같은 명사-행위자 - 네이밍 관례와 일치. -- Blocker 자신의 토글: **`On()`/`Off()` -> self** (`Block()`/`Unblock()` - 아님) — `state:Block(blocker)`가 이미 "배선(wiring)" 동작의 동사로 - "Block"을 쓰고 있어서, Blocker 자신의 토글까지 같은 단어를 쓰면 - `blocker:Block()`(블로커를 켠다)과 `state:Block(blocker)`(state를 이 - 블로커에 배선한다)가 같은 단어로 다른 두 동작을 가리키게 됨 — 자체 - API 안에서 `register`→`State` 리네임 때 겪었던 "모호함은 풀었는데 - 충돌이 새로 생긴" 패턴이 반복될 뻔한 걸 미리 피함. -- 필드: **`IsBlocked`**(Blocker 자신의 On/Off 상태) — `Enabled`보다 - 명확(enabled는 "정상 작동 중"으로도 읽혀 헷갈릴 수 있음). -- 필드: **`HasBlockedEmit`**(gated state의 대기 플래그) — `Is`/`Has` - 접두어로 불리언임을 바로 알려줌. -- 메소드: `state:Block(blocker) -> state`. - -### 재진입(네스팅) — 의도적으로 미지원, 강한 문서화 필수 - -`IsBlocked`는 카운터가 아니라 단순 불리언이고, **의도적으로 그렇게 -둔다.** 레퍼런스 카운팅으로 네스팅을 지원할 수도 있었지만, 그러면 -"`On()` 여러 번, `Off()` 실수로 적게" 같은 버그가 **영구 블록으로 조용히 -새는** 더 위험한 실패 모드를 만든다. 언어 차원에서 "On~Off 사이 코드가 -죽었는지, 스레드가 죽었는지"를 추적하는 것도 해키해서 하지 않기로 함(Rust의 -"poisoned mutex" — 락 구간 안에서 패닉이 나면 락이 오염 상태가 되는 것과 -유사한 문제의식, 그 트래킹 자체를 만들지 않기로 함). - -**대신 확정된 규칙**: 겹치는 배치가 필요하면 **각자 새 `Blocker` 인스턴스를 -만들 것** — 하나의 Blocker를 여러 컨텍스트에서 재사용/중첩하지 않는다. -`Off()`는 스태킹 없이 즉시 그 자리에서 꺼진다. **이 제약은 반드시 사용자 -문서(API 레퍼런스 수준)에 명시적으로 강조할 것** — 네스팅을 시도하면 -조용히 잘못된 시점에 조기 해제되는, 원인 추적이 어려운 버그로 이어짐. - -### 상태: 핵심 메커니즘+이름 확정. 남은 건 문서화뿐 - -`Extract`/`Add`처럼 세부 시그니처가 더 필요한 다른 항목들과 달리, Blocker는 -설계 질문이 남아있지 않음 — `base/`로 승격 가능한 수준. quadnomicon에서 -"Batch를 기각하고 왜 Blocker로 갔는가"를 비교 설명하는 게 좋은 소재(아래 -"문서화 백로그" 참고). - -## 4. Context — 기각 확정, 레이어드 Store 대안도 철회 (결정 완료) - -### 난이도 판정 요약 - -서브에이전트 조사 결과: 동기적 저작 트리에 한정한 얕은 버전(Fusion -`Contextual`류 코루틴 키 weak table push-pop)은 구현 난이도 **낮음**이지만, -quad가 정상 패턴으로 확정한 "Slot에 이벤트 핸들러/코루틴에서 비동기로 -자식이 추가되는 경우"엔 **에러 없이 조용히 기본값으로 폴백**하는 함정이 -있음. 완전 자동(비동기 추가까지 자동 전파)은 Roblox Luau에 -thread-local/async-context-propagation 훅이 없어 **사실상 불가**(플랫폼 -한계, Node `AsyncLocalStorage`/Python `contextvars`도 "자동"이 아니라 -"async 경계마다 명시적 캡처+재진입"으로 같은 문제를 품). - -### 기각 이유 - -얕은 버전조차: (1) `base/slot-plan.md`가 정상 패턴으로 명시한 Slot 비동기 -추가에서 가장 먼저, 가장 조용히 깨짐. (2) `research/debug-tooling-plan.md`의 -"모든 연결은 선언된 그래프여야 한다"는 quad-debug 철학과 충돌하는 안 보이는 -채널을 만듦. - -### 레이어드 Store 대안도 철회 (사용자 반박 수용) - -이전 라운드에서 대안으로 "레이어드 Store"(자식 Source 모음이 없는 키는 -부모로 `__index` 폴백)를 권고했는데, 사용자 반박으로 철회함: - -- quad는 이미 **타입으로 강제되는 명시적 Store 전달**을 갖고 있고, 이건 - Context의 "Provider 안 넣으면 조용히 기본값/에러"보다 **더 안전**하다 - (컴파일타임에 걸림 vs 런타임에 조용히 새는 값) — Context보다 나은 지점. -- "필드 일부만 오버라이드, 나머지는 부모 값 그대로"가 필요하면, 오버라이드 - 지점에서 `Store({...부모값, 변경필드=새값})`을 **한 번 명시적으로** - 만들어서 그 지점부터 평소처럼 prop으로 넘기면 끝 — Modifier가 이미 쓰는 - "merge, 나중 게 이김" 패턴 재사용 가능, 새 primitive 불필요. 레이어드 - Store(읽는 시점에 몇 단계를 거슬러 올라가는지 안 보이는 자동 폴백)는 - 정확히 Context와 같은 이유(디버깅 어려움 — "이 값이 왜 이거지?"를 - 추적하려면 부모 체인을 다 훑어야 함)로 얻는 것보다 잃는 게 크다. -- 서드파티 라이브러리가 뭔가 필요하면, 타입이 강제하는 명시적 요구 - (`props.Theme: Store`)가 "몰래 안 줘서 죽는다"보다 나은 실패 - 모드 — Slot 기반 저작 모델(부모가 자손을 직접 구성)에서도 이런 요청은 - 자연스럽게 props로 흐름. - -### 결론 - -Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Context가 -없는가"(명시적 Store 전달이 이미 그 역할을 하고, 타입 강제가 Context의 -실패 모드보다 안전하다는 논증)도 quadnomicon 에세이 후보로 등록(아래 -"문서화 백로그" 참고). - ## 빈 자리 아닌 것으로 확인된 것들 - **Untrack/Peek**(Solid `untrack()`, Vue `toRaw`): quad는 Vide식 암묵 @@ -411,9 +188,10 @@ Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Co ## 문서화 백로그 (2026-08-06~07, `documentation-content-map.md`에도 반영) - **quadnomicon 에세이**: - - "왜 lexical Batch를 기각하고 대신 값 기반 Blocker를 택했는가" — 코루틴 - yield 위험 분석(Batch 절)과 Blocker의 설계(3-1절)를 나란히 비교. - - "왜 Context가 없는가"(명시적 타입 강제 Store 전달이 이미 그 역할을 함). + - "왜 lexical Batch를 기각하고 대신 값 기반 Blocker를 택했는가" — + `archive/batch-rejected.md`와 `base/additional-primitives.md`의 + Blocker 절을 나란히 비교. + - "왜 Context가 없는가" — `archive/context-rejected.md` 참고. - "왜 push-invalidate/pull-recompute 설계가 laziness와 재계산 방지를 최우선 목표로 뒀는가" — Blocker 같은 파생 프리미티브가 이 목표 위에서 자연스럽게 나온 이유까지 포함해 기존 심화 콘텐츠 후보 3번(`왜 @@ -426,22 +204,12 @@ Context, 레이어드 Store 둘 다 프리미티브로 만들지 않음. "왜 Co 유연한 구조" — 명시적 의존성 선언(`:With`) 위에서도 실제 계산은 조건부로 일부만 쓸 수 있다는 팁. - "Blocker 사용 가이드" — 파이프라인 최종 연산 지점에 배치, **네스팅 - 금지를 강하게 명시**(위 3-1절 "재진입" 참고, 문서화 시 최우선 강조 - 항목). + 금지를 강하게 명시**(`base/additional-primitives.md`의 "재진입" 절 + 참고, 문서화 시 최우선 강조 항목). - "여러 Source를 한꺼번에 바꿀 때 Blocker 없이도 중복 재계산을 피하는 파이프라인/업데이트 순서 팁"(Blocker를 안 쓰는 단순 케이스용 보조 팁, 기존 "심화 최적화 팁" 항목을 Blocker 존재를 전제로 재조정). -## 제안 우선순위 - -1. **키 기반 컬렉션 재조정** — 여전히 최우선, 설계 진행 중. M0 이전 완전 - 확정이 목표. -2. **Effect** — 거의 수렴, 시그니처만 다듬으면 됨. -3. **Blocker** — 핵심 메커니즘+이름 확정, `base/`로 승격 가능한 수준. - 문서화(특히 네스팅 금지 강조)만 남음. -4. **Batch(lexical)/Context** — 둘 다 결정 완료(기각), 더 이상 검토 대상 - 아님. - ## 참고: 조사에 사용한 소스 근거 - Fusion: `State/ForPairs.luau`, `State/ForKeys.luau`, diff --git a/.claude/research/debug-tooling-plan.md b/.claude/research/debug-tooling-plan.md index e56d26e..7946195 100644 --- a/.claude/research/debug-tooling-plan.md +++ b/.claude/research/debug-tooling-plan.md @@ -328,7 +328,7 @@ UI를 클릭한 순간)에 강제로 그 계산을 트리거하는 건 이 전 **네이밍 컨벤션(사용자 제안)**: 내부 자동 생성 helper Instance는 `_`나 `QUAD_` 같은 접두어를 붙여서 Explorer에서 직접 봤을 때 헷갈리지 않게 함 — 이름 바꾸는 건 비용이 크지 않음. v1이 이미 `_quad_round`/`_quad_padding`/ -`_quad_scale` 네이밍(`research/ui-shorthand-plan.md` 참고)으로 정확히 +`_quad_scale` 네이밍(`base/ui-shorthand-plan.md` 참고)으로 정확히 이 관습을 썼던 전례 — quad-v2에서 내부 자동 생성물이 생기면 그대로 재사용. `research/documentation-plan.md`의 "UI 네이밍 컨벤션 문서" 백로그에도 이 구체적 규칙을 추가해둠. diff --git a/.claude/research/documentation-content-map.md b/.claude/research/documentation-content-map.md index 4706b53..a5f651b 100644 --- a/.claude/research/documentation-content-map.md +++ b/.claude/research/documentation-content-map.md @@ -37,7 +37,7 @@ 11. **Ref 기초** — 외부 관리 Instance 참조/마이그레이션용, `CreatedRef(fn)` + 배열 위치로 자식 전/후 표현, "프로퍼티보다도 먼저" 필요할 때만 `PreRef`(2026-08-07 세 번째 세션, `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`) +14. **UI 숏핸드(quad-roblox 한정)** — `UICorner`/`UIPadding`/`UIPaddingOffset`/`UIScale` 인라인 키 (`base/ui-shorthand-plan.md`) --- @@ -135,7 +135,7 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부 실제 일어나는 derived state)에 배치하는 게 원칙이라는 것, **네스팅 금지를 최우선으로 강조**(겹치는 배치는 각자 새 `Blocker`를 만들 것 — 안 지키면 조용히 잘못된 시점에 조기 해제되는 원인 추적 어려운 버그로 - 이어짐) — `research/additional-primitives-plan.md` 3-1절 + 이어짐) — `base/additional-primitives.md`의 "Blocker" 절 19. 여러 Source를 한꺼번에 바꿀 때 Blocker 없이도 중복 재계산/재대입을 피하는 파이프라인/업데이트 순서 팁(Blocker를 안 쓰는 단순 케이스용 보조 팁) — `research/additional-primitives-plan.md` "문서화 백로그" 절 @@ -197,13 +197,15 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**: ## 5. 문서화 아직 보류(미확정 설계라 쓰면 안 됨) - Slot 형제 순서 보장 (`slot-plan.md`) -- Tween 오버라이드/삭제후재시작/끝점이동 세부 옵션 키 이름 (`research/tween-plan.md`) -- UI 숏핸드 `RoundSize` 드롭 여부 (`research/ui-shorthand-plan.md`) +- Tween 오버라이드/삭제후재시작/끝점이동 세부 옵션 키 이름, 트윈 옵션 값 + 모양(TweenInfo vs 편의 필드) (`research/tween-plan.md`) - `Attribute` 제네릭 vs 타입별 정적 생성자 (`bind-system-plan.md`) - provider/processor 네이밍 (`module-lifecycle-plan.md`) - 키 기반 동적 컬렉션 재조정 최종 이름/시그니처(`Render`/`Draw`/`List` 등 후보만 있음, `Slot:Extract` 세부 시맨틱도 미정) — `research/ additional-primitives-plan.md`(2026-08-06 신설, 설계 진행 중) +- Effect가 `state:Effect()`로 Observer를 확장하는 형태인지, 완전히 독립된 + free function인지 (`base/additional-primitives.md`의 "미해결" 절) 이 항목들은 `.claude/question.md`에도 이미 열린 질문으로 잡혀있음 — 여기선 "확정 전엔 문서화 대상 아님"이라는 표시만 겸함. diff --git a/.claude/research/documentation-plan.md b/.claude/research/documentation-plan.md index f65f406..c8c7b07 100644 --- a/.claude/research/documentation-plan.md +++ b/.claude/research/documentation-plan.md @@ -102,7 +102,7 @@ Roblox가 Play 중 라이브 UI 편집 도구를 꺼버려서 실제 화면의 U 강제할까(과한 선택지, 참고만)? **구체적으로 결정된 하위 규칙 하나(2026-08-06)**: quad가 내부적으로 -자동 생성하는 helper Instance(예: `research/ui-shorthand-plan.md`의 +자동 생성하는 helper Instance(예: `base/ui-shorthand-plan.md`의 UICorner/UIPadding/UIScale 숏핸드가 만드는 자식)는 `_`나 `QUAD_` 같은 접두어를 붙여 Explorer에서 직접 볼 때 사용자가 만든 것과 헷갈리지 않게 함 — v1의 `_quad_round`류 네이밍 재사용(`research/debug-tooling-plan.md` diff --git a/.claude/research/pre-implementation-audit.md b/.claude/research/pre-implementation-audit.md index e7d497c..c3302cc 100644 --- a/.claude/research/pre-implementation-audit.md +++ b/.claude/research/pre-implementation-audit.md @@ -442,7 +442,7 @@ M11 착수 시. ### 2-11. UI shorthand — 기존 UICorner 자식과의 매칭 기준(이름 vs 타입)이 불명, 사용자 실수 유발 위험 높음 -**위치**: `research/ui-shorthand-plan.md` "v1 실제 메커니즘" 절. +**위치**: `base/ui-shorthand-plan.md` "v1 실제 메커니즘" 절. **문제**: v1 메커니즘은 "기존 `UICorner` 자식이 있으면 재사용, 없으면 `Instance.new("UICorner", item)`(`Name = "_quad_round"`)"라고 서술되는데, @@ -485,7 +485,7 @@ M11 착수 시. ### 3-2. Corner/PaddingAll/Scale 개별 Handler 대신 데이터 테이블 구동 단일 제네릭 Handler -**위치**: `research/ui-shorthand-plan.md` "메커니즘 — 새 아키텍처 개념 +**위치**: `base/ui-shorthand-plan.md` "메커니즘 — 새 아키텍처 개념 불필요" 절. **문제**: 문서는 "Corner/PaddingAll/Scale 같은 특수 키를 인식하는 diff --git a/.claude/research/tween-plan.md b/.claude/research/tween-plan.md index cc34469..872fd6c 100644 --- a/.claude/research/tween-plan.md +++ b/.claude/research/tween-plan.md @@ -2,12 +2,16 @@ **상태**: research — 방향은 뚜렷하게 잡혀 있고(라이브러리가 트윈을 직접 구현하지 않는다), `retract` 순서/오버라이드 기본값(Cancel)도 확정됨. 남은 건 -기본값 외 나머지 오버라이드 동작을 고르는 옵션 키의 정확한 이름/시그니처 -정도. 원본: +기본값 외 나머지 오버라이드 동작을 고르는 옵션 키의 정확한 이름/시그니처와, +트윈 옵션을 어떤 값 모양으로 받을지(아래 "트윈 옵션 값 모양" 절, 신규) +정도. **중요 — 놓치기 쉬운 포인트**: `retract`는 Destroy(완전 소멸) 시엔 +호출되지 않는다(아래 "`retract`는 완전 소멸 시엔 호출되지 않는다" 절) — +Tween 오버라이드 로직을 짤 때 "인스턴스가 파괴될 때도 이 코드가 실행될 +것"이라고 가정하면 틀림. 원본: `.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은 항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이 반응 그래프 안에 있는 설계는 명시적 반면교사 — [정정: 절 제목이 실제와 -달랐음] `base/comparison-fusion-vide.md`의 "Fusion" 절 마지막 불릿 +달랐음] `reference/comparison-fusion-vide.md`의 "Fusion" 절 마지막 불릿 ("Tween/Spring이 State그래프 안의 1급 노드") 참고. ## 확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다 @@ -79,6 +83,44 @@ Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind Destroy 시점엔 아무 것도 안 함(라이프타임 `Connected` 체크로 처리 자체를 멈추는 것만으로 충분).** +**메모 — `retract`와 `canExecute`는 서로 다른 문제를 다룬다, 나중에 quadnomicon +급에서 제대로 설명 필요.** "그럼 값 교체가 아니라 값을 계속 관측하는 쪽 +(예: `state:Observer(fn)`으로 Tween을 건 경우)은 Destroy 시 어떻게 +정리되는가?"라는 질문이 자연스럽게 따라오는데, 이건 `retract`의 영역이 +아니라 `canExecute`(라이프타임 predicate, `base/lifecycle-pattern.md`의 +"생명 바인드 유틸" 절)의 영역이다 — Destroy되면 `retract` 호출 없이 그냥 +`canExecute`가 false가 되어 이후 처리 시도 자체가 조용히 no-op된다. +store-bind 일반(Tween 포함)도 같은 결이라 실제로는 이미 일관되게 명시돼 +있지만(`base/bind-system-plan.md` "확정된 디스패치 모델" 절), "왜 이 +경로엔 retract를 쓰고 저 경로엔 canExecute를 쓰는가"라는 내부 구조상의 +이유는 quadnomicon 콘텐츠로 풀어서 설명할 필요가 있음(`research/ +documentation-content-map.md` 심화 콘텐츠 후보에 메모) — 지금은 이 메모만 +남겨두고 상세 설명은 나중 문서화 단계로 미룸. + +## 트윈 옵션 값 모양 — TweenInfo 그대로 vs 편의 필드 (신규, 열린 논의) + +**아직 논의 시작 단계 — 나중에 더 다룰 주제로만 남겨둠.** Roblox의 +`TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, +delayTime)`는 순수 포지셔널 생성자인데, Luau엔 named call 문법이 없어서 +직접 쓰면 `TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)` +처럼 각 인자가 뭘 뜻하는지 호출부만 보고 알기 어렵다. 후보: + +1. **`TweenInfo`를 그대로 받는다** — 사용자가 이미 만들어둔 `TweenInfo`를 + 재사용하고 싶은 경우엔 상관없지만, 대부분의 흔한 케이스(길이/이징만 + 바꾸고 싶음)에서 매번 포지셔널 생성자를 마주해야 함. +2. **편의 필드로 개별 인자를 받고 기본값을 제공** — 예: + `{Time=0.3, Style=Enum.EasingStyle.Quad, Reverses=false, ...}`처럼 + 이름 붙은 키로 받고 흔한 기본값(예: `Time=0.2`, `Style=Quad`, + `Direction=Out`)을 채워줌. 명시적으로 `TweenInfo`가 이미 있어서 + 재사용하고 싶다는 케이스에도 열어두면(예: `Info = someTweenInfo` + 필드로), 둘 다 지원 가능. + +**현재 소견(확정 아님)**: 2번(편의 필드 + 기본값)이 흔한 사용 경험상 더 +낫다는 쪽으로 기움 — 대부분의 호출에서 named call이 없는 `TweenInfo.new`의 +가독성 문제를 피할 수 있고, 기본값 덕에 짧은 호출도 가능해짐. 다만 +구체적인 필드 이름/기본값/`TweenInfo` 재사용 경로의 정확한 문법은 아직 +확정 아님 — 나중 논의 대상으로 남김. + ## 네임스페이스드 객체 (성능상 이유로 보류) 트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 — @@ -92,3 +134,7 @@ CollectionService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페 - 기본값(Cancel)은 확정됨. 남은 건 나머지 세 동작(오버라이드/삭제 후 재시작/ 끝점 이동 후 재시작)을 선택하는 옵션 키의 정확한 이름/시그니처 — 구현 단계에서 확정. +- 트윈 옵션 값 모양(위 "트윈 옵션 값 모양" 절, 신규) — `TweenInfo` 그대로 + 받을지 편의 필드+기본값으로 받을지, 소견은 후자 쪽이지만 확정 아님. +- 자연 완료(Completed) 시 per-instance 북키핑 정리 여부 — + `research/pre-implementation-audit.md` 2-10번 참고, M11 착수 시 확정. diff --git a/.claude/research/ui-shorthand-plan.md b/.claude/research/ui-shorthand-plan.md deleted file mode 100644 index 928e99c..0000000 --- a/.claude/research/ui-shorthand-plan.md +++ /dev/null @@ -1,102 +0,0 @@ -# UI 편의 숏핸드 (Corner/Padding/Scale 등) — 인라인 적용 계획 - -**상태**: research — 2026-08-06 세션에서 결론까지 남. `Corner`/ -`PaddingAll`/`Scale` 숏핸드 자체는 **여전히 필요**(사용자 재확정, 아래 -"결론" 절 — 이전에 이 문서가 한 차례 "포팅 불필요"로 잘못 정리했던 걸 -정정함). 패키지 배치는 `quad-roblox` 코어 직접 포함으로 확정. - -## 배경 - -사용자 기억: v1을 쓸 때 "UICorner/UIPadding/UIScale 같은 걸 직접 -`Instance.new`로 만들어 Parent하는 귀찮은 작업 없이, Frame 안에 인라인으로 -넣기만 해도 CSS 스타일처럼 적용됐다 — 코드가 줄고 읽기도 편해서 꽤 -괜찮았다"는 것. 문서 어디에도 기록된 적 없어 v1 소스(`.claude/initreq/quad`)와 -PA님 코드(`.claude/initreq/artworks`)를 서브에이전트로 조사. - -## v1 실제 메커니즘 (조사 완료) - -`class.lua`의 `SetProperty`/`GetProperty`(38~109행)와 `ProcessQuadProperty` -(134~213행)에 하드코딩된 if/elseif 분기로 특수 문자열 키 5종을 지원: - -- `RoundSize = 16` → `ImageLabel`/`ImageButton` 전용, UICorner가 아니라 - 이미지 자체의 9-slice 라운드 처리(`round.SetRound()`) — **UICorner 계열과 - 메커니즘이 다름**. -- `Corner = 8` → 숫자 하나. 기존 `UICorner` 자식이 있으면 재사용, 없으면 - `Instance.new("UICorner", item)`으로 생성(`Name = "_quad_round"`), - `CornerRadius = UDim.new(0, value)` 설정. -- `PaddingAll = UDim.new(...)` / `PaddingAllOffset = 50` → 동일 패턴, - `UIPadding`(`_quad_padding`). -- `Scale = 1.2` → 동일 패턴, `UIScale`(`_quad_scale`). - -값 모양은 항상 **리터럴 하나**(숫자/UDim) — 테이블도 `__type` 태그도 아님. -실사용 예시(`md/kr/tutorial/7_quadProperty.md`): -```lua -Frame "mainFrame" { - PaddingAllOffset = 50; - ImageFrame { RoundSize = 16; ... }; -} -``` - -**`UIListLayout`/`UIGridLayout`/flex는 이런 전용 숏핸드가 v1에 없었음** — -`ProcessQuadProperty`의 범용 자식 나열 분기(배열 인덱스로 놓인 Instance/ -Class 결과를 자동 mount, 207~213행)로 `UIListLayout{...}`을 그냥 직접 -나열했을 뿐, `List = true` 같은 전용 축약 문법은 레포 전체(PA님 코드 -포함)에서 찾지 못함. **quad-v2도 이 부분은 이미 있는 children-array + -인스턴스 생성 문법으로 그대로 커버됨 — 새로 설계할 것 없음.** 사용자 -기억 중 이 부분은 "전용 숏핸드"가 아니라 "선언형 문법 자체가 원래 -간결하다"는 것과 섞였을 가능성이 큼. - -## 결론 (2026-08-06, 한 차례 오해 후 재정정) - -**RoundSize와 Corner는 서로 다른 이유로 존재했던 별개 기능 — 혼동하지 -말 것**: -- **`RoundSize`(이미지 9-slice 라운드)**: `ImageLabel`/`Button`을 - 이미지 트릭으로 둥글게 보이게 하던 것 — **당시 Roblox에 `UICorner` 같은 - 네이티브 구현체가 없었기 때문에** 존재하던 워크어라운드. 지금은 - `UICorner`가 안정적인 네이티브 Instance라 이 이미지 트릭 자체를 그대로 - 포팅할 이유는 없음(이미지에도 그냥 실제 `UICorner`를 쓰면 됨) — - **RoundSize는 포팅 안 함**. -- **`Corner`/`PaddingAll`/`Scale`(UICorner/UIPadding/UIScale 자동 - 생성)**: 이건 워크어라운드가 아니라 **지금도 유효한 편의 기능** — - **사용자 재확정**: "UIScale 같은 건 여전히 별도의 Instance고 부모 - Frame에 영향을 주는 구조, 숏핸드는 여전히 필요하다". `UICorner`가 - 네이티브가 됐다고 해서 "별도 Instance를 만들어 부모에 Parent해야 - 한다"는 구조적 번거로움 자체가 없어지는 게 아니므로, 이 숏핸드의 - 존재 이유는 여전히 유효함 — **이전 정리("포팅 불필요")는 오해였고 - 정정함, `Corner`/`PaddingAll`/`Scale`은 그대로 포팅 대상.** - -**메커니즘 — 새 아키텍처 개념 불필요**: 이미 있는 pluggable Handler로 -그대로 커버됨. `Corner`/`PaddingAll`/`Scale` 같은 특수 키를 인식하는 -Handler(`isHandlable`이 그 키를 매칭)가 "이름 붙은 자식을 찾거나 만들고 -프로퍼티 세팅"을 `process(inst, k, v)`에 구현 — v1의 하드코딩 if/elseif -대신 정식 핸들러 계약(`isHandlable`/`priority`/`process`/`retract`)을 -따르는 것만 다름. `modifier-plan.md`가 이미 예시로 든 -`Modifier.Rounded(8)`은 이 특수 키를 flatten해서 props에 꽂아넣는 사탕 -문법일 뿐, 실제 처리는 이 Handler가 함 — Modifier를 안 거치고 -`Frame { Corner = 8 }`처럼 순수 인라인 키로 직접 써도(v1처럼) 동일하게 -작동함, `architecture.md`의 `[Attribute "Name"]`류 특수 키와 같은 층위. -자동 생성된 자식은 위 "핵심 설계 방향" 관례대로 `_`/`QUAD_` 접두어 -네이밍(`research/debug-tooling-plan.md` 9번, v1의 `_quad_round`류 -그대로 재사용). - -**패키지 배치 — `quad-roblox` 코어에 직접 포함, 확정**: "트윈도 인스턴스 -생성/제어를 직접 구현 가능한 걸 하나로 묶어 쉽게 쓰게 합친 것 — 너무 -잘게 쪼개 오버엔지니어링하기보다 확실히 하나로 코어에 넣어도 충분하다, -opt-out할 이유가 별로 없다"는 게 사용자 판단 — **작고 항상 켜져 있어도 -비용이 무시할 만한 편의 기능은 별도 opt-out 패키지로 쪼개지 말고 -`quad-roblox` 코어에 직접 포함한다**는 원칙으로 확정(이미 계획된 Tween -핸들러가 같은 모양이라는 게 근거). 이 원칙은 일반화해서 재사용 가능 — -앞으로 비슷한 "작은 인스턴스 편의 기능"이 제안되면 `quad-roblox-util` -같은 걸 새로 만들지 않고 이 선례를 따르면 됨. - -**중요도**: 낮음("이건 나중에도 쉽게 구현됨" — 사용자) — 지금 M0 우선순위를 -바꿀 이유는 없음, M10(Handlers/Attribute 등) 전후로 다른 세부 Handler와 -함께 구현하면 충분. - -## 열린 질문 (`.claude/question.md`에도 취합) - -- 이름 그대로 가져올지(`Corner`/`PaddingAll`/`PaddingAllOffset`/`Scale`) - 재검토할지 — 진행 중인 용어 정리(`CLAUDE.md` "지금 할 일" 2번)에 합류 - 대상. -- `RoundSize`(이미지 라운드)를 완전히 드롭할지, 아니면 이미지 대상에도 - 그냥 실제 `UICorner`를 자동 적용하는 것으로 대체할지 — 후순위. diff --git a/.claude/research/v1-compat-plan.md b/.claude/research/v1-compat-plan.md index 6701149..02b8cb0 100644 --- a/.claude/research/v1-compat-plan.md +++ b/.claude/research/v1-compat-plan.md @@ -61,7 +61,7 @@ v1(`.claude/initreq/quad/src`) 조사 결과, API는 성격이 다른 두 계층 핸들러 분기도 안 생김. 단, "quad-debug 추적 밖 mutate 경로가 생긴다"는 근거(4번)는 격리해도 남는 문제라 별도 검토 필요. - **RoundSize 등 특수 키**: `Corner`/`PaddingAll`/`Scale`은 이미 - `research/ui-shorthand-plan.md`에서 네이티브 포팅 확정됨 — 별도 compat + `base/ui-shorthand-plan.md`에서 네이티브 포팅 확정됨 — 별도 compat 작업 불필요. `RoundSize`(이미지 9-slice 라운드 트릭)만 UICorner 없던 시절 워크어라운드라 재현 자체가 불필요하다고 이미 결론남. - **`target()`/Linker**: 위 정정대로 "이름 있는 자식 등록 + 시그널 중계"일 diff --git a/CLAUDE.md b/CLAUDE.md index e853732..f33c44d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,10 +38,14 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 `.claude/README.md`가 색인. 요약: - `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저 `.claude/base/architecture.md`를 읽을 것. +- `.claude/reference/` — **[2026-08-07 신설]** base처럼 확정된 건 아니지만 + base 문서가 근거로 인용하는 온디맨드 참고 자료(v1 내부 동작 스냅샷, + Fusion/Vide 비교 리서치) — 항상 읽을 필요는 없고 인용될 때만 열어볼 것. - `.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` + `documentation-plan.md`/`documentation-content-map.md`/ + `framework-comparison-findings.md`/`additional-primitives-plan.md`(키 기반 + 동적 컬렉션 재조정만 남음) — 전부 후순위(급한 건 `tween-plan.md` 세부 옵션 정도). 최신 목록·우선순위는 `.claude/README.md`가 소스, 여기서 개수 반복 안 함(과거에 "두 개뿐"이라 적어놨다가 새 문서 추가될 때마다 안 갱신되는 패턴이 반복돼서 아예 안 세기로 함). @@ -676,3 +680,71 @@ M7 착수 시 `modifier-plan.md` 8번 참고하면 됨. 풀리는 문제 — `false`를 이벤트 disconnect 센티널로 쓴 선례처럼 `None` (가칭) 프리미티브를 도입하는 방향만 `base/modifier-plan.md` "2-1"절에 짧게 메모해두고 상세 설계는 다음 세션으로 미룸. + +## 2026-08-07 네 번째 세션 — `.claude/` 코퍼스 전반 정리(폴더 재편, 승격, 기각 분리) + +사용자가 코퍼스 전체를 훑고 "실제 코딩에 필요한가"를 기준으로 남길 것과 +분리할 것을 판단해 달라고 요청 — 여러 문서에 쌓인 역전 이력/quad +자체와 무관한 배경자료/이미 기각된 후보가 뒤섞여 있어 컨텍스트 크기와 +가독성 둘 다 해치고 있다는 문제의식. 아래 6가지를 처리, 전부 반영 완료: + +1. **`reference/` 폴더 신설** — `quad-v1-architecture.md`, + `comparison-fusion-vide.md`를 `base/`에서 이동. 항상 읽어야 하는 + 결정사항(`base/`)과, 다른 문서가 근거로 인용할 때만 열어보면 되는 + 온디맨드 스냅샷/비교자료(`reference/`)를 분리 — 전자는 "결정 완료", + 후자는 "결정이 아니라 결정의 근거"라는 차이. 전체 문서의 상호참조 + 경로도 전부 갱신함. +2. **`component-composition-plan.md`의 누적 역전 이력 트리밍** — + `StoreSource` 프록시 폐기 이력이 "원래 이랬다 → 이렇게 뒤집혔다"를 + 본문에서 장황하게 반복 서술하고 있었는데, 이미 `archive/ + store-source-proxy-reversed.md`에 원문·이유·비교표가 전부 보존돼 + 있으므로 본문은 최종 확정만 남기고 포인터로 압축. +3. **`ui-shorthand-plan.md`를 `research/`→`base/`로 승격, 재작성** — + (a) 이미지 라운드 트릭 `RoundSize`는 완전히 드롭, 근거는 + `archive/ui-shorthand-roundsize-dropped.md`로 분리(이 판단이 한 차례 + "Corner/PaddingAll/Scale 전체가 불필요하다"로 잘못 일반화됐다가 + 정정된 이력도 같이 보존). (b) 이름을 v1 그대로(`Corner`/`PaddingAll`/ + `Scale`)가 아니라 실제 Roblox Instance 이름과 맞춘 `UICorner`/ + `UIPadding`/`UIScale`로 확정 — v1식 짧은 이름은 Modifier 체이닝 + 메소드와 겹쳐 "진짜 UICorner 숏핸드인지 그냥 비슷한 이름의 부가 + Modifier인지" 구분이 안 된다는 사용자 지적 반영. (c) store-bind + 가능성 명시 — v1에서도 가능했던 기능이고, Tween처럼 무거운 API + 표면 없이 기존 per-instance weak-table 유틸(`base.perInstanceState`) + 재사용만으로 충분하다는 점을 추가. +4. **`additional-primitives-plan.md`를 4갈래로 분리**: 확정된 `Blocker`/ + `Effect`는 새 `base/additional-primitives.md`로 승격(Blocker는 + State와 같은 마일스톤에서 개발하기로 해서 `store-semantics.md`에 + 교차 참조 추가, `ROADMAP.md` M3에도 체크박스 반영). 기각된 `Batch` + (lexical block)와 `Context`(+대안이던 레이어드 Store)는 각각 + `archive/batch-rejected.md`/`archive/context-rejected.md`로 분리. + `research/additional-primitives-plan.md`엔 아직 실제로 열려있는 + 것(키 기반 동적 컬렉션 재조정) 하나만 남김. +5. **archive 제목 컨벤션을 둘로 분화** — 기존 `[역전됨]`(한 번 확정했다가 + 뒤집힌 것, `store-source-proxy-reversed.md`/`ref-phase-option-reversed.md`)과 + 새로 생긴 `[기각됨]`(확정한 적 없이 후보였다가 채택 안 된 것, + `batch-rejected.md`/`context-rejected.md`/`ui-shorthand-roundsize-dropped.md`)을 + 구분 — `README.md`의 `archive/` 폴더 기준 설명에 두 컨벤션 차이를 + 명시. +6. **`tween-plan.md` 보강** — `retract`가 Destroy 시엔 호출 안 된다는 + 사실을 상단 상태 요약에서도 짚도록 가시성 강화, `canExecute`(Destroy + 시 처리)와 `retract`(값 교체 시 처리)가 서로 다른 문제를 다룬다는 + 점을 quadnomicon급 문서화 숙제로 메모(지금은 상세 설명 안 하고 + 메모만). 트윈 옵션 값 모양(raw `TweenInfo` vs 이름 붙은 편의 + 필드+기본값) 논의를 새로 열어둠 — Luau가 named call을 지원 안 해서 + `TweenInfo.new(...)` 포지셔널 생성자가 읽기 어렵다는 문제의식, + 소견은 편의 필드 쪽이지만 확정 아님, 나중 논의 대상으로만 남김. + +**미해결로 남긴 것 — 임의로 결론내지 않음**: Effect가 `state:Effect()` +형태로 Observer를 확장하는 변형인지, 완전히 독립된 free function인지가 +불명확함(사용자가 "확인 필요, 아니라면 논의해야 할 상태로 남겨두라"고 +명시). 관련 하위 질문으로 `state:Observer(fn)`가 생성 시 `fn`을 즉시 +1회 실행하는지도 문서 어디에도 명시돼 있지 않음이 이번에 드러남(Effect는 +"즉시 1회 실행"이 스펙에 명시돼 있어 이 부분만 보면 둘이 겹쳐 보임). +`base/additional-primitives.md`의 "미해결" 절과 `.claude/question.md` +0번에 반영 — 구현 착수(M3~M4 전후) 전에 반드시 재확인할 것. + +**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 "다음 세션이 +할 일" 참고, `ROADMAP.md` M0부터). 이번 세션은 순수 문서 정리라 설계 +결정 자체는 늘지 않았음 — 단, M3 체크리스트에 `Blocker.luau` 항목이 +하나 추가된 것과, 위 Effect/Observer 미해결 항목은 M3~M4 착수 전에 +확인해야 함. diff --git a/ROADMAP.md b/ROADMAP.md index 7861367..b2a7bc2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -52,12 +52,21 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [ ] `Dispatch/init.luau`(`process`/`retract` 엔진, `isHandlable` 우선순위 스캔) - [ ] `Handler.luau`(핸들러 계약 타입) +- [ ] `LifetimeHandle.luau`/`PerInstanceState.luau` **인터페이스만**(타입 + 계약, 실 구현 없음 — quad-roblox 실 구현은 M8) — 원래 M8에만 + 있었으나 M4(StoreBind의 `Connected` 확인)/M6(Slot의 `canExecute`)이 + 이미 이 인터페이스를 전제로 서술돼 있어 로드맵 순서가 역전돼 + 있었음(`pre-implementation-audit.md` 우선순위1-9, `question.md` 2번 + — 2026-08-07 세 번째 세션에 반영) - [ ] mock 대상 테스트 ## M3 — Store/State/Source - [ ] `Source.luau`/`State.luau`/`Store.luau` - [ ] `store.key` dot-access 타입 추론 확인 +- [ ] `Blocker.luau`(`base/additional-primitives.md` 참고 — 여러 Source를 + 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive, + State와 밀접히 연관돼 있어 같은 마일스톤에서 개발) - [ ] mock 대상 테스트 ## M4 — 첫 end-to-end 반응형 업데이트 @@ -87,12 +96,23 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [ ] `State` 조합 타입 차단 확인(`modifier-plan.md` 7번, UB 확정) - [ ] `:Apply(factory)` 팩토리 함수 체이닝(`modifier-plan.md` 8번, 예약 키 `Apply`가 제네릭 `__index` 필드 setter와 안 겹치는지 확인) +- [ ] 인라인 키로 modifier 필드를 명시적으로 지우는 문제 확인 — `None` + (가칭) 센티널 프리미티브 도입 여부(`modifier-plan.md` 2-1번, 아직 + 미정 — 착수 전 사용자 확인 필요, 확정 안 되면 이번 마일스톤은 + 스킵하고 다음으로 미뤄도 됨) ## M8 — Ref -- [ ] `CreatedRef` 메커니즘(숫자 슬롯 참가자) -- [ ] `LifetimeHandle` 인터페이스 + quad-roblox 실제 구현(Instance 생존 확인) -- [ ] `PerInstanceState` 인터페이스 + quad-roblox 실제 구현(weak-keyed table) +- [ ] `CreatedRef` 메커니즘(숫자 슬롯 참가자) + `PreRef`(children 배열 + 전용, Modifier/Store 타입 차단, 위치 무관 호이스팅 pre-pass — + `base/bind-system-plan.md` "`phase` 옵션 폐기 → 위치로 표현, + `PreRef` 신설" 절) +- [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면 resume+소진, + 함수면 호출+유지 — 같은 배열 하나로 통합) +- [ ] `LifetimeHandle` quad-roblox 실제 구현(Instance 생존 확인, 인터페이스 + 자체는 M2로 이동됨) +- [ ] `PerInstanceState` quad-roblox 실제 구현(weak-keyed table, 인터페이스 + 자체는 M2로 이동됨) ## M9 — 컴포넌트 합성 레이어