diff --git a/.claude/README.md b/.claude/README.md index 4557bbe..62cc8ae 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -16,6 +16,7 @@ | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(CLAUDE.md 세션 로그 중복 방지) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | | `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. 아직 결과 미확인 — `luau-test/README.md`가 색인 | +| `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, CLAUDE.md의 "세션 히스토리" 절에서 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 | | `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | `research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시 diff --git a/.claude/session/2026-08-04-01-roadmap-handoff.md b/.claude/session/2026-08-04-01-roadmap-handoff.md new file mode 100644 index 0000000..91b83f6 --- /dev/null +++ b/.claude/session/2026-08-04-01-roadmap-handoff.md @@ -0,0 +1,64 @@ + + + + +## 최근 세션 요약 (2026-08-04, 6차 라운드 이후) + +**6차 라운드**: 남아있던 "급하지 않음" 질문 두 개 해소 — 태그 네임스페이싱 +충돌은 컴포넌트 단위로는 Ref가 대신 해결해줘서 심각하게 안 봄(`architecture.md` +5번), Store가 Store를 담는 경우는 없음으로 확정(Store는 Source에 준하는 +"시작점"이라 다른 반응형 값에 자동 연결되지 않음, `bind-system-plan.md`). + +**그 이후 채팅에서 세 가지 큰 스레드가 새로 열림/정리됨**: +- **Modifier 메커니즘 전체 확정** — 런타임 pluggable 핸들러가 아니라 정적 + merge, immutable+`table.clone` 기반 체이닝, 필드가 State일 수도 있는 + 경우의 setter/getter 동작까지 전부 확정(`base/modifier-plan.md`, 새로 + base 승격). 이 논의에서 "관측해야 실체화된다"는 프로젝트 전역 원칙도 + 명문화(`bind-system-plan.md`). +- **컴포넌트화 논의, 같은 날 후속 세션에서 완결** — v1의 `Class.Extend()` + 자동-store 매직은 폐기하고 React식으로 값을 명시적으로 전달하는 방향으로 + 수렴, `StoreSource`(Source를 인터페이스+구현체로 보고 Store 키에서 얇은 + 프록시로 얻는 것) 아이디어 확정. 마지막 미결이던 "modifier/Ref의 컴포넌트 + 경계 통과"도 후속 세션에서 풀림: Compose/Fusion/Vide/v1 4개 선례를 + 서브에이전트로 병렬 조사한 결과 전부 named parameter로 경계를 넘기는 + 패턴에 수렴한다는 게 확인됐고, "컴포넌트가 여러 루트를 반환한다"는 + 프레이밍 자체가 (a) Luau가 tail position 밖 다중 리턴을 지원 안 해서 + 불필요한 개념과 (b) 이미 있는 Slot 메커니즘을 섞은 것이었음이 드러나 + 정리됨 — 결론: 경계는 named parameter(`props.Modifier`/`props.Ref` + 가칭), "다중 루트"라는 별도 개념은 폐기, 여러 modifier를 하나로 합치는 + `Modifier.Merge`(가칭) 유틸 추가. `research/component-composition-plan.md` + → `base/component-composition-plan.md`로 승격 완료. +- **문서 전체 감사 및 정리** — `.claude/` 코퍼스 전체(약 15개 문서)를 + 서브에이전트로 감사해 여러 라운드에 걸쳐 쌓인 모순/중복/stale 마커를 + 대거 발견하고 수정(예: 이벤트 dot-access 확정 여부가 문서 내에서 서로 + 모순, 이미 해소된 질문이 "미해결"로 방치, 존재하지 않는 문서/섹션을 + 가리키는 끊긴 참조 다수, `TagService`/`CollectionService` 혼용 등). + `research/purity-and-effects-plan.md`도 내용이 이미 확정 상태라 `base/`로 + 승격. **이 CLAUDE.md 자체도 이번에 오래된 라운드별 인수인계 메모 3개를 + 이 요약 하나로 통합하며 정리함** — 라운드별 상세 히스토리가 필요하면 + git log와 각 `base/`/`research/` 문서 안의 라운드 표시(예: "2026-08-04 + 3차 라운드")를 참고할 것, 여기서 전부 반복하지 않음. + +**같은 날 로드맵 인수인계 라운드 — 설계 단계 마무리, 구현 준비 완료**: +- **quad-base 테스트 mock 방향 확정**: Vide 선례(`test/mock.luau`, ~300줄, + 순수 `luau` CLI, Studio 불필요) 그대로 채택, 스코프는 정적 디버깅 한정(Tween + 같은 동적 동작 제외), quad-roblox로 작성한 컴포넌트가 mock에서도 그대로 + 돌아가야 한다는 요구 없음(단순하게 감) — `architecture.md` "테스트 전략" + 절. 범용 렌더 디버깅 도구(Tween mock 포함)는 별개로 백로그. +- **구현 전 리스크 감사**: `.claude/base/` 전체 + 남은 `research/`를 + 서브에이전트로 감사해 "실제 Luau 접촉 없이 추론만으로 확정된 것" 3개 + (Store/State 반응형 코어, 디스패치 엔진, 컴포넌트 경계 modifier/Ref)를 + 식별 — 이것들은 M0 스파이크로 검증하기로 함(아래). 감사 중 `slot-plan.md`가 + 스스로 "정식 확정 안 됨"이라 표시해뒀던 "클래스가 슬롯을 받는 방법"(Named + Slot 없음)도 이번에 정식 확정, 대신 "여러 Slot이 형제로 섞일 때 순서 보장" + 이라는 새 하위 질문이 열림(다중 백엔드 관점, Roblox만이면 급하지 않음). + `State` 조합은 UB로 확정해 타입으로 막기로 함(`modifier-plan.md` + 7번), 디스패치 엔진의 일반적 무한루프는 방어 로직 없이 provider 버그로 + 간주하기로 확정(`bind-system-plan.md`). +- **루트 `ROADMAP.md` 신설** — M0(스켈레톤+기술검증 스파이크, "진짜" + 마일스톤 아님)부터 M11(Tween)까지 + 병행 가능 항목 + 백로그로 구성된 실행 + 계획, todo 체크박스 포함. 오늘은 문서 준비만 — **다음 세션이 M0부터 실제 + 시작**. + +용어 정리 제안 진행 중인 점은 위 "지금 할 일" 2번 참고. + diff --git a/.claude/session/2026-08-06-01-quad-debug-design.md b/.claude/session/2026-08-06-01-quad-debug-design.md new file mode 100644 index 0000000..f98d0aa --- /dev/null +++ b/.claude/session/2026-08-06-01-quad-debug-design.md @@ -0,0 +1,91 @@ + + + + +## 2026-08-06 세션 — quad-debug(런타임 디버깅 플러그인) 설계, 실측 검증까지 완료 + +팀원이 "실물 Frame에서 그걸 만든 코드 위치를 역추적하는 Studio 플러그인이 +있으면 좋겠다"는 피드백을 줬고, 사용자가 이걸 `quad-debug`/ +`quad-debug-roblox-plugin`으로 후순위 설계해두자고 판단해 시작된 세션. +착수는 여전히 "quad 개발이 상당 부분 끝난 뒤"로 못박혀 있음(구현 우선순위는 +안 바뀜) — 대신 base 설계(디스패치/Source/DI 생성자) 시점에 훅 확장 +지점만 고려해두면 나중이 훨씬 싸진다는 문제의식으로 지금 미리 설계만 +해둠. 전체 내용은 `research/debug-tooling-plan.md`, 요지만 요약: + +- **설계를 막던 유일한 기술적 불확실성이 실측으로 해소됨**: Roblox Studio + 플러그인과 Play 중인 게임(LocalScript)이 별도 Luau VM/스크립트 + 컨텍스트라 `BindableEvent`/`BindableFunction`이 그 경계를 실제로 + 넘는지가 문서만으로는 안 갈렸음(공식 문서는 언급 없음, DevForum엔 + 실패 사례도 있었음) — 사용자가 테스트용 플러그인/스크립트 + (`plugin-ignoreme.luau`/`game-ignoreme.luau`, 레포 루트, `*-ignoreme*` + 패턴이라 자동 gitignore)를 직접 Studio에서 돌려 **Fire/Connect, + Invoke/OnInvoke 왕복 둘 다 안정적으로 작동함을 확인**. 원리는 "Play + 진입 시 DataModel이 복제되는 게 아니라 script identity/보안 컨텍스트만 + 분리되고, Instance 자체(C++ userdata)는 어느 컨텍스트에서든 같은 + 참조를 가리킨다"는 것 — 사용자가 실측 도중 직접 정리한 설명. +- **채널은 확인됐지만 처음 구상(quad-debug-roblox가 `ReplicatedStorage`에 + Bindable을 자동 생성)은 기각** — 개발자가 의도 안 한 Instance를 게임 + 트리에 주입하는 부작용이 크다는 사용자 지적. 대신 quad 모듈 자신의 + Instance 트리 안에 두고 `CollectionService` 태그로 노출, 플러그인은 + `GetTagged`로 찾음(`GetDescendants` 전체 순회 불필요). +- Roblox Luau의 `debug` 라이브러리엔 `sethook`류가 없어(확인됨) 엔진이 + 공짜로 주는 동적 트레이싱 방법이 없음 — 대신 Fusion `src/External.luau`에 + 이미 있던 "no-op 업밸류를 나중에 실제 구현으로 통째로 교체" 패턴을 + 재사용하기로 함(quad가 이미 쓰는 "base는 인터페이스, 구현은 팩토리가 + 주입" 원칙과 같은 모양이라 새로 발명할 필요 없음). +- React DevTools 아키텍처도 서브에이전트로 조사 — 그대로 못 베끼는 것도 + 있지만(전역 훅 주입은 프로세스 경계 문제로 안 됨), **컴파일타임 소스 + 위치 주입**(Babel처럼 darklua로 흉내낼 후보)과 **얇은 스트림+on-demand + 상세조회** 원칙은 그대로 채택. +- UX 방향은 사용자가 여러 번 직접 정정: "존재하는 State 목록"이 아니라 + "무엇이 무엇에 연결됐는가" 그래프 중심, flash-on-update는 전체 상시 + 적용이 아니라 마운트/언마운트만 상시+개별 프로퍼티 변경은 현재 열어본 + Instance 한정, PropertyChangedSignal 기반 "외부 변경 감지"는 핵심 + 채널이 아니라 보조 신호일 뿐(어디서/왜 바뀌었는지가 quad-debug의 진짜 + 가치라 순수 관찰만으론 부족). **Element Inspector**(화면 클릭으로 UI + 요소 피킹)가 사용자가 실제로 가장 크게 느낀 pain point로 새로 부상 — + Roblox가 Play 중 라이브 UI 편집 도구를 꺼버려서 Explorer만으로 요소 + 찾기가 힘들다는 실사용 불만. +- 부수적으로 파생된 두 가지(quad-debug 범위 밖) 문서화 아이디어 — + UI 네이밍 컨벤션 문서, 스킬/쿨타임/재화 같은 게임 시스템에서 Store의 + 부작용 허용을 깔끔한 패턴으로 쓰는 법 문서 — 를 `research/ + documentation-plan.md`에 뼈대만 분리해서 남김(위 "지금 할 일" 4번). +- **이벤트 self 관습 확인 필요했던 항목 — 같은 날 후속 세션에서 해소됨.** + 아래 "2026-08-06 후속 세션" 절 참고. + +**같은 세션 후반, 별개 주제 두 개 추가**(quad-debug와 무관, 사용자가 +"적어는 뒀는데 안 줬는건가" 하며 새로 떠올린 것들): +- **Attribute 특수 키 타입 파라미터화** — `[Attribute<> "name"]` + 제네릭 스타일 vs `[BooleanAttribute "name"]` 타입별 정적 생성자 패밀리. + 기존 문서 어디에도 없던 신규 논의로 확인(`bind-system-plan.md` + "Attribute 특수 키" 절에 새로 추가) — 소견은 DI 인스턴스 생성 때 이미 + 쓴 "제네릭 하나 + 자주 쓰는 타입만 정적 지름길" 패턴 재사용, 확정은 아님. + Roblox Attribute가 이제 Instance 참조 타입도 지원해서 `ObjectValue` + 없이 Ref 용도로도 쓸 수 있다는 점도 확인 — quad-debug 논의의 "Value + 오브젝트 기각, Attribute 우선" 결정을 보강함. +- **UICorner/UIPadding/UIScale 인라인 편의 키** — 사용자가 v1에서 "Frame + 안에 인라인으로 넣기만 해도 CSS처럼 적용됐다"고 기억한 기능, 서브에이전트로 + v1 소스(`class.lua`) 조사해 실체 확인: `Corner`/`PaddingAll(Offset)`/ + `Scale` 3종(+ 별개 메커니즘인 `RoundSize`)이 실제로 있었음(리터럴 값 하나 + → 이름 붙은 UICorner/UIPadding/UIScale 자식을 찾거나 생성). `UIListLayout`/ + Grid/Flex 전용 숏핸드는 없었음 — 그건 이미 quad-v2에 있는 범용 + children-array 메커니즘으로 충분히 커버되므로 새로 설계할 것 없음. + **한 차례 "지금은 UICorner가 네이티브라 포팅 불필요"로 잘못 정리했다가 + 사용자가 재정정**: `RoundSize`(이미지 9-slice 라운드 트릭)만 UICorner + 없던 시절의 워크어라운드라 포팅 불필요고, `Corner`/`PaddingAll`/`Scale` + 자체는 "UIScale 등이 여전히 별도 Instance라 부모에 붙여야 하는 구조는 + 안 바뀌었다"는 이유로 **여전히 필요한 기능으로 재확정**. `research/ + ui-shorthand-plan.md`에 최종 정리 — 메커니즘은 기존 pluggable Handler로 + 그대로 커버(새 아키텍처 개념 불필요), 패키지 배치는 `quad-roblox` 코어에 + 직접 포함으로 확정(별도 `quad-roblox-util` 불필요 — "트윈도 하나로 묶어 + 코어에 넣은 선례처럼, 작고 opt-in 아닌 건 분리 안 한다"는 사용자 판단). +- **quad-debug 플러그인 UI 구조 확정** — Explorer에서 quad 내부 자동 + 생성물(예: 위 UICorner 숏핸드가 만든 것)을 직접 선택했을 때 플러그인 + 트리에 대응 노드가 없으면 부모로 대신 선택, 있으면(사용자가 직접 + bind한 경우 등, UB 아님) 정확히 그 노드 선택. 내부 자동 생성물은 + `_`/`QUAD_` 접두어로 네이밍(v1 `_quad_round`류 재사용, + `documentation-plan.md` 네이밍 컨벤션과 연결). 플러그인 UI는 세 + 상호작용면(자기 트리 뷰/리프 클릭→상세 패널/실제 Explorer 선택과 연동, + Explorer와 플러그인 트리는 별도 도킹 위젯)으로 구성된다는 것도 사용자 + 질문에 확인 응답 — `debug-tooling-plan.md` "핵심 설계 방향" 9번. + diff --git a/.claude/session/2026-08-06-02-event-self-rbvm-corpus.md b/.claude/session/2026-08-06-02-event-self-rbvm-corpus.md new file mode 100644 index 0000000..c677b0a --- /dev/null +++ b/.claude/session/2026-08-06-02-event-self-rbvm-corpus.md @@ -0,0 +1,142 @@ + + + + +## 2026-08-06 후속 세션 — 이벤트 self 관습 결정, rbvm GC 참고, 문서 코퍼스 정리 + +같은 날 이어진 세션에서 세 가지를 처리함. **다음 세션이 새로 알아야 할 것은 +없음** — 아래 전부 `base/`/`research/`/`question.md`에 실제로 반영 완료. + +**1. 이벤트 핸들러 self(Instance) 관습 — 채택하지 않기로 확정.** 위 절에서 +"확인 필요"로 남겨뒀던 것의 결론: v1의 `func(self or this, ...)` 관습은 +실존함을 확인했지만(`.claude/initreq/quad/src/event.lua` 82행, 튜토리얼 +문서화까지 있음), quad 재설계에서는 채택하지 않음. 근거 네 가지 — +(1) Ref가 이미 "생성 직후/마운트 후 Instance 접근"을 콜백으로 커버해서 +중복 채널이 됨, (2) self로 재바인드 가능한 thin wrapper를 준다면 Modifier의 +정적 flatten과 경쟁하는 두 번째 쓰기 경로가 생겨 KV 핸들러가 매번 +"flatten된 값이냐 wrapper냐"를 분기해야 하는 오버엔지니어링, (3) +quad-debug가 추적하는 반응형 그래프 밖의 mutate 경로가 공식 API로 +생기는 셈이라 `purity-and-effects-plan.md`의 이식성 원칙과 충돌, (4) +self를 넘기려면 원본 콜백을 클로저로 한 번 더 감싸야 해서 불필요한 할당 +비용 — quad는 어차피 라이프사이클 끝까지 바인딩을 들고 있어 Destroy 시 +Connection도 자연히 정리되므로(`lifecycle-pattern.md`, GC-native) 감쌀 +이유가 없음. 상세 결정문은 `base/bind-system-plan.md`의 "이벤트 핸들러는 +self(Instance)를 받지 않는다" 절. `research/debug-tooling-plan.md`/ +`.claude/question.md`의 관련 항목은 "해소됨"으로 갱신 완료, 이 결정을 +설명하는 문서화 숙제("왜 thin wrapper를 안 주는가", "권장 이벤트 핸들링 +패턴")는 `research/documentation-plan.md` 3번으로 신설(다른 두 항목과 +동일하게 아직 백로그 뼈대만). + +**2. rbvm GC 패턴 — "실물 검증됨" 근거 보강.** 사용자가 "GC 처리를 봐야 +한다면 rbvm을 확인하라, 실제 프로덕션에서 잘 돌아가는 걸 직접 확인한 +모듈"이라고 언급 — 실제로 rbvm의 GC 패턴(weak table 4종, `Instance. +Destroying` 기반 gcHold 클로저, 네임스페이스 Dispose 훅 등)은 이미 +`base/lifecycle-pattern.md`에 파일:라인까지 인용하며 상세 반영돼 있었지만 +"사용자가 직접 실행해서 확인했다"는 신뢰도 근거는 빠져있어서 그 문단을 +추가함(사람이 짠 코드라 100% 무결 보장은 아님 — 이미 발견된 버그 2건도 +근거로 같이 인용, 규범이 아니라 참고용 비교 대상이라는 톤 유지). + +**3. `.claude/` 코퍼스 전체 정리 패스.** 이전 세션들에서 쌓인 stale +참조/모순을 서브에이전트로 전수 감사 후 수정 — `modifier-plan.md`/ +`architecture.md`의 `research/component-composition-plan.md` 참조를 +승격된 `base/` 경로로 갱신, `comparison-fusion-vide.md`의 낡은 "Vide식 +암묵적 추적 vs Fusion식 명시적 축, quad는 미정" 서술을 실제 확정 사실 +(`bind-system-plan.md`의 `:With`+`:Compute` 명시적 모델 채택)로 정정, +`tween-plan.md`의 끊긴 절 참조 수정, `documentation-plan.md`의 인용 +오류 정정. `module-lifecycle-plan.md`가 스스로 "question.md에도 취합" +표시해뒀지만 누락돼 있던 "프로바이더" 이름(provider/processor/plug) +미정 항목도 `question.md`에 추가함. 여러 문서에 흩어져 있던 진짜 열린 +설계 질문들(Slot 형제 순서 보장, Attribute 타입 파라미터화, UI shorthand +이름 등)은 전부 `.claude/question.md`에 이미 반영되어 있음을 재확인만 +하고 임의로 결정하지 않음 — **이 파일이 여전히 "지금 열려있는 것"의 +단일 소스.** + +**4. Store `:Emit`, `:Compute`의 `previous` 인자, `state:Observer(fn)`, +Ref 일반화 — 네 가지 다 확정, 실제 base 문서에 반영 완료.** 같은 세션에서 +더 이어진 Store/Ref 설계 논의, 전부 `base/store-semantics.md`와 +`base/bind-system-plan.md`에 반영됨: +- **`Store:Emit(key)`** — Source 원천에 한해서만 허용(중간/파생 State엔 + 없음). 존재 이유는 clone 불가능한 userdata/엔진 객체가 우선(편의성은 + 부차적). `Get()`이 라이브 레퍼런스를 주므로 캐시해서 비교/diff하면 + 안 된다는 캐비엇 명시. Modifier는 정적 flatten이라 Store/State 경로에 + 아예 안 걸치므로 Emit과 충돌할 지점 자체가 없음(따름정리: + `Store`의 `T`는 Modifier가 될 수 없음) — `store-semantics.md`. +- **`:Compute(fn)`의 선택적 두 번째 인자 `previous`** — Compute 결과 + 자체가 무거운 userdata인 경우(예: 큰 locale 테이블 → Roblox + `LocalizationTable` 변환) 재생성 대신 이전 결과를 재사용/patch하는 + 용도, opt-in. `previous`는 "정확히 한 단계 전"이 보장 안 되므로 반드시 + full diff로 다뤄야 함(React reconciler와 같은 모양). **핵심 캐비엇**: + 이 패턴은 결과 State가 계속 능동적으로 관측(정상 prop 바인딩 또는 + `state:Observer(fn)`+명시적 `Get()`)되지 않으면 mutate 로직 자체가 + 다시 실행 안 되어 조용히 영구 정지함 — `bind-system-plan.md`. +- **`state:Observer(fn)`** — 무효화 신호만 주고 값은 안 줌, `fn` 안에서 + 명시적으로 `Get()` 해야 실제 값을 얻음(기존 "emit은 저렴한 무효화 + 신호" 원칙 재사용). 반환값 자체가 `CreatedRef`처럼 children 배열에 + 바로 놓는 leaf 값(별도 `ObserverHolder` 래퍼 불필요, 사용자가 직접 + 단순화) — 그 leaf가 살아있는 동안만 구독 유지, `canExecute`로 게이팅. + `fn` 생략 시 "이 State를 그냥 계속 능동 관측 상태로 유지"하는 유틸로 + 씀(위 `previous` 캐비엇의 해결 도구). 구현은 값 내부가 아니라 외부 + weak table로 살아있는 Observer를 추적하는 방식 권장(rbvm + `getNamespaceOf`류 선례) — `bind-system-plan.md`. +- **Ref 일반화** — "quad가 만든 instance 전용"에서 "아무 사용자 값이나 + 담는 범용 값 박스"로 확장(object-ref/function-ref 안 나눔, React + `useRef`가 선례). `.Value` + `:Wait()`(coroutine 컨텍스트용) + 콜백 + 등록(복수 허용, 이미 채워져 있으면 즉시 1회 호출) — 이걸로 "코루틴 + 기반 대기 지원 미정"이던 항목 해소. `CreatedRef`는 이 위에 얹힌 특수 + 편의 패턴으로 재정리, 상충 없음. **one-shot 여부도 해소됨 — 반복 + 재설정 가능으로 확정**(React의 자식 재생성 시 ref 재사용 패턴이 선례, + 라벨 컨테이너 재사용 예시로 확인). 콜백은 발화 후에도 안 소진되고 + 매 `:Set()`마다 다시 불림 — 소진되는 건 `:Wait()`의 개별 대기자뿐. + **Ref는 의도적으로 lazy가 아니고 `:Compute` 파생도 지원 안 함** — + State와의 이 차이가 중요(예전에 Store가 Ref 역할도 겸했다가 lazy + 모델과 섞여서 안 좋았던 경험에서 나온 의도적 분리). Ref 정의 자체가 + 넓어졌으니 용어 정리 때 이름도 같이 재검토 대상. `question.md`의 + 관련 항목은 해소됨으로 갱신. + +**5. Observer 이름 확정, Ref/Source/Store 생성자 스타일, "독립 프리미티브 vs +파생 데이터" 원칙, Modifier 세부 마무리 — 전부 확정, base 문서 반영 완료.** +- `Observer`로 확정(`ObserverHandle` 아님) — `:Connect()`→`Connection`과 + 같은 기존 명명 관례. PA님 코드의 동명 클래스와는 무관, 각주로 구분. +- **생성자 스타일**: `Source(default)`/`Ref(default)`/`Store({defaults})` + — Kotlin Compose식 "타입 이름 자체가 팩토리 함수". Ref만 예외였던 이유 + 없었음(단순 명세 공백). +- **일반 원칙 신설**: 독립 존재 가능한 프리미티브(Source/Ref/Store/ + Modifier, `Type(args)` 자유 함수 생성자) vs 원천에 종속된 파생 데이터 + (State/Observer, 원천에 대한 메소드로만 얻어짐) — `state:Observer(fn)`가 + 메소드고 자유 함수가 없는 더 근본적인 이유로 연결(`store-semantics.md`). +- **Modifier 마무리**: (a) Getter를 아예 안 만들기로 확정 — + `:FontSize(function(old)->new)`가 유일했던 use case를 인라인으로 커버. + (b) `old`는 항상 "현재 저장된 그대로"(plain이면 raw, State면 State + 핸들) 넘김 — `:Compute`의 self와 같은 결. (c) `func(state)->state`라는 + 세 번째 인자 모양은 불필요(함수 합성 + State 직접 대입으로 이미 커버). + (d) Modifier는 핸들러 계층(Ref/Slot 등)을 몰라도 됨 — 순수 데이터 + merge 레이어라 UB로 흘려보내도 문제없음. (e) **런타임 구현은 base에 + 제네릭 `__index` 하나면 충분** — `mod:FontSize(...)`가 `__index(self, + "FontSize")`로 잡히므로 클래스별 런타임 코드 불필요, FrameModifier류 + 타입 생성기는 순전히 정적 타입 체크만을 위한 것. (f) 이벤트도 + store-bind 가능하도록 확정 — 기존 재실행 래핑 재사용, `false`를 + disconnect 센티널로 씀(`nil`은 테이블에서 사라져서 부적합) — + `bind-system-plan.md`. Modifier가 이벤트 키를 담아도 되는지는 (d)로 + 자동 해소(Modifier가 애초에 키 종류를 구분 안 하므로). + +**6. 이벤트 store-bind는 부차적 옵션으로 재조정, Observer의 `:Subscribe`/ +`:Unsubscribe` 추가 — 둘 다 확정, 반영 완료.** +- 이벤트 store-bind(5번 (f))를 다시 검토 — "구현이 쉽다"가 "구현할 + 가치가 있다"를 보장 안 함을 재확인. 저빈도 UI 이벤트의 조건부 처리는 + "핸들러 하나 계속 연결 + 내부에서 `store.enabled:Get()` 분기"가 이미 + Connect/Disconnect 없이 더 싸고 표준적이라 **이걸 기본 권장 패턴으로 + 확정**. store-bind는 고빈도 신호(Heartbeat 등)나 로직 자체가 바뀌는 + 드문 케이스를 위한 부차적 옵션으로 격하(메커니즘 자체는 유지 — 일관성 + 위해 예외로 뺄 근거는 약함). 자주 재계산되는 State에 물리면 Connect/ + Disconnect churn이 숨은 비용이 된다는 캐비엇도 추가. +- **Observer의 `:Subscribe()`/`:Unsubscribe()`** — children 배열에 안 + 붙는 "전역/독립" Observer(디버깅용으로 Store에 직접 걸어 print하는 + 흔한 패턴, `RunService:IsStudio()` 가드 + BooleanValue 토글)를 위한 + 명시적 라이프사이클 경로. 이건 새 설계가 아니라 PA님 코드 교차검증 + 때 이미 예고해둔 확장 지점("GC만으로 부족하면 명시적 dispose 경로 + 추가 가능")을 실제로 채운 것. liveness 체크는 `self.Subscribed` 필드 + 우선, `self.Connection.Connected` 폴백(필드 접근이 weak table 조회보다 + 쌈). 내부 레지스트리는 자동 케이스의 weak table과 별개로 강참조 + (weak면 "살려둔다"는 목적이 무의미해짐). 둘 다 idempotent, `:Unsubscribe()`는 + 자동 케이스 조기 해제에도 재사용. + diff --git a/.claude/session/2026-08-06-03-docsite-source-state.md b/.claude/session/2026-08-06-03-docsite-source-state.md new file mode 100644 index 0000000..70200fe --- /dev/null +++ b/.claude/session/2026-08-06-03-docsite-source-state.md @@ -0,0 +1,84 @@ + + + + +## 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/.claude/session/2026-08-06-04-pre-implementation-audit.md b/.claude/session/2026-08-06-04-pre-implementation-audit.md new file mode 100644 index 0000000..73777dd --- /dev/null +++ b/.claude/session/2026-08-06-04-pre-implementation-audit.md @@ -0,0 +1,67 @@ + + + + +## 2026-08-06 네 번째 세션 — M0 착수 직전 크리티컬 감사, `research/pre-implementation-audit.md` 신설 + +사용자 요청: "실 개발 시 모호하여 인터럽트될 수 있는 부분, 나중에 결정되면 +치명적일 것 같은 것, 지금 구조가 오버엔지니어링일 수 있어 보이며 더 나은 +대안이 있는 것"을 찾아 정리해달라는 요청. `.claude/base/` 전체(architecture/ +bind-system/store-semantics/module-lifecycle/component-composition/ +modifier/purity-and-effects/slot/lifecycle-pattern/quad-v1-architecture)와 +근접 `research/`(existing-instance-bind/tween/ui-shorthand) + `ROADMAP.md`를 +4개 클러스터로 나눠 서브에이전트 4개를 병렬로 돌려 "모호성/지연결정리스크/ +단순화후보" 세 렌즈로 재감사, 결과를 `research/pre-implementation-audit.md` +로 종합. `.claude/question.md`엔 이미 취합된 것(용어 재검토, M0 스파이크 +항목 자체 등)과 겹치지 않는 새 발견만 반영. + +**작업 도중 발견한 부수 이슈**: 워크트리 생성 시점과 main 체크아웃의 +미커밋 변경사항(세 번째 세션 결과물)이 어긋나 있었음 — 워크트리는 커밋 +시점 기준으로 fork되므로 아직 커밋 안 된 변경은 안 딸려옴. 사용자가 중간에 +main에 커밋을 완료해줘서(`4b839b0`) 워크트리를 새로 만들어 재동기화함 — +**앞으로 워크트리에서 최신 설계를 감사/참조해야 하는 작업을 시작하기 전엔, +main에 미커밋 변경이 있는지(`git status`) 먼저 확인하고 필요하면 커밋을 +요청하거나 파일을 직접 동기화할 것.** + +**핵심 발견 요약** (전체 25개 항목은 `pre-implementation-audit.md` 참고, +우선순위1만 발췌): + +- **Tween.luau가 문서 전체에서 "범용 store-bind 캐치올 핸들러"의 유일한 + 구체 예시로 서술됨** — 애니메이션 없는 일반 반응형 프로퍼티 바인딩이 + 실제로 Tween 파일을 거쳐가는지, 별도 범용 핸들러가 필요한지 확정 안 됨. + 가장 구조적인 발견 — 직접 `bind-system-plan.md` 67-79행을 재확인해 + agent 발견을 검증함. +- `props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴의 + nil-hole 함정(caller가 안 넘기면 `{nil, ref, child}`에서 뒤 항목까지 + 무시될 수 있음)에 그대로 노출 — M0 스파이크 코드에 이 케이스를 반드시 + 포함시켜야 함. +- `canExecute`/`Connected`의 실제 구현 방식이 미확정인 채 코어 전역 + (Slot/Observer/store-bind retract)에 이미 재사용 확정돼 있음. +- `LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그 인터 + 페이스를 전제로 서술돼 있음 — 로드맵 순서 역전, `ROADMAP.md` 조정 필요. +- retract 시 "이전에 실제로 매치됐던 핸들러" 추적 책임, 우선순위 스캔 + 동률/매치실패 처리, provider 미주입 상태 dispatch 호출 시 동작 — + 전부 M2(Dispatch 엔진) 착수 전 한 번에 결정하면 효율적인 것들. +- Slot의 `add`/`remove`/`clear` CRUD 의미론 자체가 정의 안 돼 있음, + "재마운트 시 throw"도 추적 대상(개별 element vs Slot 컨테이너)이 + 뭉뚱그려 서술됨 — 둘 다 M6 착수 전 확정 필요. + +**단순화 후보로 지적된 것 중 사용자 판단 필요**: `:Compute(fn)`의 +`previous` 두 번째 인자 — quad의 "함수 자체가 재호출되는" 모델상 클로저 +업밸류로 이미 되는 걸 별도 API 표면으로 만든 것일 수 있음(근거 불명). + +**문서모순으로 남겨둔 것**: `State`는 "UB, 가능하면 타입으로 +차단"인데 Ref/Slot이 Modifier 필드에 들어가는 건 "UB, 방어 로직 없음" — +같은 문서(`modifier-plan.md`) 안에서 정반대 원칙이 근거 설명 없이 나란히 +적용됨. 판단이 필요해 고치지 않고 감사 문서에만 남김. + +**부수적으로 직접 고친 stale 문서(판단 불필요한 순수 동기화)**: `base/ +architecture.md` 소스트리 주석 두 곳 — `Store.luau`가 여전히 옛 `__newindex` +모델을 언급, `Ref.luau`가 여전히 "CreatedRef 메커니즘 자체"로만 서술(Ref +일반화 결정 반영 안 됨). 온톨로지 요약 절 stale은 같은 세션 도중 커밋 +`4b839b0`에서 이미 독립적으로 고쳐져 있었음을 확인 — 재작업 없이 스킵. + +**다음 세션이 할 일**: M0 착수 전에 `pre-implementation-audit.md` 우선순위1 +항목(특히 위 6개)부터 확인 — "지금 할 일" 1번 참고. `.claude/question.md` +2번에 사용자 판단이 필요한 항목 요약이 반영돼 있음. + diff --git a/.claude/session/2026-08-07-01-with-new-node.md b/.claude/session/2026-08-07-01-with-new-node.md new file mode 100644 index 0000000..8dd6e72 --- /dev/null +++ b/.claude/session/2026-08-07-01-with-new-node.md @@ -0,0 +1,39 @@ + + + + +## 2026-08-07 세션 — `:With`도 새 State 노드로 확정 + +사용자 질문에서 시작: `:With(...)`가 문서상 가변인자 표기이긴 한데, 체이닝 +(`:With(a):With(b):With(c)`)할 때마다 실제로 새 State 노드를 만드는 게 +맞는지, 아니면 값 없이 의존성 목록만 clone-then-append로 누적하는 가벼운 +빌더로 만들어 "노드가 With 호출마다 하나씩 증가하는" 낭비를 피해야 하는지가 +불명확했음. 처음엔 "빌더" 대안(진짜 State가 아닌 clone 기반 누적 객체)을 +검토했으나, 사용자가 두 가지 반례를 직접 제시하며 기각함: + +1. **디버그 그래프가 꼬임** — `quad-debug`의 핵심 UX가 "무엇이 무엇에 + 연결됐는가" 그래프인데, With/Compute가 전부 실제 노드면 코드 호출 + 체인이 그래프 엣지와 1:1 대응되지만, 빌더로 만들면 그래프 툴이 가상의 + 분기 지점을 따로 합성해야 함. +2. **clone 기반 구현이 Compute 노드 위에서 실제로 깨짐** — `c = + a:Compute(f)` 뒤에 `w = c:With(b)`를 clone으로 구현하면 `c`의 캐시 + 슬롯까지 그대로 복사되어 `w`가 `c`와 별개의 독립 캐시를 갖게 되고, + `c`/`w`가 각자 관측되면 `f`가 두 번 따로 실행됨 — `bind-system-plan.md`가 + 이미 기각해둔 "State 체인 플래튼"과 정확히 같은 실패 모드. + +**결정**: `:With`는 호출마다 self+인자들을 레퍼런스로 구독하는 새 State +노드를 만든다(clone 아님, 계산 없는 pass-through 노드). 원래 문제 제기 +(노드 남발)는 노드를 없애는 대신 `:With(...)`를 진짜 가변인자로 만들어 +해소 — `:With(a, b, c)` 한 번으로 노드 1개(구독 3개)를 만들 수 있고, +디버그 그래프도 이쪽이 더 단순해 권장 관례로 삼음. 체이닝 스타일도 여전히 +가능하나 그건 저렴한 노드가 늘어나는 것뿐이라 문제 삼을 비용이 아님. +`base/bind-system-plan.md`의 "왜 State 체인을 Modifier처럼 플래튼하지 +않는가" 절 바로 뒤에 새 소절로 반영 완료. 다른 문서(`question.md`/ +`ROADMAP.md`/`modifier-plan.md`)엔 이 결정과 모순되거나 갱신이 필요한 +서술 없음을 확인함(감사 완료) — `modifier-plan.md`가 이미 "State가 +`:With`/`:Compute`마다 새 노드를 할당"이라고 서술해뒀던 것과도 정합적. + +다음 세션이 할 일은 안 바뀜(위 2026-08-06 네 번째 세션 절 참고) — 이 +결정은 M0 스파이크(Store/State propagation 검증)가 실제로 짜볼 때 +참고할 구체 스펙이 하나 더 생긴 것뿐. + diff --git a/.claude/session/2026-08-07-02-modifier-apply.md b/.claude/session/2026-08-07-02-modifier-apply.md new file mode 100644 index 0000000..4e7c257 --- /dev/null +++ b/.claude/session/2026-08-07-02-modifier-apply.md @@ -0,0 +1,22 @@ + + + + +## 2026-08-07 두 번째 세션 — Modifier `:Apply(factory)` 팩토리 체이닝 추가 + +사용자 제안: `Boldify(mod) -> mod`처럼 어떤 modifier든 받아 적절히 변형해 +돌려주는 재사용 가능한 "팩토리 함수"(커링 지원, `Boldify(10)(mod) -> mod`)를 +`mod:Apply(Boldify(10)):Apply(Italicify)`처럼 기존 필드 setter 체이닝과 +같은 fluent 문법으로 끼워 넣을 수 있게 하자는 것 — Jetpack Compose의 커스텀 +`Modifier` 확장 함수 패턴과 같은 효용(모듈화된 스타일 프리셋 재사용)을 +Luau엔 확장 함수 문법이 없으니 콤비네이터로 흉내낸 아이디어. 채택 확정, +`base/modifier-plan.md` 8번 절에 반영 — `:Apply`는 `function(self, factory) +return factory(self) end`이 전부인 얇은 sugar(팩토리 자신이 이미 clone된 +새 Modifier를 반환하므로 Apply 자체는 clone 불필요), 기존 3번(immutable +clone 체이닝)/4번(제네릭 `__index`) 결정 위에 그대로 얹힘. 구현 시 주의점 +하나만 새로 생김: `Apply`는 제네릭 `__index`가 필드 setter를 즉석 합성하기 +전에 먼저 확인해야 하는 고정 메소드 이름이라, **Modifier 필드 이름으로는 +예약됨**(실 스타일 프로퍼티와 겹칠 일은 거의 없어 보이나 문서화 필요). +`ROADMAP.md` M7에 체크박스 추가 완료. 다음 세션이 새로 알아야 할 건 없음 — +M7 착수 시 `modifier-plan.md` 8번 참고하면 됨. + diff --git a/.claude/session/2026-08-07-03-preref.md b/.claude/session/2026-08-07-03-preref.md new file mode 100644 index 0000000..8848838 --- /dev/null +++ b/.claude/session/2026-08-07-03-preref.md @@ -0,0 +1,61 @@ + + + + +## 2026-08-07 세 번째 세션 — Ref의 KV 핸들러 처리 vs phase 타이밍, `PreRef` 신설 + +**출발점**: Ref가 Modifier처럼 밖에서 처리되는 게 아니라 KV 핸들러 +(`process(inst,k,v)`)로 처리된다면, "생성 직후"/"자식 마운트 후" 두 +콜백 타이밍(특히 self(Instance)를 안 주는 이벤트가 Ref로 self를 얻는 +경우)을 단순 for-loop 디스패치만으로 어떻게 표현하는지가 출발 질문 — +길게 이어진 단일 스레드라 아래 요약만 읽으면 됨, 상세 근거는 각 base +문서에 이미 반영됨. + +**핵심 결론(전부 `base/bind-system-plan.md`에 반영 완료)**: +- **base 디스패치 드라이버는 props 순회를 "배열 파트(children/Ref) 먼저, + 해시 파트(프로퍼티/이벤트) 나중"으로 명시적으로 두 패스 계약화**한다 + — Luau 테이블이 실제로 이렇게 순회되는 걸 사용자가 직접 확인했지만, + 그 우연한 동작에 기대지 않고 base가 스스로 이 순서를 보장(다른 + 백엔드가 다른 자료구조를 쓸 수 있어서). M0 스파이크 검증 항목에 추가. +- **`CreatedRef`의 `{phase="created"|"mounted"}` 옵션은 폐기.** 두 패스 + 계약 덕에 "자식 마운트 전/후"는 그냥 배열 안에서 Ref를 다른 children + 보다 앞/뒤에 놓는 것만으로 공짜로 표현됨 — 옵션 문법 자체가 불필요. +- **`PreRef` 신설** — "프로퍼티/이벤트 세팅보다도 먼저"(Roblox의 + `ChildAdded`/`DescendantAdded`/`Changed`류가 setup 도중 동기 발화할 + 수 있어서 self-ref가 이벤트보다 먼저 채워져야 하는 케이스)만 담당하는 + 별도 nominal 타입. `Ref`를 그대로 재사용(런타임 중복 없음)하되 + Modifier 필드 값·Source/Store 값으로는 타입으로 아예 못 들어가게 + 막고, children 배열 안에서도 위치 무관하게 항상 최우선(호이스팅) — + base 드라이버가 두 패스 루프 앞에 `PreRef`만 골라 fire하는 좁은 + pre-pass를 하나 더 둠. +- **일반 `Ref`는 Modifier/Store 어디든 계속 자유롭게 들어감** — Store를 + 통해 나중에 도착하는 Ref는 그냥 도착한 순간 처리, 별도 phase 개념 불필요. +- **`:Wait()`는 PreRef에도 그대로 유효** — fire 자체는 동기적이지만 + 호출부가 `task.spawn`이 아니라 순수 `coroutine`일 수 있어 실제 + yield-resume이 필요한 경우가 있음. "채워졌는지 먼저 확인, 없으면 + `:Wait()`" 방어 관용구를 문서화 대상으로 명시. +- **콜백/대기자 실행 구현 디테일 추가**: 같은 배열 하나를 한 번의 + 일반화 `for`로 순회하며 `type(v)=="thread"`면 `coroutine.resume`+ + 슬롯 nil 처리(1회성), 함수면 그냥 호출(유지) — 새 등록은 `table.insert` + 로 끝, 성긴 배열이어도 압축 불필요. **[정정, 2026-08-07 열 번째 세션]** + "슬롯 nil 처리"는 틀림 — 사용자가 Luau REPL로 반례 제시, 실제로는 + `None`으로 소진해야 함(`#t`/`table.insert` 안전성 문제). `base/ + bind-system-plan.md` "왜 `nil`이 아니라 `None`인가" 절이 최신. +- v1의 `OnCreated` 특수 DI 키는 이식 안 함 — `Ref():Callback(fn)`으로 + 완전 대체. + +**역전된 이전 서술은 archive로 이동**: `CreatedRef`의 `phase` 옵션과 +"Ref는 특수 처리 없는 평범한 참가자"라는 원래 서술은 +`archive/ref-phase-option-reversed.md`로 옮기고 원 위치엔 짧은 포인터만 +남김(컨텍스트 비대화 방지 목적, `archive/store-source-proxy-reversed.md`와 +같은 패턴). `architecture.md` 소스트리 주석/`question.md`(PreRef를 +용어 재검토 대상에 추가)/`research/documentation-content-map.md`(stale +`{phase=...}` 예시 갱신)도 같이 동기화함. + +**아직 미해결, 다음 세션 주제로 예고됨**: `{ Override = nil, mod }`처럼 +인라인 키로 modifier가 주는 값을 명시적으로 "지우고" 싶어도 Lua +테이블 리터럴의 `키 = nil`은 키가 아예 없는 것과 구별이 안 돼서 안 +풀리는 문제 — `false`를 이벤트 disconnect 센티널로 쓴 선례처럼 `None` +(가칭) 프리미티브를 도입하는 방향만 `base/modifier-plan.md` "2-1"절에 +짧게 메모해두고 상세 설계는 다음 세션으로 미룸. + diff --git a/.claude/session/2026-08-07-04-corpus-reorg.md b/.claude/session/2026-08-07-04-corpus-reorg.md new file mode 100644 index 0000000..8b0838f --- /dev/null +++ b/.claude/session/2026-08-07-04-corpus-reorg.md @@ -0,0 +1,63 @@ + + + + +## 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/blocker-plan.md`/`base/effect-plan.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`엔 아직 실제로 열려있는 + 것(키 기반 동적 컬렉션 재조정) 하나만 남김. **[같은 날 바로 정정]** + 처음엔 Blocker/Effect를 `base/additional-primitives.md` 한 파일로 + 합쳐 승격했으나, 사용자가 "State 볼 때 Effect까지 볼 필요는 없다, + 기존 프리미티브당 1파일 컨벤션(`modifier-plan.md`/`slot-plan.md`류)에 + 맞지 않는다"고 지적해 바로 두 파일로 재분리함 — Blocker는 + Store/State와 밀접해 교차 참조가 필요하지만 Effect는 완전히 독립된 + 요소라 애초에 같은 파일일 이유가 없었음. +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(...)` 포지셔널 생성자가 읽기 어렵다는 문제의식, + 소견은 편의 필드 쪽이지만 확정 아님, 나중 논의 대상으로만 남김. + diff --git a/.claude/session/2026-08-07-05-override-peek-isstate.md b/.claude/session/2026-08-07-05-override-peek-isstate.md new file mode 100644 index 0000000..fa7f85c --- /dev/null +++ b/.claude/session/2026-08-07-05-override-peek-isstate.md @@ -0,0 +1,126 @@ + + + + +## 2026-08-07 다섯 번째 세션 — Modifier 결합(`Override`)/읽기 접근자(`Peek`)/`isState` 확정, FuncSource 기각 사유 문서화 + +**출발점**: `:Apply`(4번째 세션 신설)처럼 Modifier에 더 있으면 좋을 게 +있는지 사용자가 제기 — `Merge`류 결합 유틸의 우선순위 문제, 그리고 +Modifier 자신이 자기 필드 값을 못 읽는 게 애매하다는 지적(예: +`Boldify`가 폰트별 굵기 보정을 하려면 현재 `Font` 필드를 읽어야 함). +같은 스레드에서 "Source가 항상 정해진 값만 담아야 하는 이유가 확정된 +건지, FuncSource(람다로 계산+self-emit하는 Source) 같은 건 왜 없는지"도 +같이 물어옴. + +**핵심 결론(전부 base 문서에 반영 완료)**: +- **`Modifier.Override(mod1, mod2, ...)`** — `component-composition-plan.md` + 3번 절에 2026-08-04부터 가칭 `Merge`로 이미 확정돼 있던 결합 유틸의 + 실제 동작을 확정하고 이름을 `Override`로 개명(중립적 "합침"이 아니라 + 명시적 "덮어쓰기"라 이름이 의미를 정직하게 반영해야 함). 뒤 인자가 + 필드 단위로 이김(기존 배열 flatten 규칙 재사용), 구현은 단순 필드별 + raw 교체 — setter가 이미 호출 시점에 함수/State를 즉시 처리해 저장하므로 + Modifier 필드는 항상 baked 값이라 특별한 분기 불필요. "baked 값 교체는 + 거기서 파생된 다른 필드에 소급 반영 안 됨"(Boldify가 FontWeight를 계산해 + 둔 뒤 Font가 Override로 바뀌어도 FontWeight는 예전 값 그대로)과 순서 + 의존성(`A:Override(B)` ≠ `B:Override(A)`) 둘 다 문서 경고 대상으로 확정. + **`Apply`로 전부 대체해 `Override`를 없애는 방안도 검토했으나 기각** — + 컴포넌트 경계(`props.Modifier`는 단일 named parameter라 배열 flatten이 + 안 닿음)라는 이미 확정된 실사용 니즈를 `Apply`만으로는 못 풀어서. + `base/modifier-plan.md` 9번 절. +- **`:Peek<>(key): T|State|nil`** — Modifier 필드를 확정(pull+recompute) + 하지 않고 raw 그대로 읽는 접근자. `Get`이 아니라 `Peek`인 이유는 이 + 프로젝트에서 `State:Get()`이 이미 "확정한다"는 의미로 굳어져 있어서 — + Modifier의 읽기는 정반대(State면 State 핸들 그대로) 동작이라 같은 + 동사를 못 씀. 반환 타입을 `T`로 자동 확정하지 않고 union 그대로 + 노출하는 이유는 4-1번 절 함수형 setter의 `old` 인자와 같은 원칙("현재 + 저장된 그대로 넘김") 재사용 — 자동 확정하면 타입에 안 드러나는 채로 + 반응성이 조용히 끊김. `.RealValue` 같은 별도 인덱싱 표면은 기각(이미 + `__index`가 setter 합성용으로 예약돼 있어 표면이 겹침). +- **`isState(x): boolean`** — `Peek`의 raw union을 분기하려면 필요. + Source가 State를 구조적으로 만족하므로 이거 하나로 Source도 같이 + 잡힘(`isSource` 불필요). duck-typing 대신 weak-key 레지스트리로 구현 + (rbvm 네임스페이스 추적과 같은 패턴 재사용) — `Peek`가 돌려주는 `T`가 + 임의의 테이블/userdata일 수 있어 duck-typing은 false positive나 일부 + Roblox userdata의 인덱싱 에러(pcall 필요)로 이어질 위험이 있음. 이 + 판별 로직 자체는 새 개념이 아니라 4-1번 setter가 이미 내부적으로 + 해야 했던 "필드가 State냐 plain이냐" 판별을 public 유틸로 승격한 것. + `base/bind-system-plan.md`의 `isState` 절. +- **FuncSource(값이 람다로 계산되고 self-emit하는 Source) 기각** — 사용자가 + 스스로 기각 논리를 제시했고("이미 Compute가 커버함"), 검증 결과 이미 + 확정된 두 원칙에서 그대로 연역됨: (1) Source는 "시작점"이라 다른 + 반응형 값에 자동 연결 안 됨(2026-08-04 6차 라운드, "Store가 Store를 + 담지 않는다" 확정 때 나온 원칙) — FuncSource는 다른 반응형 값에 종속된 + 계산이면서 겉으로는 origin인 척하는 것이라 이 원칙과 직접 충돌. + (2) `:With`가 clone 빌더가 아니라 진짜 노드여야 하는 이유(2026-08-07 + 세 번째 세션)가 "의존성이 구조적으로 안 보이면 디버그 그래프가 + 깨진다"였는데, FuncSource의 람다가 클로저로 캡쳐한 의존성은 정확히 + 그 문제를 재현함. 실제로 커버 안 되는 유스케이스도 없음 — "다른 + 반응형 값에서 계산"은 `Compute`, "clone 불가능한 값을 밖에서 바꾸고 + 알림"은 원천 Source+`Emit`으로 이미 전부 커버됨. 새 결정이 아니라 + 기존 확정 사항의 논리적 귀결이라 별도 base 절 신설 없이 여기 세션 + 요약으로만 기록(quadnomicon 소재로 재사용 가능하도록). + +**같은 세션 바로 후속 — 문서화 톤 보강(사용자 강조)**: `Override`는 범용 +조합 도구가 아니라 `Frame{mod1, mod2}`의 컴포넌트 경계판(단일 named +parameter 슬롯에 독립적으로 만들어진 값 두 개 이상을 넣어야 하는 특수 +상황)으로 좁게 문서화할 것 — "특정 modifier를 계속 바꿔나간다"는 요구는 +항상 `Apply` + 커링/일급 함수 전달을 기본 관용구로 유도. `Apply` 자체도 +`factory(self)` 호출 sugar 그 이상이 아니라는 걸 명시 — `factory`가 +`Peek`한 값이 기대와 다르면 `error`를 던지든 뭘 하든 전부 `factory` +저작자 책임, `Apply`가 검증/보장을 대신 해준다고 오해하면 안 됨. 둘 다 +`base/modifier-plan.md` 8/9번 절에 반영 완료. + +**같은 세션 두 번째 후속 — `Apply` vs `Override` 성능 기준 확정.** +"무거운 Modifier를 대량 생성할 때 `Apply`의 clone 비용이 누적되지 +않냐"는 우려에서 두 방안 검토 후 결론: **`Apply`를 mutable로 바꾸는 +방안은 기각**(3번 절 immutable 확정 이유 — 형제 서브트리 오염 방지 — +가 clone 비용 절감보다 우선순위 높음, 재확인). 대신 **판단 기준을 +"이질적/동질적 프로퍼티"가 아니라 "필드 간 계산 의존성 유무"로 +명확화** — 한쪽이 `Peek`으로 다른 쪽의 baked 값을 읽어 반영해야 하면 +이질적으로 보여도 `Apply`, 서로 완전히 독립이면 동질적으로 보여도 +`Override` 가능. 계산 의존성 없는 재사용 조각(배경/텍스트/레이아웃처럼 +서로 다른 서브시스템이 한 번만 만드는 값)은 모듈 상수로 만들어두고 +인스턴스마다 `Override`로 결합하는 게 실제 최적화 패턴 — 단 이건 +"`Override`가 내부적으로 캐싱해준다"가 아니라 사용자가 값을 재사용하는 +평범한 패턴일 뿐, 라이브러리에 새 캐싱 레이어가 생기는 게 아님을 +문서에 명시하기로 함. `base/modifier-plan.md` 9-1번 절. + +**같은 세션 세 번째 후속 — "`Apply` 경계에서만 clone, 안쪽은 mutable" +절충안도 검토 후 기각.** clone 횟수를 체인 길이가 아니라 `Apply` 호출당 +1번으로 줄이는 절충을 사용자가 직접 제시했으나, `Apply`를 거치지 않고 +setter를 단발로 직접 호출하는 흔한 경로는 여전히 mutable이라 공유 +레퍼런스가 그대로 오염될 수 있음(서브트리에서 폰트 두께만 바꿔도 터짐) +— "어디서 터지느냐만 달라지는" 비일관적 절충이라 실익 없다고 판단해 +기각. 전부 clone하는 현재 방식 유지 확정. `base/modifier-plan.md` +9-1번 (a-1) 절. + +**같은 세션 네 번째 후속(당시 CLAUDE.md에 미기록 — 2026-08-07 여섯 번째 +세션에서 뒤늦게 발견/보강) — `Override`가 서브타입 관계인 Modifier끼리 +섞일 때의 타입 시그니처는 미검증으로 열어둠.** `FrameModifier`가 +`GuiObjectModifier`의 서브타입이어야 자연스러운데, 필드 setter 메소드의 +리턴 타입이 각자 자기 자신이라(`self`) 단순 구조적 서브타이핑만으로 +`Modifier.Override(guiObjectMod, frameMod)`류가 통과하는지 추론만으로는 +결론 못 냄 — 후보안(메소드 필드는 `any`로 뭉개고 데이터 필드만 구조적 +체크)을 실 Luau로 검증 필요, 안 되면 `Override(...: any): any`로 +느슨하게 열고 이 항목으로 되돌아오는 걸 fallback으로 남김. +`base/modifier-plan.md` 9-2번, `ROADMAP.md` M7에 체크박스 반영 완료. + +**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 참고, +`ROADMAP.md` M0부터) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 +자체는 그대로. + +**미해결로 남긴 것 — 임의로 결론내지 않음**: Effect가 `state:Effect()` +형태로 Observer를 확장하는 변형인지, 완전히 독립된 free function인지가 +불명확함(사용자가 "확인 필요, 아니라면 논의해야 할 상태로 남겨두라"고 +명시). 관련 하위 질문으로 `state:Observer(fn)`가 생성 시 `fn`을 즉시 +1회 실행하는지도 문서 어디에도 명시돼 있지 않음이 이번에 드러남(Effect는 +"즉시 1회 실행"이 스펙에 명시돼 있어 이 부분만 보면 둘이 겹쳐 보임). +`base/effect-plan.md`의 "미해결" 절과 `.claude/question.md` +0번에 반영 — 구현 착수(M3~M4 전후) 전에 반드시 재확인할 것. + +**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 "다음 세션이 +할 일" 참고, `ROADMAP.md` M0부터). 이번 세션은 순수 문서 정리라 설계 +결정 자체는 늘지 않았음 — 단, M3 체크리스트에 `Blocker.luau` 항목이 +하나 추가된 것과, 위 Effect/Observer 미해결 항목은 M3~M4 착수 전에 +확인해야 함. + diff --git a/.claude/session/2026-08-07-06-ref-preref-api.md b/.claude/session/2026-08-07-06-ref-preref-api.md new file mode 100644 index 0000000..e102dc2 --- /dev/null +++ b/.claude/session/2026-08-07-06-ref-preref-api.md @@ -0,0 +1,80 @@ + + + + +## 2026-08-07 여섯 번째 세션 — Ref/PreRef 메소드 API 확정, 파일 분리, Tween GC 저장 구조 확인, Effect/Observer 관계 해소 + +사용자가 메모 형태로 두 가지를 던짐: (1) Tween 인스턴스를 per-instance +저장소에 담는 구조가 실제로 GC-안전한지, (2) Ref가 이제 충분히 완결된 +프리미티브이니 PreRef와 파일을 분리하고, `:Set`/`:Callback`/`:Wait` +세 메소드로 API를 굳히자는 제안(전부 mutation 패턴이라 자기 자신을 +반환). 둘 다 검증 후 반영 완료: + +- **Tween per-instance 저장소는 이미 확정된 구조 그대로 GC-안전함** — + `inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽 릴레이션이 중첩된 + 모양이라(`base.perInstanceState(inst)`), `inst`가 죽으면 중첩된 Tween + 인스턴스 릴레이션도 별도 정리 없이 같이 GC됨 — 새 결정 아니라 기존 + 설계(`bind-system-plan.md` "핸들러 내부 상태 저장" 절)의 확인, "왜 + GC-안전한가" 설명만 명시적으로 추가. +- **Ref API가 `.Value`(읽기 전용) + `:Set(value)`/`:Callback(fn)`/ + `:Wait(thread?)`(전부 self 반환)로 확정.** self-반환 덕에 + `if ref.Value then ref.Value else ref:Wait().Value` 관용구가 성립 — + 이걸 성립시키려고 `:Set()`이 `coroutine.resume`할 때 넘기는 인자를 + 기존 문서(세 번째 세션 원안)의 `value`에서 **`self`**로 정정함(안 + 그러면 `:Wait()`의 yield 리턴값에 `.Value`를 체이닝할 방법이 없었음). + `:Wait(thread?)`의 `thread` 인자는 생략 시 `coroutine.running()`을 + 캡처해 진짜로 yield하고, 명시적으로 넘기면 그 thread를 등록만 하고 + yield 없이 즉시 `self` 반환(코루틴 역학상 남의 thread를 여기서 대신 + 정지시킬 수 없어서) — 사용자가 직접 관리하는 스케줄러가 이미 어딘가서 + 정지시켜 둔 thread를 등록만 해두고 호출부는 안 블록되고 싶은 유스케이스. + 콜백은 여전히 raw 값을 받음(Ref 자신이 아니라). +- **파일 분리**: `Ref`는 그 자체로 완결된 프리미티브, `PreRef`도 "children + 배열 전용, 위치 무관 호이스팅"이라는 특이한 제약을 가진 별개 + 프리미티브라 기존 1프리미티브-1파일 컨벤션(Blocker/Effect 분리와 + 같은 이유)을 따라 `Ref.luau`/`PreRef.luau`로 쪼갬 — 런타임은 여전히 + 공유(`PreRef`가 `Ref`를 재사용, 브랜드 태그만 다름), `base/architecture.md` + 소스트리에 반영 완료. +- 전부 `base/bind-system-plan.md`(Ref/PreRef 절)와 `research/tween-plan.md`에 + 반영 완료. `.claude/question.md`엔 이미 반영돼 있던 "Ref 이름 자체는 + 용어 정리 대상" 항목과 모순 없음(이번 세션은 메소드 이름만 확정, Ref라는 + 타입 이름 자체는 여전히 가칭). + +**같은 세션 후반 — `.claude/question.md` 0번의 마지막 미해결 항목(Effect가 +`state:Effect()`인지 자유 함수인지) 해소.** 사용자가 직접 "정해볼까" 하고 +제기해 라이브로 논의, 다음으로 확정(전부 `base/effect-plan.md`/ +`base/bind-system-plan.md`에 반영): + +- **`state:Observer(fn)`는 등록 즉시 1회 실행되는 것으로 확정** — 근거: + (1) 이미 채워진 State를 나중에 구독하면 반영 연산이 아예 한 번도 안 + 일어나는 초기화-순서 디버깅 문제, (2) 초회 실행을 안 해야 할 구체적 + 근거가 약함, (3) 이러면 Observer 하나로 "초기값 적용"과 "이후 변경 + 반영"이 같은 코드 경로로 통일됨(store-bind 프로퍼티 핸들러가 최초 + 적용용 코드를 별도로 안 짜도 됨). +- **`Effect(fn, state?) -> EffectHandle`로 확정** — `state` 생략 시 기존 + 스펙 그대로(설치 1회 + leaf 죽을 때 확정 정리, 재실행 없음). `state` + 지정 시 **내부적으로 `state:Observer(...)`를 조합** — Observer가 이제 + 즉시 1회 실행되므로 그 첫 실행이 설치를 겸하고, 이후 무효화마다 + 직전 cleanup 호출 후 `fn` 재호출, leaf 사망 시 마지막 cleanup 1회 — + React `useEffect(fn, [dep])`와 동형. 다수 의존성은 `:With(...)`로 먼저 + 하나의 State로 묶어서 넘기는 쪽으로 확정(React식 별도 deps 배열 + 안 만듦 — 같은 일 하는 두 번째 경로 방지 원칙). Effect는 여전히 + 자유 함수(메소드 아님) — `state` 없이도 성립하는 유스케이스가 있고, + 있어도 leaf 생명주기 바인딩을 `state`가 소유하지 않아서. +- **예전에 기각했던 "Observer에 cleanup 반환 계약 추가"와 안 부딪힘** — + 그때 기각한 건 "Observer 자체에 이 복잡도를 넣지 말자"였지 패턴 자체가 + 무용하다는 게 아니었음. Effect가 opt-in 상위 계층으로 이 패턴을 제공하는 + 지금 구조가 그 기각과 정확히 양립함. +- **`fn`을 커링 스타일(팩토리가 실제 fn을 만들어 반환)로 짜는 것도 Effect/ + Observer 둘 다 모듈화 관용구로 권장** — `Modifier`의 `Boldify(10)` 커링과 + 같은 결. +- **백로그로만 기록, 결정 안 함**: `state:Apply(...)`처럼 여러 개를 커링으로 + 받아 `:With`/`:Compute` 등록을 자동화하는 조합기 아이디어(사용자 제안, + `Modifier:Apply`의 State판 대응물) — `base/bind-system-plan.md`에 백로그 + 절로만 남김, 시그니처/필요성 미검증. **(2026-08-07 일곱 번째 세션에서 + 이 방향 자체가 기각되고 훨씬 단순한 형태로 확정됨 — 아래 참고.)** +- 이걸로 `question.md` 0번(추가 프리미티브 논의)의 열린 항목은 "키 기반 + 동적 컬렉션 재조정" 하나만 남음. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미 +설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-07-07-state-apply-effect-subscribe.md b/.claude/session/2026-08-07-07-state-apply-effect-subscribe.md new file mode 100644 index 0000000..4fe3a46 --- /dev/null +++ b/.claude/session/2026-08-07-07-state-apply-effect-subscribe.md @@ -0,0 +1,58 @@ + + + + +## 2026-08-07 일곱 번째 세션 — `:Compute` 커링, `state:Apply` 확정(백로그안 기각), Effect `:Subscribe`/`:Unsubscribe` 신설, 이중 바인딩 금지 + +짧은 대화형 세션, 네 가지를 순서대로 처리 — 전부 `base/bind-system-plan.md`/ +`base/effect-plan.md`/`ROADMAP.md`/`question.md`에 반영 완료: + +1. **`:Compute(fn)`에도 커링 권장 노트 추가.** 여섯 번째 세션에서 Observer/ + Effect의 `fn`에만 문서화됐던 "팩토리가 실제 `fn`을 만들어 반환하는 + 커링 스타일 권장"이 `:Compute`엔 빠져 있었음 — 같은 결이라 자연스럽게 + 확장, `bind-system-plan.md` "`:With`+`:Compute`" 절에 추가. +2. **`state:Apply(factory)` 확정 — 원래 백로그였던 "`:With`/`:Compute` + 등록을 커링으로 자동화하는 조합기" 방향은 기각.** 사용자가 재확인한 + 실제 의도는 훨씬 단순함: `Modifier:Apply`와 똑같이 `factory(self)`를 + 체이닝 문법으로 부르는 순수 설탕(`function(self, factory) return + factory(self) end`) — `fnb(c,d)(fn(a,b)(state))`처럼 팩토리를 안에서 + 밖으로 겹쳐 읽어야 하는 중첩을 `state:With(a,b):Compute(fn(a,b)) + :Apply(fnb(c,d))`로 펴는 게 유일한 목적. 구현 비용 거의 0(State는 + Modifier와 달리 제네릭 `__index` 필드 setter 합성이 없어 이름 예약 + 충돌도 없음), 타입은 `factory: (State) -> U): U`로 Modifier보다 + 더 열어둠(팩토리가 State 밖 plain 값을 반환해 반응형 그래프를 벗어나는 + 것도 허용). Source는 기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 + 포함. `bind-system-plan.md` "`state:Apply(factory)`" 절, 구체 전/후 + 코드 예시까지 반영. 부수적으로 같은 헤더 아래 잘못 걸려 있던 Observer + `:Subscribe`/`:Unsubscribe` 내용(무관한 주제)을 별도 절로 분리하는 + 문서 버그도 수정. +3. **`EffectHandle:Subscribe()`/`:Unsubscribe()` 신설.** 지금까지 Effect의 + 유일한 생애주기 경로는 children 배열 leaf 부착뿐이라, leaf 없이 쓰는 + 모듈/스크립트 레벨 사이드 이펙트(백그라운드 시스템 등)엔 반환된 + `EffectHandle`이 막다른 길이었음 — Observer가 이미 가진 `:Subscribe`/ + `:Unsubscribe`와 같은 결로 확정. **핵심 주의점**: Effect의 + `:Unsubscribe()`는 Observer의 것을 그냥 위임하면 안 됨 — Observer의 + 계약은 "미래 재실행만 끊는다"로 충분하지만, Effect의 계약은 "생애주기가 + 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"이고 leaf 사망은 + 그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`도 동일하게 "지금 + 끝났다"는 신호로 취급해 마지막 cleanup을 트리거해야 계약이 일관됨(leaf + 가 살아있어도 마찬가지). idempotent 보장은 기존 `Subscribed` 필드 + liveness 체크 재사용으로 공짜. `base/effect-plan.md` 신규 절. +4. **Observer/Effect 이중 바인딩 금지 — `Bound`(가칭) 플래그로 즉시 + `error`.** 처음엔 "leaf 부착과 `:Subscribe()`를 동시에 써도 같은 + liveness 게이트를 공유하니 안전"이라고 적었으나, 사용자가 애초에 한 + 핸들은 라이프사이클 바인딩 경로를 하나만 가져야 한다고 정정 — 동시 + 바인딩은 UB로 확정하되, 판별 비용이 사실상 0(불리언 필드 하나)이라 + 조용한 오동작 대신 그 자리에서 `error`를 던지는 쪽으로 결정 + (엔지니어링 비용 대비 디버깅 이득이 명확). 두 진입점(`:Subscribe()` + 호출부, children 배열 leaf 부착부)이 똑같이 확인/설정하는 대칭적 게이트 + — 순서 무관. `bind-system-plan.md` "이중 바인딩 금지" 절 신설, + `effect-plan.md`의 3번 항목 서술은 이 규칙으로 대체(정정 표시 남김). + +**부수 정리**: `ROADMAP.md` M3에 `state:Apply`/Effect `:Subscribe`· +`:Unsubscribe`/이중 바인딩 금지 체크박스 추가. `question.md`에 `Bound` +이름을 용어 정리 대상(3순위)으로 추가. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 +설계 확정이라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-07-08-none-sentinel-dispatch-brand.md b/.claude/session/2026-08-07-08-none-sentinel-dispatch-brand.md new file mode 100644 index 0000000..8acf52d --- /dev/null +++ b/.claude/session/2026-08-07-08-none-sentinel-dispatch-brand.md @@ -0,0 +1,168 @@ + + + + +## 2026-08-07 여덟 번째 세션 — `None` 센티널 확정(인라인 필드 지우기), `NoneHandler`가 Tween store-bind와 같은 재귀 재디스패치임을 확인 + +세 번째 세션에서 "미확정"으로 메모만 남겨뒀던 `None` 센티널 +(`{ Override = nil, mod }`처럼 인라인 키로 modifier 값을 명시적으로 +지우고 싶어도 Lua 테이블의 `키 = nil`이 "키 없음"과 구별 안 되는 문제)을 +사용자가 "이거 결정할 게 진짜 있냐"고 다시 제기해 라이브로 짧게 논의, +확정까지 감. 전부 `base/modifier-plan.md`(2-1번)/`base/bind-system-plan.md` +(신규 절)/`base/ui-shorthand-plan.md`(신규 절)에 반영 완료: + +- **merge/setter 쪽은 아무것도 안 바뀜** — `None`은 raw 저장 계층의 그냥 + 평범한 실재값이라, 기존 merge 규칙("인라인 키 존재 시 무조건 우선", + `Override`의 "뒤 인자가 필드 단위로 이김")이 손댈 것 없이 그대로 작동함. + 처음엔 "merge 시점에 키를 지운다"는 새 분기가 필요하다고 잘못 생각했다가, + "값을 표현만 할 수 있으면 기존 규칙이 이미 다 해줌"이라는 걸로 정정. + 인라인 props 테이블 키(`{ TextColor3 = None, mod }`)와 Modifier setter + 인자(`mod:TextColor3(None)`) 둘 다 지원 — 메커니즘이 완전히 같아 구현 + 비용 거의 0("어차피 무료로 얻어지는거 아님?" — 사용자), 후자 덕에 + "특정 필드만 지우는 재사용 가능한 modifier 조각" 패턴도 공짜로 됨. + `:Peek()` 반환 타입도 `T | State | None | nil`로 확장(raw 계층에서 + `None`을 있는 그대로 돌려줌 — Peek은 확정 안 하고 그대로 넘긴다는 기존 + 9번 절 원칙 그대로). +- **실제 "지우기"는 디스패치 단계에서, 새 메커니즘 없이 풀림 — 핵심 + 발견.** 처음엔 "우선순위 최상단에서 값을 그냥 nop 처리"로 생각했다가, + 사용자가 "그럼 process에 특수 로직이 들어간다"고 지적하며 더 나은 안을 + 직접 제시: `NoneHandler`라는 평범한 pluggable 핸들러 하나를 추가 — + `isHandlable`이 `v == None`을 잡고, `process`가 `v`를 진짜 `nil`로 바꿔 + **`process(inst, k, nil)`을 재귀 호출**. 이게 바로 이미 확정돼 있던 Tween + store-bind 핸들러(`bind-system-plan.md` "확정된 디스패치 모델" 절, + `v`가 Store면 `realv`를 계산해 `process(inst,k,realv)`로 재귀)와 + **완전히 같은 패턴**이라는 걸 확인 — 새 아키텍처 개념이 하나도 안 늘어남. + base 드라이버 자체(`process(inst,k,v) -> getHandler(inst,k,v).process(...)`)는 + `None`을 전혀 모르는 순수 제네릭 그대로 유지, 개별 프로퍼티/이벤트/UI + shorthand 핸들러 시그니처도 `None`이 안 나옴(원래 있어야 했던 "`v`가 + `nil`인 경우" 처리를 재사용할 뿐). +- **`None`의 의미는 "리셋"이 아니라 "이 조합 단계에서 이 필드를 세팅 + 안 함"** — 실제로 `v=nil`을 받은 핸들러가 뭘 할지는 핸들러마다 다름(일반 + 프로퍼티는 사실상 그대로 두는 것과 다름없고, UICorner 숏핸드처럼 실제 + Instance를 만들어 붙이는 핸들러는 그 자식을 지움). 구체 사례로 + `ui-shorthand-plan.md`에 UICorner 절 신설 — `process(inst,k,nil)`이 + 만들어둔 `_quad_corner`류 자식을 직접 지움(이건 `retract`가 아니라 + `process` 자신의 로직 — `retract`는 "다른 핸들러가 키를 넘겨받는" 별개 + 시나리오 전용, 이미 확정돼 있던 원칙 재확인), 값이 자주 `nil`↔숫자로 + 토글되면 생성/제거 비용이 매번 든다는 캐비엇도 명시. +- **M2(디스패치 엔진) 착수 시 확인할 것 하나 새로 생김** — "이 키를 지금 + 누가 담당 중인가" bookkeeping이 바깥 순회 루프가 아니라 `process` 호출 + 자체 내부에서 갱신돼야 함. 안 그러면 값이 계속 `None`으로 유지되는 매 + 사이클마다 "1차 매치는 `NoneHandler`, 재귀 호출 뒤 실제 담당은 다른 + 핸들러"로 바깥 루프가 오판해 불필요한 `retract`가 반복 호출될 위험 — + `ROADMAP.md` M2에 반영, `pre-implementation-audit.md`의 "이전 매치 + 핸들러 추적" 항목과 같은 부류라 새 우선순위 등급 없이 거기 흡수. + +**부수 정리**: `question.md`에서 "미확정"이던 `None` 항목을 해소로 +제거하고, 이름 자체(`None`/`NoneHandler`)만 다른 가칭들과 같이 용어 +정리 대상(3순위)으로 새로 추가. `ROADMAP.md` M7 체크박스를 "확정 완료"로 +갱신. + +**같은 세션 후속 — `Dispatch` 함수 네이밍 정리, `canExecute` 시그니처 정정, +Tag/Attribute 전용 문서 신설.** None 논의를 파고들다 디스패치 엔진 자체의 +용어가 여러 군데서 흔들리고 있다는 게 드러나 바로 이어서 정리함. 전부 +`base/bind-system-plan.md`/`base/lifecycle-pattern.md`/`base/tag-plan.md` +(신규)/`base/attribute-plan.md`(신규)/`ROADMAP.md`에 반영 완료: + +- **제 실수 정정 — `canExecute`와 `isHandlable`은 다른 개념** (전체 경위는 + `archive/agent-mistake.md` 1번으로 옮김) — 결론만: `NoneHandler`가 + 구현해야 하는 건 `isHandlable`이지 `canExecute`가 아님. +- **`Dispatch.getHandler`/`Dispatch.process`/`Dispatch.addHandler`/ + `Dispatch.drive`로 이름 공식화.** 원래 "확정된 디스패치 모델" 절은 + "스캔+실행"과 "매치된 핸들러 자신의 처리"를 둘 다 그냥 `process`라고 + 불러 이름이 겹쳤던 게 혼동의 원인이었음 — `Dispatch.getHandler(inst,k,v): + Handler?`(순수 스캔)와 `Dispatch.process(inst,k,v)`(오케스트레이터: + getHandler → 이전 담당자 다르면 그 `retract` → 새 핸들러의 `.process`)로 + 분리, Handler 자신의 필드는 계속 `process`/`retract`(이미 확정된 이름, + 재검토 대상 아님) — 겹침은 소유자 표기(`Dispatch.process` vs + `handler.process`)로 해소, 새 이름 발명 안 함. **`Dispatch.addHandler(handler)`** + 도 신설 — concrete Handler를 우선순위 레지스트리에 등록하는 것도 + 결국 quad-roblox가 `BaseModule`을 뮤테이션하는 시점에 해줘야 하는 + 일이라(기존 "base 유틸은 인터페이스, 백엔드가 주입" 패턴과 같은 모양). + **배열→해시 두 패스 순회 드라이버 자신은 `Dispatch.drive(inst, + flattened)`로 확정** — 문서가 이미 이걸 비공식적으로 "base 디스패치 + 드라이버"라 불러왔던 걸 그대로 동사화(`apply`는 기각 — "Dispatch를 + 뮤테이션해서 결과를 낸다"는 어감이라 안 맞는다는 사용자 판단). +- **`canExecute` 시그니처 정정: `(handle) -> boolean`, zero-arg 아님.** + `lifecycle-pattern.md`가 원래 `canExecute: () -> boolean`(바인딩마다 + 클로즈오버된 람다)으로 적어뒀던 걸 정정 — 그러면 등록마다 클로저를 + 새로 만들어야 해서 "base는 인터페이스만, quad-roblox가 `BaseModule` + 뮤테이션으로 실 구현 주입"이라는 이미 확정된 패턴과 안 맞음. 공유 + 함수 하나가 되려면 "어떤 등록을 볼지" 가리키는 인자가 필요 — + `canExecute(handle: LifetimeHandle): boolean`으로 확정. **quad-roblox + 구현 스케치(참고용, base 결정 아님)**: rbvm 패턴 재사용 — Instance당 + weak-keyed per-instance 저장소에 "gchold" 배열을 두고, 절대 발화 안 + 하는 신호에 연결한 Connection의 콜백 클로저 안에 살려두고 싶은 + Observer를 업밸류로 캡쳐(콜백은 안 불려도 클로저의 업밸류는 안 죽음, + `inst`가 GC되면 gchold 배열째로 같이 죽음). `canExecute(handle)`은 이 + Connection(류)의 `.Connected`를 확인. **미확인 세부사항으로 남긴 것**: + Observer→Connection 역참조를 별도 weak 릴레이션으로 둘지 그냥 Observer + 테이블 안 평범한 필드로 넣을지(정적 해싱 필드 접근이 더 쌀 수 있음) — + quad-roblox 구현 단계에서 실측 필요. +- **`Tag`/`Attribute`도 UICorner/Tween처럼 전용 문서가 있어야 한다는 + 지적 — 맞아서 `base/tag-plan.md`/`base/attribute-plan.md` 신설.** + 둘 다 이미 `architecture.md`/`ROADMAP.md` M10에 파일로는 계획돼 + 있었지만 "1 프리미티브 1 파일" 관례(Blocker/Effect/Ref/PreRef 분리 + 선례)를 따르는 전용 설계 문서가 없었음 — 흩어져 있던 내용(Attribute의 + 타입 파라미터화 논의 등)을 모으고, 오늘 확정된 `None`/`process`/ + `retract` 동작을 반영. **핵심 발견**: Tag/Attribute 둘 다 UICorner + 숏핸드와 같은 패턴(값이 뭐든 항상 같은 핸들러가 계속 담당, 추가/제거를 + `process` 자신이 처리)이라 **retract가 필요 없음** — "확정된 디스패치 + 모델" 절이 원래 Tag/Attribute를 retract 필요 예시로 들었던 게 잘못이었음, + 바로잡고 "retract가 의미 있는 유일한 패턴은 매치되는 핸들러 *타입* + 자체가 사이클마다 바뀌는 경우(Tween↔일반 프로퍼티가 실사례)"로 좁힘. + Attribute는 특히 깔끔한 사례 — Roblox `SetAttribute(name, nil)` 자체가 + 네이티브하게 "지움"이라 `None→nil` 재디스패치가 특별 처리 없이 그대로 + 맞아떨어짐. +- `.claude/README.md`에 두 신규 문서 반영, `ROADMAP.md` M2/M10 체크박스 + 갱신(`Dispatch` 4개 함수, `canExecute` 시그니처, Tag/Attribute 문서 + 참조). + +**같은 세션 세 번째 후속 — `canExecute` 옵션 하나 더 검토 후 확정 유지, +`Brand` 통합 판별 메커니즘 신설(`isState`를 10종으로 일반화), `isHandlable`도 +`inst`를 받도록 정정.** 전부 `base/bind-system-plan.md`(`Brand` 절, 핸들러 +계약 절)/`base/modifier-plan.md`/`ROADMAP.md`/`question.md`에 반영 완료: + +- **`canExecute`를 "각 핸들 타입이 직접 구현"(`Observer.canExecute`)할지 + "공유 함수"(`canExecute(any)->boolean`)로 할지 재확인 — 공유 함수 유지, + 솔직한 이유까지 명시.** `Observer` 자체는 quad-base 레벨(엔진 무관) + 타입인데 liveness 체크(Connection 기반)는 본질적으로 엔진 종속적이라, + `Observer.canExecute`가 직접 구현하면 base/roblox 분리 원칙이 깨지거나 + 결국 내부적으로 공유 함수를 다시 호출하는 얇은 래퍼가 될 뿐 — 어느 + 쪽이든 공유 함수 쪽이 낫다는 결론 재확인(추가 논의 없이 유지). +- **`Brand` 신설 — `isState`(다섯 번째 세션)를 quad의 다른 branded 타입 + 전부(`Observer`/`Effect`/`Tag`/`Attribute`/`Tween`/`Blocker`/`Store`/ + `Source`/`Slot`)로 일반화.** 공유 weak-key 레지스트리 하나(`Brand.set`/ + `Brand.get`) + **문자열이 아니라 테이블 아이덴티티를 태그로 사용** + (사용자 제안 — Luau 인터닝 문자열도 이미 O(1) 포인터 비교라 성능 차는 + 없지만, 오타 안전성이 실질 이득: 잘못된 변수 참조는 즉시 드러나지만 + 오타난 문자열 리터럴은 조용히 어긋남). `isX`는 `Brand`를 감싼 얇은 + wrapper — 단순 항등(`isObserver`)과 집합 멤버십이 필요한 경우(`isState` + = `{State,Source}`)로 갈림. **`None`만 예외 — 싱글턴이라 레지스트리 + 없이 `x == None` 항등 비교가 더 싸고 정확**, 대신 `Brand.get`이 범용 + introspection 창구(quad-debug 용도) 역할까지 겸하도록 `None`을 특수 + 분기로 앞단에서 걸러줌 — `isNone`이 그 분기의 실제 구현체. +- **정정 — `isSource`는 별도로 필요함, 다섯 번째 세션의 "불필요" 서술을 + 뒤집음** (전체 경위는 `archive/agent-mistake.md` 2번으로 옮김) — 결론만: + `isSource`를 별도 제공, `isState`는 여전히 `{State,Source}` 둘 다 통과. +- **Luau 타입 narrowing은 자동으로 안 됨 — 사용자가 직접 확인, 명시적 + `::` 캐스팅 필요.** `isX(v)`가 참이어도 Luau가 TypeScript의 `x is T` + 같은 사용자 정의 타입 가드를 지원 안 해서 `v`의 정적 타입을 자동으로 + 안 좁혀줌 — `if isState(v) then local s = v :: State ... end`처럼 + 런타임 검증 뒤 명시적 캐스팅이 실제 패턴. 여전히 duck-typing보다 훨씬 + 안전하니 가치는 있지만 자동 narrowing을 기대하면 안 됨. +- **`isHandlable`도 `inst`를 받도록 확정 — `(inst,key,value): boolean`, + 원래 `(key,value)`였던 걸 정정.** `process`/`retract`는 처음부터 + `inst`를 항상 받았는데(핸들러 계약 원 원칙) `isHandlable`만 예외였던 + 게 애초에 약간의 불일치 — 지금 당장 `inst`로 매치가 갈리는 케이스는 + 없지만, 나중에 필요해지면 핸들러 계약 자체를 깨는 breaking change가 + 되므로 지금 넣어두는 게 훨씬 쌈. `Dispatch.getHandler`가 스캔 중 + `handler.isHandlable(inst,k,v)`로 호출하도록 갱신. +- `ROADMAP.md` M2에 `Brand.luau` 체크박스 신설, `Handler.luau`/M7의 + `isState` 항목 갱신. `question.md`에 `Brand` 이름(용어 정리 대상, + "OOP 클래스명을 얻는 느낌"에 맞는 더 나은 이름 필요 — `Tag`는 이미 + quad-roblox에서 다른 뜻으로 쓰여서 충돌) 반영. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 +설계 확정이라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-07-09-corpus-audit-createdref.md b/.claude/session/2026-08-07-09-corpus-audit-createdref.md new file mode 100644 index 0000000..5554e23 --- /dev/null +++ b/.claude/session/2026-08-07-09-corpus-audit-createdref.md @@ -0,0 +1,120 @@ + + + + +## 2026-08-07 아홉 번째 세션 — 코퍼스 전체 정합성 감사·수정, `agent-mistake.md` +신설, `CreatedRef` 이름 완전 폐기 + +여러 세션에 걸쳐 쌓인 stale 참조/자기모순을 서브에이전트 5개 병렬 감사로 +찾아내 전부 수정하고(커밋 `98bd46a`), 이어서 사용자가 직접 발견한 남은 +문제(`CreatedRef` 이름 자체가 더 이상 존재할 이유가 없다는 지적)까지 처리한 +세션. 세 부분으로 나눠 요약: + +**1. 코퍼스 정합성 감사·수정 (커밋 `98bd46a`)**: `CreatedRef`의 `phase` +옵션 폐기가 `bind-system-plan.md` 안에서 세 곳 중 두 곳에 방치돼 있던 것, +`question.md`의 `Ref` 재검토 대상 여부 자기모순, UICorner 숏핸드 개명이 +5개 문서에 전파 안 된 것, `canExecute(handle)` 시그니처 정정이 막 확정된 +직후라 두 곳에 전파 안 된 것, `architecture.md`/`ROADMAP.md`/`CLAUDE.md`의 +stale 문구·누락 참조 등 12개 항목을 수정. `store-semantics.md` 제목도 +"State는 **Store** 위의 캐시 레이어"에서 "State는 **Source** 위의 캐시 +레이어"로 정정(사용자 확인: Source 단독 존재 가능 + Store는 Source들의 +집합이라는 온톨로지가 맞음). `slot-plan.md`의 CRUD 의미론 갭 하나만 +사용자가 다음 세션에서 직접 다루기로 보류. + +**2. `archive/agent-mistake.md` 신설** — 설계 반전(`*-reversed.md`)/기각 +후보(`*-rejected.md`)와 구분되는 세 번째 archive 카테고리: 에이전트가 +문서 작성 중 스스로 낸 개념 혼동을 같은 세션 안에서 정정한 사례 전용 +(`canExecute`/`isHandlable` 혼동, `isSource` 불필요 오판 2건). CLAUDE.md +세션 로그에 전체 경위가 장황하게 남아있던 것 중 최종 결론이 이미 `base/` +문서에 반영돼 중복이던 걸 옮기고 포인터만 남김 — 앞으로도 비슷한 사례가 +생기면 여기로 옮길 것(사용자 확인). + +**3. `CreatedRef` 이름 완전 폐기 — 사용자가 직접 발견.** "Ref가 이미 다 +정해진 것 같은데 `CreatedRef`는 이제 없는 말 아니냐"는 지적: `Source(default)`/ +`Ref(default)`/`Store({defaults})`가 이미 Kotlin Compose식 "타입 이름 +자체가 팩토리 함수" 생성자 스타일로 확정돼 있었는데(2026-08-06 네 번째 +세션), `CreatedRef(fn)`라는 별도 래퍼 이름만 그 확정 이전(2026-08-04, +Ref가 아직 "instance 얻는 통로"로 좁게 정의됐던 시절)의 잔재로 계속 +남아있었던 것 — 실제로는 `Ref(default)`(또는 `PreRef(default)`) +인스턴스 자체를 children 배열 숫자 슬롯에 그대로 놓으면 `(v=Ref)` 매치 +핸들러가 처리하므로, 별도 래퍼 함수가 있을 이유 자체가 없었음. `base/ +bind-system-plan.md`(바인드 방법 절 재작성, "CreatedRef와의 관계" 절 +삭제, "phase 옵션 폐기" 절/열린질문 절 정리) · `ROADMAP.md`(M0/M8 체크 +박스) · `question.md`(용어 재검토 목록에서 제거, 해소로 표시) · +`architecture.md`(소스트리 주석) · `research/documentation-content-map.md` +전부 동기화 완료. `archive/ref-phase-option-reversed.md`(phase 옵션 +자체의 역전 이력)와 CLAUDE.md 이전 세션 로그의 `CreatedRef` 언급은 +당시 기록으로서 정확하므로 그대로 둠 — 역사적 서술과 현재 유효한 설계를 +헷갈리지 않도록 "phase 옵션 폐기" 절 제목에 "이 절이 당시 쓰던 이름 +자체도 이후 폐기됨" 포인터만 추가. + +**부수 작업 — `PreRef`/`Modifier`의 "pre-hook" 태깅 요청 처리.** 같은 +세션 앞부분에서 사용자가 "PreRef와 Modifier는 문서화 시 pre-hook 태그가 +필요해 보인다, hook과 pre-hook의 차이(취소 가능/순서 등록 가능)도 적어 +두면 좋겠다"고 제안 — 이건 런타임 설계가 아니라 문서 사이트 콘텐츠 +분류 아이디어라 base/에 "확정"으로 못박지 않고 `research/ +documentation-content-map.md`(심화 콘텐츠 후보 6번 + "문서화 아직 보류" +목록)에 사용자 원문 프레이밍 그대로 미확정 표시로 남겨둠 — `PreRef`가 +"인스턴스에 뭐가 일어나기 전에 채워진다"는 사실 자체는 이미 확정 +서술돼 있었지만(재확인 후 "메모에서 지워도 됨"으로 답변), "hook"/ +"pre-hook" 용어 채택 여부·`PreRef`의 취소 가능성·복수 `PreRef` 간 순서는 +다음에 사용자가 직접 정해야 base/로 승격 가능. **[정정, 2026-08-07 열 +번째 세션]** 같은 대화에서 "Ref 콜백/대기자 배열은 압축 없이 `self[i] += nil`로만 지워도 된다"는 설계가 이미 정확히 반영돼 있다고 여기 적었던 +건 틀림 — 실제로는 `nil`이 아니라 `None`으로 지워야 함(아래 열 번째 +세션 절 참고), 이때는 아직 발견 전이었음. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `slot-plan.md` CRUD +의미론과 "hook/pre-hook" 프레이밍 확정만 사용자가 직접 다룰 후보로 남음. + +**같은 세션 후속 — `PreRef` pre-pass 구현 위치·복수 `PreRef` 순서·동적 +경로 가드 확정.** 사용자가 구체적인 구현 방안 세 개를 직접 제시: + +1. **복수 `PreRef` 간 순서는 배열 index 순서 그대로** — 별도 규칙 발명 + 불필요, 위 "props 순회 순서" 절이 이미 확정한 "배열 파트는 index + 순서대로" 계약을 그냥 재사용하면 됨. 호이스팅은 "PreRef 대 나머지" + 에만 적용되는 규칙이지 "PreRef끼리"엔 적용될 게 없음. +2. **pre-pass가 사는 곳 — 새 `Dispatch.*` 함수 대신 이미 확정된 + `Dispatch.drive(inst, flattened)` 자신.** 사용자가 두 대안을 직접 + 제시(`Dispatch.process(inst, flatten, prerefs)`류 신설 함수 vs + `flatten(inst, nonFlatten)` 함수 자체에 얹기) — 검토 결과 둘 다 + 불필요/위험함이 드러남. 전자는 이미 `Handler.process`/`Dispatch.process` + 이름이 다른 뜻으로 확정돼 있어 겹침. 후자(flatten에 얹기)는 사용자가 + "가장 간단해 보인다"고 제안했지만, `research/existing-instance-bind-plan.md`가 + 다루는 "이미 마운트된 Instance 재바인드 시 flatten을 다시 해야 + 하는가"라는 열린 질문이 실제로 flatten이 한 인스턴스 생애주기 동안 + 여러 번 재호출될 가능성을 열어두고 있어서, 거기 PreRef fire를 얹으면 + 재바인드마다 PreRef가 또 fire되어 "이 인스턴스 하나의 construction + 훅"이라는 정의 자체가 깨짐 — 기각. `Dispatch.drive`는 최초 마운트 + 시 한 번만 불리는 게 이미 전제라 이 위험이 없어서 그대로 거기 좁은 + pre-pass 한 줄만 얹으면 충분. +3. **동적 경로로 도착한 `PreRef`는 런타임에도 명시적으로 error — + 지금까지 타입 차단만 문서화돼 있던 빈틈을 채움.** 사용자 제안 + 그대로 채택: `{isHandlable = v is PreRef, process = error(...)}` + 전용 Handler를 정상 우선순위 레지스트리에 등록(`NoneHandler`와 같은 + "한 값 종류 전담" 패턴, 새 메커니즘 아님). 리터럴 배열의 `PreRef`는 + pre-pass가 fire와 동시에 슬롯을 소진시켜 정상 두 패스에 다시 + 노출되지 않으므로, 이 Handler가 실제로 매치되는 경우는 타입 차단을 + 어떻게든 우회한 버그 케이스뿐 — no-op이 아니라 즉시 `error`가 맞음. + +전부 `base/bind-system-plan.md` "PreRef" 절에 반영, `ROADMAP.md` M8 +체크박스 갱신, `research/documentation-content-map.md`의 "복수 PreRef +순서" 미정 표시 제거(해소됨, "취소 가능성"만 계속 미정으로 남김). + +**같은 세션 두 번째 후속 — "호이스팅이 물리적 재배치가 아니라 별도 +선행 스캔"이라는 것과 소진 방식을 명시화(뒤이은 세 번째 후속에서 +`nil`→`None`으로 다시 정정됨, 아래 참고).** 사용자가 "drive에서도 +결국 PreRef를 목록에서 뽑아내야 하는데, 호이스팅 안 되면 PreRef +의미가 사라지는 거 아니냐"고 재질문 — 이전 답변이 `Dispatch.drive`가 +pre-pass를 갖는다고만 하고 정확한 알고리즘을 안 써서 나온 질문. +`Dispatch.drive`가 같은 `flattened` 테이블을 **두 번** 순회한다는 +것으로 답변: (1) pre-pass가 배열 전체를 index 순으로 훑어 `PreRef`를 +fire하며 그 자리에서 슬롯을 소진, (2) 그 다음 평소 두 패스가 같은 +테이블을 다시 순회하되 소진된 슬롯은 자연히 건너뜀. "호이스팅"은 +PreRef를 배열 앞으로 물리적으로 옮기는 게 아니라 "PreRef 전용 선행 +루프가 통째로 먼저 끝난 뒤에야 나머지가 시작된다"는 뜻이라 소스 위치와 +무관하게 항상 먼저 fire됨. **소진이 최적화가 아니라 정확성 요건인 이유도 +명시**: 안 지우면 두 번째 패스가 이미 처리된 PreRef를 `Dispatch.process`로 +다시 넘겨서, 바로 위에서 신설한 "동적 경로 가드" Handler(`(v=PreRef)`→ +`error`)가 정상 사용에도 오탐 에러를 던지게 됨. + diff --git a/.claude/session/2026-08-07-10-none-vs-nil-order.md b/.claude/session/2026-08-07-10-none-vs-nil-order.md new file mode 100644 index 0000000..752da18 --- /dev/null +++ b/.claude/session/2026-08-07-10-none-vs-nil-order.md @@ -0,0 +1,91 @@ + + + + +## 2026-08-07 열 번째 세션 — 소진 슬롯을 `nil`이 아니라 `None`으로, +사용자가 Luau REPL 반례로 직접 발견 + +같은 날 이어진 세션. 사용자가 Luau REPL에서 직접 +`for i,v in {[1]=1,[2222]=2222,[211]=211,[131]=131,[3]=3,[6]=6, +[122]=122,[11]=11,[312]=312,[821]=821,[991]=991} do print(i,v) end`을 +돌려 순회 순서가 `1, 6, 122, 11, 991, 2222, 131, 312, 3, 821, 211`로 +나온다는 걸 보여줌 — index 오름차순이 전혀 아님. 이건 위 아홉 번째 +세션에서 "PreRef pre-pass가 fire된 슬롯을 `nil`로 지우면 된다"고 적은 +것과 여섯 번째 세션에서 "Ref 콜백/대기자 배열도 `[i]=nil`로 소진하면 +된다"고 적었던 것 둘 다를 뒤집는 반례 — 키가 촘촘한 저범위 정수에서 +벗어나면(구멍이든 원래 듬성듬성이든) Luau/Lua 테이블이 해시 파트 +취급으로 넘어가 순회가 해시 버킷 순서가 됨. + +**해결 — 소진에 `nil` 대신 `None` 센티널 사용, 전 코퍼스에 전파.** +`None`은 `nil`이 아닌 실재하는 값이라 그 슬롯을 "차 있다"로 유지시켜서 +테이블이 "구멍 없는 시퀀스"라는 불변식이 안 깨짐 — 두 가지를 동시에 +해결: (1) 순서가 실제로 중요한 배열(PreRef pre-pass)의 순서 보장 유지, +(2) `table.insert`가 내부적으로 쓰는 `#t`가 Lua 명세상 구멍 있는 +테이블에서 정의되지 않은 동작이라는 문제(Ref 콜백/대기자 배열이 새 +등록 때 `table.insert`를 씀 — 순서 자체는 원래도 안 중요했지만 이 +`#t` 안전성 문제는 진짜 버그였음). **배열 파트의 `None`은 해시 파트의 +`None`(Modifier 필드 명시적 지우기, `NoneHandler` 경유)과 의미가 +다름** — 배열 파트 `None`은 처리할 핸들러가 없는 순수 빈 슬롯 표시라 +`Dispatch.process`/`NoneHandler`를 안 거치고 두 패스 루프 자신이 직접 +`if v == None then continue end`로 스킵. + +`base/bind-system-plan.md`의 "왜 `nil`이 아니라 `None`인가"(Ref +콜백/대기자 절)와 PreRef pre-pass 절에 반영, `ROADMAP.md` M0/M8 +체크박스 갱신, 위 아홉/여섯 번째 세션 문단에 정정 표시 추가(원문은 +유지, 틀렸던 부분만 짧게 정정 포인터). + +**부수 발견 — `props.Modifier`/`props.Ref` nil-hole 위험도가 이전 +서술보다 큼.** `pre-implementation-audit.md` 1-5가 이미 이 위험을 +"뒤 항목까지 무시될 수 있음"으로 국소적 피해처럼 서술해뒀는데, 이번 +REPL 실측으로 실제로는 구멍이 하나만 생겨도 **그 테이블 전체**가 순서 +보장을 잃을 수 있다는 게 드러남 — M0 스파이크에서 반드시 실측하고, +심각하면 "raw 리터럴 대신 `props.Modifier or Modifier()`로 non-nil +보장" 컨벤션 문서화까지 검토하기로 `ROADMAP.md` M0에 메모 추가. 이 +케이스는 caller가 직접 쓰는 raw Lua 리터럴이라 `None`으로 프레임워크가 +대신 채워줄 수 없어서 별도 해법이 필요함 — `None` 소진 전략과 혼동하지 +말 것. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M0 착수 시 위 +nil-hole 위험도 실측이 우선순위 높아짐. + +**같은 세션 세 번째 후속 — `props.Modifier`/`props.Ref` nil-hole +해법을 실제로 확정, 세션 clear 전 문서 완결성 점검하며 발견한 갭 +3개도 같이 보강.** 사용자가 "컴포넌트에서는 꼭 `or None`이나 +`Modifier()` 같은 걸로 nil 못하게 강제하는 걸 문서화하자"고 요청, 그 +자리에서 결정하고 clear 전 세션 전체를 다시 훑어 새로 알게 됐지만 +아직 문서에 없던 것들을 마저 채움: + +1. **`props.Modifier or None`/`props.Ref or None`을 필수 관용구로 + 확정** — `Modifier()`(빈 modifier 새로 생성)가 아니라 `None`을 쓰는 + 이유는 이미 있는 array-part `None`-스킵 메커니즘(PreRef 논의에서 + 확정)을 그대로 재사용해 새 코드/할당이 하나도 안 늘어나기 때문 — + `flatten`이 `isModifier(None) == false`라 그냥 통과시키고, 이어지는 + 두 패스 루프가 `None`을 만나면 스킵. `base/component-composition-plan.md` + "필수 관용구" 절 신설, `ROADMAP.md` M0/`pre-implementation-audit.md` + 1-5/`question.md`에 반영(1-5는 해소로 표시). +2. **`Modifier()` 바닥 생성자가 문서 어디에도 없었던 갭 발견·보강** — + `Source(default)`/`Ref(default)`/`Store({defaults})`와 나란히 있어야 + 할 "`Type(args)` 팩토리" 4번째 예시가 원래 없었음(이전 아홉 번째 + 세션에 `Modifier.Rounded(8)` stale 참조를 고치면서 실수로 체이닝 + 예시인 `mod:UICorner(8)`로 잘못 채워 넣었던 것도 같이 바로잡음). + `modifier-plan.md` 3번 절에 명시, `store-semantics.md` 예시 목록 + 정정, `ROADMAP.md` M7 체크박스 추가. +3. **`Brand` 태그 목록에 `RefTag`/`PreRefTag`/`ModifierTag`가 빠져있던 + 갭 발견·보강** — 이번 세션 내내 `isPreRef(v)`/`isModifier(v)`를 + 이미 존재하는 predicate처럼 써왔는데 정작 여덟 번째 세션의 `Brand` + 태그 목록엔 없었음. 추가하면서 **`isRef`/`isPreRef`가 `isState`와 + 달리 집합 멤버십이 아니라 단순 항등이라는 것도 명시** — + `isRef(preRefInstance)`가 참이면 일반 `(v=Ref)` 핸들러가 `PreRef`도 + 집어삼켜 PreRef 전용 pre-pass/가드 Handler 설계 전체가 무너지므로 + 반드시 배타적이어야 함. `bind-system-plan.md`의 `Brand` 절, + `ROADMAP.md` M2 체크박스에 반영. +4. **배열 파트 `None`과 해시 파트 `None`(`NoneHandler`)이 같은 센티널인데 + 처리 경로가 다르다는 걸 `None` 센티널 절 자체에 명시적으로 + 교차 참조 추가** — 이전엔 PreRef 절에만 있고 `None` 센티널 원래 + 정의 절엔 이 예외가 안 적혀 있어서, 그 절만 읽으면 모든 `None`이 + `NoneHandler`를 탄다고 오해할 수 있었음. + +전부 커밋 `98bd46a` 이후 아직 커밋 안 된 이번 대화 전체 변경사항에 +포함 — 다음 세션이 새로 알아야 할 건 없음, `ROADMAP.md` M0부터 그대로 +시작. + diff --git a/.claude/session/2026-08-08-01-relate-bindlifetime.md b/.claude/session/2026-08-08-01-relate-bindlifetime.md new file mode 100644 index 0000000..617f6a8 --- /dev/null +++ b/.claude/session/2026-08-08-01-relate-bindlifetime.md @@ -0,0 +1,93 @@ + + + + +## 2026-08-08 세션 — `Relate` 신규 프리미티브, `bindLifetime`/`canExecute` +탑레벨 함수로 확정, store-bind 재실행=Observer 재사용 명문화, `retract` +필드 생략 불가 확정 + +사용자가 store-bind/라이프사이클 관련 문서 갭 두 개를 질문하며 시작된 세션 +— 답을 찾는 과정에서 지금까지 이름만 있던 placeholder(`base.perInstanceState`)가 +실제로는 제대로 설계된 적 없는 프리미티브였다는 게 드러나 그 자리에서 +설계까지 확정까지 감. 네 가지로 정리: + +**1. store-bind의 "값이 바뀔 때마다 재귀 process" 구독 메커니즘 = +`state:Observer(fn):Subscribe()` 재사용으로 확정.** 기존 "확정된 디스패치 +모델"/"재실행 래핑" 절이 구독을 추상적으로만 서술해서 마치 새 구독 +프리미티브가 필요한 것처럼 읽혔는데, 실제로는 이미 확정된 Observer(등록 +즉시 1회 실행이라 "최초 적용"과 "이후 갱신"이 공짜로 통일됨, 자기 `Subscribed` +liveness도 이미 있음)를 그대로 쓰면 됨 — `retract`는 `observer:Unsubscribe()` +호출 하나로 끝. 새 구독 메커니즘 발명 없음. `base/bind-system-plan.md` +"Store 바인드는 특수 경우인가" 절 반영. + +**2. `retract` 필드는 no-op이라도 항상 정의해야 함 — 생략 불가로 확정.** +"모든 핸들러가 의미 있게 구현할 필요는 없음(보통 no-op)"이라는 기존 서술이 +"필드 자체를 생략해도 된다"로 오독될 수 있는 갭이었음 — `Dispatch.process`는 +담당 핸들러 타입이 바뀔 때 이전 핸들러의 `retract`를 nil 체크 없이 무조건 +호출하므로, 필드를 생략한 핸들러가 실제로 교체되는 드문 순간(Tween↔프로퍼티 +등)에 `attempt to call a nil value`로 크래시함. `base/bind-system-plan.md` +"핸들러 계약" 절에 명시, M2 체크리스트에 린트 대상으로 추가. + +**3. `Relate` 신규 프리미티브 — `bindLifetime`/`canExecute`(가 의존하는 +per-inst weak 저장소)를 제대로 설계.** 사용자 질문 경위: `Frame { observer }`처럼 +children 배열에 직접 놓는 leaf 케이스와, property store-bind 핸들러가 +**내부에서** 만드는 Observer(배열에 안 들어가므로 그 leaf 부착 경로를 안 탐)를 +처음에 잘못 섞어서 답했다가 사용자가 "state 바인딩은 결국 k,inner v를 +호출하니 i=number,v=observer로 다시 실행 안 된다"고 정정 — 후자는 +`bindLifetime(inst, observer)` 같은 별도 배관이 필요하다는 걸로 이어짐. +이게 `base/lifecycle-pattern.md`가 이미 원 사용자 메모(2026-08-04)로 +갖고 있던 "함수 안에서 만든 옵저버도 GC 대상 되어야 함" 절과 정확히 +같은 문제였음이 드러남 — 그 절이 "범용 유틸이 있어야 한다"까지만 말하고 +실제 인터페이스/이름이 없던 것. + +- **탑레벨 평범한 함수로 확정, 네임스페이스 뒤에 안 숨김** — `bindLifetime(inst,value)`/ + `canExecute(inst,value)`. `Dispatch.process`류는 "시스템 배관"이라 + 네임스페이스가 맞지만 이 둘은 `isState`/`isObserver`처럼 핸들러 작성자가 + 직접 부르는 1급 프리미티브 연산이라 `LifetimeHandle.bind(...)`식으로 + 감싸면 안 된다는 사용자 지적(정확함, 처음 제 제안이 틀렸었음). +- **`canExecute` 시그니처를 `(handle)` 단일 인자에서 `(inst, value)` + 2-인자로 재정정** — Observer 자신의 바인딩 생존(`Subscribed`)과 `inst` + 자체 생존(gcconn)이 독립된 두 조건이라 opaque `handle` 하나로 못 뭉침. + 구현은 `value`가 Observer/Effect면 자기 `Subscribed`부터 확인, 그 다음 + `inst`의 공유 gcconn `.Connected`를 봄. +- **`Relate` — `inst`를 weak 키로 하는 범용 릴레이션, 신규 프리미티브로 + 독립 승격**(`base/relate-plan.md`, 1프리미티브-1파일 컨벤션). `Relate()` + 비싱글톤 생성자 + `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`. 핵심 + 결정 세 개, 전부 사용자가 직접 제시: + 1. **자동으로 아무것도 홀드 안 함** — `inst`도 `value`도 Relate 자신은 + 안 붙잡음, weak/strong 여부는 호출부(엔진을 아는 quad-roblox)가 + 매번 명시. 자동으로 정하면 weak 키가 참조하는 값이 그 키로 되돌아 + 강참조하는 사이클이 너무 쉽게 생김. + 2. **`inst`(키) 축은 항상 weak로 고정, 자유도를 안 열어둠** — 강한 키가 + 필요한 유스케이스가 지금까지 하나도 없어서, 그 자유도 자체가 사고 + 가능성만 늘림. `Weak`/`Strong`은 오직 `value` 보관 방식. + 3. **실 구조는 `{ [inst(weak)]: { StrongMap: {[k]:v}?, WeakMap: {[k]:v(weak)}? }? }`, + 둘 다 lazy 생성**(첫 `Set` 호출 시에만 만듦) — Luau가 정적 분석으로 + 포인터 해싱을 캐싱해 반복 인덱싱은 이미 싸지지만 테이블 생성(array+hash + part 초기화) 자체는 비교적 비싸다는 게 이유. `WeakMap`의 메타테이블은 + 매번 새로 안 만들고 공유 객체 하나를 재사용. + - **비싱글톤인 이유**: 각 핸들러 모듈이 자기 톱레벨에 `local relate = + Relate()`를 하나씩 두면 key 네이밍이 모듈 간에 겹칠 걱정이 원천적으로 + 없음(`Ref`/`Store`류와 같은 "생성 가능한 값" 컨벤션). +- **`base.perInstanceState(inst)` 이름/placeholder는 완전히 폐기** — + `Relate`가 그 자리를 정식으로 대체. `bind-system-plan.md`(핸들러 내부 + 상태 저장 절)/`ui-shorthand-plan.md`/`architecture.md`(소스트리, + `Relate.luau`는 quad-base 전체가 순수 Lua라 quad-roblox 재구현 없음)/ + `question.md`(용어 정리 목록에서 `PerInstanceState` 항목 삭제, 이름 + 갈등 자체가 해소됨)/`ROADMAP.md`(M2/M8/병행가능 세 곳) 전부 동기화. + +**4. 아직 안 풀린 것 — `(i:number, v=Ref/Observer/PreRef)` children-array +leaf Handler가 quad-base/quad-roblox 중 어디 사는지.** 3번을 풀다가 +갈라져 나온 별개 질문(`Frame { ref }` 자체를 매칭하는 Handler, store-bind +내부 Observer와는 무관) — 제 제안(엔진 특정 API가 필요 없으니 quad-base, +`Dispatch/StoreBind.luau`와 같은 층위)은 사용자 확인을 못 받은 채 대화가 +3번으로 넘어감. `question.md` 2번에 미확인으로 남김, base에는 반영 안 함 +— 다음에 확인 필요. **[해소됨, 같은 날 두 번째 세션]** 아래 절 참고 — +제 원래 제안 그대로 quad-base로 확정. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M0/M2 스파이크 코드가 +검증해야 할 것 목록에 `Relate`의 lazy 서브테이블 생성/공유 메타테이블 +전략, `bindLifetime`/`canExecute`의 실제 gcconn 트릭이 새로 추가됨 — +`base/lifecycle-pattern.md`/`base/relate-plan.md`의 "실측 필요" 캐비엇 +참고. + diff --git a/.claude/session/2026-08-08-02-dispatch-singleton-naming.md b/.claude/session/2026-08-08-02-dispatch-singleton-naming.md new file mode 100644 index 0000000..5119e55 --- /dev/null +++ b/.claude/session/2026-08-08-02-dispatch-singleton-naming.md @@ -0,0 +1,92 @@ + + + + +## 2026-08-08 두 번째 세션 — Dispatch는 프리미티브가 아니라 탑레벨 싱글톤 확정, +네이밍 케이싱 컨벤션 신설, Handler를 세 번째 카테고리로 명문화 + +같은 날 이어진 세션. 사용자가 위 4번 미결 항목("Ref/Observer/PreRef leaf +Handler가 어디 사는지")을 다시 짚으며 시작 — "Handler도 실제 런타임 값이 +생기는 요소인데 왜 프리미티브로 안 다루나", "Dispatch는 어떻게 되는 거냐, +State 핸들러 안에서 `getHandler`를 부르려면 Dispatch가 이미 존재해야 +하는데" 하는 질문으로 확장돼 Dispatch 자체의 정체성(싱글톤 top-level +함수 모음 vs 인스턴스화 가능한 프리미티브) 논의로 이어짐. 네 가지로 정리, +전부 `base/bind-system-plan.md`/`base/store-semantics.md`/ +`base/architecture.md`/`question.md`/`ROADMAP.md`에 반영 완료: + +**1. Dispatch는 프리미티브가 아니라 탑레벨 싱글톤 — 확정, 지금 형태 유지.** +`Dispatch.process`/`getHandler`/`addHandler`/`drive`를 `Source`/`Ref`처럼 +생성자 있는 프리미티브로 바꿀지 검토했으나 기각. 근거: (a) Tween/ +`NoneHandler`/`StoreBind`가 자기 `process` 안에서 다시 `Dispatch.process`를 +재귀 호출해야 해서, `canExecute`/`bindLifetime`처럼 require 한 번으로 바로 +닿는 안정된 전역이어야 함 — 프리미티브화하면 모든 Handler 호출 경로에 +Dispatch 핸들을 실어날라야 하는 스레딩 비용이 생기는데 지금은 그 비용이 +없음. (b) 사용자가 우려한 "Handler가 Dispatch 원하고 Dispatch가 Handler +원해서 순환참조" 문제는 착시로 확인됨 — "Handler"가 (i) `Handler.luau`의 +순수 타입 계약(leaf, Dispatch를 몰라도 됨)과 (ii) 그 계약을 구현하는 +concrete 값 모듈(`StoreBind.luau`류, 재귀호출 위해 Dispatch를 참조) +두 가지를 가리켜서 헷갈렸던 것 — 의존 방향은 `Handler.luau` ← +`Dispatch/init.luau` ← `StoreBind.luau`로 항상 한쪽으로만 흐름, 사이클 +없음. (c) 모듈 재생성(`New()`)과의 관계도 새 설계가 필요 없음 — 이미 +확정된 "팩토리가 `BaseModule`을 뮤테이션" 패턴을 그대로 따르면 +`_initializedBy` 마커에 대해 이미 나왔던 결론("`New()`가 생기면 각 +인스턴스가 별도 테이블이 되므로 자연히 스코핑됨")이 Dispatch의 handler +레지스트리에도 그대로 적용됨. v1처럼 `require`를 감싸는 `Init(QuadId?)` +방식은 채택 안 함(id 기반 조회 자체가 Ref로 대체되며 이미 기각된 패턴). +`base/bind-system-plan.md`의 "Dispatch는 프리미티브가 아니다" 절, +`base/architecture.md` 13번 항목에 반영. + +**2. quad-base 기본 핸들러도 전부 같은 `Dispatch.addHandler` 레지스트리를 +공유 — Ref/Observer/PreRef leaf Handler 위치 확정.** `NoneHandler`/ +`Dispatch/StoreBind.luau`뿐 아니라, children 배열 숫자 슬롯에 `Ref`/ +`Observer`/`PreRef`를 직접 놓는 leaf 값을 매칭하는 Handler도 같은 부류 — +`inst`를 `any`로 취급하고 엔진 특정 API가 필요 없으니 quad-base, +`Dispatch/Leaf.luau`로 확정(위 4번 미결 항목 해소). quad-roblox의 +Property/Event/Tween 핸들러도 **같은** 레지스트리에 등록되므로, base +기본 핸들러와 backend 핸들러가 별도 경로로 안 갈리고 하나의 우선순위 +스캔을 공유한다는 것도 명시적으로 확인됨. `architecture.md` 소스트리에 +`Dispatch/Leaf.luau` 반영, `question.md`/`ROADMAP.md` M2 동기화. + +**3. Handler는 "독립 프리미티브 vs 파생 데이터" 분류의 세 번째, 별개 +카테고리 — 명문화.** 2026-08-06 후속 세션이 확정한 분류(Source/Ref/Store/ +Modifier=독립 프리미티브, State/Observer=파생 데이터)에 Handler가 왜 +안 끼는지 사용자가 재확인 요청 — 이유: Handler는 그 자체로 구현체가 +없는 **순수 타입 계약**이라 quad 사용자가 다루는 리액티브 값이 아님, +계약을 만족하는 값(`PropertyHandler`류)은 항상 **구현하는 쪽**(base +자신의 기본 핸들러 또는 quad-roblox 백엔드)이 채워 넣는 것이지 `Type(args)` +자유 함수로 사용자가 만드는 게 아니고, State/Observer처럼 어떤 원천에 +종속된 파생물도 아님. `base/store-semantics.md`의 "일반 원칙" 절 뒤에 +"세 번째 카테고리 — Handler" 절로 반영. + +**4. 네이밍 케이싱 컨벤션 신설 — 지금까지 나온 모든 이름이 이미 따르고 +있던 규칙을 문서화만 함, 리네임 없음.** 사용자 관찰: "탑레벨 함수는 +변수처럼 소문자 시작, 프리미티브 타입의 메서드는 대문자 시작(파스칼 +케이싱)이 맞아 보인다"는 규칙 제안 — 검증 결과 기존 이름 전체(생성자 +`Source`/`Ref`/`Store`/`Modifier`/`Relate`/`Effect`, 콜론 메서드 +`:Get`/`:With`/`:Set`/`:Apply`/`:Subscribe`류는 전부 대문자, `canExecute`/ +`bindLifetime`/`isState`류/`Dispatch.process`류/`Brand.set`류는 전부 +소문자)가 이미 예외 없이 이 규칙을 따르고 있었음이 확인됨. 유일하게 +애매해 보였던 `Modifier.Override(mod1, mod2, ...)`(콜론 아니고 dot-access +인데 대문자)도 규칙 위반이 아니라 세 번째 하위 규칙으로 설명됨 — 콜론 +메서드는 아니지만 **`Modifier` 타입 자신의 네임스페이스에 달린 정적 +결합 함수**라 "그 프리미티브 타입 고유의 공개 어휘"라는 점에서 생성자/ +메서드와 같은 부류. 반대로 `Dispatch.process`/`Brand.set`이 소문자인 +이유는 `Dispatch`/`Brand`가 애초에 `Type(args)` 생성자가 없는 프리미티브가 +**아닌** 내부 엔진/레지스트리라서. 최종 판단 기준: "이 이름이 특정 +프리미티브 타입 하나의 전용 소유물인가?" — 그렇다면 대문자, 아니면(여러 +타입에 걸친 범용 유틸이거나 비-프리미티브 엔진 소속) 소문자. `base/ +architecture.md`의 "코드 스타일 — 네이밍 케이싱" 절 신설. + +**같은 세션 후속 — `module-lifecycle-plan.md`의 "열린 질문" 절이 stale로 +방치돼 있던 것을 사용자가 직접 발견.** 문서 상단 "상태" 줄은 이미 +"확정되어 승격됨"이라고 말하는데 그 아래 "열린 질문" 절은 2026-08-04 +당시 그대로 남아있었음 — 그중 "프로바이더 인터페이스 시그니처 미정"/ +"네이밍 미정(provider/processor/plug)" 두 항목이 사실 그 뒤 `Handler` +계약 확정으로 이미 풀려 있었는데 이 문서에 반영이 안 됐던 것. 원문은 +남기고 각 항목에 해소 표시+포인터 추가, 절 제목도 "열린 질문이었던 것 — +전부 해소됨"으로 정정. 새 결정 아니라 순수 동기화. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 +설계/문서 정리라 M0 착수 우선순위 자체는 그대로. 위 2026-08-08 첫 세션이 +남긴 "M0/M2 스파이크 검증 목록"에 새로 추가되는 항목 없음. + diff --git a/.claude/session/2026-08-08-03-tag-redesign-dispatch-chain.md b/.claude/session/2026-08-08-03-tag-redesign-dispatch-chain.md new file mode 100644 index 0000000..7103ce7 --- /dev/null +++ b/.claude/session/2026-08-08-03-tag-redesign-dispatch-chain.md @@ -0,0 +1,93 @@ + + + + +## 2026-08-08 세 번째 세션 — Tag를 array-part 값 객체로 재설계, Dispatch +체인+`retractUnder`로 재귀 재-dispatch의 retract 전파 문제 해결 + +같은 날 이어진 세션. 사용자가 "Tag를 해시 파트 boolean 키 대신 array-part +값 객체로 바꾸는 게 낫지 않냐"는 질문으로 시작 — 상호배타 스타일 상태 +(`btn1`/`btn2`/`btn3`류, 20개까지도 가능)를 표현하려면 구 모델은 태그 +개수만큼 키를 갱신해야 해서 끔찍하다는 실사용 근거. 이 논의가 "retract가 +새 값의 타입에 따라 이전 핸들러를 정확히 찾아 부를 수 있는가"라는 훨씬 +근본적인 구멍(`pre-implementation-audit.md` 1-2번이 이미 지적해뒀던 것)을 +직접 건드리게 됐고, 몇 차례 시행착오 끝에 사용자가 제시한 "체인+ +`retractUnder`" 설계로 수렴. 세 갈래로 정리: + +**1. Tag 재설계 — array-part 값 객체, `Modifier`와 같은 immutable clone +체이닝.** `Tag(name1, name2, ...)`(가변인자 생성자, 빈 `Tag()`도 유효), +`:Added`/`:Removed`(뮤테이션처럼 안 보이게 `-ed` 어미 — 실제로는 항상 +clone 후 반환), `:Contains(name):boolean`, `:Apply(factory)`(Modifier와 +동일한 순수 체이닝 설탕), `Tag.Merged(tag1,tag2,...)`(집합 합집합, 무손실 +— Modifier의 `Override`는 필드 단위 덮어쓰기라 손실 있음, 그래서 이름도 +다름). `None` 센티널은 불필요로 확인 — 동적 토글은 `Source`/`State`가 +계산 결과로 `nil`을 리턴하면 되는 함수 인자 전달이라 테이블 리터럴의 +nil-hole 문제 자체가 없음(정적 리터럴에서 조건부로 Tag를 넣고 뺄 때는 +다른 array-part 값과 마찬가지로 기존 `None` 관용구가 그대로 유효, Tag +전용 규칙 아님). 구 모델(해시 파트 boolean, "핸들러 타입이 안 바뀌니 +retract 불필요"가 결론이었음)은 `archive/tag-hash-key-model-reversed.md`로 +역전 보존, `base/tag-plan.md` 전면 재작성. 값 타입+API(`Tag.luau`)는 +quad-base, `CollectionService` 글루(`Handlers/Tag.luau`)만 quad-roblox — +이미 확정된 "base는 인터페이스/값, backend는 process·retract 글루" +패턴(`LifetimeHandle`)을 값 타입 수준까지 그대로 확장한 것으로 확인, +새 아키텍처 개념 아님. + +**2. Tag 재설계가 "retract가 실제로 필요해지는" 첫 array-part store-bind +사례가 되며, 기존 "이전 핸들러 추적" 설계 공백이 정면으로 드러남.** +`pre-implementation-audit.md` 1-2번이 이미 "store-bind 재실행 모델에서 +realv 타입이 매 갱신마다 바뀔 수 있는데 '이전 핸들러'를 누가 추적하는지 +불명"이라고 짚어뒀던 것 — Tag가 `Tag(...)`↔`nil` 사이를 오가며 실제로 +핸들러 타입이 바뀌는 구체 사례가 되어 더 이상 미룰 수 없어짐. 시행착오 +과정: +- **1차 제안(제가 냄, 기각됨)**: Dispatch가 `(inst,k)`별로 "지금 누가 + 담당 중인가"를 슬롯 하나로 추적. **재귀/래핑 핸들러(StoreBind 등) + 에서 깨짐** — 사용자가 직접 "A→B 구조에서 A가 바뀌면 B의 retract가 + 실행되고, 재귀로 B로 다시 내려오면 retract가 없는 거 아니냐"고 반례를 + 제시 — A 자신의 생명주기(예: Observer 구독)와 A가 재귀로 위임한 B의 + 생명주기가 슬롯 하나를 두고 서로 덮어써서, A가 스스로 재-dispatch할 + 때 자길 엉뚱하게 retract하거나 반대로 안 해야 할 때 안 하는 오작동이 + 생김이 실제 트레이스로 확인됨. +- **2차 제안(제가 냄, 부분 기각)**: 각 래핑 핸들러가 자기 전용 `Relate`에 + 위임 대상을 비공개로 저장(A→B→C면 A.retract가 수동으로 B.retract를 + 부르고 B.retract가 수동으로 C.retract를 부르는 linked 구조). 동작은 + 하지만 사용자가 두 가지 지적: (a) 나중에 재바인드(`existing-instance- + bind-plan.md`) 지원을 생각하면 위임 정보가 핸들러별로 비공개 분산돼 + 있어 외부에서 못 들여다봄, (b) 각 핸들러 작성자가 "내 retract에서 + 위임 대상도 cascade해야 한다"는 걸 매번 기억해야 하는 규율 의존적 + 설계. +- **최종 채택(사용자 제안) — Dispatch가 `(inst,k)`별 핸들러 체인(순서 + 있는 배열)을 직접 소유, `Dispatch.retractUnder(inst,k,keep,v)`가 + 꼬리부터 `keep` 앞까지 훑으며 정리.** `Dispatch.process`가 매치될 + 때마다 체인에 push, 재귀/래핑 핸들러는 재-dispatch 전에 + `retractUnder(inst,k,self,newV)`를 먼저 불러 자기 밑을 정리 — 이 + 한 번의 루프가 다단 체인(A→B→C) 전체를 순서대로 정리해주므로 개별 + 핸들러의 `retract`는 더 이상 자기 위임 대상을 수동으로 안 쫓아가도 + 됨(2차 제안의 (b) 해소), 체인이 Dispatch에 중앙화돼 있어 미래 + 재바인드도 `retractUnder(inst,k,nil,newV);process(inst,k,newV)` + 두 줄로 자연스럽게 됨((a) 해소) — quad-debug의 "무엇이 무엇에 + 연결됐는가" 그래프도 이 구조를 그대로 읽으면 됨. 배열이 항상 꼬리에서만 + 추가/삭제되는 스택 모양이라 `None` 소진 이슈(구멍 있는 정수 키 순회 + 문제)도 애초에 안 생김. **`retract`는 여전히 `(inst,k,v)` 3-인자 + 유지** — 한 차례 제가 "v 제거"를 제안했다가 틀렸음(사용자가 Tag의 + 전체삭제 vs diff 분기를 근거로 정정) — 다만 최종 설계에서 diff는 + `process`(같은 핸들러 유지 시)의 몫이고 `retract`는 항상 "더 이상 + 매치 안 될 때만" 불리므로 Tag 한정으로는 `v`를 안 봐도 항상 전체 + 삭제가 맞다는 것도 확인. 순환은 기존 "일반적 무한루프 방어 안 함" + 원칙(2026-08-04) 그대로 UB. + +**3. 전부 `base/bind-system-plan.md`(신규 "Dispatch 체인" 절 + "확정된 +디스패치 모델"/"None 센티널"/"Store 바인드는 특수 경우인가" 절 갱신)/ +`base/tag-plan.md`(전면 재작성)/`archive/tag-hash-key-model-reversed.md` +(신규)/`base/architecture.md`(소스트리 `Tag.luau` 추가, 4번 항목 정정)/ +`ROADMAP.md`(M2/M4/M10)/`research/pre-implementation-audit.md`(1-2번 +해소 표시)에 반영 완료.** + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M2/M4 스파이크 +검증 목록에 `chains`/`retractUnder`가 다단 체인에서 실제로 정확히 +동작하는지가 새로 추가됨(추론만으로 확정된 것, `pre-implementation-audit.md` +류 "실제 Luau로 부딪혀본 적 없는 것" 범주). `pre-implementation-audit.md` +1-1번(Tween이 유일한 store-bind 예시라 "일반 store-bind와 Tween이 같은 +핸들러인지"가 불명확한 문제)은 Tag가 두 번째 구체 사례가 되면서 정황상 +"별개 핸들러, 둘 다 `Dispatch/StoreBind.luau` 재사용"쪽에 힘이 실리지만 +**아직 명시적으로 확정된 건 아님** — M2/M4 착수 전 마저 확인할 것. + diff --git a/.claude/session/2026-08-08-04-overridden-naming.md b/.claude/session/2026-08-08-04-overridden-naming.md new file mode 100644 index 0000000..634963b --- /dev/null +++ b/.claude/session/2026-08-08-04-overridden-naming.md @@ -0,0 +1,33 @@ + + + + +## 2026-08-08 네 번째 세션 — `Modifier.Override` → `Overridden`으로 이름 확정 + +사용자가 IDE에서 `tag-plan.md`를 보다가 "Tag가 `Added`/`Removed`처럼 +`-ed` 어미를 의도적으로 쓰는데, Modifier의 `Override`도 그냥 +`Overrided`로 하면 어떤가"라고 질문 — `-ed`/분사 어미가 "즉시 커밋되는 +뮤테이션이 아니라 이미 계산되어 반환되는 새 값"을 신호한다는 기존 관례 +(`Add`/`Remove`가 `-ed` 없이 쓰이면 뮤테이션처럼 오독될 위험이 있어 +`Added`/`Removed`로 확정했던 것과 같은 문제가 `Override`에도 그대로 +있음)에 정확히 들어맞는 좋은 관찰이었음. 다만 `Overrided`는 오기 — +`override`는 불규칙동사라 과거분사가 `overrided`가 아니라 `overridden`. +`Add`/`Remove`/`Merge`가 전부 규칙동사라 우연히 단순 `-ed` 접미만으로 +맞았던 것뿐, `Override`엔 그 규칙이 그대로 안 통함. 사용자가 이 정정에 +동의하고 확정 요청 — `Modifier.Overridden(mod1, mod2, ...)`으로 이름 +자체를 확정(더 이상 가칭 아님, 용어 정리 라운드 대상에서도 제외). + +`base/modifier-plan.md`/`base/component-composition-plan.md`/ +`base/bind-system-plan.md`/`base/tag-plan.md`(비교 문구)/ +`base/architecture.md`/`ROADMAP.md`/`research/pre-implementation-audit.md`/ +`research/documentation-content-map.md`/`.claude/README.md`/ +`.claude/question.md` 전부에서 `Override` → `Overridden`으로 기계적 +치환 + 각 문서의 "가칭"/"이름만 잠정" 표시를 "이름 확정"으로 갱신 +(`question.md`의 3순위 용어 재검토 목록에선 완전히 제거, `Peek`/ +`isState`만 그 목록에 남음). CLAUDE.md 세션 히스토리(과거 `Override` +서술)와 `archive/`는 당시 기록이라 그대로 둠 — 역사적 서술과 현재 +유효한 이름을 헷갈리지 않도록 여기 새 절로만 반영. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 순수 네이밍 +확정이라 M0 착수 우선순위나 설계 자체엔 영향 없음. + diff --git a/.claude/session/2026-08-08-05-terminology-round.md b/.claude/session/2026-08-08-05-terminology-round.md new file mode 100644 index 0000000..71c4d54 --- /dev/null +++ b/.claude/session/2026-08-08-05-terminology-round.md @@ -0,0 +1,46 @@ + + + + +## 2026-08-08 다섯 번째 세션 — 용어 정리 라운드 정리: `Handler`/`None`· +`NoneHandler`/`Ref`/`PreRef`/`Peek`/`isState` 이름 확정, `DI`→`D`/ +`canExecute`→`isAlive`는 계속 미정으로 재확인 + +사용자가 `.claude/question.md`의 3순위(사소함) 용어 재검토 목록을 훑으며 +한 번에 여러 개를 정리 — 전부 `.claude/question.md` "1. 용어 정리" 절과 +`base/module-lifecycle-plan.md`에 반영 완료: + +- **`Ref`/`PreRef`/`Peek`/`isState` — 전부 현재 이름 그대로 확정(더 나은 + 대안 없음).** `Ref`는 "지연 없는 확정된 값 박스"라는 정의를 재확인 — + leaf 노드를 담는 용도로도, leaf 노드에 바인딩하는 용도로도 쓰인다는 게 + 넓어진 정의에도 여전히 맞다는 근거. +- **`None`/`NoneHandler` — 확정.** `Undefined`/`Null`/`Nothing`도 검토했으나 + `Null`은 보통 "포인터가 비어있음"(0)을 뜻해 "값이 없음"이라는 의도와 + 미묘하게 안 맞는다는 이유로 기각, `None`이 나음. +- **"프로바이더" → `Handler`로 확정, 기각 이유 보강.** 이미 + `module-lifecycle-plan.md`에 [해소됨]으로 반영은 돼 있었으나 + `question.md` 목록에 stale로 남아있던 걸 정리. `Processor`는 계약 + 메소드 자체가 `process`라 이름이 겹쳐 거슬림, `Provider`는 + `canProvide`처럼 "공급한다"는 늬앙스인데 실제로는 처리/반응하는 + 쪽이라 안 맞고 React `Context.Provider`류와도 헷갈릴 수 있음, `Plug`는 + "꽂힌다"는 어감은 맞지만 "처리한다"는 의미가 빠져있음 — `Handler`가 + 계약(`isHandlable`/`priority`/`process`/`retract`) 전체를 가장 정확히 + 담는다는 결론. +- **`DI` → `D`는 아직 미확정.** 사용자가 "Declarative만 남기고 D로 + 줄이자"는 안을 제안 — Instance 전용이 아니라 quad-* 전반의 declare + 요소로 확장 가능하고, 엔진 종속 없이 재사용 가능하며, `D.FrameModifier` + 류 타입 프리픽스가 짧아야 한다는 실용적 이유까지 근거로 나쁘지 않은 + 제안이나, 한 글자 식별자의 검색성/자기설명력 트레이드오프를 문서에서 + 어떻게 보완할지가 남아 다음에 마저 결정하기로 함. +- **`canExecute` → `isAlive`도 계속 미정, 방향만 정리.** `isAlive`가 의미는 + 더 정확하지만 top-level `isX` 타입 판별자 계열(`isState`/`isRef`/ + `isPreRef`/`isModifier`/`isObserver`)과 접두어가 겹쳐 "이것도 타입 + 체크인가" 오해를 유발할 위험이 지적됨 — `canExecute`는 타입이 아니라 + liveness를 묻는 질문이라 `is`보다 `can` 계열 접두를 유지하는 쪽으로 + 사용자가 기욺, 구체 대안(`canRun` 등)은 다음에. +- `Brand`는 이번에도 다시 짚었지만 여전히 미정으로 재확인만 함. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `DI`/`D`와 +`canExecute`/`isAlive` 두 개만 용어 정리 라운드에 계속 남음 — +`question.md` 1순위/3순위 목록 참고. + diff --git a/.claude/session/2026-08-09-01-canbound-modifier-error.md b/.claude/session/2026-08-09-01-canbound-modifier-error.md new file mode 100644 index 0000000..94781af --- /dev/null +++ b/.claude/session/2026-08-09-01-canbound-modifier-error.md @@ -0,0 +1,90 @@ + + + + +## 2026-08-09 세션 — `canBound` 이름 확정, `:Compute`의 `previous` 방어/스코핑 +명확화, Modifier 핸들러 계층 값 UB→error 전환, Tween `initValue`/`useTween` +논의 신설 + +사용자가 `.claude/question.md`를 훑다 나온 여러 짧은 질문/제안을 한 번에 +처리. 전부 `base/`/`research/`에 반영 완료: + +1. **`Bound` → `canBound(handle): boolean` 탑레벨 함수로 확정** — 사용자 + 제안("canExecute 같은 게 있으니 canBound로 넣어도 되지 않나"), raw + 불리언 필드를 직접 노출하는 대신 `canExecute`와 같은 결의 predicate + 함수로 감쌈. 동작 자체(leaf 부착과 `:Subscribe()`는 상호 배타, 위반 + 시 즉시 에러)는 안 바뀜 — `base/bind-system-plan.md` "이중 바인딩 + 금지" 절, `base/effect-plan.md`, `.claude/question.md` 반영. +2. **`:Compute(fn)`의 `previous` 인자 — 오버엔지니어링 의심 기각, 현재 + 설계 유지.** `pre-implementation-audit.md` 3-1이 "클로저 업밸류로 + 이미 되는 걸 별도 API로 만든 것 아니냐"고 의심했던 데 대해 사용자가 + 직접 반박 — 클로저 업밸류 대안은 IIFE로 감싸는 준비 비용이 오히려 + `previous`라는 인자 하나보다 무겁고 번거로움. **부수적으로 스코핑도 + 명확화**: 처음엔 `self.Cache`처럼 `previous`를 `self`(입력) 쪽에 + 얹는 모양이 제안됐으나, `self`는 `:Compute`의 입력(receiver)이라 + 같은 `self`에서 여러 `:Compute`가 갈라지는 팬아웃(`w:Compute(g1)`, + `w:Compute(g2)`)이 있으면 `self.Cache` 슬롯이 충돌한다는 문제를 + 검토 중 발견 — `previous`는 그 대신 "이 `:Compute` 호출 하나가 만든 + 결과 State 노드" 자신에 귀속되는 것으로 정리(State가 호출마다 새 + 노드를 만든다는 기존 온톨로지의 당연한 귀결이라 새 결정은 아님). + `base/bind-system-plan.md`의 "previous" 절, `pre-implementation-audit.md` + 3-1 반영. +3. **Modifier 필드에 핸들러 계층 값(Ref/PreRef/Observer/Effect/Slot/ + Modifier)이 들어오면 UB 대신 즉시 `error`로 확정.** 기존 + "권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는 UB"였던 + 것을, 이런 값의 실사용 case가 없다는 게 확인된 이상 조용한 UB보다 + 그 자리에서 막는 쪽이 낫다는 사용자 판단으로 전환 — 이미 있는 + `Brand` 기반 predicate(`isRef`/`isPreRef`/`isObserver`/`isEffect`/ + `isSlot`/`isModifier`)를 제네릭 `__index` setter가 최종 저장 직전에 + 확인하기만 하면 되므로 구현 비용 거의 0. `isSlot`/`isEffect` + predicate가 `Brand` 절에 명시적으로 없던 갭도 같이 보강. + `pre-implementation-audit.md`가 지적했던 "`State`는 방어, + Ref/Slot은 무방비"라는 비일관성이 이걸로 절반 해소(메커니즘 차이는 + 남지만 "막을 가치가 있다"는 판단은 통일) — `base/modifier-plan.md` + "핸들러 계층을 모름" 절, `base/bind-system-plan.md`의 `Brand` 절, + `pre-implementation-audit.md` 문서모순 절 반영. +4. **UI shorthand(UICorner/UIPadding/UIScale)가 Modifier 체이닝에서도 + 되는지 — 이미 확정돼 있던 것 재확인, 새 결정 없음.** `mod:UICorner(8)`은 + 그냥 제네릭 `__index` setter가 `UICorner` 필드를 채우는 것뿐이고, + 그 필드가 Modifier flatten을 거쳐 최종 props 테이블에 얹히든 + `Frame { UICorner = 8 }`처럼 순수 인라인으로 들어가든 UICorner + Handler 입장에선 구분이 없음 — `base/ui-shorthand-plan.md`에 이미 + 명시돼 있던 내용이라 문서 변경 없음. +5. **Tween `initValue`/`useTween` — 새 열린 논의 신설, 확정 아님.** + 사용자가 두 실사용 시나리오(다이얼로그 진입 애니메이션, 트윈 우회)를 + 제기 — `initValue`(첫 마운트 시 시작값을 세팅 후 목표값으로 트윈)는 + 재검토 끝에 필요성이 낮은 쪽으로 기움(재process 시 "최초 1회" 판별 + 문제가 있어 보임), `useTween = state`(트윈을 끄고 즉시 + 스냅)은 필요성은 확인됐으나 정확한 모양/문서화 방식이 전혀 안 + 정해짐 — `research/tween-plan.md`에 신규 절로 반영, M11 착수 전 + 나중 세션에서 마저 정리하기로 함. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 +설계 확정/보강이라 M0 착수 우선순위 자체는 그대로. + +**같은 세션 후속 — `State`도 UB 대신 명시적 `error`로 확정, +"핸들러 계층 값 → error" 원칙을 State/Source 쪽까지 완전히 통일.** +사용자 질문: "Modifier 필드"뿐 아니라 "State/Source 자체의 값이 +Modifier인 경우"(`State`, `modifier-plan.md` 7번)도 같은 +방식으로 막아도 되는지 — 확정. `isModifier` predicate를 +`Source:Set()`/Store 생성 시 eager `Source(default)`/State의 +`:Compute` 결과 캐싱 지점에서 확인해 런타임 `error`, 타입 차단(Luau +가능 여부 미검증)은 필수 방어선이 아니라 되면 좋은 보너스로 격하. +**Slot은 대조적으로 계속 허용** — 사용자 확인("slot은 당연히 가능함, +retract도 되는 애고 런타임 값이라"): Slot/Tag/Attribute/Tween 등은 +정상적으로 process/retract 재귀 경로를 타는 진짜 dispatch 참가자라 +State/Source 값으로 담겨도 기존 재귀 재-dispatch가 그대로 처리해줌 — +Modifier만 예외인 건 Modifier가 애초에 dispatch 경로 자체를 안 타는 +유일한 존재라서. `base/modifier-plan.md` 7번, `base/store-semantics.md` +"따름정리" 절, `research/pre-implementation-audit.md` 2-2/문서모순 절 +(완전 해소로 갱신), `.claude/question.md`, `ROADMAP.md` M7 반영 완료 — +이걸로 `pre-implementation-audit.md`가 지적했던 "State는 +방어, Ref/Slot은 무방비"라는 비일관성이 완전히 해소됨. + +**핸드오버 준비 완료** — 이번 대화(2026-08-08~09에 걸친 세션)에서 나온 +결정은 전부 `base/`/`research/`/`question.md`/`ROADMAP.md`에 반영, +문서 간 참조도 동기화 완료. **다음 세션 예고(사용자 지정)**: Slot과 +"State에서 Slot을 뽑아내는" 키 기반 동적 컬렉션 재조정(가칭 `Keyed`는 +탈락, 최종 이름 미정) — `.claude/question.md` 0번 "키 기반 동적 +컬렉션 재조정"이 이미 최우선 항목으로 잡혀있으니 그걸 이어서 보면 됨. + diff --git a/.claude/session/2026-08-09-02-corpus-audit-archive-move.md b/.claude/session/2026-08-09-02-corpus-audit-archive-move.md new file mode 100644 index 0000000..2efa41f --- /dev/null +++ b/.claude/session/2026-08-09-02-corpus-audit-archive-move.md @@ -0,0 +1,77 @@ + + + + +## 2026-08-09 두 번째 세션 — `.claude/` 코퍼스 전체 stale 마커 감사·수정, +무효화된 인라인 서사 archive 이전 + +새 설계 결정 없음, 순수 문서 정리 세션. 서브에이전트 4개를 병렬로 띄워 +`.claude/` 전체(30여 개 문서 + `ROADMAP.md`/`HUMAN_TODO.md`/`SAFETY.md`/ +`archive/`)를 클러스터별로 감사, "이미 해소됐는데 미해결로 표시된 것"과 +"문서 간 모순"을 찾아 전부 직접 수정(커밋 전 상태 기준). 이어서 사용자 +요청으로 두 번째 라운드 — 뒤집혔거나 무효화된 설계가 정정 표시만 붙은 채 +본문에 전체 서술로 남아있는 곳을 찾아 기존 `archive/*-reversed.md`/ +`*-rejected.md` 컨벤션대로 이전(본문엔 결론+포인터만 남김), 컨텍스트 +낭비 방지 목적. 이것도 서브에이전트 3개 병렬 감사로 후보를 찾은 뒤 직접 +판단해 적용. + +**1차 라운드 — stale 마커/모순 수정 (7개 파일)**: +- `bind-system-plan.md`: `Ref` 이름이 "용어 정리 재검토 대상"으로 남아있던 + 것 — 2026-08-08 다섯 번째 세션에서 이미 확정됐는데 반영 안 됨 → 해소 + 표시로 정정. `component-composition-plan.md` §4-2 인용 오류(그 절은 + 실제로 다른 내용을 다룸 — Ref 필드 충돌 없음의 근거를 잘못 인용)와 + 폐기된 `StoreSource` 프록시와 혼동될 수 있는 "Source 양방향 프록시" + 표현도 정정. +- `documentation-content-map.md`: 폐기된 `myStore.key = value` 대입 + 문법이 예시로 남아있던 것(같은 파일 바로 다음 줄은 `:Set()`으로 옳게 + 써서 자기모순) → 정정. +- `ROADMAP.md`: 세션 인용 오류 2건(`git blame`으로 실제 커밋 시점 확인해 + 정정 — M0의 Source/State 서브타입 항목은 "세 번째 세션", M2의 + `LifetimeHandle` 순서 역전 항목은 "네 번째 세션"이 맞음), `Bound`/ + `None` "가칭" 표기가 이미 이름 확정됐는데 안 지워진 것 2건 정정, M6에 + Slot CRUD 의미론 확정 체크박스 누락돼 있던 것 추가(`pre-implementation-audit.md` + 우선순위1이 이미 지적했던 갭). +- `question.md`: `Tag`/`Added`/`Removed`/`Merged`가 `tag-plan.md`에서 + "여기서 추적 중"이라 주장했지만 실제로 빠져있던 것 추가. +- `archive/context-rejected.md`: 다른 archive 문서와 달리 base/ 포인터가 + 없던 것 보강. +- `additional-primitives-plan.md`: State/Observer를 "독립 프리미티브"로 + 잘못 묶은 표현 정정(확정된 분류는 Source/Store/Ref/Modifier/Slot/DI= + 독립 프리미티브, State/Observer=파생 데이터, 2026-08-08 두 번째 세션 + "Handler는 세 번째 카테고리" 절 참고). + +**2차 라운드 — 무효화된 인라인 서사를 archive로 이전 (신규 archive 4개)**: +- `archive/quad2-try-research-findings-rejected.md` — `bind-system-plan.md`에 + 60줄 넘게 남아있던 quad2-try(폐기된 이전 재작성 시도) 리서치 전문(OOP + 상속/커스텀 파서/Slot 빈 스텁/`Pipe` copy-on-write 4가지 확인된 죽은 + 접근 + Unix 파이프 영감이라는 원래 동기 서사)을 통째로 이전 — "반복 + 조사 금지" 결론과 `state(state)` 조합 모델 포인터만 본문에 남김. +- `archive/observer-cleanup-contract-rejected.md` — `effect-plan.md`의 + "Observer 자체에 React `useEffect`식 cleanup 반환 계약을 추가하는 안" + 기각 서술(코드 예시 포함) 이전. +- `archive/keyed-collection-state-method-rejected.md` — `additional-primitives-plan.md`의 + "키 기반 동적 컬렉션 재조정을 `state:Keyed(...)` State 메소드로 두려던" + 초안 기각 서술 이전(이 프리미티브 자체는 여전히 열린 질문 — 폼 팩터 + 결정 부분만 이전됨). +- `archive/debug-channel-replicatedstorage-rejected.md` — `debug-tooling-plan.md`의 + "`ReplicatedStorage` 자동 생성" 초안 기각 서술 이전. + +각 archive 파일은 기존 컨벤션(`[기각됨]` 제목, "현재 유효한 설계" 포인터, +`quadnomicon` 소재 메모)을 그대로 따름, `README.md`의 archive 인덱스도 +4개 항목 추가로 동기화 완료. + +**의도적으로 손 안 댄 것들**: `bind-system-plan.md`의 PreRef pre-pass +위치 관련 기각 서술, `lifecycle-pattern.md`의 `canExecute` 시그니처 +재정정 단락, `modifier-plan.md` 9-1(b)의 "동질적/이질적" 초안 — 전부 +현재 설계를 정당화하는 근거로 너무 밀착돼 있어서, 분리하면 "왜 이렇게 +안 했는지"가 같이 잘려나가 다음 에이전트가 같은 대안을 또 검토할 +위험이 있다고 판단해 그대로 둠. `documentation-content-map.md`가 최근 +추가된 5개 base 문서(`relate`/`blocker`/`effect`/`tag`/`attribute`-plan.md)의 +초심자/api/심화 분류를 아직 안 갖고 있는 것도 실제 설계 판단(콘텐츠 +분류)이 필요해 손 안 댐 — 문서 자신도 이미 "지금 당장 안 급함"이라고 +인정하고 있음. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, 위 "다음 세션 예고" +Slot/키 기반 컬렉션 재조정도 그대로) — 이번 세션은 순수 문서 위생 +작업이라 설계 우선순위엔 영향 없음. + diff --git a/.claude/session/2026-08-09-03-slot-crud-list.md b/.claude/session/2026-08-09-03-slot-crud-list.md new file mode 100644 index 0000000..f3b1e61 --- /dev/null +++ b/.claude/session/2026-08-09-03-slot-crud-list.md @@ -0,0 +1,294 @@ + + + + +## 2026-08-09 세 번째 세션 — Slot CRUD 완전 확정, 키 기반 동적 컬렉션 +재조정이 `Slot:List(...)` 메소드로 통합·승격 + +위에서 예고된 "다음 세션 주제"(Slot과 키 기반 동적 컬렉션 재조정)를 +실제로 다룬 세션. `pre-implementation-audit.md` 1-7/1-8과 +`research/additional-primitives-plan.md`의 마지막 열린 항목이 전부 +`base/slot-plan.md`에 흡수·확정됐음 — 상세는 그 문서 본문이 소스, +여기는 요지만: + +- **Slot CRUD 최종 확정**: `Add(element, index?)`/`Remove(element)`(제거+파괴)/ + `Extract(element)`(제거, 파괴 안 함)/`Clear()`(전체 `Remove`) — `get`/`set`은 + 드롭(YAGNI). 식별은 항상 element 레퍼런스 기준(인덱스 아님). 에러 조건 + 전부 즉시 `error()`(이미 다른 곳에 마운트된 element를 `Add`, 멤버 아닌 + element를 `Remove`/`Extract`) — fail-fast 톤 유지. 재진입성은 별도 가드 + 불필요(기존 "무한루프 방어 안 함" 원칙 재사용). `Slot()`은 인자 없는 + 빈 생성자로 확정. +- **`isMounted` 이중 추적 분리(1-8 해소)**: Slot 컨테이너 자신은 + `self._mounted`(트리거는 `Dispatch.process`가 이 Slot 객체에 실제로 + 호출된 시점 — 다른 모든 "마운트됨" 판정과 동일하게 dispatch-process + 기준), 개별 element는 전역 weak-set(라이브러리 전역 다중 마운트 금지 + 불변식이라 특정 Slot에 안 묶임). +- **`Extract` 후 portal 범위 — 임의의 다른 Slot으로 자유 이동 확정.** + 기존 "retract되는 slot은 폐기되지 옮겨지지 않는다"는 확정은 **프레임워크가 + store-bind 재실행으로 값을 통째로 갈아치우는 시나리오**에만 해당하고, + 사용자가 명시적으로 `Extract`→`Add` 두 번 호출해서 옮기는 것과는 다른 + 얘기라는 걸 명확히 구분(사용자 확인). +- **키 기반 동적 컬렉션 재조정 — `Slot:List(data, keyFn, renderFn) -> Slot`로 + 확정, 자유 함수/새 타입 둘 다 기각.** 처음엔 `List(...) -> Slot` 자유 + 함수를 검토했으나, "타입 이름=반환 타입"이라는 `Source(default)`류 + 팩토리 컨벤션이 깨진다는 문제를 사용자가 직접 지적 — Source⊇State식 + 구조적 서브타입도 검토했으나 List가 Slot 위에 새 공개 메소드를 안 + 얹으므로(그냥 "자동으로 채워지는 Slot") 별도 타입일 근거가 약해 기각. + 최종적으로 "원천에 종속된 파생 데이터는 메소드로만 얻어진다"(State/ + Observer와 같은 원칙, 여기 원천은 Slot 자신)로 수렴 — `Ref():Callback(fn)` + 체이닝과 같은 패턴. Fusion `ForPairs`/`ForKeys`/`ForValues` 3분할도 + 단일 `:List`로 통합 확정. +- **구현 메커니즘은 전부 기존 프리미티브 재사용, 새 개념 없음** — 사용자가 + "너무 마법같다"고 지적해 의사코드까지 구체화해서 검증: `data:Observer(fn)` + (2026-08-07 확정된 "등록 즉시 1회 실행"), `Source(item)`, 방금 확정한 + Slot CRUD의 비공개(가드 안 거치는) 버전 세 개의 조합일 뿐. `itemSources`/ + `elements`/`order`는 Slot 인스턴스의 평범한 클로저 업밸류(별도 전역 + 저장소 불필요). 리오더는 `Extract`+`Add(index)` 조합, 최소-이동 + 알고리즘 자체는 구현 시점 최적화로 미룸. +- **`renderFn(key, itemState)`의 `itemState`는 내부 `Source`를 그냥 + `State`로 다운캐스트해서 넘김 — 별도 `ReadOnlySource` 타입 안 만듦** + (사용자 확인: "그걸 위해 ReadOnlySource 같은 걸 만들 이유가 있냐 하면 + 아니다, 이미 그게 State다"). 타입 레벨 힌트만, 런타임 강제 없음(`Peek`/ + Modifier UB와 같은 "규율 위반은 방어 안 함" 기조) — 나중에 진짜 + 런타임 강제가 필요해지면 `src:Compute(function(v) return v end)`(항등 + 함수 Compute)로 `:Set` 없는 State를 만드는 가벼운 대안이 있다는 것만 + 메모. +- **백로그, 착수 안 함(연구만) — reconcile의 무조건 `:Set()` 재전파.** + `data`가 테이블 뮤테이션+`:Emit()`으로 오는 경로도 지원해야 해서 이전 + 값과 동등성 비교를 할 방법이 없고, 그래서 값이 실제로 안 바뀐 item도 + 매 재계산마다 재전파됨 — 사용자 판단: "이 재계산 비용은 우리가 핸들해야 + 할 부분은 아닌 것 같다", `Blocker`류 값-동등성 기반 전파 억제도 검토했으나 + "확정 안 하면 이전 값 자체가 없어서 비교가 안 된다"는 근본적 어려움이 + 있어 기술적으로 더 논의해볼 만한 주제로만 `research/ + additional-primitives-plan.md`에 백로깅. +- **`research/additional-primitives-plan.md` 사실상 전부 해소** — 마지막 + 열린 항목(키 기반 컬렉션)까지 없어져서, 이 문서엔 이제 새로 열린 설계 + 질문이 없음(배경 자료로만 유지). `question.md`/`ROADMAP.md`(M6 체크박스)/ + `README.md` 전부 동기화 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — Slot/키 기반 컬렉션 +재조정이 이번 세션에서 완결됐으므로 더 이상 "다음 세션 예고" 대상 아님. +남은 열린 것은 여전히 `question.md`의 `DI`→`D`/`canExecute`→`isAlive`/ +`Brand` 이름, `pre-implementation-audit.md` 1-3(우선순위 스캔 동률 처리), +"여러 Slot이 형제로 섞일 때 순서 보장"(Roblox 단일 백엔드론 급하지 않음) +정도. + +**같은 세션 후속 — quad-roblox 구현 관점에서 재검토, `Move`/`Swap` 공개 +CRUD로 추가(원시 최소화 원칙 뒤집음), `renderFn`에 `indexState` 추가.** +사용자가 "Slot 값 변경을 quad-roblox가 실제로 어떻게 따라가나"를 구체적으로 +캐물으며 세 가지가 드러남: +- **`renderFn(key, itemState) -> element`에 위치 정보가 빠져있었음** — + Roblox는 순서를 `LayoutOrder`로 표현하므로 `renderFn`이 그걸 반응형으로 + 바인딩하려면 위치도 State로 받아야 함. `itemState`와 독립된 + `indexState: State`를 추가(`renderFn(key, itemState, + indexState)`) — 값 변경/위치 변경은 서로 독립 신호라는 게 근거, Slot이 + `LayoutOrder`를 대신 관리해주는 마법은 안 둠. +- **`Extract`+`Add(index)`로 리오더를 구현하면 백엔드에서 진짜 Parent + 조작이 두 번(detach+reattach) 일어난다는 게 드러남** — Roblox + `AncestryChanged` 발화, 잠재적 깜빡임, 불필요한 재바인딩 비용까지 + 딸려올 수 있어 매 `:List` 재계산마다 흔한 케이스치고 과함. + **`Move`(O(n), 배열 splice 의미)/`Swap`(O(1), 순수 페어 교환)을 공개 + CRUD로 추가** — 둘 다 Parent를 안 건드림. `:List` 없이 수동으로 Slot을 + 구성하는 사용자에게 애초에 리오더 수단이 아예 없었다는 것도 같이 + 드러난 공백 — "원시 연산 최소화" 원칙보다 이 두 실사용 공백이 우선한다고 + 판단해 뒤집음(같은 세션 내 정정이라 별도 archive 없이 `slot-plan.md` + 본문에 "원시 최소화 원칙 정정" 절로 직접 반영). +- **base/roblox 패키지 경계에 mount/unmount 둘로는 부족, reposition + 훅이 세 번째로 필요함** — `Dispatch/Slot.luau`/`Handlers/Slot.luau`가 + 이제 "Parent 조작(mount/unmount)"뿐 아니라 "Parent 안 건드리는 재배치 + (reposition, `Move`/`Swap`)"까지 계약해야 함. quad-roblox가 이걸 + `SetSiblingIndex`로 구현할지, `LayoutOrder` 기반 정렬이라 사실상 no-op + 으로 둘지는 구현 선택으로 열어둠. +- **item 값 전파(무조건, 백로그)와 index 전파(실제 변경시만)가 비대칭인 + 이유도 명확해짐** — item 값은 외부 뮤테이션+`Emit()` 경로 때문에 "이전 + 값"을 비교할 방법이 없지만, `:List`가 전적으로 소유하는 `keyIndex`는 + "실제로 위치가 바뀌었는지"를 정확히 알 수 있어 index 쪽엔 같은 문제가 + 없음 — 그래서 index 전파는 처음부터 조건부로 구현. + +전부 `base/slot-plan.md`(CRUD 표, "원시 최소화 원칙 정정" 신규 절, `:List` +구현 스케치·설명 갱신)/`ROADMAP.md`(M6)/`README.md` 반영 완료. `question.md`엔 +새로 열린 항목 없음 — 이번 후속도 순수 확정/구현 세부 명확화. + +**같은 세션 두 번째 후속 — `Swap`을 element 레퍼런스가 아니라 인덱스 +기준으로 정정, "공개 CRUD는 가드+`raw*` 위임" 구조 명문화.** 사용자가 +`Swap(elementA, elementB)`를 바로 잡음 — element 레퍼런스로 받으면 Slot이 +element→index 역방향 맵을 안 갖고 있는 이상 두 element의 현재 위치를 각각 +찾는 데 O(n)씩(총 2n) 들어서, `Swap`이 약속한 O(1)이 그 자리에서 깨짐. +`Move`는 시프트 자체가 O(n)이라 조회 비용이 묻히지만 `Swap`은 조회 비용이 +곧 전체 비용이라 이 차이가 그대로 드러남 — `Slot:Swap(indexA, indexB)`로 +정정(호출부가 이미 "몇 번째와 몇 번째를 바꿀지"를 아는 상황, 예: 드래그 +리오더 UI, 이라는 것도 자연스러움의 근거). 이어서 사용자가 "`Slot:Move` +구현은 결국 락(`_listed`) 확인만 하고 실제 로직은 `rawMove`류에 다 있는 +구조 아니냐"고 확인 요청 — 맞다고 답하며 이걸 여섯 CRUD 전체에 적용되는 +일반 구조로 명문화: `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` 전부 +"`self._listed` 확인 + `raw*` 위임"뿐인 얇은 wrapper, 실제 로직은 전부 +`raw*` 함수 세트 하나에 있고 `:List`의 reconcile도 그 세트를 가드 없이 +직접 호출. 전부 `base/slot-plan.md`/`ROADMAP.md` 반영 완료. + +**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). + +**같은 세션 세 번째 후속 — Slot 요소 타입 제약 신설: `nil` 금지/`None` +허용/핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier) 금지, `Slot()` +제네릭화.** 사용자가 "Slot 안에 뭐가 들어갈 수 있는지 정해진 바 없다"고 +지적하며 시작 — 처음엔 제가 "Ref/Observer/PreRef도 Slot 요소로 허용, +`D.InstSlot = Slot<>`류 백엔드 별칭으로 좁히자"고 제안했으나, +사용자가 바로 반박: Slot이 동적으로 다뤄지는데 그 안에 Ref/Observer가 +들어가면 quad-roblox가 그걸 처리할 방법이 없고(특수 대응을 새로 만들어야 +해서 오버엔지니어링),애초에 왜 필요한지도 불명확하다는 지적 — 검증해보니 +정확히 맞았음: +- `Dispatch/Leaf.luau`가 처리하는 "children 배열에 Ref/Observer/PreRef가 + 직접 놓이는" 케이스는 **그 컴포넌트가 지금 만들고 있는 Instance 자기 + 자신을 가리키는 self-ref 캡처**라(`Frame { PreRef():Callback(fn) }`가 + 그 Frame 자신을 잡음), `inst`가 "지금 생성 중인 바로 그 하나의 Instance"로 + 고정돼 있어야 의미가 성립함. Slot은 특정 컴포넌트 호출 하나에 안 묶이고 + 이미 존재하는 부모에 나중에 독립적으로 붙는 동적 리스트라 이 전제 + 자체가 없음 — Slot 안의 Ref가 "무엇"을 가리켜야 하는지 정의가 안 됨. +- 대체 경로가 이미 있어 능력 손실도 없음 — 특정 child에 ref가 필요하면 + 그 child를 만드는 컴포넌트 호출 자체에 Ref를 넘기면 됨 + (`slot:Add(Frame { Ref = myRef })`). +- 사용자가 직접 대비시킨 반례도 정확함: `State`(Slot 자체가 State의 + 값)은 retract 시 통째로 버려지고 다시 채워지는 굵은 단위 교체라 이미 + 확정된 모델(폐기, 재구성)과 맞지만, Slot **요소 하나하나**로 + Ref/Observer가 들어가는 건 그런 굵은 단위 교체가 아니라 세밀한 CRUD + 대상이라 성격이 다름. +- **결론**: `Modifier` 필드가 핸들러 계층 값을 담으면 즉시 `error`로 + 확정했던 것과 같은 판별 메커니즘(`isRef`/`isPreRef`/`isObserver`/ + `isEffect`/`isModifier` Brand predicate)을 Slot에도 재사용 — 새 + 메커니즘 없이 그대로 막음. 덕분에 `Slot`의 `T`도 "실제로 마운트 + 가능한 최종 값의 타입"으로 단순해짐 — quad-roblox엔 사실상 `T = + Instance` 하나뿐이라 `D.InstSlot = Slot<>`가 사실상 "그" + Slot 타입. `nil`은 기존 배열 파트 `None` 원칙을 그대로 적용해 금지, + `None`은 `:List`의 `renderFn`이 "이 item은 이번엔 스킵"을 표현하는 + 용도로 허용 — `renderFn`의 반환 타입도 `T | None`으로 갱신. +- `Slot()`가 무인자 생성자라 `T` 추론이 안 되므로 tbox 명시적 제네릭 + 적용(`Slot<>()`)이 필요하다는 것도 같이 반영 — 정확한 문법은 + "자식으로 넘기는 클래스 스토어" 절의 기존 tbox 참고 미결과 같은 갈래로 + 묶어 열어둠. + +전부 `base/slot-plan.md`(신규 "요소 타입 제약" 절, CRUD 에러 조건, +`renderFn` 반환 타입) 반영 완료. + +**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). + +**같은 세션 네 번째 후속 — `Slot:List`의 `renderFn`을 "1회 호출"에서 +"매 사이클 호출 + `before` 재사용"으로 재설계, filter/toggle 문제 해결.** +사용자가 두 가지를 연달아 제기: (1) `renderFn`이 `None`을 반환해 "지연 +렌더"를 표현하는 아이디어는 좋지만, State 변경으로 이미 렌더된 필드를 +나중에 다시 지워야 하는 경우(filter)는 기존 "1회만 호출" 모델로 안 풀림. +(2) filter/sort를 Slot에서 어떻게 구현할지가 문제 — 흔한 회피책인 +"`Visible`만 토글"은 필터링된 item도 여전히 완전히 살아있는 Instance로 +남겨서(애니메이션/이벤트 연결 계속 돎) 200개+ 리스트에서 lazy하지 않다는 +실질적 비용이 됨. + +**해법 — 사용자가 직접 제시**: `renderFn(itemState, before: inst?): inst?` +모양으로 바꿔 **매 reconcile 사이클마다 호출**하되, 이전에 마운트된 +element(`before`, 없으면 `nil`)를 받아서 `if before then return before +end`(바꿀 거 없으면 그대로 반환, 값 갱신은 이미 물려있는 반응형 바인딩이 +자동으로 함)로 저비용 재사용 경로를 만듦 — filter 탈락 시엔 `nil` 반환으로 +**진짜 파괴**(Visible 토글 아님). 편의상 `renderFn`이 raw `nil`을 +던지는 게(Lua에서 자연스러운 관용구) `None`보다 편하다는 것도 사용자가 +지적 — 검토 결과 `renderFn`의 반환값은 raw Slot 요소로 직접 들어가는 +게 아니라 `:List`의 reconcile이 해석만 하는 것이라, `nil`을 받아도 위 +"요소 타입 제약"(raw Slot 요소는 `nil` 금지)과 전혀 안 부딪힘 — `nil`/ +`None` 둘 다 "스킵" 신호로 동일하게 받아들이기로 정리. + +**부수적으로 드러난 것 — "이전 상태를 다음 렌더에 어떻게 넘기냐" 문제는 +이미 해소돼 있었음.** 사용자가 "item이 보통 plain table이라 매 렌더마다 +Source/Store를 새로 안 만들려면 이전 상태를 어딘가 저장해야 하는데 그게 +어렵다"고 우려했으나, 확인해보니 `itemSources[key]`/`indexSources[key]`가 +`renderFn` 호출 여부와 무관하게 **처음부터 `:List` 자신이 계속 소유**하고 +있어서(원래 설계 그대로) — `renderFn`이 매 사이클 불려도 이 부분은 전혀 +안 바뀜, item이 filter 탈락 후 재등장해 Instance가 파괴됐다 새로 만들어져도 +반응형 Source는 안 끊기고 그대로 이어짐. 이 부분은 재설계가 아니라 기존 +설계가 이미 답이었다는 걸 확인한 것. + +**sort는 이번 재설계와 무관** — 호출부가 `data` 순서를 바꾸면 기존 +`keyIndex`/`Move` 메커니즘이 이미 처리, 새로 손댈 것 없음(사용자가 filter와 +같이 물었던 것 중 이건 원래도 문제 없었음). + +전부 `base/slot-plan.md`(요소 타입 제약 절 "None 허용" → "nil/None 둘 다 +금지"로 정정, `:List`의 `renderFn` 시그니처·구현 스케치·"왜 매 사이클 +호출로 바뀌었는가" 신규 절)/`ROADMAP.md`(M6)/`README.md` 반영 완료. + +**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). + +**같은 세션 다섯 번째 후속 — `renderFn` → `updateFn` 개명, `:List`가 +`Source` 생성을 그만두고 `userdata`로 그 권한을 통째로 넘김.** 사용자가 +"`renderFn`이 아니라 `updateFn`이 맞고, `itemState`도 `:List`가 강제로 +만들지 말고 원문 item + `userdata: UD?` + `prev: T?`를 주는 게 낫다"고 +제안 — 검토 후 채택, 근거: +- **`itemState`/`indexState`를 `:List`가 강제로 만드는 건 불필요한 강요였음** + — 반응형이 필요 없는 단순한 행까지 전부 `Source` 생성 비용을 지게 + 했음. `userdata`로 권한을 넘기면 필요한 item만 자기 `Source`를 만들어 + `userdata`에 담고, 나머지는 매번 raw `item`에서 다시 계산해도 됨 — + `:List`가 미리 정할 이유가 없는 선택. +- **"이전 상태를 다음 호출에 넘기는" 문제, 원래 걱정했던 것과 달리 + `userdata`라는 명시적 채널로 완전히 해소됨** — item이 plain table이라 + 매번 `Source`를 새로 안 만들려면 어딘가 저장해야 한다는 우려가 있었는데, + `userdata`가 정확히 그 저장소. +- **`prev`(구 `before`)와 `userdata`가 원래 비일관적이었음** — 사용자가 + 직접 지적: 하나(`prev`)는 `:List`가 자동 관리하는데 다른 + 하나(`userdata`)만 수동 반환을 요구했음. 해법은 **둘 사이 커플링을 + 완전히 제거** — `result`가 `nil`이라고 `:List`가 `userdata`를 자동으로 + 안 지움, 그대로 기록만 함. 흔한 경우(둘 다 리셋)는 `return nil` 하나로 + Lua가 나머지 반환 슬롯을 알아서 `nil`로 채워주고, "파괴하되 캐시는 + 남기고 싶다"는 정당한 패턴은 `return nil, ud`로 명시적으로 표현 + 가능해짐 — 이전 설계(result nil이면 userdata 자동 삭제)로는 이 패턴이 + 원천 봉쇄돼 있었음. +- **제가 놓칠 뻔한 버그를 사용자와의 논의 과정에서 직접 잡음**: `userdata`가 + 이제 `mounted`(실제 element)보다 오래 살 수 있게 되므로, 정리 루프가 + `pairs(mounted)`만 순회하면 "필터 탈락 상태(mounted=nil)로 `userdata`만 + 살아있던 key가 `data`에서 완전히 사라지는" 케이스를 못 잡고 새서 + — 직전 사이클의 전체 key 집합(`keyIndex`, 매 사이클 모든 key에 대해 + 채워짐)을 순회하도록 정정. +- **부수 효과 — "item 값 무조건 재전파" 백로그가 사라짐**: `:List`가 + 더 이상 `Source`를 안 만드므로 그 문제 자체가 `:List` 소관이 아니게 + 됨, `updateFn` 작성자의 선택으로 넘어감. +- `userdata = userdata or {}`류 lazy-init 관용구가 `UD`가 자유 제네릭인 + 채로 Luau 타입 시스템에서 잘 좁혀지는지는 실측 필요 항목으로 명시적으로 + 남김(사용자가 직접 이 불확실성을 짚음) — M0/M6 착수 시 확인. + +전부 `base/slot-plan.md`(`:List` 절 전면 재작성 — `updateFn` 시그니처/구현/ +"왜 `Source`를 `:List`가 안 만드는가" 신규 절)/`ROADMAP.md`(M6)/ +`README.md` 반영 완료. + +**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). + +**같은 세션 여섯 번째 후속 — `keyFn` 선택 인자화(파라미터 순서 정정), +`userdata` cleanup 훅 검토 후 기각·GC-native 제약 명문화, 문서화 순서 +질문은 백로그로 이관.** 사용자가 세 가지를 짧게 제기: + +1. **`Slot:List(data, keyFn, updateFn)` → `Slot:List(data, updateFn, + keyFn?)`로 파라미터 순서 정정, `keyFn` 선택 인자화.** 실사용 대부분 + (사용자 추정 80%)이 item identity 추적 없이 순번을 key로 써도 충분한 + 단순 목록이라 매번 `keyFn`을 명시하게 하는 게 불필요한 보일러플레이트 — + 생략 시 `function(item, index) return index end` 기본값. tradeoff(중간 + 삽입/삭제 시 그 뒤 항목들이 "다른 item인데 같은 key"로 오인돼 캐스케이드 + 갱신 — identity 보존 없음, 파괴/재생성 자체는 없음)는 React `key` 생략 + 시 index 기본값 등 업계 흔한 관행과 같은 결이라 새로 설명할 개념 아님. +2. **`updateFn(item?, ...)`로 바꿔 최종 제거 시 "정리용 1회 추가 호출"을 + 주는 안 — 검토 후 기각, 사용자가 직접 반례를 찾음.** 이 훅은 `data`에서 + key가 빠져 `reconcile`이 다시 도는 정상 경로에서만 발화하는데, **Slot을 + 담은 부모 Instance 자체가 `Destroy`되는(가장 흔한) 경로는 + `reconcile`이 다시 안 돌아서 이 훅이 전혀 안 불림** — 절반만 동작하는 + 정리 메커니즘은 없는 것보다 위험(사용자가 "정리가 보장된다"고 오해하고 + `Subscribe`류를 `userdata`에 넣었다가 Destroy 경로에서 조용히 샘). + `retract`가 Destroy 시 절대 안 불린다는 기존 원칙(`lifecycle-pattern.md` + "quad는 라이프사이클 중간에 있지 않다")과 정확히 같은 이유로 기각. + **대신 `userdata`엔 GC-native 값만 담고, `:Subscribe()`한 Observer류처럼 + 명시적 cleanup이 필요한 값을 담는 건 UB로 명문화** — quad 전역 + GC-native 원칙을 `:List`라는 구체 지점에 그대로 적용한 것뿐, 새 원칙 + 아님. +3. **문서화 순서(getting-started에서 단순 버전만 가르치고 나중에 + `prev`/`userdata` 최적화를 알려줄지, 아니면 Slot이 학습 순서상 후반부라 + 처음부터 완전한 형태로 가르칠지)는 결정 안 함** — `research/ + documentation-content-map.md`의 modifier/slot 절에 백로그로 추가, + 제 의견(후자 쪽으로 기욺)만 메모, 실제 콘텐츠 작성 시점 결정 사항이라 + 지금 확정 안 함. + +전부 `base/slot-plan.md`(`:List` 시그니처/코드 재정렬, `keyFn` 기본값 +설명, "`userdata`의 생명주기 제약" 신규 절)/`ROADMAP.md`(M6)/`README.md`/ +`research/documentation-content-map.md` 반영 완료. + +**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). + diff --git a/.claude/session/2026-08-09-06-length-offset-unbindlifetime.md b/.claude/session/2026-08-09-06-length-offset-unbindlifetime.md new file mode 100644 index 0000000..be2e349 --- /dev/null +++ b/.claude/session/2026-08-09-06-length-offset-unbindlifetime.md @@ -0,0 +1,114 @@ + + + + +## 2026-08-09 여섯 번째 세션 — 여러 Slot이 형제로 섞일 때 순서 보장 완전 +해소(Length/Offset), `unbindLifetime` 신설 + +**출발점**: 사용자가 미래의 `quad-web`을 가정하며 `{ Slot, Element, Slot }`처럼 +Slot이 여럿 형제로 섞일 때 최종 순서를 어떻게 보장하는지 물음 — +2026-08-04부터 "Roblox 단일 백엔드로는 급하지 않음"으로 후순위 열려있던 +질문(`slot-plan.md` "여러 Slot이 섞일 때 순서 보장" 절)을 실제로 라이브 +설계해서 완전히 풀어낸 긴 단일 스레드. 시행착오를 거쳐 최종 수렴한 결론만 +정리(중간 대안들 — "구간 예약"/`:With`+`:Compute` 체인 — 은 채택 안 됨, +사용자가 제시한 "정확한 누적합 + 플랫 재계산 루프"가 최종안): + +- **핵심 전환**: "각 원소가 절대 위치를 계산해서 전파"가 아니라 "각 + 구조적 위치가 자기 앞 형제들의 개수 누적합(`offset`)만 알면 됨" — + Roblox `LayoutOrder`가 이미 `Instance.Parent` 물리 순서와 분리된 + 정수 프로퍼티라는 사실이 이 전환을 공짜로 성립시킴. +- **`Dispatch.setLength(inst,i,len:number|State)`/ + `Dispatch.setOffsetSource(inst,i,offset:Source|None)`** — + 둘 다 Handler→Dispatch 등록(push) 방향, array part의 **모든** number + 인덱스에 대해 반드시 호출(생략 UB — Handler 구현체 작성자만의 계약, + 일반 사용자 영향 없음). `recompute`는 매번 `1..N` 전체를 도는 단순 + 루프(N은 저작 시점에 고정된 배열 리터럴 길이라 무시 가능)로, 각 + `offset:Set()` 호출 앞에서만 `Get() ~= sum` 가드를 걸어 실제로 안 + 바뀐 위치의 캐스케이드(다운스트림 `LayoutOrder` 재적용)를 막음 — + 전체 순회 비용과 `Set` 캐스케이드 비용을 분리해서 후자만 최적화. +- **각 원소의 `LayoutOrder`는 `localIndex+offset`의 State를 기존 + store-bind 프로퍼티 바인딩에 그냥 얹는 것** — 이게 이 설계의 가장 + 큰 단순화 지점: "offset 변경 시 이미 마운트된 원소를 다시 써야 한다"는 + 요구가 새 push/observer 메커니즘 없이 **이미 있는** store-bind + 재실행 모델(`state:Observer(fn):Subscribe()`) 재사용만으로 공짜로 + 풀림. +- **`setLength`의 내부 Observer는 leaf-lifetime 경로(`bindLifetime`)를 + 씀, `:Subscribe()` 아님** — 이 Observer는 특정 leaf가 아니라 `inst` + 자신에 종속된 내부 배관이라, `inst` Destroy 시 자동으로 안 죽는 + `:Subscribe()` 경로는 안 맞음. `State` 교체처럼 `inst` 전체가 + 죽기 전에 특정 위치 하나만 조기 재등록해야 하는 경우를 위해 + **`unbindLifetime(inst,value)`을 `bindLifetime`/`canExecute`의 + 세 번째 짝으로 신설** — `Dispatch.setLength`가 gchold 내부 저장 + 구조(배열/키드 테이블)를 몰라도 이전 등록을 블랙박스로 해제할 수 + 있게 캡슐화. quad-roblox 구현 스케치도 gchold를 배열 대신 `value`를 + 키로 쓰는 테이블로 바꿔 `unbindLifetime`을 O(1)로(`gchold[value] = + nil`) — base 결정은 아니고 참고용 스케치. +- **동기 순서 요구사항**: Slot의 `rawAdd`는 `Length:Set(newCount)` + (다운스트림 offset/LayoutOrder 캐스케이드가 여기서 동기적으로 끝남) + 다음에 `element.Parent = target`을 호출 — Source:Set()이 옵저버 + 체인을 동기적으로 끝까지 도는 기존 모델 덕에 별도 배리어 없이 순서만 + 지키면 자동 성립. 안 지키면 Roblox의 실시간 `UIListLayout` reflow가 + 한 프레임 잘못된 순서를 노출할 위험. +- **`Slot.Length: State`가 CRUD/`:List` 여부와 무관하게 항상 + 노출되는 프리미티브 필드로 확정** — 사용자가 직접 "n개 검색됨" UI에도 + 쓸 수 있다고 지적, `setLength`가 내부적으로 읽는 값과 완전히 동일(두 + 용도를 겸함, 별도 State 아님). `:List`의 filter=진짜 Remove 확정 + 덕에 "Visible 토글은 안 잡힘"이 자연히 성립(새 캐비엇 아님). +- **웹 백엔드(quad-web) 일반화 — base 로직 100% 재사용, backend + Handler의 "offset 변경 시 할 일"만 달라짐**: DOM `insertBefore`는 + 물리적 삽입 시 뒤 형제를 자동으로 밀어주므로, offset이 바뀌어도 + 이미 마운트된 노드를 실제로 옮길 필요가 없음 — quad-web Handler는 + offset 변경 관측 시 no-op, 숫자는 그 위치가 **다음** insert/remove + 때 쓸 물리 인덱스로만 부기됨. 처음 검토했던 "구간 예약"(고정 gap)이나 + "앵커 기반 상대 삽입" 안보다 이 방식이 dense global rank라 두 종류 + 백엔드(순서-분리 프로퍼티형/물리-순서형) 모두에 더 직접적으로 맞음. +- **백로그로만 남김**: `Slot():Single(state, updateFn?)` — `:List`의 + key-map 없이 "0 또는 1"만 다루는 가벼운 편의 메소드, 상세 설계 미착수. + +**같은 세션 후속 — `bindLifetime`/`unbindLifetime`이 실제로 뭘 하는지, +`canBound`(이중 바인딩 금지)와의 관계를 여러 차례 시행착오 끝에 정확히 +확정.** `Dispatch.setLength`가 이전 Observer 등록을 정리할 때 뭘 불러야 +하는지를 두고 제가 세 번 틀렸다가 사용자가 매번 정정 — 경위와 최종 +결론을 구분해서 기록: + +1. **1차 시도(틀림)**: `unbindLifetime`이 `canExecute`를 즉시 `false`로 + 만들어준다고 서술 — 틀림. `gchold`(순수 GC 방지용 강참조 테이블)는 + `canExecute`가 보는 값(Observer/Effect의 `.Subscribed`, 또는 `inst`의 + 공유 `gcconn.Connected`) 어디에도 안 들어감, 완전히 무관한 테이블. +2. **2차 시도(틀림)**: 그래서 "`unbindLifetime`은 필요 없고 `:Unsubscribe()` + 만 쓰면 된다"로 후퇴 — 이것도 틀림. 사용자 정정: `:Subscribe()`/ + `:Unsubscribe()`는 **`inst`와 아예 무관한 전역/독립** Observer(모듈 + 최상위 디버그 print 등, leaf도 없고 특정 Instance에도 안 묶인 경우)를 + GC로부터 지키기 위한 **전역** 강참조 테이블(`SubscribedObservers[observer] + = true/nil`)일 뿐 — `Dispatch.setLength`의 Observer처럼 처음부터 + `inst` 하나에 종속된 내부 배관에는 원래부터 안 맞는 도구. "`inst` + 연관은 전부 `bindLifetime`/`unbindLifetime`으로"가 맞는 원칙. +3. **최종 확정**: 진짜 독립된 라이프사이클 경로는 **`:Subscribe()`(전역) + 와 `bindLifetime`(inst-scoped) 둘뿐** — "children 배열 leaf 부착"은 + 세 번째 경로가 아니라 **`bindLifetime` 호출 그 자체**(`Dispatch/ + Leaf.luau`가 Observer/Effect leaf를 매치하면 그 자리에서 + `bindLifetime(inst, v)`를 호출), 이걸 제가 처음에 "leaf 부착/ + `:Subscribe()`/`bindLifetime` 셋 다 상호 배타"로 잘못 일반화했다가 + 사용자가 "leaf 부착 자체가 bindLifetime을 호출하는 거라 동일 동작, + 상호배타는 아니다"로 정정. `canBound`의 내부 플래그도 새 필드가 + 아니라 **`canExecute`가 이미 보는 `.Subscribed` 그 자체** — + `bindLifetime`/`unbindLifetime`도(Observer/Effect 값에 한해) 이 + 필드를 세팅/해제해야 `bindLifetime`으로 등록된 Observer가 + `canExecute`에서 정상적으로 "살아있음"으로 인식됨. Effect는 내부적으로 + Observer를 조합하므로 이 확장을 몰라도 자동으로 커버(사용자 확인). +4. **부수 정리**: 이미 확정돼 있던 StoreBind의 자기 재실행 Observer + 예제(`observer:Subscribe()`)도 같은 이유로 틀렸던 것이었음 확인 — + `bindLifetime`/`unbindLifetime`으로 교체. "`:Unsubscribe()`는 자동 + (리프) 케이스에도 동일하게 씀"이라던 기존 서술도 같은 이유로 정정 + (리프/`bindLifetime` 경로의 조기 해제는 `unbindLifetime` 전용, + `:Unsubscribe()`는 `inst`를 몰라 대신 처리 못 함). + +전부 `base/bind-system-plan.md`(신규 "Length/Offset" 절, "이중 바인딩 +금지" 절 정정 — 2-way로 재확정, StoreBind 예제 교체)/`base/slot-plan.md` +(열린 질문 해소, `Slot.Length` 절, `:Single` 백로그 절)/`base/ +lifecycle-pattern.md`(`unbindLifetime` 추가 + `canBound`/`.Subscribed` +연동 반영)/`ROADMAP.md`(M2/M3/M6)/`.claude/question.md` 반영 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 +설계 확정이라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-09-07-list-observer-lazy.md b/.claude/session/2026-08-09-07-list-observer-lazy.md new file mode 100644 index 0000000..2ea8978 --- /dev/null +++ b/.claude/session/2026-08-09-07-list-observer-lazy.md @@ -0,0 +1,51 @@ + + + + +## 2026-08-09 일곱 번째 세션 — `Slot:List`의 `data:Observer(fn)` 구독도 +마운트 시점 lazy `bindLifetime`으로 확정 (Destroy 후 재실행 gap 해소) + +사용자가 "Slot이 마운트된 대상이 Destroy로 죽으면 `updateFn` 재실행이 +`canExecute`로 막히고 있는 게 맞냐"고 질문하며 시작 — 확인 결과 **두 메커니즘이 +다른 상태였음**: `Dispatch.setLength`(Length/Offset, 여섯 번째 세션 확정)는 +이미 정확히 그렇게 돼 있었지만(Slot 마운트 시점에 `bindLifetime(inst,observer)`), +`Slot:List`의 `data:Observer(fn)` 구독은 `:List()` 호출 그 자리에서 즉시 +만들어져(`inst`를 모르는 시점) `bindLifetime`이 전혀 안 걸려있던 실제 gap — +사용자가 정확히 캐치함. 사용자가 이어서 "실제로 Instance에 바인드되려 시도될 +때(=마운트 시점)로 구독 자체를 lazy하게 미루면 되지 않냐"고 제안, 검증 후 +확정. `base/slot-plan.md`(`:List`의 "구현"/"구독 시점" 절 재작성 + +"base/roblox 패키지 경계" 절 보강)/`ROADMAP.md`(M6)에 반영 완료: + +- **`Dispatch.setLength`가 이미 쓰던 패턴을 그대로 재사용, 새 메커니즘 + 없음.** `:List(data,updateFn,keyFn)`는 이제 설정만 저장하고 반환 — + 실제 `data:Observer(fn)` 구독과 최초 `reconcile`은 Slot 컨테이너 자신이 + 마운트되는 순간(`Dispatch/Slot.luau`의 `process(inst,k,self)`, `self._mounted`를 + 세팅하는 바로 그 자리)에 `activateList(self,inst)`가 수행. +- **`:List()`가 마운트 이후에 불리는 경우 — `self._mounted`면 즉시 활성화로 + 확정(사용자 확인, 세 가지 대안 중 1번).** 마운트는 1회성 이벤트라 순서가 + 뒤바뀌면 그 이벤트를 못 기다리므로, `:List()`가 `self._mounted`를 직접 + 확인해서 이미 참이면 그 자리에서 즉시 `activateList` — 호출 순서 제약을 + 새로 추가하지 않음. +- **canExecute와 "등록 즉시 1회 실행"의 관계를 사용자가 직접 짚어 확정**: + `data:Observer(fn)` 등록 시점(=`bindLifetime` 호출 *이전*)의 최초 1회 + 실행은 `canExecute`/`Subscribed` 게이팅과 무관하게 무조건 일어남 — 이 + 시점엔 아직 `Subscribed`가 안 세팅돼 `canExecute`를 물으면 거짓이겠지만, + 애초에 최초 실행은 게이팅 대상이 아니라서 상관없음(`Dispatch.setLength`가 + 이미 "등록 즉시 1회와 겹쳐도 무해"로 같은 구조를 갖고 있었음). `bindLifetime`은 + 등록 직후에 걸려 **이후** 재실행만 게이팅. +- **Destroy 이후 "재실행 막기"+"관측 자체를 관두기"가 새 코드 없이 한 번에 + 해결됨** — `inst` Destroy 시 `gcconn`이 죽어 `canExecute`가 거짓이 되고 + 향후 재실행이 no-op되는 동시에, `gchold`가 `Relate(inst)`(weak-keyed) + 아래 있어서 `inst`가 죽으면 그 안에 강참조로 잡혀있던 Observer/클로저 + (`mounted`/`userdata`/`keyIndex` 포함)가 전부 GC 대상이 됨 — 명시적 + 구독 해제 코드가 안 필요함, `lifecycle-pattern.md`의 "정리는 기본적으로 + GC에 위임" 원칙 그대로. +- **부수 관찰(메모만, 설계 아님)**: 사용자가 "`Relate`로 마운트된 대상을 + weak하게 구할 수도 있겠다"고 언급 — `bindLifetime`이 `Relate(inst)` 기반이라 + 나중에 "이 `inst`에 지금 뭐가 붙어있는가" 역조회가 같은 저장소로 가능해 + 보임, quad-debug 그래프 UX와 맞닿을 수 있음. 지금 설계 안 함, 필요성 + 확인되면 그때. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 +설계 확정이라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-09-08-midreview-defects.md b/.claude/session/2026-08-09-08-midreview-defects.md new file mode 100644 index 0000000..2f9932b --- /dev/null +++ b/.claude/session/2026-08-09-08-midreview-defects.md @@ -0,0 +1,124 @@ + + + + +## 2026-08-09 여덟 번째 세션 — `.claude/base/` 전체 중간검토(질문 모드), +실제 설계 결함 다수 발견·수정 + +사용자가 "이 프로젝트의 계획을 중간검토합니다. 각 요소들에 대해서 함수나 +클래스 등의 동작을 제가 확인 가능하게 리스팅해요... 질문 모드를 쓰면 +좋겠습니다"라고 요청 — 2026-08-04 6차 라운드 때 예고해뒀던 "다음 세션 +검증 패스"를 실제로 실행한 세션. 서브에이전트 6개를 병렬로 띄워 +`.claude/base/` 전체(15개 파일, 5296줄)를 클러스터별로 정독시켜 확정된 +API/동작을 file:line 인용과 함께 그라운딩된 리스팅으로 뽑아낸 뒤, 6개 +배치(Store/State/Source+Dispatch, Ref/PreRef+Brand+Length-Offset+생명주기, +Modifier, Slot, Tag/Attribute/UI shorthand+Blocker/Effect, 컴포넌트 +경계+아키텍처)로 나눠 각 배치를 텍스트로 보여주고 바로 `AskUserQuestion` +(문제없음/문제있음)으로 확인받는 방식으로 진행 — 문제 제기된 건 그 +자리에서 바로 문서에 반영(끝까지 미루지 않음). 총 24개 확인 질문 중 +약 1/3에서 실제 설계 결함이 나옴 — 전부 사용자가 구체적인 반례/Luau +시맨틱스를 근거로 지적한 것이라 전부 그대로 수용, 방어하지 않고 수정. + +**발견·수정된 것 (파일별)**: + +- **`base/bind-system-plan.md`** (가장 많이 고침): + - `Source(default)`/`Ref(default)`의 `default` 생략이 "선택"이라는 + 서술에 "`T`가 nilable일 때만 안전하다"는 캐비엇 누락 — 추가. + `Ref`는 `:Callback`이 등록 즉시 발화해서 이 문제가 더 잘 드러남. + - Dispatch 체인 절에 "`handler.process`를 `Dispatch.process` 없이 + 직접 호출하면 UB(체인 bookkeeping이 깨져 `retract`가 영영 안 + 불리거나 정합성이 무너짐)"라는 불변식이 안 적혀 있었음 — 추가. + - **Ref 콜백/대기자 배열의 소진 슬롯을 `None`에서 `nil`로 되돌림** — + 2026-08-07 열 번째 세션에 "구멍 있는 정수 키는 순회 순서가 깨진다"는 + 이유로 `None`으로 바꿨던 게 이 배열엔 안 맞는 처방이었음(사용자 + 지적): 이 배열은 순서가 안 중요해서 일반화 `for`가 구멍이 있어도 + 전부 방문하고, 오히려 `None`을 쓰면 슬롯이 영원히 안 비어서 + `:Wait()`마다 배열이 끝없이 길어지는 새 문제가 생김 — `nil`로 + 지우고 빈 슬롯을 재사용하는 등록 함수로 바꿈. PreRef pre-pass/ + Length-Offset의 `sourceList`는 순서가 실제로 중요해서 계속 `None`이 + 맞음 — 두 사례를 헷갈리지 않게 교차 참조로 명확히 구분. + - `.Value`가 평범한 hash 필드가 아니라 `__index`로 구현돼야 하는 + 이유(콜백 배열과 같은 테이블에 있으면 `T`가 함수/스레드일 때 콜백 + 처리 루프에 오분류될 위험) 추가. + - **`isRef`/`isPreRef`를 `isState`/`isSource`와 같은 상위-하위 합성 + 패턴으로 재정정** — 원래 "서로 배타적인 형제 브랜드"였는데, `Source`가 + `State`를 만족하듯 `PreRef`도 `Ref` 런타임을 재사용하는 관계라 + 같은 방향(하위=PreRef가 상위=Ref에 포함)으로 다뤄야 일관적이라는 + 지적 — `isPreRef`가 가장 구체적인 항등, `isRef`는 그 위에 얹힌 + 상위 개념. `(v=Ref)` children leaf 매치 핸들러는 이제 + `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 함. + - `NoneHandler`가 `k` 타입을 안 가리는데 왜 배열 파트 `None`(숫자 + 키)에 실제로 안 걸리는지 명확화(배열 파트 `None`은 애초에 + `Dispatch.process`를 안 타서 `NoneHandler`가 볼 기회 자체가 없음). + - `setLength`/`setOffsetSource`의 `None` 페어링 대상을 "Ref/PreRef + 등" 예시 목록에서 "그 배열 위치의 값 자체가 `None`인 모든 경우"로 + 명시적으로 확장, 둘이 항상 짝을 맞춰야 한다는 점도 재강조. + - `:Subscribe()`가 quad 전역 GC-native 원칙의 의도적 예외(참조를 + 다 놓아도 GC 안 되고 계속 실행됨)라는 경고가 없었음 — 추가, 용도도 + "완전히 top-level" 케이스로 좁혀 문서화. +- **`base/modifier-plan.md`**: 핸들러 계층 값 → error 체크가 `State`류 + "State/Source가 감싼 내부 값"까지는 못 잡는다는 한계 — 명시적 UB로 + 문서화(오버엔지니어링 방지, 실사용 위험 낮음). +- **`base/slot-plan.md`** (가장 큰 변경): **CRUD 식별 기준을 element + 레퍼런스에서 인덱스 기준으로 전환** — `Remove(index)`/ + `Extract(index, newElement?)`/`Move(oldIndex, newIndex)`. 원래 + "인덱스는 stale해진다"는 이유로 레퍼런스 기준을 택했는데, 실제로는 + 반대(호출부가 `Add` 리턴값을 안 담고 흘려버리는 경우가 흔함)가 더 + 큰 문제였음. **`ExtractAll()`/`Get(index)`/`IndexOf(element)` 신설** + (`Get`은 "YAGNI"로 드롭했던 걸 재추가). **`Extract(index, newElement?)` + 신설** — 교체가 필요하면 기존엔 Extract+Add 이중 O(n) 시프트가 + 필요했는데, 이제 O(1) 제자리 교체 가능(이전 element 반환). +- **`base/tag-plan.md`**: `TagHandler.retract`의 전체 삭제 동작이 + 정확히 `v == nil`일 때만 맞다는 전제를 `assert`로 명시(기존엔 "v를 + 안 봐도 됨"이라고만 서술돼 있어 조건이 암묵적이었음). +- **`base/attribute-plan.md`**, **`.claude/question.md`**: 타입 + 파라미터화(`Attribute<>` 제네릭 vs `BooleanAttribute`류 정적 + 패밀리) — "미확정"에서 **"둘 다 채택"으로 확정**(내부 구현 동일, + 호출부 표기만 다름). `=` 뒤 값 타입까지 narrowing되는지는 M0/M10 + 실측 필요(안 돼도 런타임 무관)로 명시. +- **`base/ui-shorthand-plan.md`**: `UICorner`/`UIPadding`/`UIScale`이 + 타입 생성 스크립트가 만드는 `FrameModifier`류 타입의 메소드 목록에도 + 포함돼야 한다는 체크리스트 항목 추가(런타임과 무관한 순수 타입 + 생성 디테일). +- **`base/effect-plan.md`**: `EffectHandle`이 내부 Observer를 필드로 + 강참조한다는 것, `bindLifetime`/`:Subscribe()` 둘 다 `state`가 있으면 + 내부 Observer까지 cascade해야 한다는 것(안 그러면 내부 Observer의 + `canExecute` 게이팅이 올바른 `inst`를 못 봄) — 재확인 후 명시화. +- **`base/component-composition-plan.md`** (Length/Offset 다음으로 많이 + 고침): + - **"리프 바인딩엔 Source가 좁은 예외"라는 서술이 틀림 — 정정.** + `local a = Source(true); Frame { Visible = a }; a:Set(false)`처럼 + Source를 리프에 직접 물리는 건 이미 확정된 "Source가 State를 + 구조적으로 만족" 원칙이 그대로 커버하는 정상 경로였음 — "State가 + 일반적"이라는 서술은 Source를 못 쓴다는 뜻이 아니라 "여러 값에서 + 파생된 계산 결과는 State일 수밖에 없다"는 통계적 경향 서술일 + 뿐이라고 재정정. + - `props.Modifier or None` 관용구의 `None` 근거 포인터가 Ref 콜백 + 배열 정정으로 깨질 뻔한 걸 교차 참조로 바로잡음(그 배열은 순서가 + 중요한 별개 케이스라 `None`이 계속 맞음). + - `Frame { Comp{} }`에서 `Comp`가 `Slot`을 반환하는 다중 루트 우회 + 경로가 새 배선 없이 그대로 작동함을 재확인(값이 컴포넌트 호출로 + 왔든 리터럴이든 디스패치 입장에선 구분 없음). +- **`ROADMAP.md`**: 위 `Ref` `None`→`nil`/`isRef`·`isPreRef` 변경사항 + 체크박스 동기화. + +**변경 없이 확인만 된 것**: `:With`/`:Compute` 체이닝, `None` 센티널 +기본 메커니즘, Length/Offset 전체, 이중 바인딩 금지/`Relate`/생명주기, +Modifier setter/Apply/Overridden 판단 기준, `Peek`/`isState`/`None` +setter 인자, Slot 요소 타입 제약/Extract portal/`Length`, `Slot:List` +시그니처(단, 캐스케이드 성능 이슈는 `keyFn` 명시 유도로 이미 문서화돼 +있어 추가 조치 불필요), List 구독 lazy 시점, Tag 값 모양/패키지 배치, +Blocker 전체, 소스트리/네이밍 컨벤션/Handler 3분류/테스트 전략/이식성 +원칙. + +**부수 기록**: `.claude/memory`(세션 간 영속 기억)의 협업 스타일 메모에 +이번 리뷰 진행 방식(에이전트 병렬 추출 → 배치별 텍스트+AskUserQuestion +즉시 확인 → 그 자리에서 바로 문서 반영)을 다음에 재사용할 패턴으로 +기록 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션은 설계 +확정이 아니라 기존 확정 사항의 결함 수정이었지만, 결과적으로 M0 착수 +전 상태가 더 탄탄해졌을 뿐 우선순위 자체는 그대로. 이 중간검토가 +마지막 배치(6단계)까지 끝났는지, 사용자가 이어서 더 볼 부분이 있는지는 +다음 세션 시작 시 확인. + diff --git a/.claude/session/2026-08-09-12-luau-test-spikes.md b/.claude/session/2026-08-09-12-luau-test-spikes.md new file mode 100644 index 0000000..b12f579 --- /dev/null +++ b/.claude/session/2026-08-09-12-luau-test-spikes.md @@ -0,0 +1,64 @@ + + + + +## 2026-08-09 열두 번째 세션 — `.claude/luau-test/` 신설: M0 사전 검증 +스파이크 작성, 결과는 아직 미확인 + +M0가 공식적으로 짜야 할 스파이크(위 "지금 할 일" 1번, `ROADMAP.md` M0 +체크박스)와 지금까지 세션 로그 곳곳에 흩어져 있던 "실제 Luau로 부딪혀본 +적 없는 것"/"M0/M2 스파이크 검증 목록에 추가됨" 표시들을 한 곳에 모아, +사용자가 직접 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 돌려볼 +수 있는 독립 실행 스크립트 14개 + `README.md` 색인으로 만듦. 세 라운드에 +걸쳐 진행됨: + +1. **1차 작성** — 레포 루트 `luau-ignoreme/`(당시엔 git 자동 제외 폴더로 + 시작)에 M0 체크리스트 5개 항목(Store/State 다이아몬드 전파, Source가 + State를 구조적으로 만족하는 제네릭 타입, process/retract 재귀 디스패치, + 배열/해시 두 패스 순회, `props.Modifier or None` nil-hole 관용구) + + `Dispatch` 체인/`retractUnder` 다단 검증, `Relate`의 weak-table GC + 실측, `Modifier.Overridden` 서브타입 타입체크, Roblox 전용 + `bindLifetime`/`canExecute`/Attribute Instance 참조/`CollectionService` + 태그 확인까지 10개 파일 작성(01~10). +2. **2차 — 커밋 `f198fd9`("중간검토에서 발견된 설계 결함 다수 수정") 반영.** + 그 사이 사용자가 직접 `.claude/base/` 전체를 훑으며 여러 결함을 + 정정(위 절 참고) — 그 중 `02`(Ref 콜백/대기자 배열의 소진 센티널이 + `None`→`nil`로 되돌아간 것, 실제로 `None`을 쓰면 배열이 무한정 + 자라는 버그였음이 드러남)이 luau-test 내용과 정면으로 어긋나 전면 + 재작성(순서가 중요한 배열은 계속 `None`, 순서 무관+슬롯 재사용 + 필요한 배열은 `nil`이라는 최종 구분 + 무한 성장 버그의 정량적 + 재현까지 포함). `Modifier` UB→error 전환(11 신규)도 이 라운드에 + 같이 반영. 나머지 파일은 대조 결과 영향 없음을 서브에이전트+직접 + 문서 대조로 확인. +3. **3차 — 사용자 요청으로 "타입 관련 실측 필요, 특히 `luau-lsp`로 + 확인해야 할 것" 3개 추가(12~14).** base 문서 자신이 "실측 필요"라고 + 명시적으로 못박아둔 지점(`attribute-plan.md`의 `[Attribute<> + "name"] = value` 제네릭 DI 키가 실제로 값 타입을 좁혀주는지, 12번)과 + f198fd9에서 뒤집힌 결정(`isRef`/`isPreRef`가 이제 `Source`/`State`와 + 같은 포함 관계 — `PreRef`가 `Ref`의 하위 개념이 됨, `PreRef`가 + `Ref`를 구조적으로 만족하는지 타입체크까지 포함, 13번), 그리고 + 같은 세션에 새로 명시된 캐비엇(`Source(default)`/`Ref(default)`의 + `default` 생략은 `T`가 nilable일 때만 안전하다는 것을 함수 오버로드로 + 타입 레벨에서 실제로 막을 수 있는지, 14번)을 찾아 작성. +4. **폴더 이동 — `luau-ignoreme/` → `.claude/luau-test/`.** 사용자가 + "커밋해서 레포에 남기자"고 판단 — `*-ignoreme*` gitignore 패턴을 + 벗어나 일반 추적 대상으로 전환, `.claude/README.md`에 새 폴더 행 + 추가. 내용/역할은 안 바뀜, 경로 참조 문구만 동기화. + +**아직 아무것도 실행 안 됨 — 에이전트도 로컬에 `luau`/`luau-analyze`가 +없어서 직접 못 돌려봤고, 사용자가 다음에 `luau`/`luau-analyze`/ +`luau-lsp`/Roblox Studio로 직접 돌려보고 결과를 알려주기로 함.** 결과에 +따라 할 일: +- 전부 통과 → M0 실제 착수 시 이 스크립트들의 로직을 그대로 재사용하며 + 진행. +- 하나라도 걸림(특히 12/14의 타입 narrowing 실패, 07의 GC 신호 이상, + 10의 `warn` 발생, 13의 런타임 assert 실패) → 해당 `base/` 문서를 + 그 자리에서 정정. +- `.claude/luau-test/README.md`의 "결과 확인 후 할 일" 절에 파일별로 + 뭘 우선 확인해야 하는지 이미 적어둠 — 다음 세션은 그 응답을 + 대조하는 것부터 시작하면 됨. + +**다음 세션이 할 일**: 사용자가 luau-test 실행 결과를 갖고 오면 그것부터 +반영. 아직 없으면 `ROADMAP.md` M0 착수 우선순위는 그대로(위 "지금 할 일" +1번 참고) — 단, 이 폴더 결과를 먼저 확인하고 진행하는 게 순서. + diff --git a/.claude/session/2026-08-10-01-slot-add-return-index.md b/.claude/session/2026-08-10-01-slot-add-return-index.md new file mode 100644 index 0000000..d5f3d8b --- /dev/null +++ b/.claude/session/2026-08-10-01-slot-add-return-index.md @@ -0,0 +1,28 @@ + + + + +## 2026-08-10 세션 — `Slot:Add`가 삽입 인덱스를 반환하도록 확정, 범위 밖 +`index`는 clamp 대신 error + +짧은 세션. 사용자가 "`Slot:Add`/`Remove`가 어차피 void인데 삽입된 인덱스를 +반환해줘도 되지 않냐"고 제기 — 검토 후 채택, `base/slot-plan.md`의 CRUD +표/에러 조건 절에 반영 완료: + +- **`Slot:Add(element, index?): number`로 확정** — `index` 생략(끝에 추가) + 시 호출부가 실제 위치를 모르는 문제를 `Add`가 이미 계산해서 아는 값을 + 그냥 반환하는 것으로 공짜 해결(기존엔 `IndexOf`로 O(n) 역조회해야 했음). + `Move`/`Swap`이 void인 것과 안 부딪힘 — 그 둘은 호출부가 이미 위치를 + 알고 부르는 연산이라 새 정보가 없어 void인 거고, `Add`는 반대로 새 + 정보(계산된 위치)가 생기는 경우라 "반환값은 실제로 새로 알게 되는 + 정보만"이라는 같은 원칙의 연장. +- **`Add`의 `index`가 범위 밖(1..현재 개수+1)이면 즉시 `error()`, clamp + 안 함 — 사용자가 직접 근거 제시.** clamp는 "의도한 위치가 아닌데 조용히 + 성공하는" 찾기 힘든 버그 유형을 새로 만들 뿐이고, 이미 `Remove`/ + `Extract`/`Move`/`Swap` 전부가 범위 밖에서 즉시 에러인 fail-fast 톤과도 + 맞아야 함. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인이 +여전히 먼저) — 이번 세션은 이미 확정된 CRUD 표의 작은 갭 하나만 메운 +것이라 우선순위엔 영향 없음. + diff --git a/.claude/session/2026-08-10-02-dynamic-children-ub.md b/.claude/session/2026-08-10-02-dynamic-children-ub.md new file mode 100644 index 0000000..27662b8 --- /dev/null +++ b/.claude/session/2026-08-10-02-dynamic-children-ub.md @@ -0,0 +1,30 @@ + + + + +## 2026-08-10 세션 — 동적 자식 추가/제거는 `Slot`/`state`만 정당, +그 외는 UB로 명문화(문서 갭 보강) + +사용자 질문에서 시작: Slot이 마운트한 객체 수를 `Length`/`Offset` +누적합으로 세는 방식(2026-08-09 여섯 번째 세션 확정)이 되면서, 이 카운팅을 +안 거치고 quad가 관리하는 부모 Instance에 외부에서 직접 `.Parent = inst`로 +자식을 끼워 넣는 게 UB로 문서화돼 있는지 확인 요청 — 검토 결과 **문서 +어디에도 명시돼 있지 않은 진짜 갭**이었음(기존 UB 목록엔 Handler 순환/ +이중 바인딩/`Dispatch.process` 우회 직접 호출/`setLength`·`setOffsetSource` +생략 등은 있었지만 이 케이스는 빠져있었음, 인접했던 "수동 Visible 토글은 +Length가 못 잡는 게 맞다"는 캐비엇은 이미 마운트된 element를 나중에 +숨기는 별개 시나리오라 이것과 다름). + +**확정**: 동적 자식 추가/제거의 유일한 정당 경로는 `Slot` 또는 +`state`류 store-bind 뿐 — 둘 다 그 위치의 Handler가 +`Dispatch.setLength`/`Dispatch.setOffsetSource`를 정확히 호출하는 것으로 +이미 보장돼 있음. 이 두 경로를 거치지 않고 quad가 마운트해둔 부모 +Instance에 직접 `.Parent =` 대입으로 자식을 넣거나 빼면 `lengthList`/ +`sourceList`가 그 변화를 전혀 몰라 `Length` 카운트와 형제 순서(offset) +계산이 조용히 어긋남 — 새 방어 로직 없이 UB로 문서화만 함(다른 UB +케이스들과 같은 톤). `base/bind-system-plan.md`("Length/Offset" 절 +말미)/`base/slot-plan.md`("Slot.Length" 절 말미)에 반영 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션은 순수 문서 갭 보강이라 우선순위엔 영향 없음. + diff --git a/.claude/session/2026-08-10-03-tween-redesign.md b/.claude/session/2026-08-10-03-tween-redesign.md new file mode 100644 index 0000000..7290f6d --- /dev/null +++ b/.claude/session/2026-08-10-03-tween-redesign.md @@ -0,0 +1,118 @@ + + + + +## 2026-08-10 두 번째 세션 — Tween 구조 전면 재설계: 독립 Dispatch 핸들러 → +값-레벨 `Tween` 래퍼, `pre-implementation-audit.md` 1-1 완전 해소 + +사용자가 "트윈도 타입 문제가 있다 — 키 타입을 어떻게 하냐, Property +setter가 더 분발해서 `V`가 `isTween`이면 트윈 넣는 게 낫지 않냐"고 +제기하며 시작된 긴 단일 스레드. 기존 확정 모델(`[Tween(key, +tweenData...)] = storeValue`, `v`가 Store인 아무 `k`나 잡는 우선순위 +최상위 Dispatch 핸들러, 2026-08-04부터 확정)이 실은 +`research/pre-implementation-audit.md` 우선순위1-1이 이미 지적해뒀던 +구조적 모호함("애니메이션 없는 일반 반응형 프로퍼티 바인딩도 결국 +이름이 Tween인 파일을 거쳐가는가")을 안고 있었다는 걸 사용자 제안이 +정확히 겨냥한 것으로 드러나, 세션 내내 살을 붙여 완전히 재설계까지 +감. 구 모델은 `archive/tween-special-bind-key-reversed.md`로 이전, +`research/tween-plan.md`는 전면 재작성됨 — 상세 근거는 그 두 문서가 +최종 소스, 여기는 결정 흐름만 요약. + +**핵심 재설계**: State/Source 언랩(범용 `Dispatch/StoreBind.luau`, `k`/`v` +타입 무관)과 "이 값이 트윈 대상인가" 판단을 완전히 분리 — 후자는 별도 +Dispatch 핸들러/우선순위 경쟁이 아니라, **PropertyHandler가 `realv`를 +다 풀어낸 뒤 직접 하는 값-레벨 분기**(`isTween(realv)`)로 옮김. `Tween(opts: +{Value: T, ease...}) -> Tween`는 `Store({...})`와 같은 `Type(args)` +테이블 팩토리. 이 전환 하나로 우선순위1-1이 구조적으로 성립 불가능해짐 +— 범용 반응형 바인딩과 Tween이 애초에 같은 핸들러를 놓고 경쟁할 지점 +자체가 없어짐. + +**세션 중 순서대로 다듬어진 세부 결정들**(전부 최종적으로 `research/ +tween-plan.md`에 반영): + +1. **`Tween.Value`는 plain `T`만, 자체 반응 경로 없음** — 처음엔 `Value`도 + `T|State`를 받아 내부에 별도 Observer를 걸어야 하나 검토했으나, + 바깥 `:Compute`가 소스 변경마다 `Tween{Value=v,...}`를 통째로 재생성해 + StoreBind 재귀를 타므로 불필요함을 확인 — "같은 일 하는 두 번째 경로를 + 안 만든다" 원칙 재적용, `Tween = {Value: T, ease...}`로 확정. +2. **3-상태 릴레이션 슬롯으로 `hasBeenSet`과 활성 엔진 트윈 저장을 통합** — + `relate:GetStrong(inst,k)`가 `RobloxTween | true | nil` 중 하나: + `nil`=이 키 첫 세팅(애니메이션 없이 즉시 스냅, 기본값→목표값으로 + 날아오는 진입 애니메이션 버그 방지), `true`=세팅된 적 있음/활성 트윈 + 없음(정상 애니메이션 시작 가능), 엔진 객체=활성 트윈 있음(override + 정책대로 정리 먼저). 사용자가 직접 "hasBeenSet은 어차피 트윈에만 + 쓰이니 트윈 저장 슬롯 하나로 합치자"고 제안해 확정. +3. **활성 트윈이 있는데 plain 값이 들어오는 경우의 순서 규칙 신설** — + 먼저 override 정책대로 이전 트윈을 정리(멈추거나 끝냄)하고, **그 + 정리가 끝난 뒤에만** 새 값을 세팅. 순서가 뒤바뀌면 이전 트윈의 다음 + 인터폴레이션 프레임이 방금 세팅한 값을 덮어쓸 위험이 있어서 — 사용자가 + 직접 짚은 시퀀싱 버그. +4. **타입 대수: `T' = T | Tween` 치환만으로 해결, 새 타입 기계 불필요** — + 지금 프로퍼티류 필드가 전부 `T | State` 모양으로 통일돼 있는데, + 여기서 "이 필드의 `T`" 자체를 `T' = T | Tween`로 치환하면 자동으로 + `T | Tween | State>`가 나옴 — Modifier/State/Source/ + StoreBind 코드엔 `Tween` 인지 로직이 전혀 안 들어감(StoreBind는 원래도 + 페이로드 타입에 무관하게 `isState`만 봄), 타입 생성 스크립트가 필드 + 타입 문자열만 바꾸면 끝. 사용자가 직접 대수적으로 도출. +5. **`useTween` 우회 — 새 옵션 필드 없이 해소.** 이전엔 + `Tween{useTween=state}`처럼 별도 필드가 필요하다고 열어뒀으나, + 2026-08-07 일곱 번째 세션에 확정된 `state:Apply(factory)` sugar 위에 + `someState:Apply(Animate(reduceMotion, opts))`처럼 조건부로 `Tween{...}`를 + 씌우거나 안 씌우는 `:Compute` 팩토리 하나로 공짜로 풀림 — 새 base + 메커니즘 불필요. +6. **`Animate` 콤비네이터는 quad-roblox 유틸, base 프리미티브 아님** — + `Tween` 값 타입/`isTween`만 base(`quad-base/Tween.luau`)에 있고, + `Animate`는 이미 있는 `:Apply`/`:Compute`/`Tween{...}`를 조합한 편의 + 함수라 나중에 이름/모양을 자유롭게 바꿔도 base 계약에 영향 없음 — + 사용자 표현으로 "저비용 고효율 엔지니어링". +7. **패키지 경계는 Tag가 이미 밟은 분리를 그대로 재사용** — quad-base: + `Tween.luau`(값 타입만). quad-roblox: `Handlers/Property.luau`(isTween + 분기+3-상태 저장+override 정책 흡수, 기존 독립 `Handlers/Tween.luau` + 폐기) + `Animate.luau`(신규). +8. **부수 발견 — `retract`가 Tween 경로에서 사실상 필요 없어짐.** 기존 + 모델에서 "Tween↔프로퍼티 핸들러 타입 교체"가 `retract`가 실제로 + 의미를 갖는 유일한 대표 예시였는데, 새 모델에선 매치되는 Dispatch + 핸들러가 항상 PropertyHandler 하나뿐이라 이 케이스 자체가 사라짐 — + 트윈 취소/전환은 PropertyHandler 내부의 3-상태 슬롯 로직으로 대체(Tag가 + 이미 하는 "diff는 process 자신이 담당" 패턴과 같은 모양). `retract` + 필드 자체는 "생략 불가" 일반 규칙이라 여전히 정의는 해두되, 실제 + 호출은 거의 없어짐. Tag(핸들러 타입이 실제로 바뀌게 재설계되어 + `retract`가 필요해진 사례)와 Tween(핸들러 타입이 안 바뀌게 재설계되어 + `retract` 필요성이 사라진 사례)을 서로 반대 방향 사례로 archive 문서에 + 대비해둠 — quadnomicon 소재. +9. **`Tween`의 핸들러 계층 분류 정정** — `base/modifier-plan.md`가 + 원래 Tween을 Slot/Tag/Attribute와 같은 "dispatch 참가자"(State/Source에 + 담겨도 재귀 재-dispatch가 그대로 처리해주는 부류)로 묶어뒀는데, 이제 + `Tween`는 `process`/`retract`가 없는 순수 raw 데이터 값이라 `None`과 + 같은 분류로 정정 — Modifier 필드/`State`가 막는 "핸들러 + 계층 값 → error" 규칙에 안 걸린다는 결론은 안 바뀜(그냥 raw 값이라서로 + 근거가 바뀜). +10. **`initValue`(진입 애니메이션)와 hasBeenSet의 긴장 관계를 기록만 + 해둠** — hasBeenSet이 "첫 세팅은 무조건 스냅"을 기본 동작으로 + 확정했으므로, 나중에 `initValue`(다이얼로그 슬라이드-인 등)가 실제로 + 필요해지면 이 억제 동작을 명시적으로 우회하는 방법까지 같이 설계해야 + 함 — 새 결정 없이 상충 관계만 `research/tween-plan.md`에 남김. + +**여전히 열려있는 것**(M11 착수 시 확정): override 정책 4가지 중 기본값 +Cancel 외 세 옵션의 정확한 키 이름/시그니처, Tween→plain 전환에 5번째 +옵션이 필요한지, 트윈 옵션 값 모양(`TweenInfo` 그대로 vs 편의 필드 — 소견은 +후자), `Animate`의 정확한 시그니처(조건/옵션 분리 vs 통합). + +**반영된 파일**: `research/tween-plan.md`(전면 재작성, 최종 소스), +`archive/tween-special-bind-key-reversed.md`(신규, 구 모델 원문+역전 +사유), `base/bind-system-plan.md`(9곳 — "확정된 디스패치 모델"의 대표 +예시를 Tween에서 StoreBind로, `retract` 필요 패턴 예시를 Tag로 교체, +"Dispatch는 프리미티브가 아니다"/"Dispatch 체인" 절의 핸들러 목록에서 +Tween 제거, `None` 센티널 절 예시 갱신, Ref/Brand 절 문구 정정), +`base/architecture.md`(소스트리 — `quad-base/Tween.luau` 신설, +`quad-roblox/Handlers/Tween.luau` 삭제하고 `Handlers/Property.luau` +설명에 흡수, `Animate.luau` 신설), `base/modifier-plan.md`(핸들러 계층 +분류에서 Tween 제외 + 신규 "10. `Tween`와의 타입 합성" 절), +`research/pre-implementation-audit.md`(우선순위1-1 해소 표시), +`ROADMAP.md`(M11 전면 재작성, M2/M7 체크박스 갱신), `.claude/question.md`/ +`.claude/README.md`(참조 동기화). + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션도 순수 설계 확정/문서 정리라 M0 착수 우선순위 자체는 +그대로. M11 착수 시점이 오면 위 "여전히 열려있는 것" 목록부터 확인. + diff --git a/.claude/session/2026-08-10-04-onchange-key.md b/.claude/session/2026-08-10-04-onchange-key.md new file mode 100644 index 0000000..5193b33 --- /dev/null +++ b/.claude/session/2026-08-10-04-onchange-key.md @@ -0,0 +1,44 @@ + + + + +## 2026-08-10 세 번째 세션 — `OnChange` 특수 키 신설: `GetPropertyChangedSignal` +바인딩, 제네릭 없이 확정 + +사용자가 `GetPropertyChangedSignal`을 어떻게 다뤄야 할지 물으며 시작 — +이벤트는 이미 평범한 문자열 키(`inst[key]`가 곧 Signal)로 확정돼 있는데, +`GetPropertyChangedSignal(name)`은 프로퍼티 이름을 인자로 받아야 하고 그 +이름이 "값 세팅" 키 네임스페이스와 겹쳐서 같은 패턴을 못 씀 — 사용자가 +`[OnChange "PropertyName"] = function(v) ... end` 형태(타입은 콜백에 직접 +명시)와 "`OnChange.PropertyName`을 전부 코드 생성"하는 대안 두 가지를 +제시하며 의견을 물음. + +**확정**: `OnChange(name)` DI 키 팩토리, **제네릭 타입 파라미터 없음** — +`Attribute<>`와 달리 콜백 파라미터 타입은 호출부가 직접 명시. 이미 +확정된 "이벤트 바인딩은 콜백 시그니처를 Luau가 검증 못 하는 대가를 +받아들인다"는 결정과 같은 급의 트레이드오프라 새로 정당화할 것 없다는 게 +근거 — 오히려 `Attribute`처럼 제네릭으로 정확히 맞추려 들면 이벤트 키보다 +더 엄격한 걸 요구하는 셈이라 일관성이 깨짐. 프로퍼티별 정적 코드 생성 안은 +기각(`archive/onchange-per-property-codegen-rejected.md`) — Attribute의 +정적 지름길은 타입 파라미터가 좁고 고정된 프리미티브 집합(~10종)에서만 +와서 지름길 후보가 유한한데, 프로퍼티는 클래스마다 이름/타입 집합이 전부 +달라 (클래스 수 × 프로퍼티 수) 규모로 폭발함 — 겉보기엔 비슷한 절충 +같지만 실제로는 규모가 다른 문제. + +패키지 경계는 **전부 quad-roblox**(`Handlers/OnChange.luau`, `Attribute`와 +같은 배치 — `GetPropertyChangedSignal` 자체가 Roblox 엔진 API라 base에 둘 +값 타입/API 레이어가 없음). `process`는 `GetPropertyChangedSignal(name):Connect`, +`retract`는 `:Disconnect` — 일반 `Handlers/Event.luau`와 같은 결. **`State` +지원도 새 메커니즘 없이 해소** — 이미 확정된 "이벤트도 store-bind 가능 +(`false`로 disconnect)" 메커니즘이 그대로 적용됨, `OnChangeHandler`는 +`process`/`retract`만 구현하면 범용 `Dispatch/StoreBind.luau`가 State/Source +언랩+재귀 재-dispatch를 알아서 해줌. + +`base/onchange-plan.md`(신규)/`base/bind-system-plan.md`(이벤트 네이밍 절 +교차 참조)/`base/architecture.md`(소스트리 `Handlers/OnChange.luau`)/ +`ROADMAP.md`(M10 제목·체크박스)/`.claude/README.md`(base/archive 인덱스) +전부 반영 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-11-01-compute-trailing-args.md b/.claude/session/2026-08-11-01-compute-trailing-args.md new file mode 100644 index 0000000..7c6aee3 --- /dev/null +++ b/.claude/session/2026-08-11-01-compute-trailing-args.md @@ -0,0 +1,43 @@ + + + + +## 2026-08-11 세션 — `:Compute(fn, ...)` trailing-args sugar 확정, +`Effect`/`Observer`는 의도적으로 제외 + +사용자가 Vide의 암묵적 추적과 React 훅 규칙의 차이를 짚는 질문에서 출발해, +"React의 `useMemo(fn, deps)`처럼 `:With(...)` 없이 `:Compute(fn, a, b, c)`로 +바로 추가 의존성을 선언하면 더 편하지 않냐"는 제안으로 이어진 짧은 세션. +검토 끝에 확정, `base/bind-system-plan.md`(`:Compute` 절 신규 소절)/ +`base/effect-plan.md`/`ROADMAP.md`(M3)/`research/documentation-content-map.md` +(quadnomicon 후보 7번)에 반영 완료: + +- **`:Compute(fn, ...)`는 채택 — 진짜 공짜 sugar라는 게 사용자가 직접 밝힌 + 핵심 근거.** `:Compute` 호출은 원래도 결과를 담을 새 State 노드를 만들어야 + 하므로, 그 노드에 `self` 말고 `a,b,c`까지 구독(무효화 엣지)을 추가로 거는 + 건 이미 생기는 노드에 엣지만 얹는 것 — `:With(a,b,c):Compute(fn)`(노드 + 2개)보다 싼 노드 1개로 끝남. 이전에 기각됐던 `Store.Combine({a,b}, + function(av,bv)...)`(포지셔널 값 언랩이라 타입 표기가 꼬였던 안)과는 + 달리 `fn(self)` lazy 핸들 시그니처를 그대로 유지하는 제안이라 그 기각 + 사유가 안 걸림. +- **`Effect(fn, ...)`/`state:Observer(fn, ...)`류 동일 sugar는 기각 — + 사용자가 직접 구분.** Effect/Observer는 Compute와 달리 자기 자신이 + 결과를 담는 State 노드가 아닌 순수 leaf 소비자라, 의존성이 둘 이상이면 + 그걸 하나로 합칠 **새 노드**(`:With`가 만드는 것)가 실제로 필요함 — + 이건 진짜 비용이 드는 지점이라, trailing args로 감추면 "이 줄이 새 + 노드/구독을 만든다"는 걸 코드만 보고 알 수 없게 됨. `:With`가 clone + 빌더가 아니라 진짜 노드로 확정됐던 이유(2026-08-07 세 번째 세션, + "코드상의 호출 체인이 그래프 엣지와 1:1 대응돼야 quad-debug 그래프가 + 안 꼬임")와 정확히 같은 원칙 — 다중 의존성 Effect/Observer는 + `Effect(fn, state:With(a,b,c))`처럼 `:With` 호출을 코드에 그대로 노출. +- **일반 원칙**: "trailing args sugar는 그게 정말 무료일 때만 붙인다" — + 호출부가 이미 만들어야 하는 노드에 엣지만 얹는 경우(Compute)엔 sugar, + 없던 노드를 새로 만들어야 하는 경우(Effect/Observer의 다중 의존성 + 병합)엔 sugar 없이 `:With`를 명시적으로 남긴다. `Compute`만 편해지고 + `Effect`/`Observer`는 안 그런 게 겉보기엔 비일관적으로 보이지만 실은 + 이 하나의 원칙에서 나온 것이라는 게 quadnomicon 에세이 소재로 채택 + (사용자 제안). + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. + diff --git a/.claude/session/2026-08-11-02-trailing-deps-positional.md b/.claude/session/2026-08-11-02-trailing-deps-positional.md new file mode 100644 index 0000000..8f2e8a5 --- /dev/null +++ b/.claude/session/2026-08-11-02-trailing-deps-positional.md @@ -0,0 +1,54 @@ + + + + +## 2026-08-11 두 번째 세션 — trailing deps를 `fn`에 위치 인자로도 노출, +`.claude/luau-test/15` 신규 + +같은 날 바로 이어진 세션. 사용자가 방금 확정된 `:Compute(fn, ...)` +trailing-args sugar를 한 단계 더 밀어붙임 — trailing args `a,b,c`가 +이미 구독 등록용으로 넘어간다면 `fn(self, a, b, c)`처럼 그 값 자체도 +위치 인자로 노출해도 되지 않느냐는 제안. `:With`가 그렇게 안 하는 이유 +(체인이 여러 호출에 걸쳐 길어지면 순서 추적이 복잡해짐)는 `:Compute`의 +trailing args처럼 한 호출문 안에 로컬하게 다 보이는 경우엔 안 걸린다는 +것도 사용자가 직접 짚음 — 검증 결과 정확함, 채택. + +- **`:With`의 회피 근거가 이 케이스엔 안 걸림** — `:With` 체인은 여러 + 호출/스코프에 걸쳐 누적될 수 있어 최종 위치 매핑을 코드 한 줄만 보고 + 못 읽는 게 문제였는데, `:Compute(fn, a, b, c)`는 그 호출의 인자 목록 + 자체가 곧 최종 순서라 누적 문제가 원천적으로 없음. +- **커링 패턴에서의 중복/드리프트 위험도 같이 해소됨** — 지금 설계(값은 + closure로 재획득)로 커링 스타일을 쓰면 `a,b`를 두 번(클로저 캡처용 + + `:Compute`의 trailing args용) 써야 해서, 리팩터링 중 한쪽만 바뀌면 + "구독은 a에 걸려있는데 실제로 읽는 값은 다른 것"인 조용한 버그가 생길 + 수 있음 — 위치 인자로 노출하면 trailing args 목록 하나가 "무엇을 + 구독하는가"와 "fn이 몇 번째로 뭘 받는가" 둘 다의 유일한 소스가 됨. +- **`self`가 이미 raw 값이 아니라 lazy 핸들로 넘어가는 원칙을 그대로 + 적용** — `fn(self: State, dep1: State, ...)`, 각 + `depN:Get()`을 실제로 부를 때만 계산 트리거. +- **새로 드러난 문제 — `previous`(2026-08-06 확정)와의 위치 충돌.** + `previous`를 dep 개수와 무관하게 항상 마지막 인자로 고정 + (`fn(self, dep1, ..., depN, previous?)`, N=0이면 기존 시그니처로 + 축약돼 하위 호환)하는 안을 제안했으나, 이건 "제네릭 타입 팩(`...U`) + 뒤에 고정 인자가 오는" 모양이라 Luau가 실제로 타입체크 가능하게 + 표현해주는지가 불확실 — **사용자가 직접 이 지점을 짚어 실측 필요로 + 남김. [정정, 같은 날 세 번째 세션] 이 순서 자체가 틀림 — `previous?`는 + 팩 앞이어야 함, 아래 절 참고.** + +`base/bind-system-plan.md`(":Compute(fn, ...)" 절 바로 뒤에 신규 소절)/ +`ROADMAP.md`(M3)/`.claude/luau-test/README.md` 반영 완료. +`.claude/luau-test/15-type-compute-trailing-deps-typepack.luau` 신규 — +(A) 단일 dep 대조군, (B) 이형 다중 deps가 제네릭 팩으로 개별 타입으로 +풀리는지, (C) 팩 뒤에 `previous?` 고정 인자를 붙인 시그니처 자체가 +파싱/타입체크되는지 세 가지 확인(**[정정, 같은 날 세 번째 세션]** C의 +순서가 틀렸던 것으로 드러나 D 대조군이 추가됨 — 아래 절 참고). 다른 +luau-test 파일들과 마찬가지로 **에이전트가 직접 실행 못 함** — +`luau`/`luau-analyze` 바이너리가 이 환경에 없어서, 사용자가 직접 +돌려보고 결과를 알려줘야 함. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — `15`가 luau-test 결과 확인 목록에 하나 추가됨(정정된 순서는 +아래 세 번째 세션 참고). 남은 진짜 불확실성은 B(이형 다중 deps를 +제네릭 팩으로 표현 가능한지)뿐 — 실패하면 위치 인자 노출을 동종 타입 +dep 1개로 한정. + diff --git a/.claude/session/2026-08-11-03-previous-before-pack.md b/.claude/session/2026-08-11-03-previous-before-pack.md new file mode 100644 index 0000000..65df983 --- /dev/null +++ b/.claude/session/2026-08-11-03-previous-before-pack.md @@ -0,0 +1,42 @@ + + + + +## 2026-08-11 세 번째 세션 — `previous`는 팩 앞(`fn(self, previous?, +...deps)`)으로 순서 정정, "이걸 안 할 이유"였는데 살아남음 + +바로 이어진 짧은 세션. 사용자가 위 두 번째 세션에서 제안했던 +"`previous`는 dep 개수와 무관하게 항상 마지막"(`fn(self, dep1, ..., +depN, previous?)`)을 직접 정정 — "애초에 `fn(self, prev, ...)`이긴 +해야할듯. 아니면 이걸 하지 말던가." + +**정정 채택 — `previous?`는 deps 팩 **앞**(self 바로 다음)에 와야 함, +`fn(self, previous?, dep1, ..., depN)`.** 이건 단순 선호가 아니라 거의 +확실한 Luau 문법 제약에서 나오는 결론: 값 레벨 `...`(vararg)가 파라미터 +리스트 맨 끝에만 올 수 있는 것과 마찬가지로, 타입 레벨 제네릭 팩(`...U`)도 +함수 타입 시그니처에서 항상 맨 끝이어야 할 가능성이 매우 높음(팩이 +"나머지 자리를 전부 채운다"는 개념이라 그 뒤에 고정 타입이 하나 더 +오는 걸 문법 자체가 지원 안 할 것으로 추정) — 위 두 번째 세션에서 제안한 +"previous를 팩 뒤에" 순서는 이 제약과 정면으로 부딪혀 애초에 파싱/타입 +체크가 안 될 가능성이 높았음. `previous?`를 팩 **앞**에 두면 "고정 인자 +다음에 팩"이라는 정상적인 모양이 되어 이 제약과 안 부딪힘 — **구조적으로 +유일하게 안전한 순서라 이걸로 확정**. + +**트레이드오프 — deps만 쓰고 싶어도 `previous` 자리를 비워둬야 함.** +`fn(self, previous?, dep1, dep2)`이므로, `previous`가 필요 없는 흔한 +호출도 `function(self, _, dep1, dep2) ... end`처럼 안 쓰는 두 번째 자리를 +이름으로라도 채워야 함 — Luau 문법 제약상 다른 선택지가 없어서 받아들이는 +비용. 사용자가 "아니면 이걸 하지 말던가"로 던진 양자택일에서, 이 정정으로 +구조적으로 안전한 순서를 찾았으므로 **확장 자체는 폐기하지 않고 이 순서로 +유지.** + +`base/bind-system-plan.md`(위 절의 "previous와의 위치 충돌" 소절 전면 +정정)/`ROADMAP.md`(M3)/`.claude/luau-test/15-type-compute-trailing-deps- +typepack.luau`(C를 "막혀야 정상인 음성 대조군"으로 재정의, D를 정정된 +순서의 "통과해야 정상인 양성 대조군"으로 신규 추가)/`.claude/luau-test/ +README.md` 반영 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — `15`의 C/D가 예상대로 나오는지(C는 막히고 D는 통과)까지 같이 +확인해줄 것, 예상과 다르면 이 순서 결정 자체를 재검토. + diff --git a/.claude/session/2026-08-11-04-slot-list-layoutorder-index.md b/.claude/session/2026-08-11-04-slot-list-layoutorder-index.md new file mode 100644 index 0000000..3015871 --- /dev/null +++ b/.claude/session/2026-08-11-04-slot-list-layoutorder-index.md @@ -0,0 +1,152 @@ + + + + +## 2026-08-11 네 번째 세션 — `Slot:List`가 형제 순서(`LayoutOrder`)를 +자동으로 안 세팅하는 것으로 정정, `updateFn`에 `index`/`offset` 추가 + +사용자가 "`Slot:List`의 `updateFn`이 자기 자신 Slot을 얻을 방법이 +없는데, 그럼 offset을 못 보는 거 아니냐"고 질문하며 시작 — 처음엔 +"Handler(quad-roblox `Handlers/Slot.luau`)가 `rawAdd`/`rawMove` 시점에 +`localIndex+offset`을 자동으로 계산해 마운트된 원소의 `LayoutOrder`에 +직접 바인딩해준다"고 답했으나(2026-08-09 여섯 번째 세션 `bind-system-plan.md` +"Length/Offset" 절의 원 서술 그대로), 사용자가 이건 **매직**이라고 +바로 반박 — 컴포넌트가 `Frame { LayoutOrder = 5 }`처럼 자기 프로퍼티로 +이미 지정한 값을 Slot이 마운트 시점에 조용히 덮어쓰게 되고, 애초에 +"`updateFn`이 동적 요소를 전부 다룬다"는 게 원래 설계 의도였다는 것. + +**확정**: Slot/Handler는 `LayoutOrder`를 자동으로 세팅하지 않음 — +`Slot.Offset`(`Slot.Length`와 마찬가지로 공개 필드, Slot 마운트 시점에 +`Dispatch.setOffsetSource`가 등록하는 바로 그 Source를 `self.Offset`으로도 +저장)과 `index`(이제 `State`, "이 key가 지금 실제로 마운트된 +요소들 사이에서 몇 번째냐" — `keyFn`이 받는 raw `data` 배열 인덱스와는 +다른, filter로 압축된 값)를 `updateFn`에 값으로 전달만 하고, 실제로 +`LayoutOrder`(로블록스)든 CSS `order`(웹, 필요할 때만)든 어디에 어떻게 +쓸지는 전부 `updateFn` 작성자 몫 — `index:With(offset):Compute(fn)`을 +평범한 프로퍼티 store-bind로 써넣으면 됨, 새 메커니즘 아님. 수동 CRUD로 +Slot을 쓰는 사용자도 `slot.Offset`을 직접 읽어 같은 걸 스스로 구성 가능. +부수적으로 `setOffsetSource` 자체가 순수 숫자 계산이라 원래도 엔진 지식이 +필요 없었다는 것도 재확인 — `LayoutOrder` 자동 바인딩을 그 옆에 서술했던 +게 레이어링(엔진 무관 `Dispatch/Slot.luau` vs Roblox 전용 `LayoutOrder`) +위반이기도 했음. + +**`updateFn` 시그니처도 같이 정리**: `offset`/`index`(State화) 추가하면서 +파라미터 순서를 반환값 순서와 맞춤(사용자 지적) — 반환이 `(result, ud)` +(`prev`류 먼저, `userdata`류 나중)인데 기존 파라미터는 `userdata`가 +`prev`보다 앞이라 뒤집혀 있었음, `prev, userdata` 순서로 정정: + +```lua +updateFn(item, index: State, offset: Source, prev: T?, userdata: UD?): (T | nil, UD?) +``` + +**부수 발견 — 기존 `reconcile` 의사코드에 실제 버그가 있었음.** `index`를 +진짜 값으로 노출하려다 보니, `rawAdd(self, result, i)`가 raw `data` 루프 +인덱스 `i`를 그대로 위치 인자로 썼던 게 문제로 드러남 — filter로 앞쪽 +item이 마운트 안 되면 실제 마운트된 개수가 `i`보다 적어져서, `Add`의 +"범위 밖 index는 clamp 없이 error" 규칙에 걸려 그냥 터짐. `reconcile` +안에 "지금까지 실제로 마운트된 개수"만 세는 별도 압축 카운터(`pos`)를 +추가해 `rawAdd`/`rawMove`/`keyIndex`/`index` State 전부 이 값 기준으로 +통일 — filter 없이 순서대로면 `pos == i`라 흔한 경우엔 체감 차이 없음. + +전부 `base/bind-system-plan.md`(`setOffsetSource` 절, "Slot.Length와 +Slot.Offset은 별개" 절)/`base/slot-plan.md`(`:List` 파라미터 설명, 신규 +"왜 `LayoutOrder`를 Slot이 대신 안 해주는가" 절, `activateList`/`reconcile` +의사코드 전면 수정) 반영 완료. + +**같은 세션 후속 — `index`도 State가 아니라 raw number로 재정정, +`candidateIndex`로 이중 write 제거.** 사용자가 "reconcile은 sync라 +깜빡임 문제는 없지만, filter로 항목이 새로 보이게 되면 그 뒤 index를 +다 밀어줘야 하는데 Set이 반복적으로 도는 게 비효율 아니냐"고 재질문 — +검토 과정에서 사용자가 직접 더 나은 방향을 제시: `index`도 `item`과 +똑같이 raw number로 넘기고, 반응형으로 쓸지·언제 `:Set`할지는 전부 +`updateFn`이 자기 `userdata` 안에서 알아서 판단하게 두면 되지 않냐는 +것 — 채택. 이러면 `:List`가 `indexState`라는 별도 맵을 관리할 필요 +자체가 없어짐(`item`을 raw로 넘기는 것과 완전히 같은 원칙으로 통일, +"왜 `Source`를 `:List`가 안 만드는가" 절이 원래도 "item/index" 둘 다를 +언급하고 있었던 것과도 재정합). + +**`candidateIndex` 트릭으로 chicken-and-egg 문제도 해소**: `updateFn`에 +넘기는 `index`가 필요한 시점엔 아직 이 item이 살아남을지(필터 통과 +여부) 모르는데, 압축 위치(`pos`)는 원래 "생존자 개수"라 이 item 자신의 +생존 여부에 의존하는 것처럼 보였음 — 그런데 실제로는 **"이 item이 +살아남으면 차지할 위치"는 직전까지 처리된 item들의 생존 개수만으로 +이미 계산 가능**(이 item 자신의 결과와 무관)하다는 걸 확인 — +`candidateIndex = pos + 1`을 `updateFn` 호출 **전에** 계산해서 넘기고, +`result ~= nil`일 때만 `pos = candidateIndex`로 커밋. `updateFn`은 항상 +정확한 최종값을 받으므로, 새로 생기는 원소를 처음부터 `Source(index)`로 +올바르게 만들 수 있어 "임시값으로 등록 → 나중에 Set으로 정정"하는 +이중 write가 구조적으로 없어짐(브랜드 뉴 원소에 대해서도) — look-ahead +(아직 안 본 뒤쪽 item을 미리 훑는 것) 없이 여전히 단일 forward pass. + +전부 `base/slot-plan.md`(`updateFn` 시그니처를 `index: number`로 재정정, +신규 "왜 `LayoutOrder`를 Slot이 대신 안 해주는가" 절에 `userdata` 기반 +예시 코드 추가, `activateList`/`reconcile` 의사코드에서 `indexState` 맵 +전부 제거하고 `candidateIndex` 방식으로 교체) 반영 완료. + +**같은 세션 세 번째 후속 — 예시 코드의 남은 낭비 하나를 사용자가 재정정.** +`Source(index)`/`Set(index)` 분기를 `if not layoutOrder ... elseif` 식으로 +"Source 재사용 여부"만 갖고 나눴던 첫 예시가, "원소를 다시 그리는지 +(`prev == nil`)"와 독립적으로 갈려서 — `prev == nil`(새로 그림)인데 +`ud.layoutOrder`는 남아있는 경우(직전에 filter 탈락했다 재등장) 이전 +Source를 재사용하며 `:Set()`한 뒤 새 Frame을 만들면, 그 `:Set()` 시점엔 +아직 아무도 그 Source를 구독하고 있지 않아 완전히 무의미한 연산이 됨 +— 사용자 지적: "updateFn이 실행되기 전까진 이번 item이 버려질지/다시 +그려질지/source만 갱신될지 아무도 모르니 미리 Set을 해둘 수 없고, +`updateFn` 자신만 이 세 갈래를 정확히 알아서 효율적으로 나눌 수 있다." +예시를 `if not shouldShow ... return nil / if not prev then <새 Source로 +다시 그림> / <기존 Source 재사용, 실제로 다를 때만 Set>` 세 갈래로 재작성 +— "다시 그림" 갈래는 이전 Source를 절대 참조 안 하고 항상 `Source(index)`로 +새로 만듦. + +**같은 세션 네 번째 후속(핸드오버 정리) — 용어 혼동 방지 문서화, 전체 +코퍼스 stale 감사·`ROADMAP.md`/`README.md` 동기화.** 사용자가 "`key`와 +`index`가 헷갈리지 않게 문서화에 유의, `updateFn`이 명시적 책임이 많은 +함수라 문서화가 중요하다, 이 세션 내용 누락/stale 없는지 보고 핸드오버 +준비하라"고 요청 — `base/slot-plan.md`의 `Slot:List` 절 최상단에 세 +가지 값(1. `keyFn(item, index)`의 raw `index` — 원본 `data` 배열 위치, +2. `key` — `keyFn`이 계산하는 정체성, 3. `updateFn(item, index, ...)`의 +`index` — `key`와 무관한 압축된 마운트 위치, 순서 계산 전용)을 이름이 +겹치는데 서로 다르다고 명시하는 콜아웃 신설, `keyFn`/`updateFn` 파라미터 +설명 각각에도 교차 참조 추가. `updateFn`의 반환 갈래 서술(구 "prev 그대로/ +새 값/nil 반환")도 위에서 확정된 "버림/다시 그림/source만 갱신" 세 +갈래 이름으로 통일해 같은 개념이 두 가지 다른 말로 서술되던 걸 정리. +`base/bind-system-plan.md`의 `setOffsetSource` 예시(`index:With(offset)` +— `index`가 State인 것처럼 잘못 읽히던 stale 표현)도 `layoutOrder:With(offset)` +(사용자가 `userdata`에 직접 관리하는 Source)로 정정. `ROADMAP.md` M6/ +`.claude/README.md`의 `Slot:List` 요약이 이번 세션 이전 시그니처 +(`updateFn(item, index, userdata, prev)`, `offset` 없음, `LayoutOrder` +자동 처리 여부 미언급)로 멈춰 있던 것도 최신 상태로 동기화. + +`.claude/luau-test/`류 새 실측 항목은 추가되지 않음(이번 세션은 런타임 +로직/시그니처 설계이지 Luau 타입 시스템 경계 확인 대상이 아님) — 기존 +`userdata = userdata or {}` lazy-init 제네릭 narrowing 실측 필요 항목은 +그대로 유효(M0/M6 착수 시 확인). + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션도 `:List` 세부 설계 정정/문서 정리라 M0 착수 우선순위 +자체는 그대로. + +**같은 세션 다섯 번째 후속 — `key` 타입 무제약 확인, `item.id` 관용구 +문서화.** 사용자가 "캐스케이드 갱신을 막고 싶으면 `keyFn`은 string 등 +unique하기만 한 값이면 되는 거 맞지, `data` 안에 string 필드가 있으면 +그걸 쓰면 된다" 확인 요청 — 맞음(`key`는 Lua 테이블 키로만 쓰여서 타입 +제약 없음, 필요조건은 사이클 간 안정성+유일성뿐). `keyFn`이 `item`을 +그대로 받으므로 `item.id`처럼 이미 있는 안정적 필드를 그냥 반환하면 +됨(새로 뭘 만들 필요 없음). `base/slot-plan.md`의 `keyFn` tradeoff +단락에 이 확인과 `function(item) return item.id end` 관용구 예시를 +명시적으로 추가. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선). + +**같은 세션 여섯 번째 후속 — 중복 `key` 즉시 `error`로 확정.** 사용자가 +"`reconcile`이 이미 `seen[key] = true`를 하니까, 그 앞에 `if seen[key] +then error end`을 두면 거의 공짜로 중복 key를 잡을 수 있지 않냐"고 +제안 — 채택. 조용히 넘어가면 두 item이 `mounted`/`userdata`/`keyIndex`의 +같은 슬롯을 다투는 조용한 버그가 되므로, 다른 Slot CRUD 에러 조건들과 +같은 fail-fast 톤으로 그 자리에서 막음. `base/slot-plan.md`의 `reconcile` +의사코드와 `keyFn` tradeoff 단락에 반영 완료. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선). + diff --git a/.claude/session/2026-08-11-05-slot-doc-framing.md b/.claude/session/2026-08-11-05-slot-doc-framing.md new file mode 100644 index 0000000..48648c0 --- /dev/null +++ b/.claude/session/2026-08-11-05-slot-doc-framing.md @@ -0,0 +1,28 @@ + + + + +## 2026-08-11 다섯 번째 세션 — Slot 문서화 프레이밍 확정: "동적 렌더링을 +가능하게 하는 도구" + +짧은 문서화 톤 논의. 사용자가 "Slot을 동적렌더 가능하게 돕는 도구로 +설명하는 게 문서 톤상 정해져 있냐"고 질문 — 확인 결과 미정이었음(`base/ +slot-plan.md`는 "뮤터블 자식 배열, 엄격한 단일 마운트 소유권"이라는 더 +넓은 컴포지션 도구로, `documentation-content-map.md`도 초심자 단계에선 +"children 배열" 정적 구조 서술을 앞세우고 동적 CRUD/`:List`는 층을 나눠 +후순위로 배치해뒀었음). 사용자가 바로 확정 요청: **Slot의 요지 자체가 +"안에서 요소가 생기든 말든 자유롭다"는 것이라, "동적 렌더링을 가능하게 +하는 도구"로 프레이밍해도 됨** — 아직 미착수인 `Slot():Single(...)` +백로그(2026-08-09 여섯 번째 세션)도 같은 프레이밍의 특수 케이스(1개 +아니면 0개의 동적 렌더)일 뿐이라는 것도 사용자가 직접 짚음. + +`research/documentation-content-map.md`(modifier-plan.md/slot-plan.md +절 최상단에 프레이밍 확정 명시, 겸사겸사 옆에 있던 stale 마커 — +"Slot 형제 순서 보장 미확정"이 실제로는 2026-08-09 여섯 번째 세션에 +Length/Offset으로 이미 해소돼 있었던 것도 발견해 정정)/`base/ +slot-plan.md`(`:Single` 백로그 절에 이 프레이밍 적용 메모 추가) 반영 +완료. 새 런타임 설계는 없음 — 순수 문서 톤 결정. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션은 문서화 톤 결정이라 M0 착수 우선순위엔 영향 없음. + diff --git a/.claude/session/2026-08-11-06-slot-single-nesting.md b/.claude/session/2026-08-11-06-slot-single-nesting.md new file mode 100644 index 0000000..9dd6dff --- /dev/null +++ b/.claude/session/2026-08-11-06-slot-single-nesting.md @@ -0,0 +1,119 @@ + + + + +## 2026-08-11 여섯 번째 세션 — `Slot:Single` 확정, Slot-in-Slot 중첩 확정, +Length/Offset `recompute` off-by-one 버그 발견·수정 + +`Slot():Single(state, updateFn?)` 백로그(2026-08-09 여섯 번째 세션, +"`State`가 offset을 못 받아서 위쪽 Slot의 offset/length를 써야 +했다"는 동기)를 실제로 설계하다가, 더 큰 질문(Slot을 다른 Slot 안에 +넣을 수 있는가)까지 라이브로 풀어낸 긴 세션. 다섯 갈래로 정리: + +**1. `Slot:Single(state, updateFn)` — `:List` 위의 순수 sugar로 확정.** +`state`를 0/1개짜리 배열로 감싸(`:Compute`) `:List`에 위임, 고정 +key(`true`)로 `prev` 재사용을 보장, `index`는 상수라 안 넘김. 원래 +동기(offset 접근)를 이걸로 완전히 해결 — "offset을 얻으려고 컴포넌트가 +Slot을 리턴하는" 우회가 필요 없어짐. `base/slot-plan.md` +"`Slot:Single(...)`" 절. + +**2. Slot-in-Slot 중첩 확정 — 동기는 카테고리 헤더가 아니라 컴포넌트 +결합의 균일성.** 사용자가 직접 짚은 진짜 이유: `SomeComponent(props)`가 +`Instance`를 리턴하든 `Slot`(멀티루트 워크어라운드)을 리턴하든 +`outerSlot:Add(result)`가 분기 없이 동작해야 함 — 지금까지 "요소 타입 +제약"이 `Slot`을 암묵적으로 배제하고 있어서 정확히 이 케이스가 막혀 +있었음. **핵심 발견 — 메커니즘은 그대로 재사용, 새 프리미티브 불필요:** +`Dispatch.setLength`/`setOffsetSource`의 첫 인자(`inst`)가 물리 +Instance일 필요가 없다는 것(`Relate`가 아무 테이블이나 weak 키로 받음)을 +재사용해, **Slot 자신을 owner 키로 같은 두 함수를 한 번 더 부르면 +최상위 마운트와 중첩 마운트가 완전히 같은 함수 호출**이 됨 — 재귀 +`attachSlot(slot, physicalTarget, ownerKey, position)` 하나로 통합. +`Slot.Length`는 raw 개수에서 "요소별 기여도의 합"(plain=1, nested +Slot=그 `.Length`)으로 의미 변경. +- **타입 레벨로 확장하려던 "모든 instance 처리를 Slot에 위임"은 기각** — + 리터럴 배열(`Dispatch.drive`)의 요소 타입 규칙(Ref/PreRef/Observer + 허용)이 Slot의 요소 타입 규칙(같은 값들 금지)과 정반대라, 타입을 + 진짜로 통합하면 "만들어진 방식에 따라 행동이 다른 Slot"이라는 숨은 + 분기가 생김 — **메커니즘(setLength/setOffsetSource/recompute)만 + 공유하고 타입/CRUD 표면은 분리 유지**로 스케일 확정(사용자 확인: + "그게 더 엔지니어링 비용이 싸고 좋은 구현"). +- **파괴는 재귀적 `Clear()`가 아니라 flat `destroySlotTree`** — 사용자가 + 직접 비용 문제 지적("clear된 다음 length 바뀌고 위치변경 전파되는 + 구조는 안 됨"): 재귀 `Clear()`(요소별 Remove 반복)는 죽는 서브트리 + 내부에서 불필요한 shift+recompute가 요소 수만큼 반복됨 — 대신 순수 + 파괴 walk(`.Destroy()`만)+`unbindLifetime` walk로 바꾸고, outer 쪽 + recompute는 자기 위치 하나에 대해서만 1회. `unbindLifetime`이 왜 꼭 + 필요한지도 새로 드러남 — `bindLifetime`은 물리 target 생명주기에 + 걸려있어 target이 살아있는 채로 논리 서브트리만 죽는 경우(카테고리 + 삭제 등) GC가 자동으로 안 치워줌, 명시적 호출 필요(물리 target 자체가 + 죽는 경우는 기존처럼 GC가 전부 처리). +- **`Slot(initial?: {T})` 생성자로 확장** — "인자 없는 빈 생성자로 + 확정"을 뒤집음(2026-08-09 세 번째 세션 결정 정정), 단 새 마운트 + 로직이 아니라 `:Add` 반복 호출 sugar(`ipairs`의 "첫 nil에서 멈춤" + 동작이 "중간 nil UB, 그 뒤 무시"를 공짜로 구현). **`initial ~= nil`이면 + 빈 테이블이어도 즉시 `_crudUsed = true`**(사용자 지적: `Slot({})`은 + 상태상 `add():remove(1)`과 동일이라 결과가 비어있어도 "CRUD를 썼다"는 + 의도는 이미 커밋됨) — `Slot()`(진짜 `nil`)만 나중에 `:List`/`:Single` + 설치 가능. **`_crudUsed` ↔ `_listed` 상호 배타 가드도 신설** — 기존엔 + `:List` 설치 후 수동 CRUD만 막았지 반대(수동 CRUD 후 `:List` 설치)는 + 안 막아서, `:List`의 reconcile이 기존 요소를 모른 채 충돌하는 gap이 + 있었음(사용자 발견). +- **DOM 백엔드가 nested Slot을 실제 `
` 중첩으로 매핑하는 안은 + 기각** — 제가 처음 낸 "web은 물리 nesting을 지원하니 이 메커니즘이 + 아예 필요 없을 수도"라는 제안을 사용자가 직접 반박: React `<>`가 + 존재하는 이유와 정확히 같은 이유로 Slot도 의도적으로 wrapper 없는 + 그룹핑 도구라, div 매핑은 그 원칙 자체를 깨버림. 숫자 기반 메커니즘은 + web에도 그대로 필요하되, `insertBefore`/`removeChild`가 물리적으로 + 밀고/당겨주므로 이미 배치된 형제 프로퍼티 재작성은 불필요(기존 + 2026-08-09 여섯 번째 세션 확정과 정합적, 사용자가 세션 도중 직접 + 재확인). "물리적으로 이전에 어디 있었는지" 같은 backend 종속 위치 + 정보는 base 책임이 아니라 필요한 backend가 자기 `Relate`로 저장할 + 몫 — 새 설계 불필요, 이미 확정된 base/backend 경계 그대로. + +**3. `Dispatch.setLength`/`setOffsetSource`/`recompute`의 owner 키가 +물리 Instance로 한정될 필요 없다는 걸 `base/bind-system-plan.md`에 +명시.** "Length/Offset" 절에 짧은 절 신설 — 이게 위 재귀 메커니즘 전체의 +근거. + +**4. `recompute`의 off-by-one 버그 발견·수정 — 중첩과 무관한, 기존 +Length/Offset 메커니즘 자체의 버그.** 구체 숫자로 흐름을 검증하려다 +발견: 원래 코드가 `sum += lengthList[i]`를 먼저 하고 `offset:Set(sum)`을 +나중에 해서, `offset[i]`가 "자기 앞의 형제들이 기여한 개수"가 아니라 +**자기 자신을 포함한** 누적합이 되고 있었음(예: `Frame{Slot1}` 하나뿐이어도 +`Slot1.Offset`이 `Slot1.Length`가 되어버림) — 순서를 뒤집어(offset 먼저 +Set, 그 다음 sum에 자기 기여도 누적) 수정. 지금까지 실제 Luau로 돌려본 +적이 없어 아무도 못 잡았던 버그. 카테고리 헤더 예시(7개 리프)로 수정된 +공식을 검증, 정확히 저작 순서대로 LayoutOrder 1..7이 나옴을 확인. +**`offset`/`sum`은 0-based 개수, `index`는 1-based Lua 관례**라는 +점도 명시적으로 콜아웃(둘을 섞는 `index+offset` 공식이 의도된 것이지 +인덱싱 불일치가 아님). + +**5. Reentrancy 가드 검토 후 기각 — 제가 처음 제안한 `_recomputing`/ +`_dirty` 플래그가 불필요함을 사용자가 정확히 캐치.** "nested Slot이 +있으면 항상 dirty가 켜져서 불필요한 for문이 한 번 더 돈다"는 사용자 +지적을 계기로 호출 경로를 다시 추적 — **각 Slot이 `Relate(자기 자신)`으로 +독립된 `bk`를 가지므로, 중첩 Slot의 Length 변경이 상위로 전파되는 경로는 +항상 서로 다른 `bk`를 거쳐 지나감**, 즉 nesting이 있다는 사실만으로는 +같은 `(ownerKey,bk)`가 재진입되는 경로 자체가 없음이 확인됨 — 제가 +가드의 동기 자체를 잘못 짚었던 것. 진짜 재진입(부작용이 recompute +도중 같은 Slot에 다시 Add/Remove)은 이미 확정된 "일반적 재진입/무한루프 +방어 안 함, 사용자 코드 버그로 간주" 원칙 그대로 두면 되므로, 가드 +없이 off-by-one만 고친 순수 버전으로 최종 확정. **같은 세션 후속으로 +이 케이스에 명시적 이름을 붙임(사용자 제안)** — `Source⊇State`의 +"단방향"(파생값이 자기 upstream Source로 거꾸로 안 쓴다) 원칙과 같은 +카테고리 위반으로 프레이밍: recompute가 만드는 `offset`/`Length`는 +`lengthList`(upstream 입력)에서 파생된 다운스트림 값인데, 부작용이 +자기 자신의 `lengthList`를 다시 mutate하는 게 그 반대 방향 쓰기라 +"State가 자기 Source에 Set을 가하는 것"과 동일한 UB로 명명 — +새 원칙이 아니라 이미 있는 단방향 흐름 원칙의 재적용. + +**반영된 파일**: `base/slot-plan.md`(요소 타입 제약/`Slot(initial)`/ +CRUD 가드/`Slot:Single`/"Slot-in-Slot 중첩" 신규 절/`Slot.Length`), +`base/bind-system-plan.md`(owner 키 일반화/`recompute` 버그 수정), +`ROADMAP.md`(M6 체크박스 다수 추가), `.claude/question.md`(`Slot:Single` +백로그 해소 표시). + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. +`Slot`의 자기 참조 제네릭 실측이 luau-test 확인 목록에 새로 추가됨. + diff --git a/.claude/session/2026-08-11-07-reactive-raw-elements.md b/.claude/session/2026-08-11-07-reactive-raw-elements.md new file mode 100644 index 0000000..ece9d64 --- /dev/null +++ b/.claude/session/2026-08-11-07-reactive-raw-elements.md @@ -0,0 +1,87 @@ + + + + +## 2026-08-11 일곱 번째 세션 — 반응형 raw 요소: Slot이 `State`/`Source`도 +요소로 허용(`:Single` sugar로), `:List`의 index-스킵 보강 + +짧은 세션. 사용자가 "Slot이 `state`/`state`을 안 받는데, +`setLength`/`setOffsetSource`를 재사용하면 쉽게 구현 가능하지 않냐"고 +제기하며 시작 — 최초 검토안이 사용자의 즉각적인 반례로 기각되고 훨씬 +단순한 최종안으로 수렴한 단일 스레드라, 시행착오 없이 최종 확정만 +정리(경위는 아래 "기각된 최초안" 절에 압축 보존). + +**확정**: `Slot:Add`(및 `Slot(initial)` 생성자 sugar)가 받는 `element`의 +타입이 `T | State | Source`(`T = Instance | Slot`, 여섯 +번째 세션에 확정된 자기 참조 제네릭과 합성)로 확장 — 임의 깊이 nesting +가능(사용자: "이게 얼마나 nesting되는지는 유저 마음, 다 처리가 가능함"). +**구현은 새 메커니즘이 아니라 순수 `:Single` sugar**: `isState(element)`면 +그 자리에 내부적으로 `Slot():Single(element)`(nested Slot, `updateFn` +생략 시 identity 기본값)를 대신 삽입 — + +```lua +function Slot:Add(element, index) + if isState(element) then + local sub = Slot() + sub:Single(element) -- updateFn 생략 → identity 기본값 + element = sub + end + return rawAdd(self, element, index) +end +``` + +이 sugar가 성립하도록 `Slot:Single(state, updateFn?)`도 같이 확정 — +`updateFn`을 선택 인자로 완화(기본값 `function(item) return item end`). + +**기각된 최초안 — position-keyed StoreBind 구독 + `state:Compute`로 +파생한 Length.** 처음엔 `rawAdd`가 그 위치에 대해 별도 재-dispatch +구독을 걸고 Length 기여도도 `state:Compute(...)`로 파생시켜 `setLength`에 +넘기는 방식을 검토했으나, **사용자가 즉시 반례를 제시해 기각**: "state +언랩 시에 None 되는게 복잡함. 그럼 slot 사이에 None이 존재할 수 있게 +되는 거 아님? Length 계산도 달라져야 하고, Add/Remove 동작성이 문제가 +됨 — 사실상 Slot:Single이랑 정확히 같은 구현이 돼야 함." 검증 결과 +정확함 — (1) `State`(nilable)를 지원하려면 `_elements`에 `None`을 +다시 끌어들여야 함, (2) Length 계산에 예외가 생김, (3) `Move`/`Swap`이 +인덱스를 옮길 때마다 그 위치의 구독도 같이 옮겨야 하는 인덱스-구독 +동기화 부담이 생김 — **정확히 `:List`가 element가 아니라 `key` 기준으로 +설계된 이유와 정면 충돌하는 회귀**였음. + +**위 확정된 sugar가 이 세 문제를 전부 없애는 이유**: 바깥 `_elements`엔 +항상 안정적인 `sub` Slot 레퍼런스만 있어 `None`이 안 들어가고, `sub`가 +비어있는 것 자체가 이미 지원되는 정상 상태(Length 0 자동 기여)이며, +Remove/Move/Swap도 그 `sub` 레퍼런스 하나만 다루면 끝이라 인덱스-구독 +동기화가 필요 없음. raw `State` 요소는 결국 "`updateFn` 생략한 +`:Single`"일 뿐이고, `:Single`에 `updateFn`을 직접 주면 `prev`/`userdata` +patch-reuse + `offset` 접근이 되는 상위 호환 — 둘은 대체 관계가 아니라 +같은 메커니즘의 다른 `updateFn`. + +**`:Single`이 애초에 생긴 이유가 정확히 이 offset 접근**(사용자 직접 +확인): "`Single`은 정확히는 `updateFn`에서 렌더할 때 `offset`이 필요해서 +만들어진 것" — raw `State` 요소(identity `updateFn`)는 값이 바뀔 +때마다 이전 mount를 통째로 버리고 새로 만드는 coarse swap(quad가 `prev` +재사용을 안 해줌)인 반면, `updateFn`을 직접 지정한 `:Single`은 같은 +Instance를 유지하며 속성만 patch하는 fine-grained 갱신 + `offset`을 +`updateFn`에 직접 전달. 사용자가 제시한 조합 예시(`Slot{ State +--[[리스트헤더]], Slot():List() --[[아이템]] }`)를 문서에 그대로 반영 — +LayoutOrder(offset) 참여가 필요 없는 요소는 raw `State`로 가볍게, +개수/순서가 동적이라 offset이 필요한 그룹은 `Slot():List(...)`로 +감싸는 식으로 한 Slot 안에서 자유롭게 섞어 씀. + +**부수 발견(사용자) — nested-Slot 결과를 반환하는 `:List` 아이템의 +`index` 스킵.** "상위 입장에서의 인덱스는, Slot이 있으면 크게 건너뛰겠네 +아마 의도된 동작이긴 할듯"라는 지적 — 검증 결과 맞음: `updateFn`이 +nested Slot(멀티루트 컴포넌트 결과 등, Length=N)을 반환하면 그 아이템은 +물리적으로 1개가 아니라 N개를 차지하므로, `reconcile`의 `pos` 커밋이 +고정 `+1`이 아니라 `+result.Length`여야 다음 형제의 `index`가 LayoutOrder +계산에서 안 겹침 — `pos = candidateIndex - 1 + (isSlot(result) and +result.Length:Get() or 1)`로 수정, 의도된 동작으로 확정. 남는 캐비엇(그 +Length가 outer `:List`의 reconcile 없이 나중에 바뀌면 `index`가 스냅샷인 +채로 안 갱신됨)은 이미 확정된 "`index`는 raw, 실시간 반응은 `updateFn` +몫" 원칙의 당연한 연장이라 새 문제로 취급 안 함. + +**반영된 파일**: `base/slot-plan.md`(요소 타입 제약 절 갱신, "반응형 raw +요소" 신규 절, `:Single`의 `updateFn` 선택 인자화, `reconcile`의 `pos` +커밋 공식 수정)/`ROADMAP.md`(M6)/`.claude/README.md`/`.claude/question.md`. + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 +우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. diff --git a/.claude/session/2026-08-11-08-claude-md-restructure.md b/.claude/session/2026-08-11-08-claude-md-restructure.md new file mode 100644 index 0000000..ef3f060 --- /dev/null +++ b/.claude/session/2026-08-11-08-claude-md-restructure.md @@ -0,0 +1,73 @@ + + + + +## 2026-08-11 여덟 번째 세션 — CLAUDE.md 재구조화(3196줄 → 세션 로그 분리) + +**출발점**: 사용자가 CLAUDE.md가 거의 4000줄(실측 3196줄)까지 불어나 있고, +이게 컨텍스트 성능 저하를 유발하고 있는 것 같다고 지적. 요청 세 가지: +(1) 이미 뒤집혔거나 유효하지 않게 된 서술을 기존 `archive/*-reversed.md`/ +`*-rejected.md` 컨벤션처럼 나중 `quadnomicon` 개발로그 소재용으로 옮길 것 — +문서에는 valid한 것만 있어도 충분, (2) CLAUDE.md 자체를 분리해서 세션당 +가벼운 몇 줄 요약+ref로 재편할 것, (3) CLAUDE.md에 아직 반영 안 된 게 +있다면 archive 전에 먼저 처리할 것. 컨플릭이 나거나 사용자 판단이 필요한 +게 나오면 질문하라는 요청도 같이 받음(사용자가 옆에 대기 중). + +**착수 전 확인 — "반영 안 된 것" 감사.** `.claude/README.md`와 +`.claude/question.md`를 읽어 CLAUDE.md의 세션 로그가 주장하는 "전부 반영 +완료"가 실제로 맞는지 대조 — 두 문서 모두 2026-08-11 일곱 번째 세션 +(Slot 반응형 raw 요소)까지 정확히 최신 상태로 유지되고 있었음을 확인. +`base/`로 승격/`archive/`로 역전 이관된 내용도 전부 대응하는 최신 문서에 +반영돼 있었음 — CLAUDE.md의 각 세션 요약이 매번 "전부 base/에 반영 +완료"라고 스스로 기록해온 습관이 실제로 지켜지고 있었다는 뜻. 결론: +**archive 이전 전에 별도로 처리해야 할 미반영 사항 없음.** + +**구조 설계 — 세션 로그 전체가 곧 "invalid 서사"라는 재해석.** 처음엔 +"이미 archive/*-reversed.md로 안 옮겨진, 세션 로그 안에 남아있는 개별 +역전 서술"만 추가로 archive 이전할 대상으로 좁게 봤으나, 사용자 원 메시지를 +다시 읽어보니 더 근본적인 지점이 있었음 — CLAUDE.md의 세션 로그 자체가 +"당시엔 맞다고 생각했다가 다음 세션에 정정된" 시행착오 서사로 가득 차 +있고, 이게 바로 사용자가 quadnomicon 개발로그 소재로 삼고 싶어한 +"invalid해진 이야기"의 실체였음. 즉 세션 로그 섹션 전체(정정 이력 포함, +valid final 결론 포함 뒤섞인 채)를 그대로 `.claude/session/`으로 옮기는 +것 자체가 정확히 사용자가 요청한 "archive" 동작 — 개별 문장 단위로 +valid/invalid를 골라내는 추가 작업은 불필요했음(오히려 quadnomicon +devlog 원자료로서는 시행착오가 섞인 원문 그대로가 더 가치 있음). + +**실행**: +1. `grep -n "^## "`로 CLAUDE.md 세션 로그 구간(157~3196행)의 헤더 38개를 + 전부 추출, `sed -n`으로 각 구간을 `.claude/session/YYYY-MM-DD-NN-slug.md` + 파일 38개로 분리(라인 합 3040 = 3196-157+1로 무손실 분리 확인). +2. 각 세션 파일 최상단에 출처/용도를 밝히는 짧은 HTML 주석 3줄 prepend + (이 파일이 quadnomicon 원자료이고 base/가 최종 소스임을 명시). +3. CLAUDE.md 앞부분(언어 관례~작업 방식, 1~104행)은 거의 그대로 유지, + "계획 문서 구조" 절에 `.claude/session/` 신규 폴더 설명 bullet만 추가. +4. "지금 할 일" 절을 압축 — 원래 50줄 넘게 각 항목의 세부 배경을 반복 + 서술하던 걸, `.claude/question.md`/`research/pre-implementation-audit.md` + 같은 "소스" 문서를 다시 대조해 지금 실제로 열려있는 것만 짧게 남기고 + 해소된 세부사항은 걷어냄(예: 용어 정리 항목은 원래 "State가 위험, + DI가 충돌, PerInstanceState가 충돌" 3개를 나열했었는데, `PerInstanceState`는 + 2026-08-08 세션에 `Relate`로 대체돼 이름 문제 자체가 없어졌으므로 제거 + — question.md 1번을 최신 소스로 재확인 후 반영). +5. 38개 세션 전체를 훑어 각각 2~4줄 압축 요약을 새로 작성, 원래 세션 + 헤더 제목(다른 base/ 문서들이 "2026-08-08 세 번째 세션" 식으로 정확히 + 인용하고 있어 바뀌면 참조가 깨짐)을 그대로 보존한 채 `.claude/session/` + 파일 링크와 함께 "세션 히스토리" 절에 배치. +6. 이 절 자체가 다시 장문 서사로 불어나는 걸 막기 위해, 절 맨 위에 + "이 절을 갱신하는 방법" 가이드를 명문화 — 앞으로 세션이 끝나면 전체 + 서술은 `.claude/session/`에 새 파일로, CLAUDE.md엔 2~4줄+링크만 + 추가하도록 규칙화. +7. `ROADMAP.md`의 stale 참조("CLAUDE.md '최근 세션 요약'도 갱신") 발견해 + 정정, `.claude/README.md` 폴더 표에 `session/` 행 추가. + +**결과**: CLAUDE.md 3196줄 → 412줄(약 87% 감소). 원문은 전혀 유실 없이 +`.claude/session/` 38개 파일에 그대로 보존(줄 수 총합 검증 완료). 사용자 +판단이 필요한 컨플릭은 발생하지 않음 — base//question.md/README.md가 +이미 세션마다 성실히 동기화돼 있었기 때문에 순수 기계적 분리+요약 작업으로 +끝남. + +**다음 세션이 알아야 할 것**: CLAUDE.md의 "세션 히스토리" 절이 이제 +새 컨벤션의 소스 — 세션이 끝나면 이 파일 패턴을 따라 `.claude/session/`에 +새 파일을 만들고 CLAUDE.md엔 짧은 요약만 추가할 것. `ROADMAP.md`/`base/` +착수 우선순위 자체는 이번 세션으로 전혀 안 바뀜(여전히 M0부터, luau-test +결과 확인 우선). diff --git a/CLAUDE.md b/CLAUDE.md index 1698a61..5d030c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,11 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 이유+diff와 함께 보존하는 용도로도 사용 시작**(구현 완료 대상만이 아님) — `archive/store-source-proxy-reversed.md`가 첫 사례, 나중 `quadnomicon` 콘텐츠 소재로 재사용 예정. +- `.claude/session/` — **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오· + 정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 이 CLAUDE.md가 3000줄 + 넘게 불어나 성능 저하를 유발해서 분리함. 아래 "세션 히스토리"의 각 항목이 + 여기로 링크. **항상 읽을 필요 없음** — 특정 결정의 논의 과정/시행착오가 + 궁금할 때만 열어볼 것, 지금 유효한 설계는 항상 `base/`가 소스. - `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`) + 원본 요청. **읽기 전용, `.gitignore`로 커밋 제외됨** — 내용을 다른 곳으로 옮기지 말고 항상 원본 @@ -87,7 +92,7 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 좋은 예시(`.claude/initreq/code-docker/webmanager/.claude/README.md` 참고). - **문서가 쌓이면서 모순/중복/stale 마커가 생기기 쉬움 — 주기적으로 감사할 것.** 2026-08-04 세션에 실제로 전체 `.claude/` 코퍼스에서 이런 문제가 - 다수 발견되어 정리함(아래 "최근 세션 요약" 참고) — 여러 라운드에 걸쳐 + 다수 발견되어 정리함(아래 "세션 히스토리" 참고) — 여러 라운드에 걸쳐 같은 문서를 계속 고치다 보면 "정정됨" 표시가 원래 문장에 안 반영되고 방치되는 패턴이 반복되니, 큰 방향 전환이 있을 때마다 관련 문서 전체를 훑어 확인할 것. @@ -104,3093 +109,304 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 ## 지금 할 일 (우선순위순) -1. **구현 시작 — 루트 `ROADMAP.md`의 M0부터.** 설계 단계는 2026-08-04 - 로드맵 인수인계 라운드로 종료, 다음 세션은 바로 `ROADMAP.md` M0(스켈레톤+ - 기술검증 스파이크)부터 시작. M0는 "진짜" 마일스톤이 아니라, 지금까지 - 추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 세 가지(Store/State - propagation, 재귀 process/retract 디스패치, 컴포넌트 경계 named-parameter - 전달)를 던지는 코드로 검증하는 단계 — 여기서 걸리면 `base/` 문서를 그 - 자리에서 고치는 게 정상. M0 통과 후 M1(실제 스캐폴딩: `quad-base/`, - `quad-roblox/` 폴더 + `wally.toml`/`default.project.json`/`.luaurc` + - quad-base용 최소 mock 테스트 하네스)으로 진행 — 소스 트리 자체는 이미 - 확정됨(`base/architecture.md` "구현 착수" 절). 이 단계부터 - `qa-request/`/`archive/` 폴더가 실제로 쓰이기 시작함. **세부 순서/todo는 - `ROADMAP.md`가 소스** — 여기서 반복 안 함. **M0 착수 직전에 `research/ - pre-implementation-audit.md`(2026-08-06 신설)의 우선순위1 항목부터 먼저 - 확인할 것** — 특히 M0 스파이크 코드 자체에 반영해야 할 항목(props.Modifier/ - Ref를 안 넘기는 케이스 포함, `store.key` 레코드 필드 타이핑도 M0로 - 앞당기기 검토)이 있음, 아래 최신 세션 요약 참고. **M0 실제 착수 전, - `.claude/luau-test/`(2026-08-09 신설)의 사전 검증 스파이크 결과부터 - 확인할 것** — M0가 공식 짜야 할 스파이크와 겹치는 항목들을 미리 - 독립 스크립트로 만들어 사용자가 `luau`/`luau-analyze`/`luau-lsp`/ - Roblox Studio로 직접 돌려보기로 한 상태, 아직 결과 미확인. 걸리는 - 게 있으면 `base/` 문서부터 고치고, 없으면 그대로 M0 실제 코드 작성에 - 재사용하면 됨(README 참고). -2. **용어 정리 — 사용자가 별도로 요청, 진행 중.** "register"(v1) 같이 - 부정확한 이름들을 전체적으로 재검토하자는 요청 — 1차 제안 완료(우선순위 - 순: `State`가 React/Vue식 "쓸 수 있는 로컬 상태"라는 통상 의미와 반대라 - 가장 위험, `DI`가 Dependency Injection 축약어와 충돌, `PerInstanceState`가 - 핵심 프리미티브 `State`와 이름 충돌 — 세부는 `.claude/question.md` 참고), - 사용자와 같이 계속 논의 필요. 컴포넌트 경계용 `props.Modifier`/`props.Ref`/ - `Modifier.Merge` 같은 새 가칭들도 이 정리에 합류 대상. +1. **구현 시작 — 루트 `ROADMAP.md`의 M0부터.** 설계 단계는 2026-08-04 로드맵 + 인수인계 라운드로 종료. M0 착수 직전 확인할 것 둘: + - **`.claude/luau-test/`(2026-08-09 신설) 스파이크 결과** — 아직 사용자가 + `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 안 돌려봄, 결과가 + 나오면 그것부터 반영할 것(`luau-test/README.md`가 파일별로 뭘 우선 + 확인해야 하는지 이미 적어둠). 걸리는 게 있으면 `base/` 문서부터 고치고, + 없으면 그대로 M0 실제 코드 작성에 재사용. + - **`research/pre-implementation-audit.md` 우선순위1** — 신설 당시 + 11개였으나 대부분 이후 세션에서 해소됨(Tween/Tag 재설계, Slot CRUD, + `canExecute` 시그니처, `LifetimeHandle` 로드맵 순서 등). 실제로 아직 + 열려있는 건 "우선순위 스캔 동률/매치실패 처리"(1-3) 하나뿐 — + `.claude/question.md` 2번이 최신 상태. +2. **용어 정리 — 1차 제안 이후 대부분 확정, 소수만 남음.** 최신 소스는 + `.claude/question.md` 1번(개수 반복 안 함, 항목 추가/해소될 때마다 여기가 + stale해지는 패턴이 반복됐어서). 아직 열려있는 것만 짚으면: `State`(1순위, + 위험도 높음 — 업계 통념("쓸 수 있는 로컬 상태")과 반대 의미라 오해 위험), + `DI`→`D`, `Slot`, `canExecute`→`isAlive`, `Brand`. 3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만 필요, 구현 착수를 막지 않음. -4. **[백로그] 범용 렌더 디버깅 도구로서의 quad-mock.** 1번의 quad-base 테스트용 - mock과는 별개 — 정적 스냅샷을 넘어 Tween mock 같은 동적 동작까지 지원하는 - 더 큰 스코프의 디버깅 도구(`architecture.md` "테스트 전략" 절 백로그 참고). - 효용성 봐가며 나중에 검토, 지금 당장 설계할 필요 없음. **[백로그, 별개]** - 런타임 디버깅 플러그인 `quad-debug`(실물 Instance→코드 위치 역추적, - `research/debug-tooling-plan.md`)도 2026-08-06 세션에서 설계 수렴 — - 채널 실현 가능성(Studio 플러그인↔Play 중 게임 간 BindableEvent/Function - 통신)까지 실측 검증 완료, 세부 API 이름만 남음. 착수 시점은 여전히 - "quad 개발 상당 부분 끝난 뒤"로 사용자가 못박음. 같은 날 파생된 문서화 - 전략(`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`)까지 늘어남 — - 착수 우선순위 자체는 안 바뀜(여전히 후순위), 아래 최신 세션 요약 참고. +4. **[백로그]** 범용 렌더 디버깅 도구 `quad-mock`(Tween mock 등 동적 동작 + 지원, M0 mock 테스트 하네스와는 별개), 런타임 디버깅 플러그인 + `quad-debug`(Studio 플러그인, 실물 Instance→코드 위치 역추적 — 채널 + 실현 가능성은 실측 검증 완료, 세부 API 이름만 남음), 문서 사이트 전체 + 구조(초심자/api/심화/`quadnomicon` 4축 + 콘텐츠 맵) — 전부 "quad 개발 + 상당 부분 끝난 뒤"로 사용자가 못박은 후순위. 상세는 `.claude/README.md`의 + `research/` 표(`debug-tooling-plan.md`/`documentation-plan.md`/ + `documentation-content-map.md`/`framework-comparison-findings.md`). 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 (`HUMAN_TODO.md` 2번 항목). -## 최근 세션 요약 (2026-08-04, 6차 라운드 이후) - -**6차 라운드**: 남아있던 "급하지 않음" 질문 두 개 해소 — 태그 네임스페이싱 -충돌은 컴포넌트 단위로는 Ref가 대신 해결해줘서 심각하게 안 봄(`architecture.md` -5번), Store가 Store를 담는 경우는 없음으로 확정(Store는 Source에 준하는 -"시작점"이라 다른 반응형 값에 자동 연결되지 않음, `bind-system-plan.md`). - -**그 이후 채팅에서 세 가지 큰 스레드가 새로 열림/정리됨**: -- **Modifier 메커니즘 전체 확정** — 런타임 pluggable 핸들러가 아니라 정적 - merge, immutable+`table.clone` 기반 체이닝, 필드가 State일 수도 있는 - 경우의 setter/getter 동작까지 전부 확정(`base/modifier-plan.md`, 새로 - base 승격). 이 논의에서 "관측해야 실체화된다"는 프로젝트 전역 원칙도 - 명문화(`bind-system-plan.md`). -- **컴포넌트화 논의, 같은 날 후속 세션에서 완결** — v1의 `Class.Extend()` - 자동-store 매직은 폐기하고 React식으로 값을 명시적으로 전달하는 방향으로 - 수렴, `StoreSource`(Source를 인터페이스+구현체로 보고 Store 키에서 얇은 - 프록시로 얻는 것) 아이디어 확정. 마지막 미결이던 "modifier/Ref의 컴포넌트 - 경계 통과"도 후속 세션에서 풀림: Compose/Fusion/Vide/v1 4개 선례를 - 서브에이전트로 병렬 조사한 결과 전부 named parameter로 경계를 넘기는 - 패턴에 수렴한다는 게 확인됐고, "컴포넌트가 여러 루트를 반환한다"는 - 프레이밍 자체가 (a) Luau가 tail position 밖 다중 리턴을 지원 안 해서 - 불필요한 개념과 (b) 이미 있는 Slot 메커니즘을 섞은 것이었음이 드러나 - 정리됨 — 결론: 경계는 named parameter(`props.Modifier`/`props.Ref` - 가칭), "다중 루트"라는 별도 개념은 폐기, 여러 modifier를 하나로 합치는 - `Modifier.Merge`(가칭) 유틸 추가. `research/component-composition-plan.md` - → `base/component-composition-plan.md`로 승격 완료. -- **문서 전체 감사 및 정리** — `.claude/` 코퍼스 전체(약 15개 문서)를 - 서브에이전트로 감사해 여러 라운드에 걸쳐 쌓인 모순/중복/stale 마커를 - 대거 발견하고 수정(예: 이벤트 dot-access 확정 여부가 문서 내에서 서로 - 모순, 이미 해소된 질문이 "미해결"로 방치, 존재하지 않는 문서/섹션을 - 가리키는 끊긴 참조 다수, `TagService`/`CollectionService` 혼용 등). - `research/purity-and-effects-plan.md`도 내용이 이미 확정 상태라 `base/`로 - 승격. **이 CLAUDE.md 자체도 이번에 오래된 라운드별 인수인계 메모 3개를 - 이 요약 하나로 통합하며 정리함** — 라운드별 상세 히스토리가 필요하면 - git log와 각 `base/`/`research/` 문서 안의 라운드 표시(예: "2026-08-04 - 3차 라운드")를 참고할 것, 여기서 전부 반복하지 않음. - -**같은 날 로드맵 인수인계 라운드 — 설계 단계 마무리, 구현 준비 완료**: -- **quad-base 테스트 mock 방향 확정**: Vide 선례(`test/mock.luau`, ~300줄, - 순수 `luau` CLI, Studio 불필요) 그대로 채택, 스코프는 정적 디버깅 한정(Tween - 같은 동적 동작 제외), quad-roblox로 작성한 컴포넌트가 mock에서도 그대로 - 돌아가야 한다는 요구 없음(단순하게 감) — `architecture.md` "테스트 전략" - 절. 범용 렌더 디버깅 도구(Tween mock 포함)는 별개로 백로그. -- **구현 전 리스크 감사**: `.claude/base/` 전체 + 남은 `research/`를 - 서브에이전트로 감사해 "실제 Luau 접촉 없이 추론만으로 확정된 것" 3개 - (Store/State 반응형 코어, 디스패치 엔진, 컴포넌트 경계 modifier/Ref)를 - 식별 — 이것들은 M0 스파이크로 검증하기로 함(아래). 감사 중 `slot-plan.md`가 - 스스로 "정식 확정 안 됨"이라 표시해뒀던 "클래스가 슬롯을 받는 방법"(Named - Slot 없음)도 이번에 정식 확정, 대신 "여러 Slot이 형제로 섞일 때 순서 보장" - 이라는 새 하위 질문이 열림(다중 백엔드 관점, Roblox만이면 급하지 않음). - `State` 조합은 UB로 확정해 타입으로 막기로 함(`modifier-plan.md` - 7번), 디스패치 엔진의 일반적 무한루프는 방어 로직 없이 provider 버그로 - 간주하기로 확정(`bind-system-plan.md`). -- **루트 `ROADMAP.md` 신설** — M0(스켈레톤+기술검증 스파이크, "진짜" - 마일스톤 아님)부터 M11(Tween)까지 + 병행 가능 항목 + 백로그로 구성된 실행 - 계획, todo 체크박스 포함. 오늘은 문서 준비만 — **다음 세션이 M0부터 실제 - 시작**. - -용어 정리 제안 진행 중인 점은 위 "지금 할 일" 2번 참고. - -## 2026-08-06 세션 — quad-debug(런타임 디버깅 플러그인) 설계, 실측 검증까지 완료 - -팀원이 "실물 Frame에서 그걸 만든 코드 위치를 역추적하는 Studio 플러그인이 -있으면 좋겠다"는 피드백을 줬고, 사용자가 이걸 `quad-debug`/ -`quad-debug-roblox-plugin`으로 후순위 설계해두자고 판단해 시작된 세션. -착수는 여전히 "quad 개발이 상당 부분 끝난 뒤"로 못박혀 있음(구현 우선순위는 -안 바뀜) — 대신 base 설계(디스패치/Source/DI 생성자) 시점에 훅 확장 -지점만 고려해두면 나중이 훨씬 싸진다는 문제의식으로 지금 미리 설계만 -해둠. 전체 내용은 `research/debug-tooling-plan.md`, 요지만 요약: - -- **설계를 막던 유일한 기술적 불확실성이 실측으로 해소됨**: Roblox Studio - 플러그인과 Play 중인 게임(LocalScript)이 별도 Luau VM/스크립트 - 컨텍스트라 `BindableEvent`/`BindableFunction`이 그 경계를 실제로 - 넘는지가 문서만으로는 안 갈렸음(공식 문서는 언급 없음, DevForum엔 - 실패 사례도 있었음) — 사용자가 테스트용 플러그인/스크립트 - (`plugin-ignoreme.luau`/`game-ignoreme.luau`, 레포 루트, `*-ignoreme*` - 패턴이라 자동 gitignore)를 직접 Studio에서 돌려 **Fire/Connect, - Invoke/OnInvoke 왕복 둘 다 안정적으로 작동함을 확인**. 원리는 "Play - 진입 시 DataModel이 복제되는 게 아니라 script identity/보안 컨텍스트만 - 분리되고, Instance 자체(C++ userdata)는 어느 컨텍스트에서든 같은 - 참조를 가리킨다"는 것 — 사용자가 실측 도중 직접 정리한 설명. -- **채널은 확인됐지만 처음 구상(quad-debug-roblox가 `ReplicatedStorage`에 - Bindable을 자동 생성)은 기각** — 개발자가 의도 안 한 Instance를 게임 - 트리에 주입하는 부작용이 크다는 사용자 지적. 대신 quad 모듈 자신의 - Instance 트리 안에 두고 `CollectionService` 태그로 노출, 플러그인은 - `GetTagged`로 찾음(`GetDescendants` 전체 순회 불필요). -- Roblox Luau의 `debug` 라이브러리엔 `sethook`류가 없어(확인됨) 엔진이 - 공짜로 주는 동적 트레이싱 방법이 없음 — 대신 Fusion `src/External.luau`에 - 이미 있던 "no-op 업밸류를 나중에 실제 구현으로 통째로 교체" 패턴을 - 재사용하기로 함(quad가 이미 쓰는 "base는 인터페이스, 구현은 팩토리가 - 주입" 원칙과 같은 모양이라 새로 발명할 필요 없음). -- React DevTools 아키텍처도 서브에이전트로 조사 — 그대로 못 베끼는 것도 - 있지만(전역 훅 주입은 프로세스 경계 문제로 안 됨), **컴파일타임 소스 - 위치 주입**(Babel처럼 darklua로 흉내낼 후보)과 **얇은 스트림+on-demand - 상세조회** 원칙은 그대로 채택. -- UX 방향은 사용자가 여러 번 직접 정정: "존재하는 State 목록"이 아니라 - "무엇이 무엇에 연결됐는가" 그래프 중심, flash-on-update는 전체 상시 - 적용이 아니라 마운트/언마운트만 상시+개별 프로퍼티 변경은 현재 열어본 - Instance 한정, PropertyChangedSignal 기반 "외부 변경 감지"는 핵심 - 채널이 아니라 보조 신호일 뿐(어디서/왜 바뀌었는지가 quad-debug의 진짜 - 가치라 순수 관찰만으론 부족). **Element Inspector**(화면 클릭으로 UI - 요소 피킹)가 사용자가 실제로 가장 크게 느낀 pain point로 새로 부상 — - Roblox가 Play 중 라이브 UI 편집 도구를 꺼버려서 Explorer만으로 요소 - 찾기가 힘들다는 실사용 불만. -- 부수적으로 파생된 두 가지(quad-debug 범위 밖) 문서화 아이디어 — - UI 네이밍 컨벤션 문서, 스킬/쿨타임/재화 같은 게임 시스템에서 Store의 - 부작용 허용을 깔끔한 패턴으로 쓰는 법 문서 — 를 `research/ - documentation-plan.md`에 뼈대만 분리해서 남김(위 "지금 할 일" 4번). -- **이벤트 self 관습 확인 필요했던 항목 — 같은 날 후속 세션에서 해소됨.** - 아래 "2026-08-06 후속 세션" 절 참고. - -**같은 세션 후반, 별개 주제 두 개 추가**(quad-debug와 무관, 사용자가 -"적어는 뒀는데 안 줬는건가" 하며 새로 떠올린 것들): -- **Attribute 특수 키 타입 파라미터화** — `[Attribute<> "name"]` - 제네릭 스타일 vs `[BooleanAttribute "name"]` 타입별 정적 생성자 패밀리. - 기존 문서 어디에도 없던 신규 논의로 확인(`bind-system-plan.md` - "Attribute 특수 키" 절에 새로 추가) — 소견은 DI 인스턴스 생성 때 이미 - 쓴 "제네릭 하나 + 자주 쓰는 타입만 정적 지름길" 패턴 재사용, 확정은 아님. - Roblox Attribute가 이제 Instance 참조 타입도 지원해서 `ObjectValue` - 없이 Ref 용도로도 쓸 수 있다는 점도 확인 — quad-debug 논의의 "Value - 오브젝트 기각, Attribute 우선" 결정을 보강함. -- **UICorner/UIPadding/UIScale 인라인 편의 키** — 사용자가 v1에서 "Frame - 안에 인라인으로 넣기만 해도 CSS처럼 적용됐다"고 기억한 기능, 서브에이전트로 - v1 소스(`class.lua`) 조사해 실체 확인: `Corner`/`PaddingAll(Offset)`/ - `Scale` 3종(+ 별개 메커니즘인 `RoundSize`)이 실제로 있었음(리터럴 값 하나 - → 이름 붙은 UICorner/UIPadding/UIScale 자식을 찾거나 생성). `UIListLayout`/ - Grid/Flex 전용 숏핸드는 없었음 — 그건 이미 quad-v2에 있는 범용 - children-array 메커니즘으로 충분히 커버되므로 새로 설계할 것 없음. - **한 차례 "지금은 UICorner가 네이티브라 포팅 불필요"로 잘못 정리했다가 - 사용자가 재정정**: `RoundSize`(이미지 9-slice 라운드 트릭)만 UICorner - 없던 시절의 워크어라운드라 포팅 불필요고, `Corner`/`PaddingAll`/`Scale` - 자체는 "UIScale 등이 여전히 별도 Instance라 부모에 붙여야 하는 구조는 - 안 바뀌었다"는 이유로 **여전히 필요한 기능으로 재확정**. `research/ - ui-shorthand-plan.md`에 최종 정리 — 메커니즘은 기존 pluggable Handler로 - 그대로 커버(새 아키텍처 개념 불필요), 패키지 배치는 `quad-roblox` 코어에 - 직접 포함으로 확정(별도 `quad-roblox-util` 불필요 — "트윈도 하나로 묶어 - 코어에 넣은 선례처럼, 작고 opt-in 아닌 건 분리 안 한다"는 사용자 판단). -- **quad-debug 플러그인 UI 구조 확정** — Explorer에서 quad 내부 자동 - 생성물(예: 위 UICorner 숏핸드가 만든 것)을 직접 선택했을 때 플러그인 - 트리에 대응 노드가 없으면 부모로 대신 선택, 있으면(사용자가 직접 - bind한 경우 등, UB 아님) 정확히 그 노드 선택. 내부 자동 생성물은 - `_`/`QUAD_` 접두어로 네이밍(v1 `_quad_round`류 재사용, - `documentation-plan.md` 네이밍 컨벤션과 연결). 플러그인 UI는 세 - 상호작용면(자기 트리 뷰/리프 클릭→상세 패널/실제 Explorer 선택과 연동, - Explorer와 플러그인 트리는 별도 도킹 위젯)으로 구성된다는 것도 사용자 - 질문에 확인 응답 — `debug-tooling-plan.md` "핵심 설계 방향" 9번. - -## 2026-08-06 후속 세션 — 이벤트 self 관습 결정, rbvm GC 참고, 문서 코퍼스 정리 - -같은 날 이어진 세션에서 세 가지를 처리함. **다음 세션이 새로 알아야 할 것은 -없음** — 아래 전부 `base/`/`research/`/`question.md`에 실제로 반영 완료. - -**1. 이벤트 핸들러 self(Instance) 관습 — 채택하지 않기로 확정.** 위 절에서 -"확인 필요"로 남겨뒀던 것의 결론: v1의 `func(self or this, ...)` 관습은 -실존함을 확인했지만(`.claude/initreq/quad/src/event.lua` 82행, 튜토리얼 -문서화까지 있음), quad 재설계에서는 채택하지 않음. 근거 네 가지 — -(1) Ref가 이미 "생성 직후/마운트 후 Instance 접근"을 콜백으로 커버해서 -중복 채널이 됨, (2) self로 재바인드 가능한 thin wrapper를 준다면 Modifier의 -정적 flatten과 경쟁하는 두 번째 쓰기 경로가 생겨 KV 핸들러가 매번 -"flatten된 값이냐 wrapper냐"를 분기해야 하는 오버엔지니어링, (3) -quad-debug가 추적하는 반응형 그래프 밖의 mutate 경로가 공식 API로 -생기는 셈이라 `purity-and-effects-plan.md`의 이식성 원칙과 충돌, (4) -self를 넘기려면 원본 콜백을 클로저로 한 번 더 감싸야 해서 불필요한 할당 -비용 — quad는 어차피 라이프사이클 끝까지 바인딩을 들고 있어 Destroy 시 -Connection도 자연히 정리되므로(`lifecycle-pattern.md`, GC-native) 감쌀 -이유가 없음. 상세 결정문은 `base/bind-system-plan.md`의 "이벤트 핸들러는 -self(Instance)를 받지 않는다" 절. `research/debug-tooling-plan.md`/ -`.claude/question.md`의 관련 항목은 "해소됨"으로 갱신 완료, 이 결정을 -설명하는 문서화 숙제("왜 thin wrapper를 안 주는가", "권장 이벤트 핸들링 -패턴")는 `research/documentation-plan.md` 3번으로 신설(다른 두 항목과 -동일하게 아직 백로그 뼈대만). - -**2. rbvm GC 패턴 — "실물 검증됨" 근거 보강.** 사용자가 "GC 처리를 봐야 -한다면 rbvm을 확인하라, 실제 프로덕션에서 잘 돌아가는 걸 직접 확인한 -모듈"이라고 언급 — 실제로 rbvm의 GC 패턴(weak table 4종, `Instance. -Destroying` 기반 gcHold 클로저, 네임스페이스 Dispose 훅 등)은 이미 -`base/lifecycle-pattern.md`에 파일:라인까지 인용하며 상세 반영돼 있었지만 -"사용자가 직접 실행해서 확인했다"는 신뢰도 근거는 빠져있어서 그 문단을 -추가함(사람이 짠 코드라 100% 무결 보장은 아님 — 이미 발견된 버그 2건도 -근거로 같이 인용, 규범이 아니라 참고용 비교 대상이라는 톤 유지). - -**3. `.claude/` 코퍼스 전체 정리 패스.** 이전 세션들에서 쌓인 stale -참조/모순을 서브에이전트로 전수 감사 후 수정 — `modifier-plan.md`/ -`architecture.md`의 `research/component-composition-plan.md` 참조를 -승격된 `base/` 경로로 갱신, `comparison-fusion-vide.md`의 낡은 "Vide식 -암묵적 추적 vs Fusion식 명시적 축, quad는 미정" 서술을 실제 확정 사실 -(`bind-system-plan.md`의 `:With`+`:Compute` 명시적 모델 채택)로 정정, -`tween-plan.md`의 끊긴 절 참조 수정, `documentation-plan.md`의 인용 -오류 정정. `module-lifecycle-plan.md`가 스스로 "question.md에도 취합" -표시해뒀지만 누락돼 있던 "프로바이더" 이름(provider/processor/plug) -미정 항목도 `question.md`에 추가함. 여러 문서에 흩어져 있던 진짜 열린 -설계 질문들(Slot 형제 순서 보장, Attribute 타입 파라미터화, UI shorthand -이름 등)은 전부 `.claude/question.md`에 이미 반영되어 있음을 재확인만 -하고 임의로 결정하지 않음 — **이 파일이 여전히 "지금 열려있는 것"의 -단일 소스.** - -**4. Store `:Emit`, `:Compute`의 `previous` 인자, `state:Observer(fn)`, -Ref 일반화 — 네 가지 다 확정, 실제 base 문서에 반영 완료.** 같은 세션에서 -더 이어진 Store/Ref 설계 논의, 전부 `base/store-semantics.md`와 -`base/bind-system-plan.md`에 반영됨: -- **`Store:Emit(key)`** — Source 원천에 한해서만 허용(중간/파생 State엔 - 없음). 존재 이유는 clone 불가능한 userdata/엔진 객체가 우선(편의성은 - 부차적). `Get()`이 라이브 레퍼런스를 주므로 캐시해서 비교/diff하면 - 안 된다는 캐비엇 명시. Modifier는 정적 flatten이라 Store/State 경로에 - 아예 안 걸치므로 Emit과 충돌할 지점 자체가 없음(따름정리: - `Store`의 `T`는 Modifier가 될 수 없음) — `store-semantics.md`. -- **`:Compute(fn)`의 선택적 두 번째 인자 `previous`** — Compute 결과 - 자체가 무거운 userdata인 경우(예: 큰 locale 테이블 → Roblox - `LocalizationTable` 변환) 재생성 대신 이전 결과를 재사용/patch하는 - 용도, opt-in. `previous`는 "정확히 한 단계 전"이 보장 안 되므로 반드시 - full diff로 다뤄야 함(React reconciler와 같은 모양). **핵심 캐비엇**: - 이 패턴은 결과 State가 계속 능동적으로 관측(정상 prop 바인딩 또는 - `state:Observer(fn)`+명시적 `Get()`)되지 않으면 mutate 로직 자체가 - 다시 실행 안 되어 조용히 영구 정지함 — `bind-system-plan.md`. -- **`state:Observer(fn)`** — 무효화 신호만 주고 값은 안 줌, `fn` 안에서 - 명시적으로 `Get()` 해야 실제 값을 얻음(기존 "emit은 저렴한 무효화 - 신호" 원칙 재사용). 반환값 자체가 `CreatedRef`처럼 children 배열에 - 바로 놓는 leaf 값(별도 `ObserverHolder` 래퍼 불필요, 사용자가 직접 - 단순화) — 그 leaf가 살아있는 동안만 구독 유지, `canExecute`로 게이팅. - `fn` 생략 시 "이 State를 그냥 계속 능동 관측 상태로 유지"하는 유틸로 - 씀(위 `previous` 캐비엇의 해결 도구). 구현은 값 내부가 아니라 외부 - weak table로 살아있는 Observer를 추적하는 방식 권장(rbvm - `getNamespaceOf`류 선례) — `bind-system-plan.md`. -- **Ref 일반화** — "quad가 만든 instance 전용"에서 "아무 사용자 값이나 - 담는 범용 값 박스"로 확장(object-ref/function-ref 안 나눔, React - `useRef`가 선례). `.Value` + `:Wait()`(coroutine 컨텍스트용) + 콜백 - 등록(복수 허용, 이미 채워져 있으면 즉시 1회 호출) — 이걸로 "코루틴 - 기반 대기 지원 미정"이던 항목 해소. `CreatedRef`는 이 위에 얹힌 특수 - 편의 패턴으로 재정리, 상충 없음. **one-shot 여부도 해소됨 — 반복 - 재설정 가능으로 확정**(React의 자식 재생성 시 ref 재사용 패턴이 선례, - 라벨 컨테이너 재사용 예시로 확인). 콜백은 발화 후에도 안 소진되고 - 매 `:Set()`마다 다시 불림 — 소진되는 건 `:Wait()`의 개별 대기자뿐. - **Ref는 의도적으로 lazy가 아니고 `:Compute` 파생도 지원 안 함** — - State와의 이 차이가 중요(예전에 Store가 Ref 역할도 겸했다가 lazy - 모델과 섞여서 안 좋았던 경험에서 나온 의도적 분리). Ref 정의 자체가 - 넓어졌으니 용어 정리 때 이름도 같이 재검토 대상. `question.md`의 - 관련 항목은 해소됨으로 갱신. - -**5. Observer 이름 확정, Ref/Source/Store 생성자 스타일, "독립 프리미티브 vs -파생 데이터" 원칙, Modifier 세부 마무리 — 전부 확정, base 문서 반영 완료.** -- `Observer`로 확정(`ObserverHandle` 아님) — `:Connect()`→`Connection`과 - 같은 기존 명명 관례. PA님 코드의 동명 클래스와는 무관, 각주로 구분. -- **생성자 스타일**: `Source(default)`/`Ref(default)`/`Store({defaults})` - — Kotlin Compose식 "타입 이름 자체가 팩토리 함수". Ref만 예외였던 이유 - 없었음(단순 명세 공백). -- **일반 원칙 신설**: 독립 존재 가능한 프리미티브(Source/Ref/Store/ - Modifier, `Type(args)` 자유 함수 생성자) vs 원천에 종속된 파생 데이터 - (State/Observer, 원천에 대한 메소드로만 얻어짐) — `state:Observer(fn)`가 - 메소드고 자유 함수가 없는 더 근본적인 이유로 연결(`store-semantics.md`). -- **Modifier 마무리**: (a) Getter를 아예 안 만들기로 확정 — - `:FontSize(function(old)->new)`가 유일했던 use case를 인라인으로 커버. - (b) `old`는 항상 "현재 저장된 그대로"(plain이면 raw, State면 State - 핸들) 넘김 — `:Compute`의 self와 같은 결. (c) `func(state)->state`라는 - 세 번째 인자 모양은 불필요(함수 합성 + State 직접 대입으로 이미 커버). - (d) Modifier는 핸들러 계층(Ref/Slot 등)을 몰라도 됨 — 순수 데이터 - merge 레이어라 UB로 흘려보내도 문제없음. (e) **런타임 구현은 base에 - 제네릭 `__index` 하나면 충분** — `mod:FontSize(...)`가 `__index(self, - "FontSize")`로 잡히므로 클래스별 런타임 코드 불필요, FrameModifier류 - 타입 생성기는 순전히 정적 타입 체크만을 위한 것. (f) 이벤트도 - store-bind 가능하도록 확정 — 기존 재실행 래핑 재사용, `false`를 - disconnect 센티널로 씀(`nil`은 테이블에서 사라져서 부적합) — - `bind-system-plan.md`. Modifier가 이벤트 키를 담아도 되는지는 (d)로 - 자동 해소(Modifier가 애초에 키 종류를 구분 안 하므로). - -**6. 이벤트 store-bind는 부차적 옵션으로 재조정, Observer의 `:Subscribe`/ -`:Unsubscribe` 추가 — 둘 다 확정, 반영 완료.** -- 이벤트 store-bind(5번 (f))를 다시 검토 — "구현이 쉽다"가 "구현할 - 가치가 있다"를 보장 안 함을 재확인. 저빈도 UI 이벤트의 조건부 처리는 - "핸들러 하나 계속 연결 + 내부에서 `store.enabled:Get()` 분기"가 이미 - Connect/Disconnect 없이 더 싸고 표준적이라 **이걸 기본 권장 패턴으로 - 확정**. store-bind는 고빈도 신호(Heartbeat 등)나 로직 자체가 바뀌는 - 드문 케이스를 위한 부차적 옵션으로 격하(메커니즘 자체는 유지 — 일관성 - 위해 예외로 뺄 근거는 약함). 자주 재계산되는 State에 물리면 Connect/ - Disconnect churn이 숨은 비용이 된다는 캐비엇도 추가. -- **Observer의 `:Subscribe()`/`:Unsubscribe()`** — children 배열에 안 - 붙는 "전역/독립" Observer(디버깅용으로 Store에 직접 걸어 print하는 - 흔한 패턴, `RunService:IsStudio()` 가드 + BooleanValue 토글)를 위한 - 명시적 라이프사이클 경로. 이건 새 설계가 아니라 PA님 코드 교차검증 - 때 이미 예고해둔 확장 지점("GC만으로 부족하면 명시적 dispose 경로 - 추가 가능")을 실제로 채운 것. liveness 체크는 `self.Subscribed` 필드 - 우선, `self.Connection.Connected` 폴백(필드 접근이 weak table 조회보다 - 쌈). 내부 레지스트리는 자동 케이스의 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번 용어 정리 - 라운드까지 가칭. - -## 2026-08-06 네 번째 세션 — M0 착수 직전 크리티컬 감사, `research/pre-implementation-audit.md` 신설 - -사용자 요청: "실 개발 시 모호하여 인터럽트될 수 있는 부분, 나중에 결정되면 -치명적일 것 같은 것, 지금 구조가 오버엔지니어링일 수 있어 보이며 더 나은 -대안이 있는 것"을 찾아 정리해달라는 요청. `.claude/base/` 전체(architecture/ -bind-system/store-semantics/module-lifecycle/component-composition/ -modifier/purity-and-effects/slot/lifecycle-pattern/quad-v1-architecture)와 -근접 `research/`(existing-instance-bind/tween/ui-shorthand) + `ROADMAP.md`를 -4개 클러스터로 나눠 서브에이전트 4개를 병렬로 돌려 "모호성/지연결정리스크/ -단순화후보" 세 렌즈로 재감사, 결과를 `research/pre-implementation-audit.md` -로 종합. `.claude/question.md`엔 이미 취합된 것(용어 재검토, M0 스파이크 -항목 자체 등)과 겹치지 않는 새 발견만 반영. - -**작업 도중 발견한 부수 이슈**: 워크트리 생성 시점과 main 체크아웃의 -미커밋 변경사항(세 번째 세션 결과물)이 어긋나 있었음 — 워크트리는 커밋 -시점 기준으로 fork되므로 아직 커밋 안 된 변경은 안 딸려옴. 사용자가 중간에 -main에 커밋을 완료해줘서(`4b839b0`) 워크트리를 새로 만들어 재동기화함 — -**앞으로 워크트리에서 최신 설계를 감사/참조해야 하는 작업을 시작하기 전엔, -main에 미커밋 변경이 있는지(`git status`) 먼저 확인하고 필요하면 커밋을 -요청하거나 파일을 직접 동기화할 것.** - -**핵심 발견 요약** (전체 25개 항목은 `pre-implementation-audit.md` 참고, -우선순위1만 발췌): - -- **Tween.luau가 문서 전체에서 "범용 store-bind 캐치올 핸들러"의 유일한 - 구체 예시로 서술됨** — 애니메이션 없는 일반 반응형 프로퍼티 바인딩이 - 실제로 Tween 파일을 거쳐가는지, 별도 범용 핸들러가 필요한지 확정 안 됨. - 가장 구조적인 발견 — 직접 `bind-system-plan.md` 67-79행을 재확인해 - agent 발견을 검증함. -- `props.Modifier`/`props.Ref` forwarding 관례가 Lua 배열 리터럴의 - nil-hole 함정(caller가 안 넘기면 `{nil, ref, child}`에서 뒤 항목까지 - 무시될 수 있음)에 그대로 노출 — M0 스파이크 코드에 이 케이스를 반드시 - 포함시켜야 함. -- `canExecute`/`Connected`의 실제 구현 방식이 미확정인 채 코어 전역 - (Slot/Observer/store-bind retract)에 이미 재사용 확정돼 있음. -- `LifetimeHandle` 인터페이스가 M8에 배치돼 있지만 M4/M6이 이미 그 인터 - 페이스를 전제로 서술돼 있음 — 로드맵 순서 역전, `ROADMAP.md` 조정 필요. -- retract 시 "이전에 실제로 매치됐던 핸들러" 추적 책임, 우선순위 스캔 - 동률/매치실패 처리, provider 미주입 상태 dispatch 호출 시 동작 — - 전부 M2(Dispatch 엔진) 착수 전 한 번에 결정하면 효율적인 것들. -- Slot의 `add`/`remove`/`clear` CRUD 의미론 자체가 정의 안 돼 있음, - "재마운트 시 throw"도 추적 대상(개별 element vs Slot 컨테이너)이 - 뭉뚱그려 서술됨 — 둘 다 M6 착수 전 확정 필요. - -**단순화 후보로 지적된 것 중 사용자 판단 필요**: `:Compute(fn)`의 -`previous` 두 번째 인자 — quad의 "함수 자체가 재호출되는" 모델상 클로저 -업밸류로 이미 되는 걸 별도 API 표면으로 만든 것일 수 있음(근거 불명). - -**문서모순으로 남겨둔 것**: `State`는 "UB, 가능하면 타입으로 -차단"인데 Ref/Slot이 Modifier 필드에 들어가는 건 "UB, 방어 로직 없음" — -같은 문서(`modifier-plan.md`) 안에서 정반대 원칙이 근거 설명 없이 나란히 -적용됨. 판단이 필요해 고치지 않고 감사 문서에만 남김. - -**부수적으로 직접 고친 stale 문서(판단 불필요한 순수 동기화)**: `base/ -architecture.md` 소스트리 주석 두 곳 — `Store.luau`가 여전히 옛 `__newindex` -모델을 언급, `Ref.luau`가 여전히 "CreatedRef 메커니즘 자체"로만 서술(Ref -일반화 결정 반영 안 됨). 온톨로지 요약 절 stale은 같은 세션 도중 커밋 -`4b839b0`에서 이미 독립적으로 고쳐져 있었음을 확인 — 재작업 없이 스킵. - -**다음 세션이 할 일**: M0 착수 전에 `pre-implementation-audit.md` 우선순위1 -항목(특히 위 6개)부터 확인 — "지금 할 일" 1번 참고. `.claude/question.md` -2번에 사용자 판단이 필요한 항목 요약이 반영돼 있음. - -## 2026-08-07 세션 — `:With`도 새 State 노드로 확정 - -사용자 질문에서 시작: `:With(...)`가 문서상 가변인자 표기이긴 한데, 체이닝 -(`:With(a):With(b):With(c)`)할 때마다 실제로 새 State 노드를 만드는 게 -맞는지, 아니면 값 없이 의존성 목록만 clone-then-append로 누적하는 가벼운 -빌더로 만들어 "노드가 With 호출마다 하나씩 증가하는" 낭비를 피해야 하는지가 -불명확했음. 처음엔 "빌더" 대안(진짜 State가 아닌 clone 기반 누적 객체)을 -검토했으나, 사용자가 두 가지 반례를 직접 제시하며 기각함: - -1. **디버그 그래프가 꼬임** — `quad-debug`의 핵심 UX가 "무엇이 무엇에 - 연결됐는가" 그래프인데, With/Compute가 전부 실제 노드면 코드 호출 - 체인이 그래프 엣지와 1:1 대응되지만, 빌더로 만들면 그래프 툴이 가상의 - 분기 지점을 따로 합성해야 함. -2. **clone 기반 구현이 Compute 노드 위에서 실제로 깨짐** — `c = - a:Compute(f)` 뒤에 `w = c:With(b)`를 clone으로 구현하면 `c`의 캐시 - 슬롯까지 그대로 복사되어 `w`가 `c`와 별개의 독립 캐시를 갖게 되고, - `c`/`w`가 각자 관측되면 `f`가 두 번 따로 실행됨 — `bind-system-plan.md`가 - 이미 기각해둔 "State 체인 플래튼"과 정확히 같은 실패 모드. - -**결정**: `:With`는 호출마다 self+인자들을 레퍼런스로 구독하는 새 State -노드를 만든다(clone 아님, 계산 없는 pass-through 노드). 원래 문제 제기 -(노드 남발)는 노드를 없애는 대신 `:With(...)`를 진짜 가변인자로 만들어 -해소 — `:With(a, b, c)` 한 번으로 노드 1개(구독 3개)를 만들 수 있고, -디버그 그래프도 이쪽이 더 단순해 권장 관례로 삼음. 체이닝 스타일도 여전히 -가능하나 그건 저렴한 노드가 늘어나는 것뿐이라 문제 삼을 비용이 아님. -`base/bind-system-plan.md`의 "왜 State 체인을 Modifier처럼 플래튼하지 -않는가" 절 바로 뒤에 새 소절로 반영 완료. 다른 문서(`question.md`/ -`ROADMAP.md`/`modifier-plan.md`)엔 이 결정과 모순되거나 갱신이 필요한 -서술 없음을 확인함(감사 완료) — `modifier-plan.md`가 이미 "State가 -`:With`/`:Compute`마다 새 노드를 할당"이라고 서술해뒀던 것과도 정합적. - -다음 세션이 할 일은 안 바뀜(위 2026-08-06 네 번째 세션 절 참고) — 이 -결정은 M0 스파이크(Store/State propagation 검증)가 실제로 짜볼 때 -참고할 구체 스펙이 하나 더 생긴 것뿐. - -## 2026-08-07 두 번째 세션 — Modifier `:Apply(factory)` 팩토리 체이닝 추가 - -사용자 제안: `Boldify(mod) -> mod`처럼 어떤 modifier든 받아 적절히 변형해 -돌려주는 재사용 가능한 "팩토리 함수"(커링 지원, `Boldify(10)(mod) -> mod`)를 -`mod:Apply(Boldify(10)):Apply(Italicify)`처럼 기존 필드 setter 체이닝과 -같은 fluent 문법으로 끼워 넣을 수 있게 하자는 것 — Jetpack Compose의 커스텀 -`Modifier` 확장 함수 패턴과 같은 효용(모듈화된 스타일 프리셋 재사용)을 -Luau엔 확장 함수 문법이 없으니 콤비네이터로 흉내낸 아이디어. 채택 확정, -`base/modifier-plan.md` 8번 절에 반영 — `:Apply`는 `function(self, factory) -return factory(self) end`이 전부인 얇은 sugar(팩토리 자신이 이미 clone된 -새 Modifier를 반환하므로 Apply 자체는 clone 불필요), 기존 3번(immutable -clone 체이닝)/4번(제네릭 `__index`) 결정 위에 그대로 얹힘. 구현 시 주의점 -하나만 새로 생김: `Apply`는 제네릭 `__index`가 필드 setter를 즉석 합성하기 -전에 먼저 확인해야 하는 고정 메소드 이름이라, **Modifier 필드 이름으로는 -예약됨**(실 스타일 프로퍼티와 겹칠 일은 거의 없어 보이나 문서화 필요). -`ROADMAP.md` M7에 체크박스 추가 완료. 다음 세션이 새로 알아야 할 건 없음 — -M7 착수 시 `modifier-plan.md` 8번 참고하면 됨. - -## 2026-08-07 세 번째 세션 — Ref의 KV 핸들러 처리 vs phase 타이밍, `PreRef` 신설 - -**출발점**: Ref가 Modifier처럼 밖에서 처리되는 게 아니라 KV 핸들러 -(`process(inst,k,v)`)로 처리된다면, "생성 직후"/"자식 마운트 후" 두 -콜백 타이밍(특히 self(Instance)를 안 주는 이벤트가 Ref로 self를 얻는 -경우)을 단순 for-loop 디스패치만으로 어떻게 표현하는지가 출발 질문 — -길게 이어진 단일 스레드라 아래 요약만 읽으면 됨, 상세 근거는 각 base -문서에 이미 반영됨. - -**핵심 결론(전부 `base/bind-system-plan.md`에 반영 완료)**: -- **base 디스패치 드라이버는 props 순회를 "배열 파트(children/Ref) 먼저, - 해시 파트(프로퍼티/이벤트) 나중"으로 명시적으로 두 패스 계약화**한다 - — Luau 테이블이 실제로 이렇게 순회되는 걸 사용자가 직접 확인했지만, - 그 우연한 동작에 기대지 않고 base가 스스로 이 순서를 보장(다른 - 백엔드가 다른 자료구조를 쓸 수 있어서). M0 스파이크 검증 항목에 추가. -- **`CreatedRef`의 `{phase="created"|"mounted"}` 옵션은 폐기.** 두 패스 - 계약 덕에 "자식 마운트 전/후"는 그냥 배열 안에서 Ref를 다른 children - 보다 앞/뒤에 놓는 것만으로 공짜로 표현됨 — 옵션 문법 자체가 불필요. -- **`PreRef` 신설** — "프로퍼티/이벤트 세팅보다도 먼저"(Roblox의 - `ChildAdded`/`DescendantAdded`/`Changed`류가 setup 도중 동기 발화할 - 수 있어서 self-ref가 이벤트보다 먼저 채워져야 하는 케이스)만 담당하는 - 별도 nominal 타입. `Ref`를 그대로 재사용(런타임 중복 없음)하되 - Modifier 필드 값·Source/Store 값으로는 타입으로 아예 못 들어가게 - 막고, children 배열 안에서도 위치 무관하게 항상 최우선(호이스팅) — - base 드라이버가 두 패스 루프 앞에 `PreRef`만 골라 fire하는 좁은 - pre-pass를 하나 더 둠. -- **일반 `Ref`는 Modifier/Store 어디든 계속 자유롭게 들어감** — Store를 - 통해 나중에 도착하는 Ref는 그냥 도착한 순간 처리, 별도 phase 개념 불필요. -- **`:Wait()`는 PreRef에도 그대로 유효** — fire 자체는 동기적이지만 - 호출부가 `task.spawn`이 아니라 순수 `coroutine`일 수 있어 실제 - yield-resume이 필요한 경우가 있음. "채워졌는지 먼저 확인, 없으면 - `:Wait()`" 방어 관용구를 문서화 대상으로 명시. -- **콜백/대기자 실행 구현 디테일 추가**: 같은 배열 하나를 한 번의 - 일반화 `for`로 순회하며 `type(v)=="thread"`면 `coroutine.resume`+ - 슬롯 nil 처리(1회성), 함수면 그냥 호출(유지) — 새 등록은 `table.insert` - 로 끝, 성긴 배열이어도 압축 불필요. **[정정, 2026-08-07 열 번째 세션]** - "슬롯 nil 처리"는 틀림 — 사용자가 Luau REPL로 반례 제시, 실제로는 - `None`으로 소진해야 함(`#t`/`table.insert` 안전성 문제). `base/ - bind-system-plan.md` "왜 `nil`이 아니라 `None`인가" 절이 최신. -- v1의 `OnCreated` 특수 DI 키는 이식 안 함 — `Ref():Callback(fn)`으로 - 완전 대체. - -**역전된 이전 서술은 archive로 이동**: `CreatedRef`의 `phase` 옵션과 -"Ref는 특수 처리 없는 평범한 참가자"라는 원래 서술은 -`archive/ref-phase-option-reversed.md`로 옮기고 원 위치엔 짧은 포인터만 -남김(컨텍스트 비대화 방지 목적, `archive/store-source-proxy-reversed.md`와 -같은 패턴). `architecture.md` 소스트리 주석/`question.md`(PreRef를 -용어 재검토 대상에 추가)/`research/documentation-content-map.md`(stale -`{phase=...}` 예시 갱신)도 같이 동기화함. - -**아직 미해결, 다음 세션 주제로 예고됨**: `{ Override = nil, mod }`처럼 -인라인 키로 modifier가 주는 값을 명시적으로 "지우고" 싶어도 Lua -테이블 리터럴의 `키 = nil`은 키가 아예 없는 것과 구별이 안 돼서 안 -풀리는 문제 — `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/blocker-plan.md`/`base/effect-plan.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`엔 아직 실제로 열려있는 - 것(키 기반 동적 컬렉션 재조정) 하나만 남김. **[같은 날 바로 정정]** - 처음엔 Blocker/Effect를 `base/additional-primitives.md` 한 파일로 - 합쳐 승격했으나, 사용자가 "State 볼 때 Effect까지 볼 필요는 없다, - 기존 프리미티브당 1파일 컨벤션(`modifier-plan.md`/`slot-plan.md`류)에 - 맞지 않는다"고 지적해 바로 두 파일로 재분리함 — Blocker는 - Store/State와 밀접해 교차 참조가 필요하지만 Effect는 완전히 독립된 - 요소라 애초에 같은 파일일 이유가 없었음. -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(...)` 포지셔널 생성자가 읽기 어렵다는 문제의식, - 소견은 편의 필드 쪽이지만 확정 아님, 나중 논의 대상으로만 남김. - -## 2026-08-07 다섯 번째 세션 — Modifier 결합(`Override`)/읽기 접근자(`Peek`)/`isState` 확정, FuncSource 기각 사유 문서화 - -**출발점**: `:Apply`(4번째 세션 신설)처럼 Modifier에 더 있으면 좋을 게 -있는지 사용자가 제기 — `Merge`류 결합 유틸의 우선순위 문제, 그리고 -Modifier 자신이 자기 필드 값을 못 읽는 게 애매하다는 지적(예: -`Boldify`가 폰트별 굵기 보정을 하려면 현재 `Font` 필드를 읽어야 함). -같은 스레드에서 "Source가 항상 정해진 값만 담아야 하는 이유가 확정된 -건지, FuncSource(람다로 계산+self-emit하는 Source) 같은 건 왜 없는지"도 -같이 물어옴. - -**핵심 결론(전부 base 문서에 반영 완료)**: -- **`Modifier.Override(mod1, mod2, ...)`** — `component-composition-plan.md` - 3번 절에 2026-08-04부터 가칭 `Merge`로 이미 확정돼 있던 결합 유틸의 - 실제 동작을 확정하고 이름을 `Override`로 개명(중립적 "합침"이 아니라 - 명시적 "덮어쓰기"라 이름이 의미를 정직하게 반영해야 함). 뒤 인자가 - 필드 단위로 이김(기존 배열 flatten 규칙 재사용), 구현은 단순 필드별 - raw 교체 — setter가 이미 호출 시점에 함수/State를 즉시 처리해 저장하므로 - Modifier 필드는 항상 baked 값이라 특별한 분기 불필요. "baked 값 교체는 - 거기서 파생된 다른 필드에 소급 반영 안 됨"(Boldify가 FontWeight를 계산해 - 둔 뒤 Font가 Override로 바뀌어도 FontWeight는 예전 값 그대로)과 순서 - 의존성(`A:Override(B)` ≠ `B:Override(A)`) 둘 다 문서 경고 대상으로 확정. - **`Apply`로 전부 대체해 `Override`를 없애는 방안도 검토했으나 기각** — - 컴포넌트 경계(`props.Modifier`는 단일 named parameter라 배열 flatten이 - 안 닿음)라는 이미 확정된 실사용 니즈를 `Apply`만으로는 못 풀어서. - `base/modifier-plan.md` 9번 절. -- **`:Peek<>(key): T|State|nil`** — Modifier 필드를 확정(pull+recompute) - 하지 않고 raw 그대로 읽는 접근자. `Get`이 아니라 `Peek`인 이유는 이 - 프로젝트에서 `State:Get()`이 이미 "확정한다"는 의미로 굳어져 있어서 — - Modifier의 읽기는 정반대(State면 State 핸들 그대로) 동작이라 같은 - 동사를 못 씀. 반환 타입을 `T`로 자동 확정하지 않고 union 그대로 - 노출하는 이유는 4-1번 절 함수형 setter의 `old` 인자와 같은 원칙("현재 - 저장된 그대로 넘김") 재사용 — 자동 확정하면 타입에 안 드러나는 채로 - 반응성이 조용히 끊김. `.RealValue` 같은 별도 인덱싱 표면은 기각(이미 - `__index`가 setter 합성용으로 예약돼 있어 표면이 겹침). -- **`isState(x): boolean`** — `Peek`의 raw union을 분기하려면 필요. - Source가 State를 구조적으로 만족하므로 이거 하나로 Source도 같이 - 잡힘(`isSource` 불필요). duck-typing 대신 weak-key 레지스트리로 구현 - (rbvm 네임스페이스 추적과 같은 패턴 재사용) — `Peek`가 돌려주는 `T`가 - 임의의 테이블/userdata일 수 있어 duck-typing은 false positive나 일부 - Roblox userdata의 인덱싱 에러(pcall 필요)로 이어질 위험이 있음. 이 - 판별 로직 자체는 새 개념이 아니라 4-1번 setter가 이미 내부적으로 - 해야 했던 "필드가 State냐 plain이냐" 판별을 public 유틸로 승격한 것. - `base/bind-system-plan.md`의 `isState` 절. -- **FuncSource(값이 람다로 계산되고 self-emit하는 Source) 기각** — 사용자가 - 스스로 기각 논리를 제시했고("이미 Compute가 커버함"), 검증 결과 이미 - 확정된 두 원칙에서 그대로 연역됨: (1) Source는 "시작점"이라 다른 - 반응형 값에 자동 연결 안 됨(2026-08-04 6차 라운드, "Store가 Store를 - 담지 않는다" 확정 때 나온 원칙) — FuncSource는 다른 반응형 값에 종속된 - 계산이면서 겉으로는 origin인 척하는 것이라 이 원칙과 직접 충돌. - (2) `:With`가 clone 빌더가 아니라 진짜 노드여야 하는 이유(2026-08-07 - 세 번째 세션)가 "의존성이 구조적으로 안 보이면 디버그 그래프가 - 깨진다"였는데, FuncSource의 람다가 클로저로 캡쳐한 의존성은 정확히 - 그 문제를 재현함. 실제로 커버 안 되는 유스케이스도 없음 — "다른 - 반응형 값에서 계산"은 `Compute`, "clone 불가능한 값을 밖에서 바꾸고 - 알림"은 원천 Source+`Emit`으로 이미 전부 커버됨. 새 결정이 아니라 - 기존 확정 사항의 논리적 귀결이라 별도 base 절 신설 없이 여기 세션 - 요약으로만 기록(quadnomicon 소재로 재사용 가능하도록). - -**같은 세션 바로 후속 — 문서화 톤 보강(사용자 강조)**: `Override`는 범용 -조합 도구가 아니라 `Frame{mod1, mod2}`의 컴포넌트 경계판(단일 named -parameter 슬롯에 독립적으로 만들어진 값 두 개 이상을 넣어야 하는 특수 -상황)으로 좁게 문서화할 것 — "특정 modifier를 계속 바꿔나간다"는 요구는 -항상 `Apply` + 커링/일급 함수 전달을 기본 관용구로 유도. `Apply` 자체도 -`factory(self)` 호출 sugar 그 이상이 아니라는 걸 명시 — `factory`가 -`Peek`한 값이 기대와 다르면 `error`를 던지든 뭘 하든 전부 `factory` -저작자 책임, `Apply`가 검증/보장을 대신 해준다고 오해하면 안 됨. 둘 다 -`base/modifier-plan.md` 8/9번 절에 반영 완료. - -**같은 세션 두 번째 후속 — `Apply` vs `Override` 성능 기준 확정.** -"무거운 Modifier를 대량 생성할 때 `Apply`의 clone 비용이 누적되지 -않냐"는 우려에서 두 방안 검토 후 결론: **`Apply`를 mutable로 바꾸는 -방안은 기각**(3번 절 immutable 확정 이유 — 형제 서브트리 오염 방지 — -가 clone 비용 절감보다 우선순위 높음, 재확인). 대신 **판단 기준을 -"이질적/동질적 프로퍼티"가 아니라 "필드 간 계산 의존성 유무"로 -명확화** — 한쪽이 `Peek`으로 다른 쪽의 baked 값을 읽어 반영해야 하면 -이질적으로 보여도 `Apply`, 서로 완전히 독립이면 동질적으로 보여도 -`Override` 가능. 계산 의존성 없는 재사용 조각(배경/텍스트/레이아웃처럼 -서로 다른 서브시스템이 한 번만 만드는 값)은 모듈 상수로 만들어두고 -인스턴스마다 `Override`로 결합하는 게 실제 최적화 패턴 — 단 이건 -"`Override`가 내부적으로 캐싱해준다"가 아니라 사용자가 값을 재사용하는 -평범한 패턴일 뿐, 라이브러리에 새 캐싱 레이어가 생기는 게 아님을 -문서에 명시하기로 함. `base/modifier-plan.md` 9-1번 절. - -**같은 세션 세 번째 후속 — "`Apply` 경계에서만 clone, 안쪽은 mutable" -절충안도 검토 후 기각.** clone 횟수를 체인 길이가 아니라 `Apply` 호출당 -1번으로 줄이는 절충을 사용자가 직접 제시했으나, `Apply`를 거치지 않고 -setter를 단발로 직접 호출하는 흔한 경로는 여전히 mutable이라 공유 -레퍼런스가 그대로 오염될 수 있음(서브트리에서 폰트 두께만 바꿔도 터짐) -— "어디서 터지느냐만 달라지는" 비일관적 절충이라 실익 없다고 판단해 -기각. 전부 clone하는 현재 방식 유지 확정. `base/modifier-plan.md` -9-1번 (a-1) 절. - -**같은 세션 네 번째 후속(당시 CLAUDE.md에 미기록 — 2026-08-07 여섯 번째 -세션에서 뒤늦게 발견/보강) — `Override`가 서브타입 관계인 Modifier끼리 -섞일 때의 타입 시그니처는 미검증으로 열어둠.** `FrameModifier`가 -`GuiObjectModifier`의 서브타입이어야 자연스러운데, 필드 setter 메소드의 -리턴 타입이 각자 자기 자신이라(`self`) 단순 구조적 서브타이핑만으로 -`Modifier.Override(guiObjectMod, frameMod)`류가 통과하는지 추론만으로는 -결론 못 냄 — 후보안(메소드 필드는 `any`로 뭉개고 데이터 필드만 구조적 -체크)을 실 Luau로 검증 필요, 안 되면 `Override(...: any): any`로 -느슨하게 열고 이 항목으로 되돌아오는 걸 fallback으로 남김. -`base/modifier-plan.md` 9-2번, `ROADMAP.md` M7에 체크박스 반영 완료. - -**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 참고, -`ROADMAP.md` M0부터) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 -자체는 그대로. - -**미해결로 남긴 것 — 임의로 결론내지 않음**: Effect가 `state:Effect()` -형태로 Observer를 확장하는 변형인지, 완전히 독립된 free function인지가 -불명확함(사용자가 "확인 필요, 아니라면 논의해야 할 상태로 남겨두라"고 -명시). 관련 하위 질문으로 `state:Observer(fn)`가 생성 시 `fn`을 즉시 -1회 실행하는지도 문서 어디에도 명시돼 있지 않음이 이번에 드러남(Effect는 -"즉시 1회 실행"이 스펙에 명시돼 있어 이 부분만 보면 둘이 겹쳐 보임). -`base/effect-plan.md`의 "미해결" 절과 `.claude/question.md` -0번에 반영 — 구현 착수(M3~M4 전후) 전에 반드시 재확인할 것. - -**다음 세션이 할 일**: 안 바뀜(위 2026-08-06 네 번째 세션 절 "다음 세션이 -할 일" 참고, `ROADMAP.md` M0부터). 이번 세션은 순수 문서 정리라 설계 -결정 자체는 늘지 않았음 — 단, M3 체크리스트에 `Blocker.luau` 항목이 -하나 추가된 것과, 위 Effect/Observer 미해결 항목은 M3~M4 착수 전에 -확인해야 함. - -## 2026-08-07 여섯 번째 세션 — Ref/PreRef 메소드 API 확정, 파일 분리, Tween GC 저장 구조 확인, Effect/Observer 관계 해소 - -사용자가 메모 형태로 두 가지를 던짐: (1) Tween 인스턴스를 per-instance -저장소에 담는 구조가 실제로 GC-안전한지, (2) Ref가 이제 충분히 완결된 -프리미티브이니 PreRef와 파일을 분리하고, `:Set`/`:Callback`/`:Wait` -세 메소드로 API를 굳히자는 제안(전부 mutation 패턴이라 자기 자신을 -반환). 둘 다 검증 후 반영 완료: - -- **Tween per-instance 저장소는 이미 확정된 구조 그대로 GC-안전함** — - `inst`로 weak-keyed된 바깥 릴레이션 안에 `k`별 안쪽 릴레이션이 중첩된 - 모양이라(`base.perInstanceState(inst)`), `inst`가 죽으면 중첩된 Tween - 인스턴스 릴레이션도 별도 정리 없이 같이 GC됨 — 새 결정 아니라 기존 - 설계(`bind-system-plan.md` "핸들러 내부 상태 저장" 절)의 확인, "왜 - GC-안전한가" 설명만 명시적으로 추가. -- **Ref API가 `.Value`(읽기 전용) + `:Set(value)`/`:Callback(fn)`/ - `:Wait(thread?)`(전부 self 반환)로 확정.** self-반환 덕에 - `if ref.Value then ref.Value else ref:Wait().Value` 관용구가 성립 — - 이걸 성립시키려고 `:Set()`이 `coroutine.resume`할 때 넘기는 인자를 - 기존 문서(세 번째 세션 원안)의 `value`에서 **`self`**로 정정함(안 - 그러면 `:Wait()`의 yield 리턴값에 `.Value`를 체이닝할 방법이 없었음). - `:Wait(thread?)`의 `thread` 인자는 생략 시 `coroutine.running()`을 - 캡처해 진짜로 yield하고, 명시적으로 넘기면 그 thread를 등록만 하고 - yield 없이 즉시 `self` 반환(코루틴 역학상 남의 thread를 여기서 대신 - 정지시킬 수 없어서) — 사용자가 직접 관리하는 스케줄러가 이미 어딘가서 - 정지시켜 둔 thread를 등록만 해두고 호출부는 안 블록되고 싶은 유스케이스. - 콜백은 여전히 raw 값을 받음(Ref 자신이 아니라). -- **파일 분리**: `Ref`는 그 자체로 완결된 프리미티브, `PreRef`도 "children - 배열 전용, 위치 무관 호이스팅"이라는 특이한 제약을 가진 별개 - 프리미티브라 기존 1프리미티브-1파일 컨벤션(Blocker/Effect 분리와 - 같은 이유)을 따라 `Ref.luau`/`PreRef.luau`로 쪼갬 — 런타임은 여전히 - 공유(`PreRef`가 `Ref`를 재사용, 브랜드 태그만 다름), `base/architecture.md` - 소스트리에 반영 완료. -- 전부 `base/bind-system-plan.md`(Ref/PreRef 절)와 `research/tween-plan.md`에 - 반영 완료. `.claude/question.md`엔 이미 반영돼 있던 "Ref 이름 자체는 - 용어 정리 대상" 항목과 모순 없음(이번 세션은 메소드 이름만 확정, Ref라는 - 타입 이름 자체는 여전히 가칭). - -**같은 세션 후반 — `.claude/question.md` 0번의 마지막 미해결 항목(Effect가 -`state:Effect()`인지 자유 함수인지) 해소.** 사용자가 직접 "정해볼까" 하고 -제기해 라이브로 논의, 다음으로 확정(전부 `base/effect-plan.md`/ -`base/bind-system-plan.md`에 반영): - -- **`state:Observer(fn)`는 등록 즉시 1회 실행되는 것으로 확정** — 근거: - (1) 이미 채워진 State를 나중에 구독하면 반영 연산이 아예 한 번도 안 - 일어나는 초기화-순서 디버깅 문제, (2) 초회 실행을 안 해야 할 구체적 - 근거가 약함, (3) 이러면 Observer 하나로 "초기값 적용"과 "이후 변경 - 반영"이 같은 코드 경로로 통일됨(store-bind 프로퍼티 핸들러가 최초 - 적용용 코드를 별도로 안 짜도 됨). -- **`Effect(fn, state?) -> EffectHandle`로 확정** — `state` 생략 시 기존 - 스펙 그대로(설치 1회 + leaf 죽을 때 확정 정리, 재실행 없음). `state` - 지정 시 **내부적으로 `state:Observer(...)`를 조합** — Observer가 이제 - 즉시 1회 실행되므로 그 첫 실행이 설치를 겸하고, 이후 무효화마다 - 직전 cleanup 호출 후 `fn` 재호출, leaf 사망 시 마지막 cleanup 1회 — - React `useEffect(fn, [dep])`와 동형. 다수 의존성은 `:With(...)`로 먼저 - 하나의 State로 묶어서 넘기는 쪽으로 확정(React식 별도 deps 배열 - 안 만듦 — 같은 일 하는 두 번째 경로 방지 원칙). Effect는 여전히 - 자유 함수(메소드 아님) — `state` 없이도 성립하는 유스케이스가 있고, - 있어도 leaf 생명주기 바인딩을 `state`가 소유하지 않아서. -- **예전에 기각했던 "Observer에 cleanup 반환 계약 추가"와 안 부딪힘** — - 그때 기각한 건 "Observer 자체에 이 복잡도를 넣지 말자"였지 패턴 자체가 - 무용하다는 게 아니었음. Effect가 opt-in 상위 계층으로 이 패턴을 제공하는 - 지금 구조가 그 기각과 정확히 양립함. -- **`fn`을 커링 스타일(팩토리가 실제 fn을 만들어 반환)로 짜는 것도 Effect/ - Observer 둘 다 모듈화 관용구로 권장** — `Modifier`의 `Boldify(10)` 커링과 - 같은 결. -- **백로그로만 기록, 결정 안 함**: `state:Apply(...)`처럼 여러 개를 커링으로 - 받아 `:With`/`:Compute` 등록을 자동화하는 조합기 아이디어(사용자 제안, - `Modifier:Apply`의 State판 대응물) — `base/bind-system-plan.md`에 백로그 - 절로만 남김, 시그니처/필요성 미검증. **(2026-08-07 일곱 번째 세션에서 - 이 방향 자체가 기각되고 훨씬 단순한 형태로 확정됨 — 아래 참고.)** -- 이걸로 `question.md` 0번(추가 프리미티브 논의)의 열린 항목은 "키 기반 - 동적 컬렉션 재조정" 하나만 남음. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미 -설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-07 일곱 번째 세션 — `:Compute` 커링, `state:Apply` 확정(백로그안 기각), Effect `:Subscribe`/`:Unsubscribe` 신설, 이중 바인딩 금지 - -짧은 대화형 세션, 네 가지를 순서대로 처리 — 전부 `base/bind-system-plan.md`/ -`base/effect-plan.md`/`ROADMAP.md`/`question.md`에 반영 완료: - -1. **`:Compute(fn)`에도 커링 권장 노트 추가.** 여섯 번째 세션에서 Observer/ - Effect의 `fn`에만 문서화됐던 "팩토리가 실제 `fn`을 만들어 반환하는 - 커링 스타일 권장"이 `:Compute`엔 빠져 있었음 — 같은 결이라 자연스럽게 - 확장, `bind-system-plan.md` "`:With`+`:Compute`" 절에 추가. -2. **`state:Apply(factory)` 확정 — 원래 백로그였던 "`:With`/`:Compute` - 등록을 커링으로 자동화하는 조합기" 방향은 기각.** 사용자가 재확인한 - 실제 의도는 훨씬 단순함: `Modifier:Apply`와 똑같이 `factory(self)`를 - 체이닝 문법으로 부르는 순수 설탕(`function(self, factory) return - factory(self) end`) — `fnb(c,d)(fn(a,b)(state))`처럼 팩토리를 안에서 - 밖으로 겹쳐 읽어야 하는 중첩을 `state:With(a,b):Compute(fn(a,b)) - :Apply(fnb(c,d))`로 펴는 게 유일한 목적. 구현 비용 거의 0(State는 - Modifier와 달리 제네릭 `__index` 필드 setter 합성이 없어 이름 예약 - 충돌도 없음), 타입은 `factory: (State) -> U): U`로 Modifier보다 - 더 열어둠(팩토리가 State 밖 plain 값을 반환해 반응형 그래프를 벗어나는 - 것도 허용). Source는 기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 - 포함. `bind-system-plan.md` "`state:Apply(factory)`" 절, 구체 전/후 - 코드 예시까지 반영. 부수적으로 같은 헤더 아래 잘못 걸려 있던 Observer - `:Subscribe`/`:Unsubscribe` 내용(무관한 주제)을 별도 절로 분리하는 - 문서 버그도 수정. -3. **`EffectHandle:Subscribe()`/`:Unsubscribe()` 신설.** 지금까지 Effect의 - 유일한 생애주기 경로는 children 배열 leaf 부착뿐이라, leaf 없이 쓰는 - 모듈/스크립트 레벨 사이드 이펙트(백그라운드 시스템 등)엔 반환된 - `EffectHandle`이 막다른 길이었음 — Observer가 이미 가진 `:Subscribe`/ - `:Unsubscribe`와 같은 결로 확정. **핵심 주의점**: Effect의 - `:Unsubscribe()`는 Observer의 것을 그냥 위임하면 안 됨 — Observer의 - 계약은 "미래 재실행만 끊는다"로 충분하지만, Effect의 계약은 "생애주기가 - 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"이고 leaf 사망은 - 그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`도 동일하게 "지금 - 끝났다"는 신호로 취급해 마지막 cleanup을 트리거해야 계약이 일관됨(leaf - 가 살아있어도 마찬가지). idempotent 보장은 기존 `Subscribed` 필드 - liveness 체크 재사용으로 공짜. `base/effect-plan.md` 신규 절. -4. **Observer/Effect 이중 바인딩 금지 — `Bound`(가칭) 플래그로 즉시 - `error`.** 처음엔 "leaf 부착과 `:Subscribe()`를 동시에 써도 같은 - liveness 게이트를 공유하니 안전"이라고 적었으나, 사용자가 애초에 한 - 핸들은 라이프사이클 바인딩 경로를 하나만 가져야 한다고 정정 — 동시 - 바인딩은 UB로 확정하되, 판별 비용이 사실상 0(불리언 필드 하나)이라 - 조용한 오동작 대신 그 자리에서 `error`를 던지는 쪽으로 결정 - (엔지니어링 비용 대비 디버깅 이득이 명확). 두 진입점(`:Subscribe()` - 호출부, children 배열 leaf 부착부)이 똑같이 확인/설정하는 대칭적 게이트 - — 순서 무관. `bind-system-plan.md` "이중 바인딩 금지" 절 신설, - `effect-plan.md`의 3번 항목 서술은 이 규칙으로 대체(정정 표시 남김). - -**부수 정리**: `ROADMAP.md` M3에 `state:Apply`/Effect `:Subscribe`· -`:Unsubscribe`/이중 바인딩 금지 체크박스 추가. `question.md`에 `Bound` -이름을 용어 정리 대상(3순위)으로 추가. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 -설계 확정이라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-07 여덟 번째 세션 — `None` 센티널 확정(인라인 필드 지우기), `NoneHandler`가 Tween store-bind와 같은 재귀 재디스패치임을 확인 - -세 번째 세션에서 "미확정"으로 메모만 남겨뒀던 `None` 센티널 -(`{ Override = nil, mod }`처럼 인라인 키로 modifier 값을 명시적으로 -지우고 싶어도 Lua 테이블의 `키 = nil`이 "키 없음"과 구별 안 되는 문제)을 -사용자가 "이거 결정할 게 진짜 있냐"고 다시 제기해 라이브로 짧게 논의, -확정까지 감. 전부 `base/modifier-plan.md`(2-1번)/`base/bind-system-plan.md` -(신규 절)/`base/ui-shorthand-plan.md`(신규 절)에 반영 완료: - -- **merge/setter 쪽은 아무것도 안 바뀜** — `None`은 raw 저장 계층의 그냥 - 평범한 실재값이라, 기존 merge 규칙("인라인 키 존재 시 무조건 우선", - `Override`의 "뒤 인자가 필드 단위로 이김")이 손댈 것 없이 그대로 작동함. - 처음엔 "merge 시점에 키를 지운다"는 새 분기가 필요하다고 잘못 생각했다가, - "값을 표현만 할 수 있으면 기존 규칙이 이미 다 해줌"이라는 걸로 정정. - 인라인 props 테이블 키(`{ TextColor3 = None, mod }`)와 Modifier setter - 인자(`mod:TextColor3(None)`) 둘 다 지원 — 메커니즘이 완전히 같아 구현 - 비용 거의 0("어차피 무료로 얻어지는거 아님?" — 사용자), 후자 덕에 - "특정 필드만 지우는 재사용 가능한 modifier 조각" 패턴도 공짜로 됨. - `:Peek()` 반환 타입도 `T | State | None | nil`로 확장(raw 계층에서 - `None`을 있는 그대로 돌려줌 — Peek은 확정 안 하고 그대로 넘긴다는 기존 - 9번 절 원칙 그대로). -- **실제 "지우기"는 디스패치 단계에서, 새 메커니즘 없이 풀림 — 핵심 - 발견.** 처음엔 "우선순위 최상단에서 값을 그냥 nop 처리"로 생각했다가, - 사용자가 "그럼 process에 특수 로직이 들어간다"고 지적하며 더 나은 안을 - 직접 제시: `NoneHandler`라는 평범한 pluggable 핸들러 하나를 추가 — - `isHandlable`이 `v == None`을 잡고, `process`가 `v`를 진짜 `nil`로 바꿔 - **`process(inst, k, nil)`을 재귀 호출**. 이게 바로 이미 확정돼 있던 Tween - store-bind 핸들러(`bind-system-plan.md` "확정된 디스패치 모델" 절, - `v`가 Store면 `realv`를 계산해 `process(inst,k,realv)`로 재귀)와 - **완전히 같은 패턴**이라는 걸 확인 — 새 아키텍처 개념이 하나도 안 늘어남. - base 드라이버 자체(`process(inst,k,v) -> getHandler(inst,k,v).process(...)`)는 - `None`을 전혀 모르는 순수 제네릭 그대로 유지, 개별 프로퍼티/이벤트/UI - shorthand 핸들러 시그니처도 `None`이 안 나옴(원래 있어야 했던 "`v`가 - `nil`인 경우" 처리를 재사용할 뿐). -- **`None`의 의미는 "리셋"이 아니라 "이 조합 단계에서 이 필드를 세팅 - 안 함"** — 실제로 `v=nil`을 받은 핸들러가 뭘 할지는 핸들러마다 다름(일반 - 프로퍼티는 사실상 그대로 두는 것과 다름없고, UICorner 숏핸드처럼 실제 - Instance를 만들어 붙이는 핸들러는 그 자식을 지움). 구체 사례로 - `ui-shorthand-plan.md`에 UICorner 절 신설 — `process(inst,k,nil)`이 - 만들어둔 `_quad_corner`류 자식을 직접 지움(이건 `retract`가 아니라 - `process` 자신의 로직 — `retract`는 "다른 핸들러가 키를 넘겨받는" 별개 - 시나리오 전용, 이미 확정돼 있던 원칙 재확인), 값이 자주 `nil`↔숫자로 - 토글되면 생성/제거 비용이 매번 든다는 캐비엇도 명시. -- **M2(디스패치 엔진) 착수 시 확인할 것 하나 새로 생김** — "이 키를 지금 - 누가 담당 중인가" bookkeeping이 바깥 순회 루프가 아니라 `process` 호출 - 자체 내부에서 갱신돼야 함. 안 그러면 값이 계속 `None`으로 유지되는 매 - 사이클마다 "1차 매치는 `NoneHandler`, 재귀 호출 뒤 실제 담당은 다른 - 핸들러"로 바깥 루프가 오판해 불필요한 `retract`가 반복 호출될 위험 — - `ROADMAP.md` M2에 반영, `pre-implementation-audit.md`의 "이전 매치 - 핸들러 추적" 항목과 같은 부류라 새 우선순위 등급 없이 거기 흡수. - -**부수 정리**: `question.md`에서 "미확정"이던 `None` 항목을 해소로 -제거하고, 이름 자체(`None`/`NoneHandler`)만 다른 가칭들과 같이 용어 -정리 대상(3순위)으로 새로 추가. `ROADMAP.md` M7 체크박스를 "확정 완료"로 -갱신. - -**같은 세션 후속 — `Dispatch` 함수 네이밍 정리, `canExecute` 시그니처 정정, -Tag/Attribute 전용 문서 신설.** None 논의를 파고들다 디스패치 엔진 자체의 -용어가 여러 군데서 흔들리고 있다는 게 드러나 바로 이어서 정리함. 전부 -`base/bind-system-plan.md`/`base/lifecycle-pattern.md`/`base/tag-plan.md` -(신규)/`base/attribute-plan.md`(신규)/`ROADMAP.md`에 반영 완료: - -- **제 실수 정정 — `canExecute`와 `isHandlable`은 다른 개념** (전체 경위는 - `archive/agent-mistake.md` 1번으로 옮김) — 결론만: `NoneHandler`가 - 구현해야 하는 건 `isHandlable`이지 `canExecute`가 아님. -- **`Dispatch.getHandler`/`Dispatch.process`/`Dispatch.addHandler`/ - `Dispatch.drive`로 이름 공식화.** 원래 "확정된 디스패치 모델" 절은 - "스캔+실행"과 "매치된 핸들러 자신의 처리"를 둘 다 그냥 `process`라고 - 불러 이름이 겹쳤던 게 혼동의 원인이었음 — `Dispatch.getHandler(inst,k,v): - Handler?`(순수 스캔)와 `Dispatch.process(inst,k,v)`(오케스트레이터: - getHandler → 이전 담당자 다르면 그 `retract` → 새 핸들러의 `.process`)로 - 분리, Handler 자신의 필드는 계속 `process`/`retract`(이미 확정된 이름, - 재검토 대상 아님) — 겹침은 소유자 표기(`Dispatch.process` vs - `handler.process`)로 해소, 새 이름 발명 안 함. **`Dispatch.addHandler(handler)`** - 도 신설 — concrete Handler를 우선순위 레지스트리에 등록하는 것도 - 결국 quad-roblox가 `BaseModule`을 뮤테이션하는 시점에 해줘야 하는 - 일이라(기존 "base 유틸은 인터페이스, 백엔드가 주입" 패턴과 같은 모양). - **배열→해시 두 패스 순회 드라이버 자신은 `Dispatch.drive(inst, - flattened)`로 확정** — 문서가 이미 이걸 비공식적으로 "base 디스패치 - 드라이버"라 불러왔던 걸 그대로 동사화(`apply`는 기각 — "Dispatch를 - 뮤테이션해서 결과를 낸다"는 어감이라 안 맞는다는 사용자 판단). -- **`canExecute` 시그니처 정정: `(handle) -> boolean`, zero-arg 아님.** - `lifecycle-pattern.md`가 원래 `canExecute: () -> boolean`(바인딩마다 - 클로즈오버된 람다)으로 적어뒀던 걸 정정 — 그러면 등록마다 클로저를 - 새로 만들어야 해서 "base는 인터페이스만, quad-roblox가 `BaseModule` - 뮤테이션으로 실 구현 주입"이라는 이미 확정된 패턴과 안 맞음. 공유 - 함수 하나가 되려면 "어떤 등록을 볼지" 가리키는 인자가 필요 — - `canExecute(handle: LifetimeHandle): boolean`으로 확정. **quad-roblox - 구현 스케치(참고용, base 결정 아님)**: rbvm 패턴 재사용 — Instance당 - weak-keyed per-instance 저장소에 "gchold" 배열을 두고, 절대 발화 안 - 하는 신호에 연결한 Connection의 콜백 클로저 안에 살려두고 싶은 - Observer를 업밸류로 캡쳐(콜백은 안 불려도 클로저의 업밸류는 안 죽음, - `inst`가 GC되면 gchold 배열째로 같이 죽음). `canExecute(handle)`은 이 - Connection(류)의 `.Connected`를 확인. **미확인 세부사항으로 남긴 것**: - Observer→Connection 역참조를 별도 weak 릴레이션으로 둘지 그냥 Observer - 테이블 안 평범한 필드로 넣을지(정적 해싱 필드 접근이 더 쌀 수 있음) — - quad-roblox 구현 단계에서 실측 필요. -- **`Tag`/`Attribute`도 UICorner/Tween처럼 전용 문서가 있어야 한다는 - 지적 — 맞아서 `base/tag-plan.md`/`base/attribute-plan.md` 신설.** - 둘 다 이미 `architecture.md`/`ROADMAP.md` M10에 파일로는 계획돼 - 있었지만 "1 프리미티브 1 파일" 관례(Blocker/Effect/Ref/PreRef 분리 - 선례)를 따르는 전용 설계 문서가 없었음 — 흩어져 있던 내용(Attribute의 - 타입 파라미터화 논의 등)을 모으고, 오늘 확정된 `None`/`process`/ - `retract` 동작을 반영. **핵심 발견**: Tag/Attribute 둘 다 UICorner - 숏핸드와 같은 패턴(값이 뭐든 항상 같은 핸들러가 계속 담당, 추가/제거를 - `process` 자신이 처리)이라 **retract가 필요 없음** — "확정된 디스패치 - 모델" 절이 원래 Tag/Attribute를 retract 필요 예시로 들었던 게 잘못이었음, - 바로잡고 "retract가 의미 있는 유일한 패턴은 매치되는 핸들러 *타입* - 자체가 사이클마다 바뀌는 경우(Tween↔일반 프로퍼티가 실사례)"로 좁힘. - Attribute는 특히 깔끔한 사례 — Roblox `SetAttribute(name, nil)` 자체가 - 네이티브하게 "지움"이라 `None→nil` 재디스패치가 특별 처리 없이 그대로 - 맞아떨어짐. -- `.claude/README.md`에 두 신규 문서 반영, `ROADMAP.md` M2/M10 체크박스 - 갱신(`Dispatch` 4개 함수, `canExecute` 시그니처, Tag/Attribute 문서 - 참조). - -**같은 세션 세 번째 후속 — `canExecute` 옵션 하나 더 검토 후 확정 유지, -`Brand` 통합 판별 메커니즘 신설(`isState`를 10종으로 일반화), `isHandlable`도 -`inst`를 받도록 정정.** 전부 `base/bind-system-plan.md`(`Brand` 절, 핸들러 -계약 절)/`base/modifier-plan.md`/`ROADMAP.md`/`question.md`에 반영 완료: - -- **`canExecute`를 "각 핸들 타입이 직접 구현"(`Observer.canExecute`)할지 - "공유 함수"(`canExecute(any)->boolean`)로 할지 재확인 — 공유 함수 유지, - 솔직한 이유까지 명시.** `Observer` 자체는 quad-base 레벨(엔진 무관) - 타입인데 liveness 체크(Connection 기반)는 본질적으로 엔진 종속적이라, - `Observer.canExecute`가 직접 구현하면 base/roblox 분리 원칙이 깨지거나 - 결국 내부적으로 공유 함수를 다시 호출하는 얇은 래퍼가 될 뿐 — 어느 - 쪽이든 공유 함수 쪽이 낫다는 결론 재확인(추가 논의 없이 유지). -- **`Brand` 신설 — `isState`(다섯 번째 세션)를 quad의 다른 branded 타입 - 전부(`Observer`/`Effect`/`Tag`/`Attribute`/`Tween`/`Blocker`/`Store`/ - `Source`/`Slot`)로 일반화.** 공유 weak-key 레지스트리 하나(`Brand.set`/ - `Brand.get`) + **문자열이 아니라 테이블 아이덴티티를 태그로 사용** - (사용자 제안 — Luau 인터닝 문자열도 이미 O(1) 포인터 비교라 성능 차는 - 없지만, 오타 안전성이 실질 이득: 잘못된 변수 참조는 즉시 드러나지만 - 오타난 문자열 리터럴은 조용히 어긋남). `isX`는 `Brand`를 감싼 얇은 - wrapper — 단순 항등(`isObserver`)과 집합 멤버십이 필요한 경우(`isState` - = `{State,Source}`)로 갈림. **`None`만 예외 — 싱글턴이라 레지스트리 - 없이 `x == None` 항등 비교가 더 싸고 정확**, 대신 `Brand.get`이 범용 - introspection 창구(quad-debug 용도) 역할까지 겸하도록 `None`을 특수 - 분기로 앞단에서 걸러줌 — `isNone`이 그 분기의 실제 구현체. -- **정정 — `isSource`는 별도로 필요함, 다섯 번째 세션의 "불필요" 서술을 - 뒤집음** (전체 경위는 `archive/agent-mistake.md` 2번으로 옮김) — 결론만: - `isSource`를 별도 제공, `isState`는 여전히 `{State,Source}` 둘 다 통과. -- **Luau 타입 narrowing은 자동으로 안 됨 — 사용자가 직접 확인, 명시적 - `::` 캐스팅 필요.** `isX(v)`가 참이어도 Luau가 TypeScript의 `x is T` - 같은 사용자 정의 타입 가드를 지원 안 해서 `v`의 정적 타입을 자동으로 - 안 좁혀줌 — `if isState(v) then local s = v :: State ... end`처럼 - 런타임 검증 뒤 명시적 캐스팅이 실제 패턴. 여전히 duck-typing보다 훨씬 - 안전하니 가치는 있지만 자동 narrowing을 기대하면 안 됨. -- **`isHandlable`도 `inst`를 받도록 확정 — `(inst,key,value): boolean`, - 원래 `(key,value)`였던 걸 정정.** `process`/`retract`는 처음부터 - `inst`를 항상 받았는데(핸들러 계약 원 원칙) `isHandlable`만 예외였던 - 게 애초에 약간의 불일치 — 지금 당장 `inst`로 매치가 갈리는 케이스는 - 없지만, 나중에 필요해지면 핸들러 계약 자체를 깨는 breaking change가 - 되므로 지금 넣어두는 게 훨씬 쌈. `Dispatch.getHandler`가 스캔 중 - `handler.isHandlable(inst,k,v)`로 호출하도록 갱신. -- `ROADMAP.md` M2에 `Brand.luau` 체크박스 신설, `Handler.luau`/M7의 - `isState` 항목 갱신. `question.md`에 `Brand` 이름(용어 정리 대상, - "OOP 클래스명을 얻는 느낌"에 맞는 더 나은 이름 필요 — `Tag`는 이미 - quad-roblox에서 다른 뜻으로 쓰여서 충돌) 반영. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 -설계 확정이라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-07 아홉 번째 세션 — 코퍼스 전체 정합성 감사·수정, `agent-mistake.md` -신설, `CreatedRef` 이름 완전 폐기 - -여러 세션에 걸쳐 쌓인 stale 참조/자기모순을 서브에이전트 5개 병렬 감사로 -찾아내 전부 수정하고(커밋 `98bd46a`), 이어서 사용자가 직접 발견한 남은 -문제(`CreatedRef` 이름 자체가 더 이상 존재할 이유가 없다는 지적)까지 처리한 -세션. 세 부분으로 나눠 요약: - -**1. 코퍼스 정합성 감사·수정 (커밋 `98bd46a`)**: `CreatedRef`의 `phase` -옵션 폐기가 `bind-system-plan.md` 안에서 세 곳 중 두 곳에 방치돼 있던 것, -`question.md`의 `Ref` 재검토 대상 여부 자기모순, UICorner 숏핸드 개명이 -5개 문서에 전파 안 된 것, `canExecute(handle)` 시그니처 정정이 막 확정된 -직후라 두 곳에 전파 안 된 것, `architecture.md`/`ROADMAP.md`/`CLAUDE.md`의 -stale 문구·누락 참조 등 12개 항목을 수정. `store-semantics.md` 제목도 -"State는 **Store** 위의 캐시 레이어"에서 "State는 **Source** 위의 캐시 -레이어"로 정정(사용자 확인: Source 단독 존재 가능 + Store는 Source들의 -집합이라는 온톨로지가 맞음). `slot-plan.md`의 CRUD 의미론 갭 하나만 -사용자가 다음 세션에서 직접 다루기로 보류. - -**2. `archive/agent-mistake.md` 신설** — 설계 반전(`*-reversed.md`)/기각 -후보(`*-rejected.md`)와 구분되는 세 번째 archive 카테고리: 에이전트가 -문서 작성 중 스스로 낸 개념 혼동을 같은 세션 안에서 정정한 사례 전용 -(`canExecute`/`isHandlable` 혼동, `isSource` 불필요 오판 2건). CLAUDE.md -세션 로그에 전체 경위가 장황하게 남아있던 것 중 최종 결론이 이미 `base/` -문서에 반영돼 중복이던 걸 옮기고 포인터만 남김 — 앞으로도 비슷한 사례가 -생기면 여기로 옮길 것(사용자 확인). - -**3. `CreatedRef` 이름 완전 폐기 — 사용자가 직접 발견.** "Ref가 이미 다 -정해진 것 같은데 `CreatedRef`는 이제 없는 말 아니냐"는 지적: `Source(default)`/ -`Ref(default)`/`Store({defaults})`가 이미 Kotlin Compose식 "타입 이름 -자체가 팩토리 함수" 생성자 스타일로 확정돼 있었는데(2026-08-06 네 번째 -세션), `CreatedRef(fn)`라는 별도 래퍼 이름만 그 확정 이전(2026-08-04, -Ref가 아직 "instance 얻는 통로"로 좁게 정의됐던 시절)의 잔재로 계속 -남아있었던 것 — 실제로는 `Ref(default)`(또는 `PreRef(default)`) -인스턴스 자체를 children 배열 숫자 슬롯에 그대로 놓으면 `(v=Ref)` 매치 -핸들러가 처리하므로, 별도 래퍼 함수가 있을 이유 자체가 없었음. `base/ -bind-system-plan.md`(바인드 방법 절 재작성, "CreatedRef와의 관계" 절 -삭제, "phase 옵션 폐기" 절/열린질문 절 정리) · `ROADMAP.md`(M0/M8 체크 -박스) · `question.md`(용어 재검토 목록에서 제거, 해소로 표시) · -`architecture.md`(소스트리 주석) · `research/documentation-content-map.md` -전부 동기화 완료. `archive/ref-phase-option-reversed.md`(phase 옵션 -자체의 역전 이력)와 CLAUDE.md 이전 세션 로그의 `CreatedRef` 언급은 -당시 기록으로서 정확하므로 그대로 둠 — 역사적 서술과 현재 유효한 설계를 -헷갈리지 않도록 "phase 옵션 폐기" 절 제목에 "이 절이 당시 쓰던 이름 -자체도 이후 폐기됨" 포인터만 추가. - -**부수 작업 — `PreRef`/`Modifier`의 "pre-hook" 태깅 요청 처리.** 같은 -세션 앞부분에서 사용자가 "PreRef와 Modifier는 문서화 시 pre-hook 태그가 -필요해 보인다, hook과 pre-hook의 차이(취소 가능/순서 등록 가능)도 적어 -두면 좋겠다"고 제안 — 이건 런타임 설계가 아니라 문서 사이트 콘텐츠 -분류 아이디어라 base/에 "확정"으로 못박지 않고 `research/ -documentation-content-map.md`(심화 콘텐츠 후보 6번 + "문서화 아직 보류" -목록)에 사용자 원문 프레이밍 그대로 미확정 표시로 남겨둠 — `PreRef`가 -"인스턴스에 뭐가 일어나기 전에 채워진다"는 사실 자체는 이미 확정 -서술돼 있었지만(재확인 후 "메모에서 지워도 됨"으로 답변), "hook"/ -"pre-hook" 용어 채택 여부·`PreRef`의 취소 가능성·복수 `PreRef` 간 순서는 -다음에 사용자가 직접 정해야 base/로 승격 가능. **[정정, 2026-08-07 열 -번째 세션]** 같은 대화에서 "Ref 콜백/대기자 배열은 압축 없이 `self[i] -= nil`로만 지워도 된다"는 설계가 이미 정확히 반영돼 있다고 여기 적었던 -건 틀림 — 실제로는 `nil`이 아니라 `None`으로 지워야 함(아래 열 번째 -세션 절 참고), 이때는 아직 발견 전이었음. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `slot-plan.md` CRUD -의미론과 "hook/pre-hook" 프레이밍 확정만 사용자가 직접 다룰 후보로 남음. - -**같은 세션 후속 — `PreRef` pre-pass 구현 위치·복수 `PreRef` 순서·동적 -경로 가드 확정.** 사용자가 구체적인 구현 방안 세 개를 직접 제시: - -1. **복수 `PreRef` 간 순서는 배열 index 순서 그대로** — 별도 규칙 발명 - 불필요, 위 "props 순회 순서" 절이 이미 확정한 "배열 파트는 index - 순서대로" 계약을 그냥 재사용하면 됨. 호이스팅은 "PreRef 대 나머지" - 에만 적용되는 규칙이지 "PreRef끼리"엔 적용될 게 없음. -2. **pre-pass가 사는 곳 — 새 `Dispatch.*` 함수 대신 이미 확정된 - `Dispatch.drive(inst, flattened)` 자신.** 사용자가 두 대안을 직접 - 제시(`Dispatch.process(inst, flatten, prerefs)`류 신설 함수 vs - `flatten(inst, nonFlatten)` 함수 자체에 얹기) — 검토 결과 둘 다 - 불필요/위험함이 드러남. 전자는 이미 `Handler.process`/`Dispatch.process` - 이름이 다른 뜻으로 확정돼 있어 겹침. 후자(flatten에 얹기)는 사용자가 - "가장 간단해 보인다"고 제안했지만, `research/existing-instance-bind-plan.md`가 - 다루는 "이미 마운트된 Instance 재바인드 시 flatten을 다시 해야 - 하는가"라는 열린 질문이 실제로 flatten이 한 인스턴스 생애주기 동안 - 여러 번 재호출될 가능성을 열어두고 있어서, 거기 PreRef fire를 얹으면 - 재바인드마다 PreRef가 또 fire되어 "이 인스턴스 하나의 construction - 훅"이라는 정의 자체가 깨짐 — 기각. `Dispatch.drive`는 최초 마운트 - 시 한 번만 불리는 게 이미 전제라 이 위험이 없어서 그대로 거기 좁은 - pre-pass 한 줄만 얹으면 충분. -3. **동적 경로로 도착한 `PreRef`는 런타임에도 명시적으로 error — - 지금까지 타입 차단만 문서화돼 있던 빈틈을 채움.** 사용자 제안 - 그대로 채택: `{isHandlable = v is PreRef, process = error(...)}` - 전용 Handler를 정상 우선순위 레지스트리에 등록(`NoneHandler`와 같은 - "한 값 종류 전담" 패턴, 새 메커니즘 아님). 리터럴 배열의 `PreRef`는 - pre-pass가 fire와 동시에 슬롯을 소진시켜 정상 두 패스에 다시 - 노출되지 않으므로, 이 Handler가 실제로 매치되는 경우는 타입 차단을 - 어떻게든 우회한 버그 케이스뿐 — no-op이 아니라 즉시 `error`가 맞음. - -전부 `base/bind-system-plan.md` "PreRef" 절에 반영, `ROADMAP.md` M8 -체크박스 갱신, `research/documentation-content-map.md`의 "복수 PreRef -순서" 미정 표시 제거(해소됨, "취소 가능성"만 계속 미정으로 남김). - -**같은 세션 두 번째 후속 — "호이스팅이 물리적 재배치가 아니라 별도 -선행 스캔"이라는 것과 소진 방식을 명시화(뒤이은 세 번째 후속에서 -`nil`→`None`으로 다시 정정됨, 아래 참고).** 사용자가 "drive에서도 -결국 PreRef를 목록에서 뽑아내야 하는데, 호이스팅 안 되면 PreRef -의미가 사라지는 거 아니냐"고 재질문 — 이전 답변이 `Dispatch.drive`가 -pre-pass를 갖는다고만 하고 정확한 알고리즘을 안 써서 나온 질문. -`Dispatch.drive`가 같은 `flattened` 테이블을 **두 번** 순회한다는 -것으로 답변: (1) pre-pass가 배열 전체를 index 순으로 훑어 `PreRef`를 -fire하며 그 자리에서 슬롯을 소진, (2) 그 다음 평소 두 패스가 같은 -테이블을 다시 순회하되 소진된 슬롯은 자연히 건너뜀. "호이스팅"은 -PreRef를 배열 앞으로 물리적으로 옮기는 게 아니라 "PreRef 전용 선행 -루프가 통째로 먼저 끝난 뒤에야 나머지가 시작된다"는 뜻이라 소스 위치와 -무관하게 항상 먼저 fire됨. **소진이 최적화가 아니라 정확성 요건인 이유도 -명시**: 안 지우면 두 번째 패스가 이미 처리된 PreRef를 `Dispatch.process`로 -다시 넘겨서, 바로 위에서 신설한 "동적 경로 가드" Handler(`(v=PreRef)`→ -`error`)가 정상 사용에도 오탐 에러를 던지게 됨. - -## 2026-08-07 열 번째 세션 — 소진 슬롯을 `nil`이 아니라 `None`으로, -사용자가 Luau REPL 반례로 직접 발견 - -같은 날 이어진 세션. 사용자가 Luau REPL에서 직접 -`for i,v in {[1]=1,[2222]=2222,[211]=211,[131]=131,[3]=3,[6]=6, -[122]=122,[11]=11,[312]=312,[821]=821,[991]=991} do print(i,v) end`을 -돌려 순회 순서가 `1, 6, 122, 11, 991, 2222, 131, 312, 3, 821, 211`로 -나온다는 걸 보여줌 — index 오름차순이 전혀 아님. 이건 위 아홉 번째 -세션에서 "PreRef pre-pass가 fire된 슬롯을 `nil`로 지우면 된다"고 적은 -것과 여섯 번째 세션에서 "Ref 콜백/대기자 배열도 `[i]=nil`로 소진하면 -된다"고 적었던 것 둘 다를 뒤집는 반례 — 키가 촘촘한 저범위 정수에서 -벗어나면(구멍이든 원래 듬성듬성이든) Luau/Lua 테이블이 해시 파트 -취급으로 넘어가 순회가 해시 버킷 순서가 됨. - -**해결 — 소진에 `nil` 대신 `None` 센티널 사용, 전 코퍼스에 전파.** -`None`은 `nil`이 아닌 실재하는 값이라 그 슬롯을 "차 있다"로 유지시켜서 -테이블이 "구멍 없는 시퀀스"라는 불변식이 안 깨짐 — 두 가지를 동시에 -해결: (1) 순서가 실제로 중요한 배열(PreRef pre-pass)의 순서 보장 유지, -(2) `table.insert`가 내부적으로 쓰는 `#t`가 Lua 명세상 구멍 있는 -테이블에서 정의되지 않은 동작이라는 문제(Ref 콜백/대기자 배열이 새 -등록 때 `table.insert`를 씀 — 순서 자체는 원래도 안 중요했지만 이 -`#t` 안전성 문제는 진짜 버그였음). **배열 파트의 `None`은 해시 파트의 -`None`(Modifier 필드 명시적 지우기, `NoneHandler` 경유)과 의미가 -다름** — 배열 파트 `None`은 처리할 핸들러가 없는 순수 빈 슬롯 표시라 -`Dispatch.process`/`NoneHandler`를 안 거치고 두 패스 루프 자신이 직접 -`if v == None then continue end`로 스킵. - -`base/bind-system-plan.md`의 "왜 `nil`이 아니라 `None`인가"(Ref -콜백/대기자 절)와 PreRef pre-pass 절에 반영, `ROADMAP.md` M0/M8 -체크박스 갱신, 위 아홉/여섯 번째 세션 문단에 정정 표시 추가(원문은 -유지, 틀렸던 부분만 짧게 정정 포인터). - -**부수 발견 — `props.Modifier`/`props.Ref` nil-hole 위험도가 이전 -서술보다 큼.** `pre-implementation-audit.md` 1-5가 이미 이 위험을 -"뒤 항목까지 무시될 수 있음"으로 국소적 피해처럼 서술해뒀는데, 이번 -REPL 실측으로 실제로는 구멍이 하나만 생겨도 **그 테이블 전체**가 순서 -보장을 잃을 수 있다는 게 드러남 — M0 스파이크에서 반드시 실측하고, -심각하면 "raw 리터럴 대신 `props.Modifier or Modifier()`로 non-nil -보장" 컨벤션 문서화까지 검토하기로 `ROADMAP.md` M0에 메모 추가. 이 -케이스는 caller가 직접 쓰는 raw Lua 리터럴이라 `None`으로 프레임워크가 -대신 채워줄 수 없어서 별도 해법이 필요함 — `None` 소진 전략과 혼동하지 -말 것. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M0 착수 시 위 -nil-hole 위험도 실측이 우선순위 높아짐. - -**같은 세션 세 번째 후속 — `props.Modifier`/`props.Ref` nil-hole -해법을 실제로 확정, 세션 clear 전 문서 완결성 점검하며 발견한 갭 -3개도 같이 보강.** 사용자가 "컴포넌트에서는 꼭 `or None`이나 -`Modifier()` 같은 걸로 nil 못하게 강제하는 걸 문서화하자"고 요청, 그 -자리에서 결정하고 clear 전 세션 전체를 다시 훑어 새로 알게 됐지만 -아직 문서에 없던 것들을 마저 채움: - -1. **`props.Modifier or None`/`props.Ref or None`을 필수 관용구로 - 확정** — `Modifier()`(빈 modifier 새로 생성)가 아니라 `None`을 쓰는 - 이유는 이미 있는 array-part `None`-스킵 메커니즘(PreRef 논의에서 - 확정)을 그대로 재사용해 새 코드/할당이 하나도 안 늘어나기 때문 — - `flatten`이 `isModifier(None) == false`라 그냥 통과시키고, 이어지는 - 두 패스 루프가 `None`을 만나면 스킵. `base/component-composition-plan.md` - "필수 관용구" 절 신설, `ROADMAP.md` M0/`pre-implementation-audit.md` - 1-5/`question.md`에 반영(1-5는 해소로 표시). -2. **`Modifier()` 바닥 생성자가 문서 어디에도 없었던 갭 발견·보강** — - `Source(default)`/`Ref(default)`/`Store({defaults})`와 나란히 있어야 - 할 "`Type(args)` 팩토리" 4번째 예시가 원래 없었음(이전 아홉 번째 - 세션에 `Modifier.Rounded(8)` stale 참조를 고치면서 실수로 체이닝 - 예시인 `mod:UICorner(8)`로 잘못 채워 넣었던 것도 같이 바로잡음). - `modifier-plan.md` 3번 절에 명시, `store-semantics.md` 예시 목록 - 정정, `ROADMAP.md` M7 체크박스 추가. -3. **`Brand` 태그 목록에 `RefTag`/`PreRefTag`/`ModifierTag`가 빠져있던 - 갭 발견·보강** — 이번 세션 내내 `isPreRef(v)`/`isModifier(v)`를 - 이미 존재하는 predicate처럼 써왔는데 정작 여덟 번째 세션의 `Brand` - 태그 목록엔 없었음. 추가하면서 **`isRef`/`isPreRef`가 `isState`와 - 달리 집합 멤버십이 아니라 단순 항등이라는 것도 명시** — - `isRef(preRefInstance)`가 참이면 일반 `(v=Ref)` 핸들러가 `PreRef`도 - 집어삼켜 PreRef 전용 pre-pass/가드 Handler 설계 전체가 무너지므로 - 반드시 배타적이어야 함. `bind-system-plan.md`의 `Brand` 절, - `ROADMAP.md` M2 체크박스에 반영. -4. **배열 파트 `None`과 해시 파트 `None`(`NoneHandler`)이 같은 센티널인데 - 처리 경로가 다르다는 걸 `None` 센티널 절 자체에 명시적으로 - 교차 참조 추가** — 이전엔 PreRef 절에만 있고 `None` 센티널 원래 - 정의 절엔 이 예외가 안 적혀 있어서, 그 절만 읽으면 모든 `None`이 - `NoneHandler`를 탄다고 오해할 수 있었음. - -전부 커밋 `98bd46a` 이후 아직 커밋 안 된 이번 대화 전체 변경사항에 -포함 — 다음 세션이 새로 알아야 할 건 없음, `ROADMAP.md` M0부터 그대로 -시작. - -## 2026-08-08 세션 — `Relate` 신규 프리미티브, `bindLifetime`/`canExecute` -탑레벨 함수로 확정, store-bind 재실행=Observer 재사용 명문화, `retract` -필드 생략 불가 확정 - -사용자가 store-bind/라이프사이클 관련 문서 갭 두 개를 질문하며 시작된 세션 -— 답을 찾는 과정에서 지금까지 이름만 있던 placeholder(`base.perInstanceState`)가 -실제로는 제대로 설계된 적 없는 프리미티브였다는 게 드러나 그 자리에서 -설계까지 확정까지 감. 네 가지로 정리: - -**1. store-bind의 "값이 바뀔 때마다 재귀 process" 구독 메커니즘 = -`state:Observer(fn):Subscribe()` 재사용으로 확정.** 기존 "확정된 디스패치 -모델"/"재실행 래핑" 절이 구독을 추상적으로만 서술해서 마치 새 구독 -프리미티브가 필요한 것처럼 읽혔는데, 실제로는 이미 확정된 Observer(등록 -즉시 1회 실행이라 "최초 적용"과 "이후 갱신"이 공짜로 통일됨, 자기 `Subscribed` -liveness도 이미 있음)를 그대로 쓰면 됨 — `retract`는 `observer:Unsubscribe()` -호출 하나로 끝. 새 구독 메커니즘 발명 없음. `base/bind-system-plan.md` -"Store 바인드는 특수 경우인가" 절 반영. - -**2. `retract` 필드는 no-op이라도 항상 정의해야 함 — 생략 불가로 확정.** -"모든 핸들러가 의미 있게 구현할 필요는 없음(보통 no-op)"이라는 기존 서술이 -"필드 자체를 생략해도 된다"로 오독될 수 있는 갭이었음 — `Dispatch.process`는 -담당 핸들러 타입이 바뀔 때 이전 핸들러의 `retract`를 nil 체크 없이 무조건 -호출하므로, 필드를 생략한 핸들러가 실제로 교체되는 드문 순간(Tween↔프로퍼티 -등)에 `attempt to call a nil value`로 크래시함. `base/bind-system-plan.md` -"핸들러 계약" 절에 명시, M2 체크리스트에 린트 대상으로 추가. - -**3. `Relate` 신규 프리미티브 — `bindLifetime`/`canExecute`(가 의존하는 -per-inst weak 저장소)를 제대로 설계.** 사용자 질문 경위: `Frame { observer }`처럼 -children 배열에 직접 놓는 leaf 케이스와, property store-bind 핸들러가 -**내부에서** 만드는 Observer(배열에 안 들어가므로 그 leaf 부착 경로를 안 탐)를 -처음에 잘못 섞어서 답했다가 사용자가 "state 바인딩은 결국 k,inner v를 -호출하니 i=number,v=observer로 다시 실행 안 된다"고 정정 — 후자는 -`bindLifetime(inst, observer)` 같은 별도 배관이 필요하다는 걸로 이어짐. -이게 `base/lifecycle-pattern.md`가 이미 원 사용자 메모(2026-08-04)로 -갖고 있던 "함수 안에서 만든 옵저버도 GC 대상 되어야 함" 절과 정확히 -같은 문제였음이 드러남 — 그 절이 "범용 유틸이 있어야 한다"까지만 말하고 -실제 인터페이스/이름이 없던 것. - -- **탑레벨 평범한 함수로 확정, 네임스페이스 뒤에 안 숨김** — `bindLifetime(inst,value)`/ - `canExecute(inst,value)`. `Dispatch.process`류는 "시스템 배관"이라 - 네임스페이스가 맞지만 이 둘은 `isState`/`isObserver`처럼 핸들러 작성자가 - 직접 부르는 1급 프리미티브 연산이라 `LifetimeHandle.bind(...)`식으로 - 감싸면 안 된다는 사용자 지적(정확함, 처음 제 제안이 틀렸었음). -- **`canExecute` 시그니처를 `(handle)` 단일 인자에서 `(inst, value)` - 2-인자로 재정정** — Observer 자신의 바인딩 생존(`Subscribed`)과 `inst` - 자체 생존(gcconn)이 독립된 두 조건이라 opaque `handle` 하나로 못 뭉침. - 구현은 `value`가 Observer/Effect면 자기 `Subscribed`부터 확인, 그 다음 - `inst`의 공유 gcconn `.Connected`를 봄. -- **`Relate` — `inst`를 weak 키로 하는 범용 릴레이션, 신규 프리미티브로 - 독립 승격**(`base/relate-plan.md`, 1프리미티브-1파일 컨벤션). `Relate()` - 비싱글톤 생성자 + `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`. 핵심 - 결정 세 개, 전부 사용자가 직접 제시: - 1. **자동으로 아무것도 홀드 안 함** — `inst`도 `value`도 Relate 자신은 - 안 붙잡음, weak/strong 여부는 호출부(엔진을 아는 quad-roblox)가 - 매번 명시. 자동으로 정하면 weak 키가 참조하는 값이 그 키로 되돌아 - 강참조하는 사이클이 너무 쉽게 생김. - 2. **`inst`(키) 축은 항상 weak로 고정, 자유도를 안 열어둠** — 강한 키가 - 필요한 유스케이스가 지금까지 하나도 없어서, 그 자유도 자체가 사고 - 가능성만 늘림. `Weak`/`Strong`은 오직 `value` 보관 방식. - 3. **실 구조는 `{ [inst(weak)]: { StrongMap: {[k]:v}?, WeakMap: {[k]:v(weak)}? }? }`, - 둘 다 lazy 생성**(첫 `Set` 호출 시에만 만듦) — Luau가 정적 분석으로 - 포인터 해싱을 캐싱해 반복 인덱싱은 이미 싸지지만 테이블 생성(array+hash - part 초기화) 자체는 비교적 비싸다는 게 이유. `WeakMap`의 메타테이블은 - 매번 새로 안 만들고 공유 객체 하나를 재사용. - - **비싱글톤인 이유**: 각 핸들러 모듈이 자기 톱레벨에 `local relate = - Relate()`를 하나씩 두면 key 네이밍이 모듈 간에 겹칠 걱정이 원천적으로 - 없음(`Ref`/`Store`류와 같은 "생성 가능한 값" 컨벤션). -- **`base.perInstanceState(inst)` 이름/placeholder는 완전히 폐기** — - `Relate`가 그 자리를 정식으로 대체. `bind-system-plan.md`(핸들러 내부 - 상태 저장 절)/`ui-shorthand-plan.md`/`architecture.md`(소스트리, - `Relate.luau`는 quad-base 전체가 순수 Lua라 quad-roblox 재구현 없음)/ - `question.md`(용어 정리 목록에서 `PerInstanceState` 항목 삭제, 이름 - 갈등 자체가 해소됨)/`ROADMAP.md`(M2/M8/병행가능 세 곳) 전부 동기화. - -**4. 아직 안 풀린 것 — `(i:number, v=Ref/Observer/PreRef)` children-array -leaf Handler가 quad-base/quad-roblox 중 어디 사는지.** 3번을 풀다가 -갈라져 나온 별개 질문(`Frame { ref }` 자체를 매칭하는 Handler, store-bind -내부 Observer와는 무관) — 제 제안(엔진 특정 API가 필요 없으니 quad-base, -`Dispatch/StoreBind.luau`와 같은 층위)은 사용자 확인을 못 받은 채 대화가 -3번으로 넘어감. `question.md` 2번에 미확인으로 남김, base에는 반영 안 함 -— 다음에 확인 필요. **[해소됨, 같은 날 두 번째 세션]** 아래 절 참고 — -제 원래 제안 그대로 quad-base로 확정. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M0/M2 스파이크 코드가 -검증해야 할 것 목록에 `Relate`의 lazy 서브테이블 생성/공유 메타테이블 -전략, `bindLifetime`/`canExecute`의 실제 gcconn 트릭이 새로 추가됨 — -`base/lifecycle-pattern.md`/`base/relate-plan.md`의 "실측 필요" 캐비엇 -참고. - -## 2026-08-08 두 번째 세션 — Dispatch는 프리미티브가 아니라 탑레벨 싱글톤 확정, -네이밍 케이싱 컨벤션 신설, Handler를 세 번째 카테고리로 명문화 - -같은 날 이어진 세션. 사용자가 위 4번 미결 항목("Ref/Observer/PreRef leaf -Handler가 어디 사는지")을 다시 짚으며 시작 — "Handler도 실제 런타임 값이 -생기는 요소인데 왜 프리미티브로 안 다루나", "Dispatch는 어떻게 되는 거냐, -State 핸들러 안에서 `getHandler`를 부르려면 Dispatch가 이미 존재해야 -하는데" 하는 질문으로 확장돼 Dispatch 자체의 정체성(싱글톤 top-level -함수 모음 vs 인스턴스화 가능한 프리미티브) 논의로 이어짐. 네 가지로 정리, -전부 `base/bind-system-plan.md`/`base/store-semantics.md`/ -`base/architecture.md`/`question.md`/`ROADMAP.md`에 반영 완료: - -**1. Dispatch는 프리미티브가 아니라 탑레벨 싱글톤 — 확정, 지금 형태 유지.** -`Dispatch.process`/`getHandler`/`addHandler`/`drive`를 `Source`/`Ref`처럼 -생성자 있는 프리미티브로 바꿀지 검토했으나 기각. 근거: (a) Tween/ -`NoneHandler`/`StoreBind`가 자기 `process` 안에서 다시 `Dispatch.process`를 -재귀 호출해야 해서, `canExecute`/`bindLifetime`처럼 require 한 번으로 바로 -닿는 안정된 전역이어야 함 — 프리미티브화하면 모든 Handler 호출 경로에 -Dispatch 핸들을 실어날라야 하는 스레딩 비용이 생기는데 지금은 그 비용이 -없음. (b) 사용자가 우려한 "Handler가 Dispatch 원하고 Dispatch가 Handler -원해서 순환참조" 문제는 착시로 확인됨 — "Handler"가 (i) `Handler.luau`의 -순수 타입 계약(leaf, Dispatch를 몰라도 됨)과 (ii) 그 계약을 구현하는 -concrete 값 모듈(`StoreBind.luau`류, 재귀호출 위해 Dispatch를 참조) -두 가지를 가리켜서 헷갈렸던 것 — 의존 방향은 `Handler.luau` ← -`Dispatch/init.luau` ← `StoreBind.luau`로 항상 한쪽으로만 흐름, 사이클 -없음. (c) 모듈 재생성(`New()`)과의 관계도 새 설계가 필요 없음 — 이미 -확정된 "팩토리가 `BaseModule`을 뮤테이션" 패턴을 그대로 따르면 -`_initializedBy` 마커에 대해 이미 나왔던 결론("`New()`가 생기면 각 -인스턴스가 별도 테이블이 되므로 자연히 스코핑됨")이 Dispatch의 handler -레지스트리에도 그대로 적용됨. v1처럼 `require`를 감싸는 `Init(QuadId?)` -방식은 채택 안 함(id 기반 조회 자체가 Ref로 대체되며 이미 기각된 패턴). -`base/bind-system-plan.md`의 "Dispatch는 프리미티브가 아니다" 절, -`base/architecture.md` 13번 항목에 반영. - -**2. quad-base 기본 핸들러도 전부 같은 `Dispatch.addHandler` 레지스트리를 -공유 — Ref/Observer/PreRef leaf Handler 위치 확정.** `NoneHandler`/ -`Dispatch/StoreBind.luau`뿐 아니라, children 배열 숫자 슬롯에 `Ref`/ -`Observer`/`PreRef`를 직접 놓는 leaf 값을 매칭하는 Handler도 같은 부류 — -`inst`를 `any`로 취급하고 엔진 특정 API가 필요 없으니 quad-base, -`Dispatch/Leaf.luau`로 확정(위 4번 미결 항목 해소). quad-roblox의 -Property/Event/Tween 핸들러도 **같은** 레지스트리에 등록되므로, base -기본 핸들러와 backend 핸들러가 별도 경로로 안 갈리고 하나의 우선순위 -스캔을 공유한다는 것도 명시적으로 확인됨. `architecture.md` 소스트리에 -`Dispatch/Leaf.luau` 반영, `question.md`/`ROADMAP.md` M2 동기화. - -**3. Handler는 "독립 프리미티브 vs 파생 데이터" 분류의 세 번째, 별개 -카테고리 — 명문화.** 2026-08-06 후속 세션이 확정한 분류(Source/Ref/Store/ -Modifier=독립 프리미티브, State/Observer=파생 데이터)에 Handler가 왜 -안 끼는지 사용자가 재확인 요청 — 이유: Handler는 그 자체로 구현체가 -없는 **순수 타입 계약**이라 quad 사용자가 다루는 리액티브 값이 아님, -계약을 만족하는 값(`PropertyHandler`류)은 항상 **구현하는 쪽**(base -자신의 기본 핸들러 또는 quad-roblox 백엔드)이 채워 넣는 것이지 `Type(args)` -자유 함수로 사용자가 만드는 게 아니고, State/Observer처럼 어떤 원천에 -종속된 파생물도 아님. `base/store-semantics.md`의 "일반 원칙" 절 뒤에 -"세 번째 카테고리 — Handler" 절로 반영. - -**4. 네이밍 케이싱 컨벤션 신설 — 지금까지 나온 모든 이름이 이미 따르고 -있던 규칙을 문서화만 함, 리네임 없음.** 사용자 관찰: "탑레벨 함수는 -변수처럼 소문자 시작, 프리미티브 타입의 메서드는 대문자 시작(파스칼 -케이싱)이 맞아 보인다"는 규칙 제안 — 검증 결과 기존 이름 전체(생성자 -`Source`/`Ref`/`Store`/`Modifier`/`Relate`/`Effect`, 콜론 메서드 -`:Get`/`:With`/`:Set`/`:Apply`/`:Subscribe`류는 전부 대문자, `canExecute`/ -`bindLifetime`/`isState`류/`Dispatch.process`류/`Brand.set`류는 전부 -소문자)가 이미 예외 없이 이 규칙을 따르고 있었음이 확인됨. 유일하게 -애매해 보였던 `Modifier.Override(mod1, mod2, ...)`(콜론 아니고 dot-access -인데 대문자)도 규칙 위반이 아니라 세 번째 하위 규칙으로 설명됨 — 콜론 -메서드는 아니지만 **`Modifier` 타입 자신의 네임스페이스에 달린 정적 -결합 함수**라 "그 프리미티브 타입 고유의 공개 어휘"라는 점에서 생성자/ -메서드와 같은 부류. 반대로 `Dispatch.process`/`Brand.set`이 소문자인 -이유는 `Dispatch`/`Brand`가 애초에 `Type(args)` 생성자가 없는 프리미티브가 -**아닌** 내부 엔진/레지스트리라서. 최종 판단 기준: "이 이름이 특정 -프리미티브 타입 하나의 전용 소유물인가?" — 그렇다면 대문자, 아니면(여러 -타입에 걸친 범용 유틸이거나 비-프리미티브 엔진 소속) 소문자. `base/ -architecture.md`의 "코드 스타일 — 네이밍 케이싱" 절 신설. - -**같은 세션 후속 — `module-lifecycle-plan.md`의 "열린 질문" 절이 stale로 -방치돼 있던 것을 사용자가 직접 발견.** 문서 상단 "상태" 줄은 이미 -"확정되어 승격됨"이라고 말하는데 그 아래 "열린 질문" 절은 2026-08-04 -당시 그대로 남아있었음 — 그중 "프로바이더 인터페이스 시그니처 미정"/ -"네이밍 미정(provider/processor/plug)" 두 항목이 사실 그 뒤 `Handler` -계약 확정으로 이미 풀려 있었는데 이 문서에 반영이 안 됐던 것. 원문은 -남기고 각 항목에 해소 표시+포인터 추가, 절 제목도 "열린 질문이었던 것 — -전부 해소됨"으로 정정. 새 결정 아니라 순수 동기화. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 -설계/문서 정리라 M0 착수 우선순위 자체는 그대로. 위 2026-08-08 첫 세션이 -남긴 "M0/M2 스파이크 검증 목록"에 새로 추가되는 항목 없음. - -## 2026-08-08 세 번째 세션 — Tag를 array-part 값 객체로 재설계, Dispatch -체인+`retractUnder`로 재귀 재-dispatch의 retract 전파 문제 해결 - -같은 날 이어진 세션. 사용자가 "Tag를 해시 파트 boolean 키 대신 array-part -값 객체로 바꾸는 게 낫지 않냐"는 질문으로 시작 — 상호배타 스타일 상태 -(`btn1`/`btn2`/`btn3`류, 20개까지도 가능)를 표현하려면 구 모델은 태그 -개수만큼 키를 갱신해야 해서 끔찍하다는 실사용 근거. 이 논의가 "retract가 -새 값의 타입에 따라 이전 핸들러를 정확히 찾아 부를 수 있는가"라는 훨씬 -근본적인 구멍(`pre-implementation-audit.md` 1-2번이 이미 지적해뒀던 것)을 -직접 건드리게 됐고, 몇 차례 시행착오 끝에 사용자가 제시한 "체인+ -`retractUnder`" 설계로 수렴. 세 갈래로 정리: - -**1. Tag 재설계 — array-part 값 객체, `Modifier`와 같은 immutable clone -체이닝.** `Tag(name1, name2, ...)`(가변인자 생성자, 빈 `Tag()`도 유효), -`:Added`/`:Removed`(뮤테이션처럼 안 보이게 `-ed` 어미 — 실제로는 항상 -clone 후 반환), `:Contains(name):boolean`, `:Apply(factory)`(Modifier와 -동일한 순수 체이닝 설탕), `Tag.Merged(tag1,tag2,...)`(집합 합집합, 무손실 -— Modifier의 `Override`는 필드 단위 덮어쓰기라 손실 있음, 그래서 이름도 -다름). `None` 센티널은 불필요로 확인 — 동적 토글은 `Source`/`State`가 -계산 결과로 `nil`을 리턴하면 되는 함수 인자 전달이라 테이블 리터럴의 -nil-hole 문제 자체가 없음(정적 리터럴에서 조건부로 Tag를 넣고 뺄 때는 -다른 array-part 값과 마찬가지로 기존 `None` 관용구가 그대로 유효, Tag -전용 규칙 아님). 구 모델(해시 파트 boolean, "핸들러 타입이 안 바뀌니 -retract 불필요"가 결론이었음)은 `archive/tag-hash-key-model-reversed.md`로 -역전 보존, `base/tag-plan.md` 전면 재작성. 값 타입+API(`Tag.luau`)는 -quad-base, `CollectionService` 글루(`Handlers/Tag.luau`)만 quad-roblox — -이미 확정된 "base는 인터페이스/값, backend는 process·retract 글루" -패턴(`LifetimeHandle`)을 값 타입 수준까지 그대로 확장한 것으로 확인, -새 아키텍처 개념 아님. - -**2. Tag 재설계가 "retract가 실제로 필요해지는" 첫 array-part store-bind -사례가 되며, 기존 "이전 핸들러 추적" 설계 공백이 정면으로 드러남.** -`pre-implementation-audit.md` 1-2번이 이미 "store-bind 재실행 모델에서 -realv 타입이 매 갱신마다 바뀔 수 있는데 '이전 핸들러'를 누가 추적하는지 -불명"이라고 짚어뒀던 것 — Tag가 `Tag(...)`↔`nil` 사이를 오가며 실제로 -핸들러 타입이 바뀌는 구체 사례가 되어 더 이상 미룰 수 없어짐. 시행착오 -과정: -- **1차 제안(제가 냄, 기각됨)**: Dispatch가 `(inst,k)`별로 "지금 누가 - 담당 중인가"를 슬롯 하나로 추적. **재귀/래핑 핸들러(StoreBind 등) - 에서 깨짐** — 사용자가 직접 "A→B 구조에서 A가 바뀌면 B의 retract가 - 실행되고, 재귀로 B로 다시 내려오면 retract가 없는 거 아니냐"고 반례를 - 제시 — A 자신의 생명주기(예: Observer 구독)와 A가 재귀로 위임한 B의 - 생명주기가 슬롯 하나를 두고 서로 덮어써서, A가 스스로 재-dispatch할 - 때 자길 엉뚱하게 retract하거나 반대로 안 해야 할 때 안 하는 오작동이 - 생김이 실제 트레이스로 확인됨. -- **2차 제안(제가 냄, 부분 기각)**: 각 래핑 핸들러가 자기 전용 `Relate`에 - 위임 대상을 비공개로 저장(A→B→C면 A.retract가 수동으로 B.retract를 - 부르고 B.retract가 수동으로 C.retract를 부르는 linked 구조). 동작은 - 하지만 사용자가 두 가지 지적: (a) 나중에 재바인드(`existing-instance- - bind-plan.md`) 지원을 생각하면 위임 정보가 핸들러별로 비공개 분산돼 - 있어 외부에서 못 들여다봄, (b) 각 핸들러 작성자가 "내 retract에서 - 위임 대상도 cascade해야 한다"는 걸 매번 기억해야 하는 규율 의존적 - 설계. -- **최종 채택(사용자 제안) — Dispatch가 `(inst,k)`별 핸들러 체인(순서 - 있는 배열)을 직접 소유, `Dispatch.retractUnder(inst,k,keep,v)`가 - 꼬리부터 `keep` 앞까지 훑으며 정리.** `Dispatch.process`가 매치될 - 때마다 체인에 push, 재귀/래핑 핸들러는 재-dispatch 전에 - `retractUnder(inst,k,self,newV)`를 먼저 불러 자기 밑을 정리 — 이 - 한 번의 루프가 다단 체인(A→B→C) 전체를 순서대로 정리해주므로 개별 - 핸들러의 `retract`는 더 이상 자기 위임 대상을 수동으로 안 쫓아가도 - 됨(2차 제안의 (b) 해소), 체인이 Dispatch에 중앙화돼 있어 미래 - 재바인드도 `retractUnder(inst,k,nil,newV);process(inst,k,newV)` - 두 줄로 자연스럽게 됨((a) 해소) — quad-debug의 "무엇이 무엇에 - 연결됐는가" 그래프도 이 구조를 그대로 읽으면 됨. 배열이 항상 꼬리에서만 - 추가/삭제되는 스택 모양이라 `None` 소진 이슈(구멍 있는 정수 키 순회 - 문제)도 애초에 안 생김. **`retract`는 여전히 `(inst,k,v)` 3-인자 - 유지** — 한 차례 제가 "v 제거"를 제안했다가 틀렸음(사용자가 Tag의 - 전체삭제 vs diff 분기를 근거로 정정) — 다만 최종 설계에서 diff는 - `process`(같은 핸들러 유지 시)의 몫이고 `retract`는 항상 "더 이상 - 매치 안 될 때만" 불리므로 Tag 한정으로는 `v`를 안 봐도 항상 전체 - 삭제가 맞다는 것도 확인. 순환은 기존 "일반적 무한루프 방어 안 함" - 원칙(2026-08-04) 그대로 UB. - -**3. 전부 `base/bind-system-plan.md`(신규 "Dispatch 체인" 절 + "확정된 -디스패치 모델"/"None 센티널"/"Store 바인드는 특수 경우인가" 절 갱신)/ -`base/tag-plan.md`(전면 재작성)/`archive/tag-hash-key-model-reversed.md` -(신규)/`base/architecture.md`(소스트리 `Tag.luau` 추가, 4번 항목 정정)/ -`ROADMAP.md`(M2/M4/M10)/`research/pre-implementation-audit.md`(1-2번 -해소 표시)에 반영 완료.** - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). M2/M4 스파이크 -검증 목록에 `chains`/`retractUnder`가 다단 체인에서 실제로 정확히 -동작하는지가 새로 추가됨(추론만으로 확정된 것, `pre-implementation-audit.md` -류 "실제 Luau로 부딪혀본 적 없는 것" 범주). `pre-implementation-audit.md` -1-1번(Tween이 유일한 store-bind 예시라 "일반 store-bind와 Tween이 같은 -핸들러인지"가 불명확한 문제)은 Tag가 두 번째 구체 사례가 되면서 정황상 -"별개 핸들러, 둘 다 `Dispatch/StoreBind.luau` 재사용"쪽에 힘이 실리지만 -**아직 명시적으로 확정된 건 아님** — M2/M4 착수 전 마저 확인할 것. - -## 2026-08-08 네 번째 세션 — `Modifier.Override` → `Overridden`으로 이름 확정 - -사용자가 IDE에서 `tag-plan.md`를 보다가 "Tag가 `Added`/`Removed`처럼 -`-ed` 어미를 의도적으로 쓰는데, Modifier의 `Override`도 그냥 -`Overrided`로 하면 어떤가"라고 질문 — `-ed`/분사 어미가 "즉시 커밋되는 -뮤테이션이 아니라 이미 계산되어 반환되는 새 값"을 신호한다는 기존 관례 -(`Add`/`Remove`가 `-ed` 없이 쓰이면 뮤테이션처럼 오독될 위험이 있어 -`Added`/`Removed`로 확정했던 것과 같은 문제가 `Override`에도 그대로 -있음)에 정확히 들어맞는 좋은 관찰이었음. 다만 `Overrided`는 오기 — -`override`는 불규칙동사라 과거분사가 `overrided`가 아니라 `overridden`. -`Add`/`Remove`/`Merge`가 전부 규칙동사라 우연히 단순 `-ed` 접미만으로 -맞았던 것뿐, `Override`엔 그 규칙이 그대로 안 통함. 사용자가 이 정정에 -동의하고 확정 요청 — `Modifier.Overridden(mod1, mod2, ...)`으로 이름 -자체를 확정(더 이상 가칭 아님, 용어 정리 라운드 대상에서도 제외). - -`base/modifier-plan.md`/`base/component-composition-plan.md`/ -`base/bind-system-plan.md`/`base/tag-plan.md`(비교 문구)/ -`base/architecture.md`/`ROADMAP.md`/`research/pre-implementation-audit.md`/ -`research/documentation-content-map.md`/`.claude/README.md`/ -`.claude/question.md` 전부에서 `Override` → `Overridden`으로 기계적 -치환 + 각 문서의 "가칭"/"이름만 잠정" 표시를 "이름 확정"으로 갱신 -(`question.md`의 3순위 용어 재검토 목록에선 완전히 제거, `Peek`/ -`isState`만 그 목록에 남음). CLAUDE.md 세션 히스토리(과거 `Override` -서술)와 `archive/`는 당시 기록이라 그대로 둠 — 역사적 서술과 현재 -유효한 이름을 헷갈리지 않도록 여기 새 절로만 반영. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 순수 네이밍 -확정이라 M0 착수 우선순위나 설계 자체엔 영향 없음. - -## 2026-08-08 다섯 번째 세션 — 용어 정리 라운드 정리: `Handler`/`None`· -`NoneHandler`/`Ref`/`PreRef`/`Peek`/`isState` 이름 확정, `DI`→`D`/ -`canExecute`→`isAlive`는 계속 미정으로 재확인 - -사용자가 `.claude/question.md`의 3순위(사소함) 용어 재검토 목록을 훑으며 -한 번에 여러 개를 정리 — 전부 `.claude/question.md` "1. 용어 정리" 절과 -`base/module-lifecycle-plan.md`에 반영 완료: - -- **`Ref`/`PreRef`/`Peek`/`isState` — 전부 현재 이름 그대로 확정(더 나은 - 대안 없음).** `Ref`는 "지연 없는 확정된 값 박스"라는 정의를 재확인 — - leaf 노드를 담는 용도로도, leaf 노드에 바인딩하는 용도로도 쓰인다는 게 - 넓어진 정의에도 여전히 맞다는 근거. -- **`None`/`NoneHandler` — 확정.** `Undefined`/`Null`/`Nothing`도 검토했으나 - `Null`은 보통 "포인터가 비어있음"(0)을 뜻해 "값이 없음"이라는 의도와 - 미묘하게 안 맞는다는 이유로 기각, `None`이 나음. -- **"프로바이더" → `Handler`로 확정, 기각 이유 보강.** 이미 - `module-lifecycle-plan.md`에 [해소됨]으로 반영은 돼 있었으나 - `question.md` 목록에 stale로 남아있던 걸 정리. `Processor`는 계약 - 메소드 자체가 `process`라 이름이 겹쳐 거슬림, `Provider`는 - `canProvide`처럼 "공급한다"는 늬앙스인데 실제로는 처리/반응하는 - 쪽이라 안 맞고 React `Context.Provider`류와도 헷갈릴 수 있음, `Plug`는 - "꽂힌다"는 어감은 맞지만 "처리한다"는 의미가 빠져있음 — `Handler`가 - 계약(`isHandlable`/`priority`/`process`/`retract`) 전체를 가장 정확히 - 담는다는 결론. -- **`DI` → `D`는 아직 미확정.** 사용자가 "Declarative만 남기고 D로 - 줄이자"는 안을 제안 — Instance 전용이 아니라 quad-* 전반의 declare - 요소로 확장 가능하고, 엔진 종속 없이 재사용 가능하며, `D.FrameModifier` - 류 타입 프리픽스가 짧아야 한다는 실용적 이유까지 근거로 나쁘지 않은 - 제안이나, 한 글자 식별자의 검색성/자기설명력 트레이드오프를 문서에서 - 어떻게 보완할지가 남아 다음에 마저 결정하기로 함. -- **`canExecute` → `isAlive`도 계속 미정, 방향만 정리.** `isAlive`가 의미는 - 더 정확하지만 top-level `isX` 타입 판별자 계열(`isState`/`isRef`/ - `isPreRef`/`isModifier`/`isObserver`)과 접두어가 겹쳐 "이것도 타입 - 체크인가" 오해를 유발할 위험이 지적됨 — `canExecute`는 타입이 아니라 - liveness를 묻는 질문이라 `is`보다 `can` 계열 접두를 유지하는 쪽으로 - 사용자가 기욺, 구체 대안(`canRun` 등)은 다음에. -- `Brand`는 이번에도 다시 짚었지만 여전히 미정으로 재확인만 함. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터). `DI`/`D`와 -`canExecute`/`isAlive` 두 개만 용어 정리 라운드에 계속 남음 — -`question.md` 1순위/3순위 목록 참고. - -## 2026-08-09 세션 — `canBound` 이름 확정, `:Compute`의 `previous` 방어/스코핑 -명확화, Modifier 핸들러 계층 값 UB→error 전환, Tween `initValue`/`useTween` -논의 신설 - -사용자가 `.claude/question.md`를 훑다 나온 여러 짧은 질문/제안을 한 번에 -처리. 전부 `base/`/`research/`에 반영 완료: - -1. **`Bound` → `canBound(handle): boolean` 탑레벨 함수로 확정** — 사용자 - 제안("canExecute 같은 게 있으니 canBound로 넣어도 되지 않나"), raw - 불리언 필드를 직접 노출하는 대신 `canExecute`와 같은 결의 predicate - 함수로 감쌈. 동작 자체(leaf 부착과 `:Subscribe()`는 상호 배타, 위반 - 시 즉시 에러)는 안 바뀜 — `base/bind-system-plan.md` "이중 바인딩 - 금지" 절, `base/effect-plan.md`, `.claude/question.md` 반영. -2. **`:Compute(fn)`의 `previous` 인자 — 오버엔지니어링 의심 기각, 현재 - 설계 유지.** `pre-implementation-audit.md` 3-1이 "클로저 업밸류로 - 이미 되는 걸 별도 API로 만든 것 아니냐"고 의심했던 데 대해 사용자가 - 직접 반박 — 클로저 업밸류 대안은 IIFE로 감싸는 준비 비용이 오히려 - `previous`라는 인자 하나보다 무겁고 번거로움. **부수적으로 스코핑도 - 명확화**: 처음엔 `self.Cache`처럼 `previous`를 `self`(입력) 쪽에 - 얹는 모양이 제안됐으나, `self`는 `:Compute`의 입력(receiver)이라 - 같은 `self`에서 여러 `:Compute`가 갈라지는 팬아웃(`w:Compute(g1)`, - `w:Compute(g2)`)이 있으면 `self.Cache` 슬롯이 충돌한다는 문제를 - 검토 중 발견 — `previous`는 그 대신 "이 `:Compute` 호출 하나가 만든 - 결과 State 노드" 자신에 귀속되는 것으로 정리(State가 호출마다 새 - 노드를 만든다는 기존 온톨로지의 당연한 귀결이라 새 결정은 아님). - `base/bind-system-plan.md`의 "previous" 절, `pre-implementation-audit.md` - 3-1 반영. -3. **Modifier 필드에 핸들러 계층 값(Ref/PreRef/Observer/Effect/Slot/ - Modifier)이 들어오면 UB 대신 즉시 `error`로 확정.** 기존 - "권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는 UB"였던 - 것을, 이런 값의 실사용 case가 없다는 게 확인된 이상 조용한 UB보다 - 그 자리에서 막는 쪽이 낫다는 사용자 판단으로 전환 — 이미 있는 - `Brand` 기반 predicate(`isRef`/`isPreRef`/`isObserver`/`isEffect`/ - `isSlot`/`isModifier`)를 제네릭 `__index` setter가 최종 저장 직전에 - 확인하기만 하면 되므로 구현 비용 거의 0. `isSlot`/`isEffect` - predicate가 `Brand` 절에 명시적으로 없던 갭도 같이 보강. - `pre-implementation-audit.md`가 지적했던 "`State`는 방어, - Ref/Slot은 무방비"라는 비일관성이 이걸로 절반 해소(메커니즘 차이는 - 남지만 "막을 가치가 있다"는 판단은 통일) — `base/modifier-plan.md` - "핸들러 계층을 모름" 절, `base/bind-system-plan.md`의 `Brand` 절, - `pre-implementation-audit.md` 문서모순 절 반영. -4. **UI shorthand(UICorner/UIPadding/UIScale)가 Modifier 체이닝에서도 - 되는지 — 이미 확정돼 있던 것 재확인, 새 결정 없음.** `mod:UICorner(8)`은 - 그냥 제네릭 `__index` setter가 `UICorner` 필드를 채우는 것뿐이고, - 그 필드가 Modifier flatten을 거쳐 최종 props 테이블에 얹히든 - `Frame { UICorner = 8 }`처럼 순수 인라인으로 들어가든 UICorner - Handler 입장에선 구분이 없음 — `base/ui-shorthand-plan.md`에 이미 - 명시돼 있던 내용이라 문서 변경 없음. -5. **Tween `initValue`/`useTween` — 새 열린 논의 신설, 확정 아님.** - 사용자가 두 실사용 시나리오(다이얼로그 진입 애니메이션, 트윈 우회)를 - 제기 — `initValue`(첫 마운트 시 시작값을 세팅 후 목표값으로 트윈)는 - 재검토 끝에 필요성이 낮은 쪽으로 기움(재process 시 "최초 1회" 판별 - 문제가 있어 보임), `useTween = state`(트윈을 끄고 즉시 - 스냅)은 필요성은 확인됐으나 정확한 모양/문서화 방식이 전혀 안 - 정해짐 — `research/tween-plan.md`에 신규 절로 반영, M11 착수 전 - 나중 세션에서 마저 정리하기로 함. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 -설계 확정/보강이라 M0 착수 우선순위 자체는 그대로. - -**같은 세션 후속 — `State`도 UB 대신 명시적 `error`로 확정, -"핸들러 계층 값 → error" 원칙을 State/Source 쪽까지 완전히 통일.** -사용자 질문: "Modifier 필드"뿐 아니라 "State/Source 자체의 값이 -Modifier인 경우"(`State`, `modifier-plan.md` 7번)도 같은 -방식으로 막아도 되는지 — 확정. `isModifier` predicate를 -`Source:Set()`/Store 생성 시 eager `Source(default)`/State의 -`:Compute` 결과 캐싱 지점에서 확인해 런타임 `error`, 타입 차단(Luau -가능 여부 미검증)은 필수 방어선이 아니라 되면 좋은 보너스로 격하. -**Slot은 대조적으로 계속 허용** — 사용자 확인("slot은 당연히 가능함, -retract도 되는 애고 런타임 값이라"): Slot/Tag/Attribute/Tween 등은 -정상적으로 process/retract 재귀 경로를 타는 진짜 dispatch 참가자라 -State/Source 값으로 담겨도 기존 재귀 재-dispatch가 그대로 처리해줌 — -Modifier만 예외인 건 Modifier가 애초에 dispatch 경로 자체를 안 타는 -유일한 존재라서. `base/modifier-plan.md` 7번, `base/store-semantics.md` -"따름정리" 절, `research/pre-implementation-audit.md` 2-2/문서모순 절 -(완전 해소로 갱신), `.claude/question.md`, `ROADMAP.md` M7 반영 완료 — -이걸로 `pre-implementation-audit.md`가 지적했던 "State는 -방어, Ref/Slot은 무방비"라는 비일관성이 완전히 해소됨. - -**핸드오버 준비 완료** — 이번 대화(2026-08-08~09에 걸친 세션)에서 나온 -결정은 전부 `base/`/`research/`/`question.md`/`ROADMAP.md`에 반영, -문서 간 참조도 동기화 완료. **다음 세션 예고(사용자 지정)**: Slot과 -"State에서 Slot을 뽑아내는" 키 기반 동적 컬렉션 재조정(가칭 `Keyed`는 -탈락, 최종 이름 미정) — `.claude/question.md` 0번 "키 기반 동적 -컬렉션 재조정"이 이미 최우선 항목으로 잡혀있으니 그걸 이어서 보면 됨. - -## 2026-08-09 두 번째 세션 — `.claude/` 코퍼스 전체 stale 마커 감사·수정, -무효화된 인라인 서사 archive 이전 - -새 설계 결정 없음, 순수 문서 정리 세션. 서브에이전트 4개를 병렬로 띄워 -`.claude/` 전체(30여 개 문서 + `ROADMAP.md`/`HUMAN_TODO.md`/`SAFETY.md`/ -`archive/`)를 클러스터별로 감사, "이미 해소됐는데 미해결로 표시된 것"과 -"문서 간 모순"을 찾아 전부 직접 수정(커밋 전 상태 기준). 이어서 사용자 -요청으로 두 번째 라운드 — 뒤집혔거나 무효화된 설계가 정정 표시만 붙은 채 -본문에 전체 서술로 남아있는 곳을 찾아 기존 `archive/*-reversed.md`/ -`*-rejected.md` 컨벤션대로 이전(본문엔 결론+포인터만 남김), 컨텍스트 -낭비 방지 목적. 이것도 서브에이전트 3개 병렬 감사로 후보를 찾은 뒤 직접 -판단해 적용. - -**1차 라운드 — stale 마커/모순 수정 (7개 파일)**: -- `bind-system-plan.md`: `Ref` 이름이 "용어 정리 재검토 대상"으로 남아있던 - 것 — 2026-08-08 다섯 번째 세션에서 이미 확정됐는데 반영 안 됨 → 해소 - 표시로 정정. `component-composition-plan.md` §4-2 인용 오류(그 절은 - 실제로 다른 내용을 다룸 — Ref 필드 충돌 없음의 근거를 잘못 인용)와 - 폐기된 `StoreSource` 프록시와 혼동될 수 있는 "Source 양방향 프록시" - 표현도 정정. -- `documentation-content-map.md`: 폐기된 `myStore.key = value` 대입 - 문법이 예시로 남아있던 것(같은 파일 바로 다음 줄은 `:Set()`으로 옳게 - 써서 자기모순) → 정정. -- `ROADMAP.md`: 세션 인용 오류 2건(`git blame`으로 실제 커밋 시점 확인해 - 정정 — M0의 Source/State 서브타입 항목은 "세 번째 세션", M2의 - `LifetimeHandle` 순서 역전 항목은 "네 번째 세션"이 맞음), `Bound`/ - `None` "가칭" 표기가 이미 이름 확정됐는데 안 지워진 것 2건 정정, M6에 - Slot CRUD 의미론 확정 체크박스 누락돼 있던 것 추가(`pre-implementation-audit.md` - 우선순위1이 이미 지적했던 갭). -- `question.md`: `Tag`/`Added`/`Removed`/`Merged`가 `tag-plan.md`에서 - "여기서 추적 중"이라 주장했지만 실제로 빠져있던 것 추가. -- `archive/context-rejected.md`: 다른 archive 문서와 달리 base/ 포인터가 - 없던 것 보강. -- `additional-primitives-plan.md`: State/Observer를 "독립 프리미티브"로 - 잘못 묶은 표현 정정(확정된 분류는 Source/Store/Ref/Modifier/Slot/DI= - 독립 프리미티브, State/Observer=파생 데이터, 2026-08-08 두 번째 세션 - "Handler는 세 번째 카테고리" 절 참고). - -**2차 라운드 — 무효화된 인라인 서사를 archive로 이전 (신규 archive 4개)**: -- `archive/quad2-try-research-findings-rejected.md` — `bind-system-plan.md`에 - 60줄 넘게 남아있던 quad2-try(폐기된 이전 재작성 시도) 리서치 전문(OOP - 상속/커스텀 파서/Slot 빈 스텁/`Pipe` copy-on-write 4가지 확인된 죽은 - 접근 + Unix 파이프 영감이라는 원래 동기 서사)을 통째로 이전 — "반복 - 조사 금지" 결론과 `state(state)` 조합 모델 포인터만 본문에 남김. -- `archive/observer-cleanup-contract-rejected.md` — `effect-plan.md`의 - "Observer 자체에 React `useEffect`식 cleanup 반환 계약을 추가하는 안" - 기각 서술(코드 예시 포함) 이전. -- `archive/keyed-collection-state-method-rejected.md` — `additional-primitives-plan.md`의 - "키 기반 동적 컬렉션 재조정을 `state:Keyed(...)` State 메소드로 두려던" - 초안 기각 서술 이전(이 프리미티브 자체는 여전히 열린 질문 — 폼 팩터 - 결정 부분만 이전됨). -- `archive/debug-channel-replicatedstorage-rejected.md` — `debug-tooling-plan.md`의 - "`ReplicatedStorage` 자동 생성" 초안 기각 서술 이전. - -각 archive 파일은 기존 컨벤션(`[기각됨]` 제목, "현재 유효한 설계" 포인터, -`quadnomicon` 소재 메모)을 그대로 따름, `README.md`의 archive 인덱스도 -4개 항목 추가로 동기화 완료. - -**의도적으로 손 안 댄 것들**: `bind-system-plan.md`의 PreRef pre-pass -위치 관련 기각 서술, `lifecycle-pattern.md`의 `canExecute` 시그니처 -재정정 단락, `modifier-plan.md` 9-1(b)의 "동질적/이질적" 초안 — 전부 -현재 설계를 정당화하는 근거로 너무 밀착돼 있어서, 분리하면 "왜 이렇게 -안 했는지"가 같이 잘려나가 다음 에이전트가 같은 대안을 또 검토할 -위험이 있다고 판단해 그대로 둠. `documentation-content-map.md`가 최근 -추가된 5개 base 문서(`relate`/`blocker`/`effect`/`tag`/`attribute`-plan.md)의 -초심자/api/심화 분류를 아직 안 갖고 있는 것도 실제 설계 판단(콘텐츠 -분류)이 필요해 손 안 댐 — 문서 자신도 이미 "지금 당장 안 급함"이라고 -인정하고 있음. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, 위 "다음 세션 예고" -Slot/키 기반 컬렉션 재조정도 그대로) — 이번 세션은 순수 문서 위생 -작업이라 설계 우선순위엔 영향 없음. - -## 2026-08-09 세 번째 세션 — Slot CRUD 완전 확정, 키 기반 동적 컬렉션 -재조정이 `Slot:List(...)` 메소드로 통합·승격 - -위에서 예고된 "다음 세션 주제"(Slot과 키 기반 동적 컬렉션 재조정)를 -실제로 다룬 세션. `pre-implementation-audit.md` 1-7/1-8과 -`research/additional-primitives-plan.md`의 마지막 열린 항목이 전부 -`base/slot-plan.md`에 흡수·확정됐음 — 상세는 그 문서 본문이 소스, -여기는 요지만: - -- **Slot CRUD 최종 확정**: `Add(element, index?)`/`Remove(element)`(제거+파괴)/ - `Extract(element)`(제거, 파괴 안 함)/`Clear()`(전체 `Remove`) — `get`/`set`은 - 드롭(YAGNI). 식별은 항상 element 레퍼런스 기준(인덱스 아님). 에러 조건 - 전부 즉시 `error()`(이미 다른 곳에 마운트된 element를 `Add`, 멤버 아닌 - element를 `Remove`/`Extract`) — fail-fast 톤 유지. 재진입성은 별도 가드 - 불필요(기존 "무한루프 방어 안 함" 원칙 재사용). `Slot()`은 인자 없는 - 빈 생성자로 확정. -- **`isMounted` 이중 추적 분리(1-8 해소)**: Slot 컨테이너 자신은 - `self._mounted`(트리거는 `Dispatch.process`가 이 Slot 객체에 실제로 - 호출된 시점 — 다른 모든 "마운트됨" 판정과 동일하게 dispatch-process - 기준), 개별 element는 전역 weak-set(라이브러리 전역 다중 마운트 금지 - 불변식이라 특정 Slot에 안 묶임). -- **`Extract` 후 portal 범위 — 임의의 다른 Slot으로 자유 이동 확정.** - 기존 "retract되는 slot은 폐기되지 옮겨지지 않는다"는 확정은 **프레임워크가 - store-bind 재실행으로 값을 통째로 갈아치우는 시나리오**에만 해당하고, - 사용자가 명시적으로 `Extract`→`Add` 두 번 호출해서 옮기는 것과는 다른 - 얘기라는 걸 명확히 구분(사용자 확인). -- **키 기반 동적 컬렉션 재조정 — `Slot:List(data, keyFn, renderFn) -> Slot`로 - 확정, 자유 함수/새 타입 둘 다 기각.** 처음엔 `List(...) -> Slot` 자유 - 함수를 검토했으나, "타입 이름=반환 타입"이라는 `Source(default)`류 - 팩토리 컨벤션이 깨진다는 문제를 사용자가 직접 지적 — Source⊇State식 - 구조적 서브타입도 검토했으나 List가 Slot 위에 새 공개 메소드를 안 - 얹으므로(그냥 "자동으로 채워지는 Slot") 별도 타입일 근거가 약해 기각. - 최종적으로 "원천에 종속된 파생 데이터는 메소드로만 얻어진다"(State/ - Observer와 같은 원칙, 여기 원천은 Slot 자신)로 수렴 — `Ref():Callback(fn)` - 체이닝과 같은 패턴. Fusion `ForPairs`/`ForKeys`/`ForValues` 3분할도 - 단일 `:List`로 통합 확정. -- **구현 메커니즘은 전부 기존 프리미티브 재사용, 새 개념 없음** — 사용자가 - "너무 마법같다"고 지적해 의사코드까지 구체화해서 검증: `data:Observer(fn)` - (2026-08-07 확정된 "등록 즉시 1회 실행"), `Source(item)`, 방금 확정한 - Slot CRUD의 비공개(가드 안 거치는) 버전 세 개의 조합일 뿐. `itemSources`/ - `elements`/`order`는 Slot 인스턴스의 평범한 클로저 업밸류(별도 전역 - 저장소 불필요). 리오더는 `Extract`+`Add(index)` 조합, 최소-이동 - 알고리즘 자체는 구현 시점 최적화로 미룸. -- **`renderFn(key, itemState)`의 `itemState`는 내부 `Source`를 그냥 - `State`로 다운캐스트해서 넘김 — 별도 `ReadOnlySource` 타입 안 만듦** - (사용자 확인: "그걸 위해 ReadOnlySource 같은 걸 만들 이유가 있냐 하면 - 아니다, 이미 그게 State다"). 타입 레벨 힌트만, 런타임 강제 없음(`Peek`/ - Modifier UB와 같은 "규율 위반은 방어 안 함" 기조) — 나중에 진짜 - 런타임 강제가 필요해지면 `src:Compute(function(v) return v end)`(항등 - 함수 Compute)로 `:Set` 없는 State를 만드는 가벼운 대안이 있다는 것만 - 메모. -- **백로그, 착수 안 함(연구만) — reconcile의 무조건 `:Set()` 재전파.** - `data`가 테이블 뮤테이션+`:Emit()`으로 오는 경로도 지원해야 해서 이전 - 값과 동등성 비교를 할 방법이 없고, 그래서 값이 실제로 안 바뀐 item도 - 매 재계산마다 재전파됨 — 사용자 판단: "이 재계산 비용은 우리가 핸들해야 - 할 부분은 아닌 것 같다", `Blocker`류 값-동등성 기반 전파 억제도 검토했으나 - "확정 안 하면 이전 값 자체가 없어서 비교가 안 된다"는 근본적 어려움이 - 있어 기술적으로 더 논의해볼 만한 주제로만 `research/ - additional-primitives-plan.md`에 백로깅. -- **`research/additional-primitives-plan.md` 사실상 전부 해소** — 마지막 - 열린 항목(키 기반 컬렉션)까지 없어져서, 이 문서엔 이제 새로 열린 설계 - 질문이 없음(배경 자료로만 유지). `question.md`/`ROADMAP.md`(M6 체크박스)/ - `README.md` 전부 동기화 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — Slot/키 기반 컬렉션 -재조정이 이번 세션에서 완결됐으므로 더 이상 "다음 세션 예고" 대상 아님. -남은 열린 것은 여전히 `question.md`의 `DI`→`D`/`canExecute`→`isAlive`/ -`Brand` 이름, `pre-implementation-audit.md` 1-3(우선순위 스캔 동률 처리), -"여러 Slot이 형제로 섞일 때 순서 보장"(Roblox 단일 백엔드론 급하지 않음) -정도. - -**같은 세션 후속 — quad-roblox 구현 관점에서 재검토, `Move`/`Swap` 공개 -CRUD로 추가(원시 최소화 원칙 뒤집음), `renderFn`에 `indexState` 추가.** -사용자가 "Slot 값 변경을 quad-roblox가 실제로 어떻게 따라가나"를 구체적으로 -캐물으며 세 가지가 드러남: -- **`renderFn(key, itemState) -> element`에 위치 정보가 빠져있었음** — - Roblox는 순서를 `LayoutOrder`로 표현하므로 `renderFn`이 그걸 반응형으로 - 바인딩하려면 위치도 State로 받아야 함. `itemState`와 독립된 - `indexState: State`를 추가(`renderFn(key, itemState, - indexState)`) — 값 변경/위치 변경은 서로 독립 신호라는 게 근거, Slot이 - `LayoutOrder`를 대신 관리해주는 마법은 안 둠. -- **`Extract`+`Add(index)`로 리오더를 구현하면 백엔드에서 진짜 Parent - 조작이 두 번(detach+reattach) 일어난다는 게 드러남** — Roblox - `AncestryChanged` 발화, 잠재적 깜빡임, 불필요한 재바인딩 비용까지 - 딸려올 수 있어 매 `:List` 재계산마다 흔한 케이스치고 과함. - **`Move`(O(n), 배열 splice 의미)/`Swap`(O(1), 순수 페어 교환)을 공개 - CRUD로 추가** — 둘 다 Parent를 안 건드림. `:List` 없이 수동으로 Slot을 - 구성하는 사용자에게 애초에 리오더 수단이 아예 없었다는 것도 같이 - 드러난 공백 — "원시 연산 최소화" 원칙보다 이 두 실사용 공백이 우선한다고 - 판단해 뒤집음(같은 세션 내 정정이라 별도 archive 없이 `slot-plan.md` - 본문에 "원시 최소화 원칙 정정" 절로 직접 반영). -- **base/roblox 패키지 경계에 mount/unmount 둘로는 부족, reposition - 훅이 세 번째로 필요함** — `Dispatch/Slot.luau`/`Handlers/Slot.luau`가 - 이제 "Parent 조작(mount/unmount)"뿐 아니라 "Parent 안 건드리는 재배치 - (reposition, `Move`/`Swap`)"까지 계약해야 함. quad-roblox가 이걸 - `SetSiblingIndex`로 구현할지, `LayoutOrder` 기반 정렬이라 사실상 no-op - 으로 둘지는 구현 선택으로 열어둠. -- **item 값 전파(무조건, 백로그)와 index 전파(실제 변경시만)가 비대칭인 - 이유도 명확해짐** — item 값은 외부 뮤테이션+`Emit()` 경로 때문에 "이전 - 값"을 비교할 방법이 없지만, `:List`가 전적으로 소유하는 `keyIndex`는 - "실제로 위치가 바뀌었는지"를 정확히 알 수 있어 index 쪽엔 같은 문제가 - 없음 — 그래서 index 전파는 처음부터 조건부로 구현. - -전부 `base/slot-plan.md`(CRUD 표, "원시 최소화 원칙 정정" 신규 절, `:List` -구현 스케치·설명 갱신)/`ROADMAP.md`(M6)/`README.md` 반영 완료. `question.md`엔 -새로 열린 항목 없음 — 이번 후속도 순수 확정/구현 세부 명확화. - -**같은 세션 두 번째 후속 — `Swap`을 element 레퍼런스가 아니라 인덱스 -기준으로 정정, "공개 CRUD는 가드+`raw*` 위임" 구조 명문화.** 사용자가 -`Swap(elementA, elementB)`를 바로 잡음 — element 레퍼런스로 받으면 Slot이 -element→index 역방향 맵을 안 갖고 있는 이상 두 element의 현재 위치를 각각 -찾는 데 O(n)씩(총 2n) 들어서, `Swap`이 약속한 O(1)이 그 자리에서 깨짐. -`Move`는 시프트 자체가 O(n)이라 조회 비용이 묻히지만 `Swap`은 조회 비용이 -곧 전체 비용이라 이 차이가 그대로 드러남 — `Slot:Swap(indexA, indexB)`로 -정정(호출부가 이미 "몇 번째와 몇 번째를 바꿀지"를 아는 상황, 예: 드래그 -리오더 UI, 이라는 것도 자연스러움의 근거). 이어서 사용자가 "`Slot:Move` -구현은 결국 락(`_listed`) 확인만 하고 실제 로직은 `rawMove`류에 다 있는 -구조 아니냐"고 확인 요청 — 맞다고 답하며 이걸 여섯 CRUD 전체에 적용되는 -일반 구조로 명문화: `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` 전부 -"`self._listed` 확인 + `raw*` 위임"뿐인 얇은 wrapper, 실제 로직은 전부 -`raw*` 함수 세트 하나에 있고 `:List`의 reconcile도 그 세트를 가드 없이 -직접 호출. 전부 `base/slot-plan.md`/`ROADMAP.md` 반영 완료. - -**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). - -**같은 세션 세 번째 후속 — Slot 요소 타입 제약 신설: `nil` 금지/`None` -허용/핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier) 금지, `Slot()` -제네릭화.** 사용자가 "Slot 안에 뭐가 들어갈 수 있는지 정해진 바 없다"고 -지적하며 시작 — 처음엔 제가 "Ref/Observer/PreRef도 Slot 요소로 허용, -`D.InstSlot = Slot<>`류 백엔드 별칭으로 좁히자"고 제안했으나, -사용자가 바로 반박: Slot이 동적으로 다뤄지는데 그 안에 Ref/Observer가 -들어가면 quad-roblox가 그걸 처리할 방법이 없고(특수 대응을 새로 만들어야 -해서 오버엔지니어링),애초에 왜 필요한지도 불명확하다는 지적 — 검증해보니 -정확히 맞았음: -- `Dispatch/Leaf.luau`가 처리하는 "children 배열에 Ref/Observer/PreRef가 - 직접 놓이는" 케이스는 **그 컴포넌트가 지금 만들고 있는 Instance 자기 - 자신을 가리키는 self-ref 캡처**라(`Frame { PreRef():Callback(fn) }`가 - 그 Frame 자신을 잡음), `inst`가 "지금 생성 중인 바로 그 하나의 Instance"로 - 고정돼 있어야 의미가 성립함. Slot은 특정 컴포넌트 호출 하나에 안 묶이고 - 이미 존재하는 부모에 나중에 독립적으로 붙는 동적 리스트라 이 전제 - 자체가 없음 — Slot 안의 Ref가 "무엇"을 가리켜야 하는지 정의가 안 됨. -- 대체 경로가 이미 있어 능력 손실도 없음 — 특정 child에 ref가 필요하면 - 그 child를 만드는 컴포넌트 호출 자체에 Ref를 넘기면 됨 - (`slot:Add(Frame { Ref = myRef })`). -- 사용자가 직접 대비시킨 반례도 정확함: `State`(Slot 자체가 State의 - 값)은 retract 시 통째로 버려지고 다시 채워지는 굵은 단위 교체라 이미 - 확정된 모델(폐기, 재구성)과 맞지만, Slot **요소 하나하나**로 - Ref/Observer가 들어가는 건 그런 굵은 단위 교체가 아니라 세밀한 CRUD - 대상이라 성격이 다름. -- **결론**: `Modifier` 필드가 핸들러 계층 값을 담으면 즉시 `error`로 - 확정했던 것과 같은 판별 메커니즘(`isRef`/`isPreRef`/`isObserver`/ - `isEffect`/`isModifier` Brand predicate)을 Slot에도 재사용 — 새 - 메커니즘 없이 그대로 막음. 덕분에 `Slot`의 `T`도 "실제로 마운트 - 가능한 최종 값의 타입"으로 단순해짐 — quad-roblox엔 사실상 `T = - Instance` 하나뿐이라 `D.InstSlot = Slot<>`가 사실상 "그" - Slot 타입. `nil`은 기존 배열 파트 `None` 원칙을 그대로 적용해 금지, - `None`은 `:List`의 `renderFn`이 "이 item은 이번엔 스킵"을 표현하는 - 용도로 허용 — `renderFn`의 반환 타입도 `T | None`으로 갱신. -- `Slot()`가 무인자 생성자라 `T` 추론이 안 되므로 tbox 명시적 제네릭 - 적용(`Slot<>()`)이 필요하다는 것도 같이 반영 — 정확한 문법은 - "자식으로 넘기는 클래스 스토어" 절의 기존 tbox 참고 미결과 같은 갈래로 - 묶어 열어둠. - -전부 `base/slot-plan.md`(신규 "요소 타입 제약" 절, CRUD 에러 조건, -`renderFn` 반환 타입) 반영 완료. - -**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). - -**같은 세션 네 번째 후속 — `Slot:List`의 `renderFn`을 "1회 호출"에서 -"매 사이클 호출 + `before` 재사용"으로 재설계, filter/toggle 문제 해결.** -사용자가 두 가지를 연달아 제기: (1) `renderFn`이 `None`을 반환해 "지연 -렌더"를 표현하는 아이디어는 좋지만, State 변경으로 이미 렌더된 필드를 -나중에 다시 지워야 하는 경우(filter)는 기존 "1회만 호출" 모델로 안 풀림. -(2) filter/sort를 Slot에서 어떻게 구현할지가 문제 — 흔한 회피책인 -"`Visible`만 토글"은 필터링된 item도 여전히 완전히 살아있는 Instance로 -남겨서(애니메이션/이벤트 연결 계속 돎) 200개+ 리스트에서 lazy하지 않다는 -실질적 비용이 됨. - -**해법 — 사용자가 직접 제시**: `renderFn(itemState, before: inst?): inst?` -모양으로 바꿔 **매 reconcile 사이클마다 호출**하되, 이전에 마운트된 -element(`before`, 없으면 `nil`)를 받아서 `if before then return before -end`(바꿀 거 없으면 그대로 반환, 값 갱신은 이미 물려있는 반응형 바인딩이 -자동으로 함)로 저비용 재사용 경로를 만듦 — filter 탈락 시엔 `nil` 반환으로 -**진짜 파괴**(Visible 토글 아님). 편의상 `renderFn`이 raw `nil`을 -던지는 게(Lua에서 자연스러운 관용구) `None`보다 편하다는 것도 사용자가 -지적 — 검토 결과 `renderFn`의 반환값은 raw Slot 요소로 직접 들어가는 -게 아니라 `:List`의 reconcile이 해석만 하는 것이라, `nil`을 받아도 위 -"요소 타입 제약"(raw Slot 요소는 `nil` 금지)과 전혀 안 부딪힘 — `nil`/ -`None` 둘 다 "스킵" 신호로 동일하게 받아들이기로 정리. - -**부수적으로 드러난 것 — "이전 상태를 다음 렌더에 어떻게 넘기냐" 문제는 -이미 해소돼 있었음.** 사용자가 "item이 보통 plain table이라 매 렌더마다 -Source/Store를 새로 안 만들려면 이전 상태를 어딘가 저장해야 하는데 그게 -어렵다"고 우려했으나, 확인해보니 `itemSources[key]`/`indexSources[key]`가 -`renderFn` 호출 여부와 무관하게 **처음부터 `:List` 자신이 계속 소유**하고 -있어서(원래 설계 그대로) — `renderFn`이 매 사이클 불려도 이 부분은 전혀 -안 바뀜, item이 filter 탈락 후 재등장해 Instance가 파괴됐다 새로 만들어져도 -반응형 Source는 안 끊기고 그대로 이어짐. 이 부분은 재설계가 아니라 기존 -설계가 이미 답이었다는 걸 확인한 것. - -**sort는 이번 재설계와 무관** — 호출부가 `data` 순서를 바꾸면 기존 -`keyIndex`/`Move` 메커니즘이 이미 처리, 새로 손댈 것 없음(사용자가 filter와 -같이 물었던 것 중 이건 원래도 문제 없었음). - -전부 `base/slot-plan.md`(요소 타입 제약 절 "None 허용" → "nil/None 둘 다 -금지"로 정정, `:List`의 `renderFn` 시그니처·구현 스케치·"왜 매 사이클 -호출로 바뀌었는가" 신규 절)/`ROADMAP.md`(M6)/`README.md` 반영 완료. - -**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). - -**같은 세션 다섯 번째 후속 — `renderFn` → `updateFn` 개명, `:List`가 -`Source` 생성을 그만두고 `userdata`로 그 권한을 통째로 넘김.** 사용자가 -"`renderFn`이 아니라 `updateFn`이 맞고, `itemState`도 `:List`가 강제로 -만들지 말고 원문 item + `userdata: UD?` + `prev: T?`를 주는 게 낫다"고 -제안 — 검토 후 채택, 근거: -- **`itemState`/`indexState`를 `:List`가 강제로 만드는 건 불필요한 강요였음** - — 반응형이 필요 없는 단순한 행까지 전부 `Source` 생성 비용을 지게 - 했음. `userdata`로 권한을 넘기면 필요한 item만 자기 `Source`를 만들어 - `userdata`에 담고, 나머지는 매번 raw `item`에서 다시 계산해도 됨 — - `:List`가 미리 정할 이유가 없는 선택. -- **"이전 상태를 다음 호출에 넘기는" 문제, 원래 걱정했던 것과 달리 - `userdata`라는 명시적 채널로 완전히 해소됨** — item이 plain table이라 - 매번 `Source`를 새로 안 만들려면 어딘가 저장해야 한다는 우려가 있었는데, - `userdata`가 정확히 그 저장소. -- **`prev`(구 `before`)와 `userdata`가 원래 비일관적이었음** — 사용자가 - 직접 지적: 하나(`prev`)는 `:List`가 자동 관리하는데 다른 - 하나(`userdata`)만 수동 반환을 요구했음. 해법은 **둘 사이 커플링을 - 완전히 제거** — `result`가 `nil`이라고 `:List`가 `userdata`를 자동으로 - 안 지움, 그대로 기록만 함. 흔한 경우(둘 다 리셋)는 `return nil` 하나로 - Lua가 나머지 반환 슬롯을 알아서 `nil`로 채워주고, "파괴하되 캐시는 - 남기고 싶다"는 정당한 패턴은 `return nil, ud`로 명시적으로 표현 - 가능해짐 — 이전 설계(result nil이면 userdata 자동 삭제)로는 이 패턴이 - 원천 봉쇄돼 있었음. -- **제가 놓칠 뻔한 버그를 사용자와의 논의 과정에서 직접 잡음**: `userdata`가 - 이제 `mounted`(실제 element)보다 오래 살 수 있게 되므로, 정리 루프가 - `pairs(mounted)`만 순회하면 "필터 탈락 상태(mounted=nil)로 `userdata`만 - 살아있던 key가 `data`에서 완전히 사라지는" 케이스를 못 잡고 새서 - — 직전 사이클의 전체 key 집합(`keyIndex`, 매 사이클 모든 key에 대해 - 채워짐)을 순회하도록 정정. -- **부수 효과 — "item 값 무조건 재전파" 백로그가 사라짐**: `:List`가 - 더 이상 `Source`를 안 만드므로 그 문제 자체가 `:List` 소관이 아니게 - 됨, `updateFn` 작성자의 선택으로 넘어감. -- `userdata = userdata or {}`류 lazy-init 관용구가 `UD`가 자유 제네릭인 - 채로 Luau 타입 시스템에서 잘 좁혀지는지는 실측 필요 항목으로 명시적으로 - 남김(사용자가 직접 이 불확실성을 짚음) — M0/M6 착수 시 확인. - -전부 `base/slot-plan.md`(`:List` 절 전면 재작성 — `updateFn` 시그니처/구현/ -"왜 `Source`를 `:List`가 안 만드는가" 신규 절)/`ROADMAP.md`(M6)/ -`README.md` 반영 완료. - -**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). - -**같은 세션 여섯 번째 후속 — `keyFn` 선택 인자화(파라미터 순서 정정), -`userdata` cleanup 훅 검토 후 기각·GC-native 제약 명문화, 문서화 순서 -질문은 백로그로 이관.** 사용자가 세 가지를 짧게 제기: - -1. **`Slot:List(data, keyFn, updateFn)` → `Slot:List(data, updateFn, - keyFn?)`로 파라미터 순서 정정, `keyFn` 선택 인자화.** 실사용 대부분 - (사용자 추정 80%)이 item identity 추적 없이 순번을 key로 써도 충분한 - 단순 목록이라 매번 `keyFn`을 명시하게 하는 게 불필요한 보일러플레이트 — - 생략 시 `function(item, index) return index end` 기본값. tradeoff(중간 - 삽입/삭제 시 그 뒤 항목들이 "다른 item인데 같은 key"로 오인돼 캐스케이드 - 갱신 — identity 보존 없음, 파괴/재생성 자체는 없음)는 React `key` 생략 - 시 index 기본값 등 업계 흔한 관행과 같은 결이라 새로 설명할 개념 아님. -2. **`updateFn(item?, ...)`로 바꿔 최종 제거 시 "정리용 1회 추가 호출"을 - 주는 안 — 검토 후 기각, 사용자가 직접 반례를 찾음.** 이 훅은 `data`에서 - key가 빠져 `reconcile`이 다시 도는 정상 경로에서만 발화하는데, **Slot을 - 담은 부모 Instance 자체가 `Destroy`되는(가장 흔한) 경로는 - `reconcile`이 다시 안 돌아서 이 훅이 전혀 안 불림** — 절반만 동작하는 - 정리 메커니즘은 없는 것보다 위험(사용자가 "정리가 보장된다"고 오해하고 - `Subscribe`류를 `userdata`에 넣었다가 Destroy 경로에서 조용히 샘). - `retract`가 Destroy 시 절대 안 불린다는 기존 원칙(`lifecycle-pattern.md` - "quad는 라이프사이클 중간에 있지 않다")과 정확히 같은 이유로 기각. - **대신 `userdata`엔 GC-native 값만 담고, `:Subscribe()`한 Observer류처럼 - 명시적 cleanup이 필요한 값을 담는 건 UB로 명문화** — quad 전역 - GC-native 원칙을 `:List`라는 구체 지점에 그대로 적용한 것뿐, 새 원칙 - 아님. -3. **문서화 순서(getting-started에서 단순 버전만 가르치고 나중에 - `prev`/`userdata` 최적화를 알려줄지, 아니면 Slot이 학습 순서상 후반부라 - 처음부터 완전한 형태로 가르칠지)는 결정 안 함** — `research/ - documentation-content-map.md`의 modifier/slot 절에 백로그로 추가, - 제 의견(후자 쪽으로 기욺)만 메모, 실제 콘텐츠 작성 시점 결정 사항이라 - 지금 확정 안 함. - -전부 `base/slot-plan.md`(`:List` 시그니처/코드 재정렬, `keyFn` 기본값 -설명, "`userdata`의 생명주기 제약" 신규 절)/`ROADMAP.md`(M6)/`README.md`/ -`research/documentation-content-map.md` 반영 완료. - -**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터). - -## 2026-08-09 여섯 번째 세션 — 여러 Slot이 형제로 섞일 때 순서 보장 완전 -해소(Length/Offset), `unbindLifetime` 신설 - -**출발점**: 사용자가 미래의 `quad-web`을 가정하며 `{ Slot, Element, Slot }`처럼 -Slot이 여럿 형제로 섞일 때 최종 순서를 어떻게 보장하는지 물음 — -2026-08-04부터 "Roblox 단일 백엔드로는 급하지 않음"으로 후순위 열려있던 -질문(`slot-plan.md` "여러 Slot이 섞일 때 순서 보장" 절)을 실제로 라이브 -설계해서 완전히 풀어낸 긴 단일 스레드. 시행착오를 거쳐 최종 수렴한 결론만 -정리(중간 대안들 — "구간 예약"/`:With`+`:Compute` 체인 — 은 채택 안 됨, -사용자가 제시한 "정확한 누적합 + 플랫 재계산 루프"가 최종안): - -- **핵심 전환**: "각 원소가 절대 위치를 계산해서 전파"가 아니라 "각 - 구조적 위치가 자기 앞 형제들의 개수 누적합(`offset`)만 알면 됨" — - Roblox `LayoutOrder`가 이미 `Instance.Parent` 물리 순서와 분리된 - 정수 프로퍼티라는 사실이 이 전환을 공짜로 성립시킴. -- **`Dispatch.setLength(inst,i,len:number|State)`/ - `Dispatch.setOffsetSource(inst,i,offset:Source|None)`** — - 둘 다 Handler→Dispatch 등록(push) 방향, array part의 **모든** number - 인덱스에 대해 반드시 호출(생략 UB — Handler 구현체 작성자만의 계약, - 일반 사용자 영향 없음). `recompute`는 매번 `1..N` 전체를 도는 단순 - 루프(N은 저작 시점에 고정된 배열 리터럴 길이라 무시 가능)로, 각 - `offset:Set()` 호출 앞에서만 `Get() ~= sum` 가드를 걸어 실제로 안 - 바뀐 위치의 캐스케이드(다운스트림 `LayoutOrder` 재적용)를 막음 — - 전체 순회 비용과 `Set` 캐스케이드 비용을 분리해서 후자만 최적화. -- **각 원소의 `LayoutOrder`는 `localIndex+offset`의 State를 기존 - store-bind 프로퍼티 바인딩에 그냥 얹는 것** — 이게 이 설계의 가장 - 큰 단순화 지점: "offset 변경 시 이미 마운트된 원소를 다시 써야 한다"는 - 요구가 새 push/observer 메커니즘 없이 **이미 있는** store-bind - 재실행 모델(`state:Observer(fn):Subscribe()`) 재사용만으로 공짜로 - 풀림. -- **`setLength`의 내부 Observer는 leaf-lifetime 경로(`bindLifetime`)를 - 씀, `:Subscribe()` 아님** — 이 Observer는 특정 leaf가 아니라 `inst` - 자신에 종속된 내부 배관이라, `inst` Destroy 시 자동으로 안 죽는 - `:Subscribe()` 경로는 안 맞음. `State` 교체처럼 `inst` 전체가 - 죽기 전에 특정 위치 하나만 조기 재등록해야 하는 경우를 위해 - **`unbindLifetime(inst,value)`을 `bindLifetime`/`canExecute`의 - 세 번째 짝으로 신설** — `Dispatch.setLength`가 gchold 내부 저장 - 구조(배열/키드 테이블)를 몰라도 이전 등록을 블랙박스로 해제할 수 - 있게 캡슐화. quad-roblox 구현 스케치도 gchold를 배열 대신 `value`를 - 키로 쓰는 테이블로 바꿔 `unbindLifetime`을 O(1)로(`gchold[value] = - nil`) — base 결정은 아니고 참고용 스케치. -- **동기 순서 요구사항**: Slot의 `rawAdd`는 `Length:Set(newCount)` - (다운스트림 offset/LayoutOrder 캐스케이드가 여기서 동기적으로 끝남) - 다음에 `element.Parent = target`을 호출 — Source:Set()이 옵저버 - 체인을 동기적으로 끝까지 도는 기존 모델 덕에 별도 배리어 없이 순서만 - 지키면 자동 성립. 안 지키면 Roblox의 실시간 `UIListLayout` reflow가 - 한 프레임 잘못된 순서를 노출할 위험. -- **`Slot.Length: State`가 CRUD/`:List` 여부와 무관하게 항상 - 노출되는 프리미티브 필드로 확정** — 사용자가 직접 "n개 검색됨" UI에도 - 쓸 수 있다고 지적, `setLength`가 내부적으로 읽는 값과 완전히 동일(두 - 용도를 겸함, 별도 State 아님). `:List`의 filter=진짜 Remove 확정 - 덕에 "Visible 토글은 안 잡힘"이 자연히 성립(새 캐비엇 아님). -- **웹 백엔드(quad-web) 일반화 — base 로직 100% 재사용, backend - Handler의 "offset 변경 시 할 일"만 달라짐**: DOM `insertBefore`는 - 물리적 삽입 시 뒤 형제를 자동으로 밀어주므로, offset이 바뀌어도 - 이미 마운트된 노드를 실제로 옮길 필요가 없음 — quad-web Handler는 - offset 변경 관측 시 no-op, 숫자는 그 위치가 **다음** insert/remove - 때 쓸 물리 인덱스로만 부기됨. 처음 검토했던 "구간 예약"(고정 gap)이나 - "앵커 기반 상대 삽입" 안보다 이 방식이 dense global rank라 두 종류 - 백엔드(순서-분리 프로퍼티형/물리-순서형) 모두에 더 직접적으로 맞음. -- **백로그로만 남김**: `Slot():Single(state, updateFn?)` — `:List`의 - key-map 없이 "0 또는 1"만 다루는 가벼운 편의 메소드, 상세 설계 미착수. - -**같은 세션 후속 — `bindLifetime`/`unbindLifetime`이 실제로 뭘 하는지, -`canBound`(이중 바인딩 금지)와의 관계를 여러 차례 시행착오 끝에 정확히 -확정.** `Dispatch.setLength`가 이전 Observer 등록을 정리할 때 뭘 불러야 -하는지를 두고 제가 세 번 틀렸다가 사용자가 매번 정정 — 경위와 최종 -결론을 구분해서 기록: - -1. **1차 시도(틀림)**: `unbindLifetime`이 `canExecute`를 즉시 `false`로 - 만들어준다고 서술 — 틀림. `gchold`(순수 GC 방지용 강참조 테이블)는 - `canExecute`가 보는 값(Observer/Effect의 `.Subscribed`, 또는 `inst`의 - 공유 `gcconn.Connected`) 어디에도 안 들어감, 완전히 무관한 테이블. -2. **2차 시도(틀림)**: 그래서 "`unbindLifetime`은 필요 없고 `:Unsubscribe()` - 만 쓰면 된다"로 후퇴 — 이것도 틀림. 사용자 정정: `:Subscribe()`/ - `:Unsubscribe()`는 **`inst`와 아예 무관한 전역/독립** Observer(모듈 - 최상위 디버그 print 등, leaf도 없고 특정 Instance에도 안 묶인 경우)를 - GC로부터 지키기 위한 **전역** 강참조 테이블(`SubscribedObservers[observer] - = true/nil`)일 뿐 — `Dispatch.setLength`의 Observer처럼 처음부터 - `inst` 하나에 종속된 내부 배관에는 원래부터 안 맞는 도구. "`inst` - 연관은 전부 `bindLifetime`/`unbindLifetime`으로"가 맞는 원칙. -3. **최종 확정**: 진짜 독립된 라이프사이클 경로는 **`:Subscribe()`(전역) - 와 `bindLifetime`(inst-scoped) 둘뿐** — "children 배열 leaf 부착"은 - 세 번째 경로가 아니라 **`bindLifetime` 호출 그 자체**(`Dispatch/ - Leaf.luau`가 Observer/Effect leaf를 매치하면 그 자리에서 - `bindLifetime(inst, v)`를 호출), 이걸 제가 처음에 "leaf 부착/ - `:Subscribe()`/`bindLifetime` 셋 다 상호 배타"로 잘못 일반화했다가 - 사용자가 "leaf 부착 자체가 bindLifetime을 호출하는 거라 동일 동작, - 상호배타는 아니다"로 정정. `canBound`의 내부 플래그도 새 필드가 - 아니라 **`canExecute`가 이미 보는 `.Subscribed` 그 자체** — - `bindLifetime`/`unbindLifetime`도(Observer/Effect 값에 한해) 이 - 필드를 세팅/해제해야 `bindLifetime`으로 등록된 Observer가 - `canExecute`에서 정상적으로 "살아있음"으로 인식됨. Effect는 내부적으로 - Observer를 조합하므로 이 확장을 몰라도 자동으로 커버(사용자 확인). -4. **부수 정리**: 이미 확정돼 있던 StoreBind의 자기 재실행 Observer - 예제(`observer:Subscribe()`)도 같은 이유로 틀렸던 것이었음 확인 — - `bindLifetime`/`unbindLifetime`으로 교체. "`:Unsubscribe()`는 자동 - (리프) 케이스에도 동일하게 씀"이라던 기존 서술도 같은 이유로 정정 - (리프/`bindLifetime` 경로의 조기 해제는 `unbindLifetime` 전용, - `:Unsubscribe()`는 `inst`를 몰라 대신 처리 못 함). - -전부 `base/bind-system-plan.md`(신규 "Length/Offset" 절, "이중 바인딩 -금지" 절 정정 — 2-way로 재확정, StoreBind 예제 교체)/`base/slot-plan.md` -(열린 질문 해소, `Slot.Length` 절, `:Single` 백로그 절)/`base/ -lifecycle-pattern.md`(`unbindLifetime` 추가 + `canBound`/`.Subscribed` -연동 반영)/`ROADMAP.md`(M2/M3/M6)/`.claude/question.md` 반영 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 -설계 확정이라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-09 일곱 번째 세션 — `Slot:List`의 `data:Observer(fn)` 구독도 -마운트 시점 lazy `bindLifetime`으로 확정 (Destroy 후 재실행 gap 해소) - -사용자가 "Slot이 마운트된 대상이 Destroy로 죽으면 `updateFn` 재실행이 -`canExecute`로 막히고 있는 게 맞냐"고 질문하며 시작 — 확인 결과 **두 메커니즘이 -다른 상태였음**: `Dispatch.setLength`(Length/Offset, 여섯 번째 세션 확정)는 -이미 정확히 그렇게 돼 있었지만(Slot 마운트 시점에 `bindLifetime(inst,observer)`), -`Slot:List`의 `data:Observer(fn)` 구독은 `:List()` 호출 그 자리에서 즉시 -만들어져(`inst`를 모르는 시점) `bindLifetime`이 전혀 안 걸려있던 실제 gap — -사용자가 정확히 캐치함. 사용자가 이어서 "실제로 Instance에 바인드되려 시도될 -때(=마운트 시점)로 구독 자체를 lazy하게 미루면 되지 않냐"고 제안, 검증 후 -확정. `base/slot-plan.md`(`:List`의 "구현"/"구독 시점" 절 재작성 + -"base/roblox 패키지 경계" 절 보강)/`ROADMAP.md`(M6)에 반영 완료: - -- **`Dispatch.setLength`가 이미 쓰던 패턴을 그대로 재사용, 새 메커니즘 - 없음.** `:List(data,updateFn,keyFn)`는 이제 설정만 저장하고 반환 — - 실제 `data:Observer(fn)` 구독과 최초 `reconcile`은 Slot 컨테이너 자신이 - 마운트되는 순간(`Dispatch/Slot.luau`의 `process(inst,k,self)`, `self._mounted`를 - 세팅하는 바로 그 자리)에 `activateList(self,inst)`가 수행. -- **`:List()`가 마운트 이후에 불리는 경우 — `self._mounted`면 즉시 활성화로 - 확정(사용자 확인, 세 가지 대안 중 1번).** 마운트는 1회성 이벤트라 순서가 - 뒤바뀌면 그 이벤트를 못 기다리므로, `:List()`가 `self._mounted`를 직접 - 확인해서 이미 참이면 그 자리에서 즉시 `activateList` — 호출 순서 제약을 - 새로 추가하지 않음. -- **canExecute와 "등록 즉시 1회 실행"의 관계를 사용자가 직접 짚어 확정**: - `data:Observer(fn)` 등록 시점(=`bindLifetime` 호출 *이전*)의 최초 1회 - 실행은 `canExecute`/`Subscribed` 게이팅과 무관하게 무조건 일어남 — 이 - 시점엔 아직 `Subscribed`가 안 세팅돼 `canExecute`를 물으면 거짓이겠지만, - 애초에 최초 실행은 게이팅 대상이 아니라서 상관없음(`Dispatch.setLength`가 - 이미 "등록 즉시 1회와 겹쳐도 무해"로 같은 구조를 갖고 있었음). `bindLifetime`은 - 등록 직후에 걸려 **이후** 재실행만 게이팅. -- **Destroy 이후 "재실행 막기"+"관측 자체를 관두기"가 새 코드 없이 한 번에 - 해결됨** — `inst` Destroy 시 `gcconn`이 죽어 `canExecute`가 거짓이 되고 - 향후 재실행이 no-op되는 동시에, `gchold`가 `Relate(inst)`(weak-keyed) - 아래 있어서 `inst`가 죽으면 그 안에 강참조로 잡혀있던 Observer/클로저 - (`mounted`/`userdata`/`keyIndex` 포함)가 전부 GC 대상이 됨 — 명시적 - 구독 해제 코드가 안 필요함, `lifecycle-pattern.md`의 "정리는 기본적으로 - GC에 위임" 원칙 그대로. -- **부수 관찰(메모만, 설계 아님)**: 사용자가 "`Relate`로 마운트된 대상을 - weak하게 구할 수도 있겠다"고 언급 — `bindLifetime`이 `Relate(inst)` 기반이라 - 나중에 "이 `inst`에 지금 뭐가 붙어있는가" 역조회가 같은 저장소로 가능해 - 보임, quad-debug 그래프 UX와 맞닿을 수 있음. 지금 설계 안 함, 필요성 - 확인되면 그때. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수 -설계 확정이라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-09 여덟 번째 세션 — `.claude/base/` 전체 중간검토(질문 모드), -실제 설계 결함 다수 발견·수정 - -사용자가 "이 프로젝트의 계획을 중간검토합니다. 각 요소들에 대해서 함수나 -클래스 등의 동작을 제가 확인 가능하게 리스팅해요... 질문 모드를 쓰면 -좋겠습니다"라고 요청 — 2026-08-04 6차 라운드 때 예고해뒀던 "다음 세션 -검증 패스"를 실제로 실행한 세션. 서브에이전트 6개를 병렬로 띄워 -`.claude/base/` 전체(15개 파일, 5296줄)를 클러스터별로 정독시켜 확정된 -API/동작을 file:line 인용과 함께 그라운딩된 리스팅으로 뽑아낸 뒤, 6개 -배치(Store/State/Source+Dispatch, Ref/PreRef+Brand+Length-Offset+생명주기, -Modifier, Slot, Tag/Attribute/UI shorthand+Blocker/Effect, 컴포넌트 -경계+아키텍처)로 나눠 각 배치를 텍스트로 보여주고 바로 `AskUserQuestion` -(문제없음/문제있음)으로 확인받는 방식으로 진행 — 문제 제기된 건 그 -자리에서 바로 문서에 반영(끝까지 미루지 않음). 총 24개 확인 질문 중 -약 1/3에서 실제 설계 결함이 나옴 — 전부 사용자가 구체적인 반례/Luau -시맨틱스를 근거로 지적한 것이라 전부 그대로 수용, 방어하지 않고 수정. - -**발견·수정된 것 (파일별)**: - -- **`base/bind-system-plan.md`** (가장 많이 고침): - - `Source(default)`/`Ref(default)`의 `default` 생략이 "선택"이라는 - 서술에 "`T`가 nilable일 때만 안전하다"는 캐비엇 누락 — 추가. - `Ref`는 `:Callback`이 등록 즉시 발화해서 이 문제가 더 잘 드러남. - - Dispatch 체인 절에 "`handler.process`를 `Dispatch.process` 없이 - 직접 호출하면 UB(체인 bookkeeping이 깨져 `retract`가 영영 안 - 불리거나 정합성이 무너짐)"라는 불변식이 안 적혀 있었음 — 추가. - - **Ref 콜백/대기자 배열의 소진 슬롯을 `None`에서 `nil`로 되돌림** — - 2026-08-07 열 번째 세션에 "구멍 있는 정수 키는 순회 순서가 깨진다"는 - 이유로 `None`으로 바꿨던 게 이 배열엔 안 맞는 처방이었음(사용자 - 지적): 이 배열은 순서가 안 중요해서 일반화 `for`가 구멍이 있어도 - 전부 방문하고, 오히려 `None`을 쓰면 슬롯이 영원히 안 비어서 - `:Wait()`마다 배열이 끝없이 길어지는 새 문제가 생김 — `nil`로 - 지우고 빈 슬롯을 재사용하는 등록 함수로 바꿈. PreRef pre-pass/ - Length-Offset의 `sourceList`는 순서가 실제로 중요해서 계속 `None`이 - 맞음 — 두 사례를 헷갈리지 않게 교차 참조로 명확히 구분. - - `.Value`가 평범한 hash 필드가 아니라 `__index`로 구현돼야 하는 - 이유(콜백 배열과 같은 테이블에 있으면 `T`가 함수/스레드일 때 콜백 - 처리 루프에 오분류될 위험) 추가. - - **`isRef`/`isPreRef`를 `isState`/`isSource`와 같은 상위-하위 합성 - 패턴으로 재정정** — 원래 "서로 배타적인 형제 브랜드"였는데, `Source`가 - `State`를 만족하듯 `PreRef`도 `Ref` 런타임을 재사용하는 관계라 - 같은 방향(하위=PreRef가 상위=Ref에 포함)으로 다뤄야 일관적이라는 - 지적 — `isPreRef`가 가장 구체적인 항등, `isRef`는 그 위에 얹힌 - 상위 개념. `(v=Ref)` children leaf 매치 핸들러는 이제 - `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 함. - - `NoneHandler`가 `k` 타입을 안 가리는데 왜 배열 파트 `None`(숫자 - 키)에 실제로 안 걸리는지 명확화(배열 파트 `None`은 애초에 - `Dispatch.process`를 안 타서 `NoneHandler`가 볼 기회 자체가 없음). - - `setLength`/`setOffsetSource`의 `None` 페어링 대상을 "Ref/PreRef - 등" 예시 목록에서 "그 배열 위치의 값 자체가 `None`인 모든 경우"로 - 명시적으로 확장, 둘이 항상 짝을 맞춰야 한다는 점도 재강조. - - `:Subscribe()`가 quad 전역 GC-native 원칙의 의도적 예외(참조를 - 다 놓아도 GC 안 되고 계속 실행됨)라는 경고가 없었음 — 추가, 용도도 - "완전히 top-level" 케이스로 좁혀 문서화. -- **`base/modifier-plan.md`**: 핸들러 계층 값 → error 체크가 `State`류 - "State/Source가 감싼 내부 값"까지는 못 잡는다는 한계 — 명시적 UB로 - 문서화(오버엔지니어링 방지, 실사용 위험 낮음). -- **`base/slot-plan.md`** (가장 큰 변경): **CRUD 식별 기준을 element - 레퍼런스에서 인덱스 기준으로 전환** — `Remove(index)`/ - `Extract(index, newElement?)`/`Move(oldIndex, newIndex)`. 원래 - "인덱스는 stale해진다"는 이유로 레퍼런스 기준을 택했는데, 실제로는 - 반대(호출부가 `Add` 리턴값을 안 담고 흘려버리는 경우가 흔함)가 더 - 큰 문제였음. **`ExtractAll()`/`Get(index)`/`IndexOf(element)` 신설** - (`Get`은 "YAGNI"로 드롭했던 걸 재추가). **`Extract(index, newElement?)` - 신설** — 교체가 필요하면 기존엔 Extract+Add 이중 O(n) 시프트가 - 필요했는데, 이제 O(1) 제자리 교체 가능(이전 element 반환). -- **`base/tag-plan.md`**: `TagHandler.retract`의 전체 삭제 동작이 - 정확히 `v == nil`일 때만 맞다는 전제를 `assert`로 명시(기존엔 "v를 - 안 봐도 됨"이라고만 서술돼 있어 조건이 암묵적이었음). -- **`base/attribute-plan.md`**, **`.claude/question.md`**: 타입 - 파라미터화(`Attribute<>` 제네릭 vs `BooleanAttribute`류 정적 - 패밀리) — "미확정"에서 **"둘 다 채택"으로 확정**(내부 구현 동일, - 호출부 표기만 다름). `=` 뒤 값 타입까지 narrowing되는지는 M0/M10 - 실측 필요(안 돼도 런타임 무관)로 명시. -- **`base/ui-shorthand-plan.md`**: `UICorner`/`UIPadding`/`UIScale`이 - 타입 생성 스크립트가 만드는 `FrameModifier`류 타입의 메소드 목록에도 - 포함돼야 한다는 체크리스트 항목 추가(런타임과 무관한 순수 타입 - 생성 디테일). -- **`base/effect-plan.md`**: `EffectHandle`이 내부 Observer를 필드로 - 강참조한다는 것, `bindLifetime`/`:Subscribe()` 둘 다 `state`가 있으면 - 내부 Observer까지 cascade해야 한다는 것(안 그러면 내부 Observer의 - `canExecute` 게이팅이 올바른 `inst`를 못 봄) — 재확인 후 명시화. -- **`base/component-composition-plan.md`** (Length/Offset 다음으로 많이 - 고침): - - **"리프 바인딩엔 Source가 좁은 예외"라는 서술이 틀림 — 정정.** - `local a = Source(true); Frame { Visible = a }; a:Set(false)`처럼 - Source를 리프에 직접 물리는 건 이미 확정된 "Source가 State를 - 구조적으로 만족" 원칙이 그대로 커버하는 정상 경로였음 — "State가 - 일반적"이라는 서술은 Source를 못 쓴다는 뜻이 아니라 "여러 값에서 - 파생된 계산 결과는 State일 수밖에 없다"는 통계적 경향 서술일 - 뿐이라고 재정정. - - `props.Modifier or None` 관용구의 `None` 근거 포인터가 Ref 콜백 - 배열 정정으로 깨질 뻔한 걸 교차 참조로 바로잡음(그 배열은 순서가 - 중요한 별개 케이스라 `None`이 계속 맞음). - - `Frame { Comp{} }`에서 `Comp`가 `Slot`을 반환하는 다중 루트 우회 - 경로가 새 배선 없이 그대로 작동함을 재확인(값이 컴포넌트 호출로 - 왔든 리터럴이든 디스패치 입장에선 구분 없음). -- **`ROADMAP.md`**: 위 `Ref` `None`→`nil`/`isRef`·`isPreRef` 변경사항 - 체크박스 동기화. - -**변경 없이 확인만 된 것**: `:With`/`:Compute` 체이닝, `None` 센티널 -기본 메커니즘, Length/Offset 전체, 이중 바인딩 금지/`Relate`/생명주기, -Modifier setter/Apply/Overridden 판단 기준, `Peek`/`isState`/`None` -setter 인자, Slot 요소 타입 제약/Extract portal/`Length`, `Slot:List` -시그니처(단, 캐스케이드 성능 이슈는 `keyFn` 명시 유도로 이미 문서화돼 -있어 추가 조치 불필요), List 구독 lazy 시점, Tag 값 모양/패키지 배치, -Blocker 전체, 소스트리/네이밍 컨벤션/Handler 3분류/테스트 전략/이식성 -원칙. - -**부수 기록**: `.claude/memory`(세션 간 영속 기억)의 협업 스타일 메모에 -이번 리뷰 진행 방식(에이전트 병렬 추출 → 배치별 텍스트+AskUserQuestion -즉시 확인 → 그 자리에서 바로 문서 반영)을 다음에 재사용할 패턴으로 -기록 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션은 설계 -확정이 아니라 기존 확정 사항의 결함 수정이었지만, 결과적으로 M0 착수 -전 상태가 더 탄탄해졌을 뿐 우선순위 자체는 그대로. 이 중간검토가 -마지막 배치(6단계)까지 끝났는지, 사용자가 이어서 더 볼 부분이 있는지는 -다음 세션 시작 시 확인. - -## 2026-08-09 열두 번째 세션 — `.claude/luau-test/` 신설: M0 사전 검증 -스파이크 작성, 결과는 아직 미확인 - -M0가 공식적으로 짜야 할 스파이크(위 "지금 할 일" 1번, `ROADMAP.md` M0 -체크박스)와 지금까지 세션 로그 곳곳에 흩어져 있던 "실제 Luau로 부딪혀본 -적 없는 것"/"M0/M2 스파이크 검증 목록에 추가됨" 표시들을 한 곳에 모아, -사용자가 직접 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 돌려볼 -수 있는 독립 실행 스크립트 14개 + `README.md` 색인으로 만듦. 세 라운드에 -걸쳐 진행됨: - -1. **1차 작성** — 레포 루트 `luau-ignoreme/`(당시엔 git 자동 제외 폴더로 - 시작)에 M0 체크리스트 5개 항목(Store/State 다이아몬드 전파, Source가 - State를 구조적으로 만족하는 제네릭 타입, process/retract 재귀 디스패치, - 배열/해시 두 패스 순회, `props.Modifier or None` nil-hole 관용구) + - `Dispatch` 체인/`retractUnder` 다단 검증, `Relate`의 weak-table GC - 실측, `Modifier.Overridden` 서브타입 타입체크, Roblox 전용 - `bindLifetime`/`canExecute`/Attribute Instance 참조/`CollectionService` - 태그 확인까지 10개 파일 작성(01~10). -2. **2차 — 커밋 `f198fd9`("중간검토에서 발견된 설계 결함 다수 수정") 반영.** - 그 사이 사용자가 직접 `.claude/base/` 전체를 훑으며 여러 결함을 - 정정(위 절 참고) — 그 중 `02`(Ref 콜백/대기자 배열의 소진 센티널이 - `None`→`nil`로 되돌아간 것, 실제로 `None`을 쓰면 배열이 무한정 - 자라는 버그였음이 드러남)이 luau-test 내용과 정면으로 어긋나 전면 - 재작성(순서가 중요한 배열은 계속 `None`, 순서 무관+슬롯 재사용 - 필요한 배열은 `nil`이라는 최종 구분 + 무한 성장 버그의 정량적 - 재현까지 포함). `Modifier` UB→error 전환(11 신규)도 이 라운드에 - 같이 반영. 나머지 파일은 대조 결과 영향 없음을 서브에이전트+직접 - 문서 대조로 확인. -3. **3차 — 사용자 요청으로 "타입 관련 실측 필요, 특히 `luau-lsp`로 - 확인해야 할 것" 3개 추가(12~14).** base 문서 자신이 "실측 필요"라고 - 명시적으로 못박아둔 지점(`attribute-plan.md`의 `[Attribute<> - "name"] = value` 제네릭 DI 키가 실제로 값 타입을 좁혀주는지, 12번)과 - f198fd9에서 뒤집힌 결정(`isRef`/`isPreRef`가 이제 `Source`/`State`와 - 같은 포함 관계 — `PreRef`가 `Ref`의 하위 개념이 됨, `PreRef`가 - `Ref`를 구조적으로 만족하는지 타입체크까지 포함, 13번), 그리고 - 같은 세션에 새로 명시된 캐비엇(`Source(default)`/`Ref(default)`의 - `default` 생략은 `T`가 nilable일 때만 안전하다는 것을 함수 오버로드로 - 타입 레벨에서 실제로 막을 수 있는지, 14번)을 찾아 작성. -4. **폴더 이동 — `luau-ignoreme/` → `.claude/luau-test/`.** 사용자가 - "커밋해서 레포에 남기자"고 판단 — `*-ignoreme*` gitignore 패턴을 - 벗어나 일반 추적 대상으로 전환, `.claude/README.md`에 새 폴더 행 - 추가. 내용/역할은 안 바뀜, 경로 참조 문구만 동기화. - -**아직 아무것도 실행 안 됨 — 에이전트도 로컬에 `luau`/`luau-analyze`가 -없어서 직접 못 돌려봤고, 사용자가 다음에 `luau`/`luau-analyze`/ -`luau-lsp`/Roblox Studio로 직접 돌려보고 결과를 알려주기로 함.** 결과에 -따라 할 일: -- 전부 통과 → M0 실제 착수 시 이 스크립트들의 로직을 그대로 재사용하며 - 진행. -- 하나라도 걸림(특히 12/14의 타입 narrowing 실패, 07의 GC 신호 이상, - 10의 `warn` 발생, 13의 런타임 assert 실패) → 해당 `base/` 문서를 - 그 자리에서 정정. -- `.claude/luau-test/README.md`의 "결과 확인 후 할 일" 절에 파일별로 - 뭘 우선 확인해야 하는지 이미 적어둠 — 다음 세션은 그 응답을 - 대조하는 것부터 시작하면 됨. - -**다음 세션이 할 일**: 사용자가 luau-test 실행 결과를 갖고 오면 그것부터 -반영. 아직 없으면 `ROADMAP.md` M0 착수 우선순위는 그대로(위 "지금 할 일" -1번 참고) — 단, 이 폴더 결과를 먼저 확인하고 진행하는 게 순서. - -## 2026-08-10 세션 — `Slot:Add`가 삽입 인덱스를 반환하도록 확정, 범위 밖 -`index`는 clamp 대신 error - -짧은 세션. 사용자가 "`Slot:Add`/`Remove`가 어차피 void인데 삽입된 인덱스를 -반환해줘도 되지 않냐"고 제기 — 검토 후 채택, `base/slot-plan.md`의 CRUD -표/에러 조건 절에 반영 완료: - -- **`Slot:Add(element, index?): number`로 확정** — `index` 생략(끝에 추가) - 시 호출부가 실제 위치를 모르는 문제를 `Add`가 이미 계산해서 아는 값을 - 그냥 반환하는 것으로 공짜 해결(기존엔 `IndexOf`로 O(n) 역조회해야 했음). - `Move`/`Swap`이 void인 것과 안 부딪힘 — 그 둘은 호출부가 이미 위치를 - 알고 부르는 연산이라 새 정보가 없어 void인 거고, `Add`는 반대로 새 - 정보(계산된 위치)가 생기는 경우라 "반환값은 실제로 새로 알게 되는 - 정보만"이라는 같은 원칙의 연장. -- **`Add`의 `index`가 범위 밖(1..현재 개수+1)이면 즉시 `error()`, clamp - 안 함 — 사용자가 직접 근거 제시.** clamp는 "의도한 위치가 아닌데 조용히 - 성공하는" 찾기 힘든 버그 유형을 새로 만들 뿐이고, 이미 `Remove`/ - `Extract`/`Move`/`Swap` 전부가 범위 밖에서 즉시 에러인 fail-fast 톤과도 - 맞아야 함. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인이 -여전히 먼저) — 이번 세션은 이미 확정된 CRUD 표의 작은 갭 하나만 메운 -것이라 우선순위엔 영향 없음. - -## 2026-08-10 세션 — 동적 자식 추가/제거는 `Slot`/`state`만 정당, -그 외는 UB로 명문화(문서 갭 보강) - -사용자 질문에서 시작: Slot이 마운트한 객체 수를 `Length`/`Offset` -누적합으로 세는 방식(2026-08-09 여섯 번째 세션 확정)이 되면서, 이 카운팅을 -안 거치고 quad가 관리하는 부모 Instance에 외부에서 직접 `.Parent = inst`로 -자식을 끼워 넣는 게 UB로 문서화돼 있는지 확인 요청 — 검토 결과 **문서 -어디에도 명시돼 있지 않은 진짜 갭**이었음(기존 UB 목록엔 Handler 순환/ -이중 바인딩/`Dispatch.process` 우회 직접 호출/`setLength`·`setOffsetSource` -생략 등은 있었지만 이 케이스는 빠져있었음, 인접했던 "수동 Visible 토글은 -Length가 못 잡는 게 맞다"는 캐비엇은 이미 마운트된 element를 나중에 -숨기는 별개 시나리오라 이것과 다름). - -**확정**: 동적 자식 추가/제거의 유일한 정당 경로는 `Slot` 또는 -`state`류 store-bind 뿐 — 둘 다 그 위치의 Handler가 -`Dispatch.setLength`/`Dispatch.setOffsetSource`를 정확히 호출하는 것으로 -이미 보장돼 있음. 이 두 경로를 거치지 않고 quad가 마운트해둔 부모 -Instance에 직접 `.Parent =` 대입으로 자식을 넣거나 빼면 `lengthList`/ -`sourceList`가 그 변화를 전혀 몰라 `Length` 카운트와 형제 순서(offset) -계산이 조용히 어긋남 — 새 방어 로직 없이 UB로 문서화만 함(다른 UB -케이스들과 같은 톤). `base/bind-system-plan.md`("Length/Offset" 절 -말미)/`base/slot-plan.md`("Slot.Length" 절 말미)에 반영 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션은 순수 문서 갭 보강이라 우선순위엔 영향 없음. - -## 2026-08-10 두 번째 세션 — Tween 구조 전면 재설계: 독립 Dispatch 핸들러 → -값-레벨 `Tween` 래퍼, `pre-implementation-audit.md` 1-1 완전 해소 - -사용자가 "트윈도 타입 문제가 있다 — 키 타입을 어떻게 하냐, Property -setter가 더 분발해서 `V`가 `isTween`이면 트윈 넣는 게 낫지 않냐"고 -제기하며 시작된 긴 단일 스레드. 기존 확정 모델(`[Tween(key, -tweenData...)] = storeValue`, `v`가 Store인 아무 `k`나 잡는 우선순위 -최상위 Dispatch 핸들러, 2026-08-04부터 확정)이 실은 -`research/pre-implementation-audit.md` 우선순위1-1이 이미 지적해뒀던 -구조적 모호함("애니메이션 없는 일반 반응형 프로퍼티 바인딩도 결국 -이름이 Tween인 파일을 거쳐가는가")을 안고 있었다는 걸 사용자 제안이 -정확히 겨냥한 것으로 드러나, 세션 내내 살을 붙여 완전히 재설계까지 -감. 구 모델은 `archive/tween-special-bind-key-reversed.md`로 이전, -`research/tween-plan.md`는 전면 재작성됨 — 상세 근거는 그 두 문서가 -최종 소스, 여기는 결정 흐름만 요약. - -**핵심 재설계**: State/Source 언랩(범용 `Dispatch/StoreBind.luau`, `k`/`v` -타입 무관)과 "이 값이 트윈 대상인가" 판단을 완전히 분리 — 후자는 별도 -Dispatch 핸들러/우선순위 경쟁이 아니라, **PropertyHandler가 `realv`를 -다 풀어낸 뒤 직접 하는 값-레벨 분기**(`isTween(realv)`)로 옮김. `Tween(opts: -{Value: T, ease...}) -> Tween`는 `Store({...})`와 같은 `Type(args)` -테이블 팩토리. 이 전환 하나로 우선순위1-1이 구조적으로 성립 불가능해짐 -— 범용 반응형 바인딩과 Tween이 애초에 같은 핸들러를 놓고 경쟁할 지점 -자체가 없어짐. - -**세션 중 순서대로 다듬어진 세부 결정들**(전부 최종적으로 `research/ -tween-plan.md`에 반영): - -1. **`Tween.Value`는 plain `T`만, 자체 반응 경로 없음** — 처음엔 `Value`도 - `T|State`를 받아 내부에 별도 Observer를 걸어야 하나 검토했으나, - 바깥 `:Compute`가 소스 변경마다 `Tween{Value=v,...}`를 통째로 재생성해 - StoreBind 재귀를 타므로 불필요함을 확인 — "같은 일 하는 두 번째 경로를 - 안 만든다" 원칙 재적용, `Tween = {Value: T, ease...}`로 확정. -2. **3-상태 릴레이션 슬롯으로 `hasBeenSet`과 활성 엔진 트윈 저장을 통합** — - `relate:GetStrong(inst,k)`가 `RobloxTween | true | nil` 중 하나: - `nil`=이 키 첫 세팅(애니메이션 없이 즉시 스냅, 기본값→목표값으로 - 날아오는 진입 애니메이션 버그 방지), `true`=세팅된 적 있음/활성 트윈 - 없음(정상 애니메이션 시작 가능), 엔진 객체=활성 트윈 있음(override - 정책대로 정리 먼저). 사용자가 직접 "hasBeenSet은 어차피 트윈에만 - 쓰이니 트윈 저장 슬롯 하나로 합치자"고 제안해 확정. -3. **활성 트윈이 있는데 plain 값이 들어오는 경우의 순서 규칙 신설** — - 먼저 override 정책대로 이전 트윈을 정리(멈추거나 끝냄)하고, **그 - 정리가 끝난 뒤에만** 새 값을 세팅. 순서가 뒤바뀌면 이전 트윈의 다음 - 인터폴레이션 프레임이 방금 세팅한 값을 덮어쓸 위험이 있어서 — 사용자가 - 직접 짚은 시퀀싱 버그. -4. **타입 대수: `T' = T | Tween` 치환만으로 해결, 새 타입 기계 불필요** — - 지금 프로퍼티류 필드가 전부 `T | State` 모양으로 통일돼 있는데, - 여기서 "이 필드의 `T`" 자체를 `T' = T | Tween`로 치환하면 자동으로 - `T | Tween | State>`가 나옴 — Modifier/State/Source/ - StoreBind 코드엔 `Tween` 인지 로직이 전혀 안 들어감(StoreBind는 원래도 - 페이로드 타입에 무관하게 `isState`만 봄), 타입 생성 스크립트가 필드 - 타입 문자열만 바꾸면 끝. 사용자가 직접 대수적으로 도출. -5. **`useTween` 우회 — 새 옵션 필드 없이 해소.** 이전엔 - `Tween{useTween=state}`처럼 별도 필드가 필요하다고 열어뒀으나, - 2026-08-07 일곱 번째 세션에 확정된 `state:Apply(factory)` sugar 위에 - `someState:Apply(Animate(reduceMotion, opts))`처럼 조건부로 `Tween{...}`를 - 씌우거나 안 씌우는 `:Compute` 팩토리 하나로 공짜로 풀림 — 새 base - 메커니즘 불필요. -6. **`Animate` 콤비네이터는 quad-roblox 유틸, base 프리미티브 아님** — - `Tween` 값 타입/`isTween`만 base(`quad-base/Tween.luau`)에 있고, - `Animate`는 이미 있는 `:Apply`/`:Compute`/`Tween{...}`를 조합한 편의 - 함수라 나중에 이름/모양을 자유롭게 바꿔도 base 계약에 영향 없음 — - 사용자 표현으로 "저비용 고효율 엔지니어링". -7. **패키지 경계는 Tag가 이미 밟은 분리를 그대로 재사용** — quad-base: - `Tween.luau`(값 타입만). quad-roblox: `Handlers/Property.luau`(isTween - 분기+3-상태 저장+override 정책 흡수, 기존 독립 `Handlers/Tween.luau` - 폐기) + `Animate.luau`(신규). -8. **부수 발견 — `retract`가 Tween 경로에서 사실상 필요 없어짐.** 기존 - 모델에서 "Tween↔프로퍼티 핸들러 타입 교체"가 `retract`가 실제로 - 의미를 갖는 유일한 대표 예시였는데, 새 모델에선 매치되는 Dispatch - 핸들러가 항상 PropertyHandler 하나뿐이라 이 케이스 자체가 사라짐 — - 트윈 취소/전환은 PropertyHandler 내부의 3-상태 슬롯 로직으로 대체(Tag가 - 이미 하는 "diff는 process 자신이 담당" 패턴과 같은 모양). `retract` - 필드 자체는 "생략 불가" 일반 규칙이라 여전히 정의는 해두되, 실제 - 호출은 거의 없어짐. Tag(핸들러 타입이 실제로 바뀌게 재설계되어 - `retract`가 필요해진 사례)와 Tween(핸들러 타입이 안 바뀌게 재설계되어 - `retract` 필요성이 사라진 사례)을 서로 반대 방향 사례로 archive 문서에 - 대비해둠 — quadnomicon 소재. -9. **`Tween`의 핸들러 계층 분류 정정** — `base/modifier-plan.md`가 - 원래 Tween을 Slot/Tag/Attribute와 같은 "dispatch 참가자"(State/Source에 - 담겨도 재귀 재-dispatch가 그대로 처리해주는 부류)로 묶어뒀는데, 이제 - `Tween`는 `process`/`retract`가 없는 순수 raw 데이터 값이라 `None`과 - 같은 분류로 정정 — Modifier 필드/`State`가 막는 "핸들러 - 계층 값 → error" 규칙에 안 걸린다는 결론은 안 바뀜(그냥 raw 값이라서로 - 근거가 바뀜). -10. **`initValue`(진입 애니메이션)와 hasBeenSet의 긴장 관계를 기록만 - 해둠** — hasBeenSet이 "첫 세팅은 무조건 스냅"을 기본 동작으로 - 확정했으므로, 나중에 `initValue`(다이얼로그 슬라이드-인 등)가 실제로 - 필요해지면 이 억제 동작을 명시적으로 우회하는 방법까지 같이 설계해야 - 함 — 새 결정 없이 상충 관계만 `research/tween-plan.md`에 남김. - -**여전히 열려있는 것**(M11 착수 시 확정): override 정책 4가지 중 기본값 -Cancel 외 세 옵션의 정확한 키 이름/시그니처, Tween→plain 전환에 5번째 -옵션이 필요한지, 트윈 옵션 값 모양(`TweenInfo` 그대로 vs 편의 필드 — 소견은 -후자), `Animate`의 정확한 시그니처(조건/옵션 분리 vs 통합). - -**반영된 파일**: `research/tween-plan.md`(전면 재작성, 최종 소스), -`archive/tween-special-bind-key-reversed.md`(신규, 구 모델 원문+역전 -사유), `base/bind-system-plan.md`(9곳 — "확정된 디스패치 모델"의 대표 -예시를 Tween에서 StoreBind로, `retract` 필요 패턴 예시를 Tag로 교체, -"Dispatch는 프리미티브가 아니다"/"Dispatch 체인" 절의 핸들러 목록에서 -Tween 제거, `None` 센티널 절 예시 갱신, Ref/Brand 절 문구 정정), -`base/architecture.md`(소스트리 — `quad-base/Tween.luau` 신설, -`quad-roblox/Handlers/Tween.luau` 삭제하고 `Handlers/Property.luau` -설명에 흡수, `Animate.luau` 신설), `base/modifier-plan.md`(핸들러 계층 -분류에서 Tween 제외 + 신규 "10. `Tween`와의 타입 합성" 절), -`research/pre-implementation-audit.md`(우선순위1-1 해소 표시), -`ROADMAP.md`(M11 전면 재작성, M2/M7 체크박스 갱신), `.claude/question.md`/ -`.claude/README.md`(참조 동기화). - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션도 순수 설계 확정/문서 정리라 M0 착수 우선순위 자체는 -그대로. M11 착수 시점이 오면 위 "여전히 열려있는 것" 목록부터 확인. - -## 2026-08-10 세 번째 세션 — `OnChange` 특수 키 신설: `GetPropertyChangedSignal` -바인딩, 제네릭 없이 확정 - -사용자가 `GetPropertyChangedSignal`을 어떻게 다뤄야 할지 물으며 시작 — -이벤트는 이미 평범한 문자열 키(`inst[key]`가 곧 Signal)로 확정돼 있는데, -`GetPropertyChangedSignal(name)`은 프로퍼티 이름을 인자로 받아야 하고 그 -이름이 "값 세팅" 키 네임스페이스와 겹쳐서 같은 패턴을 못 씀 — 사용자가 -`[OnChange "PropertyName"] = function(v) ... end` 형태(타입은 콜백에 직접 -명시)와 "`OnChange.PropertyName`을 전부 코드 생성"하는 대안 두 가지를 -제시하며 의견을 물음. - -**확정**: `OnChange(name)` DI 키 팩토리, **제네릭 타입 파라미터 없음** — -`Attribute<>`와 달리 콜백 파라미터 타입은 호출부가 직접 명시. 이미 -확정된 "이벤트 바인딩은 콜백 시그니처를 Luau가 검증 못 하는 대가를 -받아들인다"는 결정과 같은 급의 트레이드오프라 새로 정당화할 것 없다는 게 -근거 — 오히려 `Attribute`처럼 제네릭으로 정확히 맞추려 들면 이벤트 키보다 -더 엄격한 걸 요구하는 셈이라 일관성이 깨짐. 프로퍼티별 정적 코드 생성 안은 -기각(`archive/onchange-per-property-codegen-rejected.md`) — Attribute의 -정적 지름길은 타입 파라미터가 좁고 고정된 프리미티브 집합(~10종)에서만 -와서 지름길 후보가 유한한데, 프로퍼티는 클래스마다 이름/타입 집합이 전부 -달라 (클래스 수 × 프로퍼티 수) 규모로 폭발함 — 겉보기엔 비슷한 절충 -같지만 실제로는 규모가 다른 문제. - -패키지 경계는 **전부 quad-roblox**(`Handlers/OnChange.luau`, `Attribute`와 -같은 배치 — `GetPropertyChangedSignal` 자체가 Roblox 엔진 API라 base에 둘 -값 타입/API 레이어가 없음). `process`는 `GetPropertyChangedSignal(name):Connect`, -`retract`는 `:Disconnect` — 일반 `Handlers/Event.luau`와 같은 결. **`State` -지원도 새 메커니즘 없이 해소** — 이미 확정된 "이벤트도 store-bind 가능 -(`false`로 disconnect)" 메커니즘이 그대로 적용됨, `OnChangeHandler`는 -`process`/`retract`만 구현하면 범용 `Dispatch/StoreBind.luau`가 State/Source -언랩+재귀 재-dispatch를 알아서 해줌. - -`base/onchange-plan.md`(신규)/`base/bind-system-plan.md`(이벤트 네이밍 절 -교차 참조)/`base/architecture.md`(소스트리 `Handlers/OnChange.luau`)/ -`ROADMAP.md`(M10 제목·체크박스)/`.claude/README.md`(base/archive 인덱스) -전부 반영 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-11 세션 — `:Compute(fn, ...)` trailing-args sugar 확정, -`Effect`/`Observer`는 의도적으로 제외 - -사용자가 Vide의 암묵적 추적과 React 훅 규칙의 차이를 짚는 질문에서 출발해, -"React의 `useMemo(fn, deps)`처럼 `:With(...)` 없이 `:Compute(fn, a, b, c)`로 -바로 추가 의존성을 선언하면 더 편하지 않냐"는 제안으로 이어진 짧은 세션. -검토 끝에 확정, `base/bind-system-plan.md`(`:Compute` 절 신규 소절)/ -`base/effect-plan.md`/`ROADMAP.md`(M3)/`research/documentation-content-map.md` -(quadnomicon 후보 7번)에 반영 완료: - -- **`:Compute(fn, ...)`는 채택 — 진짜 공짜 sugar라는 게 사용자가 직접 밝힌 - 핵심 근거.** `:Compute` 호출은 원래도 결과를 담을 새 State 노드를 만들어야 - 하므로, 그 노드에 `self` 말고 `a,b,c`까지 구독(무효화 엣지)을 추가로 거는 - 건 이미 생기는 노드에 엣지만 얹는 것 — `:With(a,b,c):Compute(fn)`(노드 - 2개)보다 싼 노드 1개로 끝남. 이전에 기각됐던 `Store.Combine({a,b}, - function(av,bv)...)`(포지셔널 값 언랩이라 타입 표기가 꼬였던 안)과는 - 달리 `fn(self)` lazy 핸들 시그니처를 그대로 유지하는 제안이라 그 기각 - 사유가 안 걸림. -- **`Effect(fn, ...)`/`state:Observer(fn, ...)`류 동일 sugar는 기각 — - 사용자가 직접 구분.** Effect/Observer는 Compute와 달리 자기 자신이 - 결과를 담는 State 노드가 아닌 순수 leaf 소비자라, 의존성이 둘 이상이면 - 그걸 하나로 합칠 **새 노드**(`:With`가 만드는 것)가 실제로 필요함 — - 이건 진짜 비용이 드는 지점이라, trailing args로 감추면 "이 줄이 새 - 노드/구독을 만든다"는 걸 코드만 보고 알 수 없게 됨. `:With`가 clone - 빌더가 아니라 진짜 노드로 확정됐던 이유(2026-08-07 세 번째 세션, - "코드상의 호출 체인이 그래프 엣지와 1:1 대응돼야 quad-debug 그래프가 - 안 꼬임")와 정확히 같은 원칙 — 다중 의존성 Effect/Observer는 - `Effect(fn, state:With(a,b,c))`처럼 `:With` 호출을 코드에 그대로 노출. -- **일반 원칙**: "trailing args sugar는 그게 정말 무료일 때만 붙인다" — - 호출부가 이미 만들어야 하는 노드에 엣지만 얹는 경우(Compute)엔 sugar, - 없던 노드를 새로 만들어야 하는 경우(Effect/Observer의 다중 의존성 - 병합)엔 sugar 없이 `:With`를 명시적으로 남긴다. `Compute`만 편해지고 - `Effect`/`Observer`는 안 그런 게 겉보기엔 비일관적으로 보이지만 실은 - 이 하나의 원칙에서 나온 것이라는 게 quadnomicon 에세이 소재로 채택 - (사용자 제안). - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. - -## 2026-08-11 두 번째 세션 — trailing deps를 `fn`에 위치 인자로도 노출, -`.claude/luau-test/15` 신규 - -같은 날 바로 이어진 세션. 사용자가 방금 확정된 `:Compute(fn, ...)` -trailing-args sugar를 한 단계 더 밀어붙임 — trailing args `a,b,c`가 -이미 구독 등록용으로 넘어간다면 `fn(self, a, b, c)`처럼 그 값 자체도 -위치 인자로 노출해도 되지 않느냐는 제안. `:With`가 그렇게 안 하는 이유 -(체인이 여러 호출에 걸쳐 길어지면 순서 추적이 복잡해짐)는 `:Compute`의 -trailing args처럼 한 호출문 안에 로컬하게 다 보이는 경우엔 안 걸린다는 -것도 사용자가 직접 짚음 — 검증 결과 정확함, 채택. - -- **`:With`의 회피 근거가 이 케이스엔 안 걸림** — `:With` 체인은 여러 - 호출/스코프에 걸쳐 누적될 수 있어 최종 위치 매핑을 코드 한 줄만 보고 - 못 읽는 게 문제였는데, `:Compute(fn, a, b, c)`는 그 호출의 인자 목록 - 자체가 곧 최종 순서라 누적 문제가 원천적으로 없음. -- **커링 패턴에서의 중복/드리프트 위험도 같이 해소됨** — 지금 설계(값은 - closure로 재획득)로 커링 스타일을 쓰면 `a,b`를 두 번(클로저 캡처용 + - `:Compute`의 trailing args용) 써야 해서, 리팩터링 중 한쪽만 바뀌면 - "구독은 a에 걸려있는데 실제로 읽는 값은 다른 것"인 조용한 버그가 생길 - 수 있음 — 위치 인자로 노출하면 trailing args 목록 하나가 "무엇을 - 구독하는가"와 "fn이 몇 번째로 뭘 받는가" 둘 다의 유일한 소스가 됨. -- **`self`가 이미 raw 값이 아니라 lazy 핸들로 넘어가는 원칙을 그대로 - 적용** — `fn(self: State, dep1: State, ...)`, 각 - `depN:Get()`을 실제로 부를 때만 계산 트리거. -- **새로 드러난 문제 — `previous`(2026-08-06 확정)와의 위치 충돌.** - `previous`를 dep 개수와 무관하게 항상 마지막 인자로 고정 - (`fn(self, dep1, ..., depN, previous?)`, N=0이면 기존 시그니처로 - 축약돼 하위 호환)하는 안을 제안했으나, 이건 "제네릭 타입 팩(`...U`) - 뒤에 고정 인자가 오는" 모양이라 Luau가 실제로 타입체크 가능하게 - 표현해주는지가 불확실 — **사용자가 직접 이 지점을 짚어 실측 필요로 - 남김. [정정, 같은 날 세 번째 세션] 이 순서 자체가 틀림 — `previous?`는 - 팩 앞이어야 함, 아래 절 참고.** - -`base/bind-system-plan.md`(":Compute(fn, ...)" 절 바로 뒤에 신규 소절)/ -`ROADMAP.md`(M3)/`.claude/luau-test/README.md` 반영 완료. -`.claude/luau-test/15-type-compute-trailing-deps-typepack.luau` 신규 — -(A) 단일 dep 대조군, (B) 이형 다중 deps가 제네릭 팩으로 개별 타입으로 -풀리는지, (C) 팩 뒤에 `previous?` 고정 인자를 붙인 시그니처 자체가 -파싱/타입체크되는지 세 가지 확인(**[정정, 같은 날 세 번째 세션]** C의 -순서가 틀렸던 것으로 드러나 D 대조군이 추가됨 — 아래 절 참고). 다른 -luau-test 파일들과 마찬가지로 **에이전트가 직접 실행 못 함** — -`luau`/`luau-analyze` 바이너리가 이 환경에 없어서, 사용자가 직접 -돌려보고 결과를 알려줘야 함. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — `15`가 luau-test 결과 확인 목록에 하나 추가됨(정정된 순서는 -아래 세 번째 세션 참고). 남은 진짜 불확실성은 B(이형 다중 deps를 -제네릭 팩으로 표현 가능한지)뿐 — 실패하면 위치 인자 노출을 동종 타입 -dep 1개로 한정. - -## 2026-08-11 세 번째 세션 — `previous`는 팩 앞(`fn(self, previous?, -...deps)`)으로 순서 정정, "이걸 안 할 이유"였는데 살아남음 - -바로 이어진 짧은 세션. 사용자가 위 두 번째 세션에서 제안했던 -"`previous`는 dep 개수와 무관하게 항상 마지막"(`fn(self, dep1, ..., -depN, previous?)`)을 직접 정정 — "애초에 `fn(self, prev, ...)`이긴 -해야할듯. 아니면 이걸 하지 말던가." - -**정정 채택 — `previous?`는 deps 팩 **앞**(self 바로 다음)에 와야 함, -`fn(self, previous?, dep1, ..., depN)`.** 이건 단순 선호가 아니라 거의 -확실한 Luau 문법 제약에서 나오는 결론: 값 레벨 `...`(vararg)가 파라미터 -리스트 맨 끝에만 올 수 있는 것과 마찬가지로, 타입 레벨 제네릭 팩(`...U`)도 -함수 타입 시그니처에서 항상 맨 끝이어야 할 가능성이 매우 높음(팩이 -"나머지 자리를 전부 채운다"는 개념이라 그 뒤에 고정 타입이 하나 더 -오는 걸 문법 자체가 지원 안 할 것으로 추정) — 위 두 번째 세션에서 제안한 -"previous를 팩 뒤에" 순서는 이 제약과 정면으로 부딪혀 애초에 파싱/타입 -체크가 안 될 가능성이 높았음. `previous?`를 팩 **앞**에 두면 "고정 인자 -다음에 팩"이라는 정상적인 모양이 되어 이 제약과 안 부딪힘 — **구조적으로 -유일하게 안전한 순서라 이걸로 확정**. - -**트레이드오프 — deps만 쓰고 싶어도 `previous` 자리를 비워둬야 함.** -`fn(self, previous?, dep1, dep2)`이므로, `previous`가 필요 없는 흔한 -호출도 `function(self, _, dep1, dep2) ... end`처럼 안 쓰는 두 번째 자리를 -이름으로라도 채워야 함 — Luau 문법 제약상 다른 선택지가 없어서 받아들이는 -비용. 사용자가 "아니면 이걸 하지 말던가"로 던진 양자택일에서, 이 정정으로 -구조적으로 안전한 순서를 찾았으므로 **확장 자체는 폐기하지 않고 이 순서로 -유지.** - -`base/bind-system-plan.md`(위 절의 "previous와의 위치 충돌" 소절 전면 -정정)/`ROADMAP.md`(M3)/`.claude/luau-test/15-type-compute-trailing-deps- -typepack.luau`(C를 "막혀야 정상인 음성 대조군"으로 재정의, D를 정정된 -순서의 "통과해야 정상인 양성 대조군"으로 신규 추가)/`.claude/luau-test/ -README.md` 반영 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — `15`의 C/D가 예상대로 나오는지(C는 막히고 D는 통과)까지 같이 -확인해줄 것, 예상과 다르면 이 순서 결정 자체를 재검토. - -## 2026-08-11 네 번째 세션 — `Slot:List`가 형제 순서(`LayoutOrder`)를 -자동으로 안 세팅하는 것으로 정정, `updateFn`에 `index`/`offset` 추가 - -사용자가 "`Slot:List`의 `updateFn`이 자기 자신 Slot을 얻을 방법이 -없는데, 그럼 offset을 못 보는 거 아니냐"고 질문하며 시작 — 처음엔 -"Handler(quad-roblox `Handlers/Slot.luau`)가 `rawAdd`/`rawMove` 시점에 -`localIndex+offset`을 자동으로 계산해 마운트된 원소의 `LayoutOrder`에 -직접 바인딩해준다"고 답했으나(2026-08-09 여섯 번째 세션 `bind-system-plan.md` -"Length/Offset" 절의 원 서술 그대로), 사용자가 이건 **매직**이라고 -바로 반박 — 컴포넌트가 `Frame { LayoutOrder = 5 }`처럼 자기 프로퍼티로 -이미 지정한 값을 Slot이 마운트 시점에 조용히 덮어쓰게 되고, 애초에 -"`updateFn`이 동적 요소를 전부 다룬다"는 게 원래 설계 의도였다는 것. - -**확정**: Slot/Handler는 `LayoutOrder`를 자동으로 세팅하지 않음 — -`Slot.Offset`(`Slot.Length`와 마찬가지로 공개 필드, Slot 마운트 시점에 -`Dispatch.setOffsetSource`가 등록하는 바로 그 Source를 `self.Offset`으로도 -저장)과 `index`(이제 `State`, "이 key가 지금 실제로 마운트된 -요소들 사이에서 몇 번째냐" — `keyFn`이 받는 raw `data` 배열 인덱스와는 -다른, filter로 압축된 값)를 `updateFn`에 값으로 전달만 하고, 실제로 -`LayoutOrder`(로블록스)든 CSS `order`(웹, 필요할 때만)든 어디에 어떻게 -쓸지는 전부 `updateFn` 작성자 몫 — `index:With(offset):Compute(fn)`을 -평범한 프로퍼티 store-bind로 써넣으면 됨, 새 메커니즘 아님. 수동 CRUD로 -Slot을 쓰는 사용자도 `slot.Offset`을 직접 읽어 같은 걸 스스로 구성 가능. -부수적으로 `setOffsetSource` 자체가 순수 숫자 계산이라 원래도 엔진 지식이 -필요 없었다는 것도 재확인 — `LayoutOrder` 자동 바인딩을 그 옆에 서술했던 -게 레이어링(엔진 무관 `Dispatch/Slot.luau` vs Roblox 전용 `LayoutOrder`) -위반이기도 했음. - -**`updateFn` 시그니처도 같이 정리**: `offset`/`index`(State화) 추가하면서 -파라미터 순서를 반환값 순서와 맞춤(사용자 지적) — 반환이 `(result, ud)` -(`prev`류 먼저, `userdata`류 나중)인데 기존 파라미터는 `userdata`가 -`prev`보다 앞이라 뒤집혀 있었음, `prev, userdata` 순서로 정정: - -```lua -updateFn(item, index: State, offset: Source, prev: T?, userdata: UD?): (T | nil, UD?) -``` - -**부수 발견 — 기존 `reconcile` 의사코드에 실제 버그가 있었음.** `index`를 -진짜 값으로 노출하려다 보니, `rawAdd(self, result, i)`가 raw `data` 루프 -인덱스 `i`를 그대로 위치 인자로 썼던 게 문제로 드러남 — filter로 앞쪽 -item이 마운트 안 되면 실제 마운트된 개수가 `i`보다 적어져서, `Add`의 -"범위 밖 index는 clamp 없이 error" 규칙에 걸려 그냥 터짐. `reconcile` -안에 "지금까지 실제로 마운트된 개수"만 세는 별도 압축 카운터(`pos`)를 -추가해 `rawAdd`/`rawMove`/`keyIndex`/`index` State 전부 이 값 기준으로 -통일 — filter 없이 순서대로면 `pos == i`라 흔한 경우엔 체감 차이 없음. - -전부 `base/bind-system-plan.md`(`setOffsetSource` 절, "Slot.Length와 -Slot.Offset은 별개" 절)/`base/slot-plan.md`(`:List` 파라미터 설명, 신규 -"왜 `LayoutOrder`를 Slot이 대신 안 해주는가" 절, `activateList`/`reconcile` -의사코드 전면 수정) 반영 완료. - -**같은 세션 후속 — `index`도 State가 아니라 raw number로 재정정, -`candidateIndex`로 이중 write 제거.** 사용자가 "reconcile은 sync라 -깜빡임 문제는 없지만, filter로 항목이 새로 보이게 되면 그 뒤 index를 -다 밀어줘야 하는데 Set이 반복적으로 도는 게 비효율 아니냐"고 재질문 — -검토 과정에서 사용자가 직접 더 나은 방향을 제시: `index`도 `item`과 -똑같이 raw number로 넘기고, 반응형으로 쓸지·언제 `:Set`할지는 전부 -`updateFn`이 자기 `userdata` 안에서 알아서 판단하게 두면 되지 않냐는 -것 — 채택. 이러면 `:List`가 `indexState`라는 별도 맵을 관리할 필요 -자체가 없어짐(`item`을 raw로 넘기는 것과 완전히 같은 원칙으로 통일, -"왜 `Source`를 `:List`가 안 만드는가" 절이 원래도 "item/index" 둘 다를 -언급하고 있었던 것과도 재정합). - -**`candidateIndex` 트릭으로 chicken-and-egg 문제도 해소**: `updateFn`에 -넘기는 `index`가 필요한 시점엔 아직 이 item이 살아남을지(필터 통과 -여부) 모르는데, 압축 위치(`pos`)는 원래 "생존자 개수"라 이 item 자신의 -생존 여부에 의존하는 것처럼 보였음 — 그런데 실제로는 **"이 item이 -살아남으면 차지할 위치"는 직전까지 처리된 item들의 생존 개수만으로 -이미 계산 가능**(이 item 자신의 결과와 무관)하다는 걸 확인 — -`candidateIndex = pos + 1`을 `updateFn` 호출 **전에** 계산해서 넘기고, -`result ~= nil`일 때만 `pos = candidateIndex`로 커밋. `updateFn`은 항상 -정확한 최종값을 받으므로, 새로 생기는 원소를 처음부터 `Source(index)`로 -올바르게 만들 수 있어 "임시값으로 등록 → 나중에 Set으로 정정"하는 -이중 write가 구조적으로 없어짐(브랜드 뉴 원소에 대해서도) — look-ahead -(아직 안 본 뒤쪽 item을 미리 훑는 것) 없이 여전히 단일 forward pass. - -전부 `base/slot-plan.md`(`updateFn` 시그니처를 `index: number`로 재정정, -신규 "왜 `LayoutOrder`를 Slot이 대신 안 해주는가" 절에 `userdata` 기반 -예시 코드 추가, `activateList`/`reconcile` 의사코드에서 `indexState` 맵 -전부 제거하고 `candidateIndex` 방식으로 교체) 반영 완료. - -**같은 세션 세 번째 후속 — 예시 코드의 남은 낭비 하나를 사용자가 재정정.** -`Source(index)`/`Set(index)` 분기를 `if not layoutOrder ... elseif` 식으로 -"Source 재사용 여부"만 갖고 나눴던 첫 예시가, "원소를 다시 그리는지 -(`prev == nil`)"와 독립적으로 갈려서 — `prev == nil`(새로 그림)인데 -`ud.layoutOrder`는 남아있는 경우(직전에 filter 탈락했다 재등장) 이전 -Source를 재사용하며 `:Set()`한 뒤 새 Frame을 만들면, 그 `:Set()` 시점엔 -아직 아무도 그 Source를 구독하고 있지 않아 완전히 무의미한 연산이 됨 -— 사용자 지적: "updateFn이 실행되기 전까진 이번 item이 버려질지/다시 -그려질지/source만 갱신될지 아무도 모르니 미리 Set을 해둘 수 없고, -`updateFn` 자신만 이 세 갈래를 정확히 알아서 효율적으로 나눌 수 있다." -예시를 `if not shouldShow ... return nil / if not prev then <새 Source로 -다시 그림> / <기존 Source 재사용, 실제로 다를 때만 Set>` 세 갈래로 재작성 -— "다시 그림" 갈래는 이전 Source를 절대 참조 안 하고 항상 `Source(index)`로 -새로 만듦. - -**같은 세션 네 번째 후속(핸드오버 정리) — 용어 혼동 방지 문서화, 전체 -코퍼스 stale 감사·`ROADMAP.md`/`README.md` 동기화.** 사용자가 "`key`와 -`index`가 헷갈리지 않게 문서화에 유의, `updateFn`이 명시적 책임이 많은 -함수라 문서화가 중요하다, 이 세션 내용 누락/stale 없는지 보고 핸드오버 -준비하라"고 요청 — `base/slot-plan.md`의 `Slot:List` 절 최상단에 세 -가지 값(1. `keyFn(item, index)`의 raw `index` — 원본 `data` 배열 위치, -2. `key` — `keyFn`이 계산하는 정체성, 3. `updateFn(item, index, ...)`의 -`index` — `key`와 무관한 압축된 마운트 위치, 순서 계산 전용)을 이름이 -겹치는데 서로 다르다고 명시하는 콜아웃 신설, `keyFn`/`updateFn` 파라미터 -설명 각각에도 교차 참조 추가. `updateFn`의 반환 갈래 서술(구 "prev 그대로/ -새 값/nil 반환")도 위에서 확정된 "버림/다시 그림/source만 갱신" 세 -갈래 이름으로 통일해 같은 개념이 두 가지 다른 말로 서술되던 걸 정리. -`base/bind-system-plan.md`의 `setOffsetSource` 예시(`index:With(offset)` -— `index`가 State인 것처럼 잘못 읽히던 stale 표현)도 `layoutOrder:With(offset)` -(사용자가 `userdata`에 직접 관리하는 Source)로 정정. `ROADMAP.md` M6/ -`.claude/README.md`의 `Slot:List` 요약이 이번 세션 이전 시그니처 -(`updateFn(item, index, userdata, prev)`, `offset` 없음, `LayoutOrder` -자동 처리 여부 미언급)로 멈춰 있던 것도 최신 상태로 동기화. - -`.claude/luau-test/`류 새 실측 항목은 추가되지 않음(이번 세션은 런타임 -로직/시그니처 설계이지 Luau 타입 시스템 경계 확인 대상이 아님) — 기존 -`userdata = userdata or {}` lazy-init 제네릭 narrowing 실측 필요 항목은 -그대로 유효(M0/M6 착수 시 확인). - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션도 `:List` 세부 설계 정정/문서 정리라 M0 착수 우선순위 -자체는 그대로. - -**같은 세션 다섯 번째 후속 — `key` 타입 무제약 확인, `item.id` 관용구 -문서화.** 사용자가 "캐스케이드 갱신을 막고 싶으면 `keyFn`은 string 등 -unique하기만 한 값이면 되는 거 맞지, `data` 안에 string 필드가 있으면 -그걸 쓰면 된다" 확인 요청 — 맞음(`key`는 Lua 테이블 키로만 쓰여서 타입 -제약 없음, 필요조건은 사이클 간 안정성+유일성뿐). `keyFn`이 `item`을 -그대로 받으므로 `item.id`처럼 이미 있는 안정적 필드를 그냥 반환하면 -됨(새로 뭘 만들 필요 없음). `base/slot-plan.md`의 `keyFn` tradeoff -단락에 이 확인과 `function(item) return item.id end` 관용구 예시를 -명시적으로 추가. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선). - -**같은 세션 여섯 번째 후속 — 중복 `key` 즉시 `error`로 확정.** 사용자가 -"`reconcile`이 이미 `seen[key] = true`를 하니까, 그 앞에 `if seen[key] -then error end`을 두면 거의 공짜로 중복 key를 잡을 수 있지 않냐"고 -제안 — 채택. 조용히 넘어가면 두 item이 `mounted`/`userdata`/`keyIndex`의 -같은 슬롯을 다투는 조용한 버그가 되므로, 다른 Slot CRUD 에러 조건들과 -같은 fail-fast 톤으로 그 자리에서 막음. `base/slot-plan.md`의 `reconcile` -의사코드와 `keyFn` tradeoff 단락에 반영 완료. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선). - -## 2026-08-11 다섯 번째 세션 — Slot 문서화 프레이밍 확정: "동적 렌더링을 -가능하게 하는 도구" - -짧은 문서화 톤 논의. 사용자가 "Slot을 동적렌더 가능하게 돕는 도구로 -설명하는 게 문서 톤상 정해져 있냐"고 질문 — 확인 결과 미정이었음(`base/ -slot-plan.md`는 "뮤터블 자식 배열, 엄격한 단일 마운트 소유권"이라는 더 -넓은 컴포지션 도구로, `documentation-content-map.md`도 초심자 단계에선 -"children 배열" 정적 구조 서술을 앞세우고 동적 CRUD/`:List`는 층을 나눠 -후순위로 배치해뒀었음). 사용자가 바로 확정 요청: **Slot의 요지 자체가 -"안에서 요소가 생기든 말든 자유롭다"는 것이라, "동적 렌더링을 가능하게 -하는 도구"로 프레이밍해도 됨** — 아직 미착수인 `Slot():Single(...)` -백로그(2026-08-09 여섯 번째 세션)도 같은 프레이밍의 특수 케이스(1개 -아니면 0개의 동적 렌더)일 뿐이라는 것도 사용자가 직접 짚음. - -`research/documentation-content-map.md`(modifier-plan.md/slot-plan.md -절 최상단에 프레이밍 확정 명시, 겸사겸사 옆에 있던 stale 마커 — -"Slot 형제 순서 보장 미확정"이 실제로는 2026-08-09 여섯 번째 세션에 -Length/Offset으로 이미 해소돼 있었던 것도 발견해 정정)/`base/ -slot-plan.md`(`:Single` 백로그 절에 이 프레이밍 적용 메모 추가) 반영 -완료. 새 런타임 설계는 없음 — 순수 문서 톤 결정. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션은 문서화 톤 결정이라 M0 착수 우선순위엔 영향 없음. - -## 2026-08-11 여섯 번째 세션 — `Slot:Single` 확정, Slot-in-Slot 중첩 확정, -Length/Offset `recompute` off-by-one 버그 발견·수정 - -`Slot():Single(state, updateFn?)` 백로그(2026-08-09 여섯 번째 세션, -"`State`가 offset을 못 받아서 위쪽 Slot의 offset/length를 써야 -했다"는 동기)를 실제로 설계하다가, 더 큰 질문(Slot을 다른 Slot 안에 -넣을 수 있는가)까지 라이브로 풀어낸 긴 세션. 다섯 갈래로 정리: - -**1. `Slot:Single(state, updateFn)` — `:List` 위의 순수 sugar로 확정.** -`state`를 0/1개짜리 배열로 감싸(`:Compute`) `:List`에 위임, 고정 -key(`true`)로 `prev` 재사용을 보장, `index`는 상수라 안 넘김. 원래 -동기(offset 접근)를 이걸로 완전히 해결 — "offset을 얻으려고 컴포넌트가 -Slot을 리턴하는" 우회가 필요 없어짐. `base/slot-plan.md` -"`Slot:Single(...)`" 절. - -**2. Slot-in-Slot 중첩 확정 — 동기는 카테고리 헤더가 아니라 컴포넌트 -결합의 균일성.** 사용자가 직접 짚은 진짜 이유: `SomeComponent(props)`가 -`Instance`를 리턴하든 `Slot`(멀티루트 워크어라운드)을 리턴하든 -`outerSlot:Add(result)`가 분기 없이 동작해야 함 — 지금까지 "요소 타입 -제약"이 `Slot`을 암묵적으로 배제하고 있어서 정확히 이 케이스가 막혀 -있었음. **핵심 발견 — 메커니즘은 그대로 재사용, 새 프리미티브 불필요:** -`Dispatch.setLength`/`setOffsetSource`의 첫 인자(`inst`)가 물리 -Instance일 필요가 없다는 것(`Relate`가 아무 테이블이나 weak 키로 받음)을 -재사용해, **Slot 자신을 owner 키로 같은 두 함수를 한 번 더 부르면 -최상위 마운트와 중첩 마운트가 완전히 같은 함수 호출**이 됨 — 재귀 -`attachSlot(slot, physicalTarget, ownerKey, position)` 하나로 통합. -`Slot.Length`는 raw 개수에서 "요소별 기여도의 합"(plain=1, nested -Slot=그 `.Length`)으로 의미 변경. -- **타입 레벨로 확장하려던 "모든 instance 처리를 Slot에 위임"은 기각** — - 리터럴 배열(`Dispatch.drive`)의 요소 타입 규칙(Ref/PreRef/Observer - 허용)이 Slot의 요소 타입 규칙(같은 값들 금지)과 정반대라, 타입을 - 진짜로 통합하면 "만들어진 방식에 따라 행동이 다른 Slot"이라는 숨은 - 분기가 생김 — **메커니즘(setLength/setOffsetSource/recompute)만 - 공유하고 타입/CRUD 표면은 분리 유지**로 스케일 확정(사용자 확인: - "그게 더 엔지니어링 비용이 싸고 좋은 구현"). -- **파괴는 재귀적 `Clear()`가 아니라 flat `destroySlotTree`** — 사용자가 - 직접 비용 문제 지적("clear된 다음 length 바뀌고 위치변경 전파되는 - 구조는 안 됨"): 재귀 `Clear()`(요소별 Remove 반복)는 죽는 서브트리 - 내부에서 불필요한 shift+recompute가 요소 수만큼 반복됨 — 대신 순수 - 파괴 walk(`.Destroy()`만)+`unbindLifetime` walk로 바꾸고, outer 쪽 - recompute는 자기 위치 하나에 대해서만 1회. `unbindLifetime`이 왜 꼭 - 필요한지도 새로 드러남 — `bindLifetime`은 물리 target 생명주기에 - 걸려있어 target이 살아있는 채로 논리 서브트리만 죽는 경우(카테고리 - 삭제 등) GC가 자동으로 안 치워줌, 명시적 호출 필요(물리 target 자체가 - 죽는 경우는 기존처럼 GC가 전부 처리). -- **`Slot(initial?: {T})` 생성자로 확장** — "인자 없는 빈 생성자로 - 확정"을 뒤집음(2026-08-09 세 번째 세션 결정 정정), 단 새 마운트 - 로직이 아니라 `:Add` 반복 호출 sugar(`ipairs`의 "첫 nil에서 멈춤" - 동작이 "중간 nil UB, 그 뒤 무시"를 공짜로 구현). **`initial ~= nil`이면 - 빈 테이블이어도 즉시 `_crudUsed = true`**(사용자 지적: `Slot({})`은 - 상태상 `add():remove(1)`과 동일이라 결과가 비어있어도 "CRUD를 썼다"는 - 의도는 이미 커밋됨) — `Slot()`(진짜 `nil`)만 나중에 `:List`/`:Single` - 설치 가능. **`_crudUsed` ↔ `_listed` 상호 배타 가드도 신설** — 기존엔 - `:List` 설치 후 수동 CRUD만 막았지 반대(수동 CRUD 후 `:List` 설치)는 - 안 막아서, `:List`의 reconcile이 기존 요소를 모른 채 충돌하는 gap이 - 있었음(사용자 발견). -- **DOM 백엔드가 nested Slot을 실제 `
` 중첩으로 매핑하는 안은 - 기각** — 제가 처음 낸 "web은 물리 nesting을 지원하니 이 메커니즘이 - 아예 필요 없을 수도"라는 제안을 사용자가 직접 반박: React `<>`가 - 존재하는 이유와 정확히 같은 이유로 Slot도 의도적으로 wrapper 없는 - 그룹핑 도구라, div 매핑은 그 원칙 자체를 깨버림. 숫자 기반 메커니즘은 - web에도 그대로 필요하되, `insertBefore`/`removeChild`가 물리적으로 - 밀고/당겨주므로 이미 배치된 형제 프로퍼티 재작성은 불필요(기존 - 2026-08-09 여섯 번째 세션 확정과 정합적, 사용자가 세션 도중 직접 - 재확인). "물리적으로 이전에 어디 있었는지" 같은 backend 종속 위치 - 정보는 base 책임이 아니라 필요한 backend가 자기 `Relate`로 저장할 - 몫 — 새 설계 불필요, 이미 확정된 base/backend 경계 그대로. - -**3. `Dispatch.setLength`/`setOffsetSource`/`recompute`의 owner 키가 -물리 Instance로 한정될 필요 없다는 걸 `base/bind-system-plan.md`에 -명시.** "Length/Offset" 절에 짧은 절 신설 — 이게 위 재귀 메커니즘 전체의 -근거. - -**4. `recompute`의 off-by-one 버그 발견·수정 — 중첩과 무관한, 기존 -Length/Offset 메커니즘 자체의 버그.** 구체 숫자로 흐름을 검증하려다 -발견: 원래 코드가 `sum += lengthList[i]`를 먼저 하고 `offset:Set(sum)`을 -나중에 해서, `offset[i]`가 "자기 앞의 형제들이 기여한 개수"가 아니라 -**자기 자신을 포함한** 누적합이 되고 있었음(예: `Frame{Slot1}` 하나뿐이어도 -`Slot1.Offset`이 `Slot1.Length`가 되어버림) — 순서를 뒤집어(offset 먼저 -Set, 그 다음 sum에 자기 기여도 누적) 수정. 지금까지 실제 Luau로 돌려본 -적이 없어 아무도 못 잡았던 버그. 카테고리 헤더 예시(7개 리프)로 수정된 -공식을 검증, 정확히 저작 순서대로 LayoutOrder 1..7이 나옴을 확인. -**`offset`/`sum`은 0-based 개수, `index`는 1-based Lua 관례**라는 -점도 명시적으로 콜아웃(둘을 섞는 `index+offset` 공식이 의도된 것이지 -인덱싱 불일치가 아님). - -**5. Reentrancy 가드 검토 후 기각 — 제가 처음 제안한 `_recomputing`/ -`_dirty` 플래그가 불필요함을 사용자가 정확히 캐치.** "nested Slot이 -있으면 항상 dirty가 켜져서 불필요한 for문이 한 번 더 돈다"는 사용자 -지적을 계기로 호출 경로를 다시 추적 — **각 Slot이 `Relate(자기 자신)`으로 -독립된 `bk`를 가지므로, 중첩 Slot의 Length 변경이 상위로 전파되는 경로는 -항상 서로 다른 `bk`를 거쳐 지나감**, 즉 nesting이 있다는 사실만으로는 -같은 `(ownerKey,bk)`가 재진입되는 경로 자체가 없음이 확인됨 — 제가 -가드의 동기 자체를 잘못 짚었던 것. 진짜 재진입(부작용이 recompute -도중 같은 Slot에 다시 Add/Remove)은 이미 확정된 "일반적 재진입/무한루프 -방어 안 함, 사용자 코드 버그로 간주" 원칙 그대로 두면 되므로, 가드 -없이 off-by-one만 고친 순수 버전으로 최종 확정. **같은 세션 후속으로 -이 케이스에 명시적 이름을 붙임(사용자 제안)** — `Source⊇State`의 -"단방향"(파생값이 자기 upstream Source로 거꾸로 안 쓴다) 원칙과 같은 -카테고리 위반으로 프레이밍: recompute가 만드는 `offset`/`Length`는 -`lengthList`(upstream 입력)에서 파생된 다운스트림 값인데, 부작용이 -자기 자신의 `lengthList`를 다시 mutate하는 게 그 반대 방향 쓰기라 -"State가 자기 Source에 Set을 가하는 것"과 동일한 UB로 명명 — -새 원칙이 아니라 이미 있는 단방향 흐름 원칙의 재적용. - -**반영된 파일**: `base/slot-plan.md`(요소 타입 제약/`Slot(initial)`/ -CRUD 가드/`Slot:Single`/"Slot-in-Slot 중첩" 신규 절/`Slot.Length`), -`base/bind-system-plan.md`(owner 키 일반화/`recompute` 버그 수정), -`ROADMAP.md`(M6 체크박스 다수 추가), `.claude/question.md`(`Slot:Single` -백로그 해소 표시). - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. -`Slot`의 자기 참조 제네릭 실측이 luau-test 확인 목록에 새로 추가됨. - -## 2026-08-11 일곱 번째 세션 — 반응형 raw 요소: Slot이 `State`/`Source`도 -요소로 허용(`:Single` sugar로), `:List`의 index-스킵 보강 - -짧은 세션. 사용자가 "Slot이 `state`/`state`을 안 받는데, -`setLength`/`setOffsetSource`를 재사용하면 쉽게 구현 가능하지 않냐"고 -제기하며 시작 — 최초 검토안이 사용자의 즉각적인 반례로 기각되고 훨씬 -단순한 최종안으로 수렴한 단일 스레드라, 시행착오 없이 최종 확정만 -정리(경위는 아래 "기각된 최초안" 절에 압축 보존). - -**확정**: `Slot:Add`(및 `Slot(initial)` 생성자 sugar)가 받는 `element`의 -타입이 `T | State | Source`(`T = Instance | Slot`, 여섯 -번째 세션에 확정된 자기 참조 제네릭과 합성)로 확장 — 임의 깊이 nesting -가능(사용자: "이게 얼마나 nesting되는지는 유저 마음, 다 처리가 가능함"). -**구현은 새 메커니즘이 아니라 순수 `:Single` sugar**: `isState(element)`면 -그 자리에 내부적으로 `Slot():Single(element)`(nested Slot, `updateFn` -생략 시 identity 기본값)를 대신 삽입 — - -```lua -function Slot:Add(element, index) - if isState(element) then - local sub = Slot() - sub:Single(element) -- updateFn 생략 → identity 기본값 - element = sub - end - return rawAdd(self, element, index) -end -``` - -이 sugar가 성립하도록 `Slot:Single(state, updateFn?)`도 같이 확정 — -`updateFn`을 선택 인자로 완화(기본값 `function(item) return item end`). - -**기각된 최초안 — position-keyed StoreBind 구독 + `state:Compute`로 -파생한 Length.** 처음엔 `rawAdd`가 그 위치에 대해 별도 재-dispatch -구독을 걸고 Length 기여도도 `state:Compute(...)`로 파생시켜 `setLength`에 -넘기는 방식을 검토했으나, **사용자가 즉시 반례를 제시해 기각**: "state -언랩 시에 None 되는게 복잡함. 그럼 slot 사이에 None이 존재할 수 있게 -되는 거 아님? Length 계산도 달라져야 하고, Add/Remove 동작성이 문제가 -됨 — 사실상 Slot:Single이랑 정확히 같은 구현이 돼야 함." 검증 결과 -정확함 — (1) `State`(nilable)를 지원하려면 `_elements`에 `None`을 -다시 끌어들여야 함, (2) Length 계산에 예외가 생김, (3) `Move`/`Swap`이 -인덱스를 옮길 때마다 그 위치의 구독도 같이 옮겨야 하는 인덱스-구독 -동기화 부담이 생김 — **정확히 `:List`가 element가 아니라 `key` 기준으로 -설계된 이유와 정면 충돌하는 회귀**였음. - -**위 확정된 sugar가 이 세 문제를 전부 없애는 이유**: 바깥 `_elements`엔 -항상 안정적인 `sub` Slot 레퍼런스만 있어 `None`이 안 들어가고, `sub`가 -비어있는 것 자체가 이미 지원되는 정상 상태(Length 0 자동 기여)이며, -Remove/Move/Swap도 그 `sub` 레퍼런스 하나만 다루면 끝이라 인덱스-구독 -동기화가 필요 없음. raw `State` 요소는 결국 "`updateFn` 생략한 -`:Single`"일 뿐이고, `:Single`에 `updateFn`을 직접 주면 `prev`/`userdata` -patch-reuse + `offset` 접근이 되는 상위 호환 — 둘은 대체 관계가 아니라 -같은 메커니즘의 다른 `updateFn`. - -**`:Single`이 애초에 생긴 이유가 정확히 이 offset 접근**(사용자 직접 -확인): "`Single`은 정확히는 `updateFn`에서 렌더할 때 `offset`이 필요해서 -만들어진 것" — raw `State` 요소(identity `updateFn`)는 값이 바뀔 -때마다 이전 mount를 통째로 버리고 새로 만드는 coarse swap(quad가 `prev` -재사용을 안 해줌)인 반면, `updateFn`을 직접 지정한 `:Single`은 같은 -Instance를 유지하며 속성만 patch하는 fine-grained 갱신 + `offset`을 -`updateFn`에 직접 전달. 사용자가 제시한 조합 예시(`Slot{ State ---[[리스트헤더]], Slot():List() --[[아이템]] }`)를 문서에 그대로 반영 — -LayoutOrder(offset) 참여가 필요 없는 요소는 raw `State`로 가볍게, -개수/순서가 동적이라 offset이 필요한 그룹은 `Slot():List(...)`로 -감싸는 식으로 한 Slot 안에서 자유롭게 섞어 씀. - -**부수 발견(사용자) — nested-Slot 결과를 반환하는 `:List` 아이템의 -`index` 스킵.** "상위 입장에서의 인덱스는, Slot이 있으면 크게 건너뛰겠네 -아마 의도된 동작이긴 할듯"라는 지적 — 검증 결과 맞음: `updateFn`이 -nested Slot(멀티루트 컴포넌트 결과 등, Length=N)을 반환하면 그 아이템은 -물리적으로 1개가 아니라 N개를 차지하므로, `reconcile`의 `pos` 커밋이 -고정 `+1`이 아니라 `+result.Length`여야 다음 형제의 `index`가 LayoutOrder -계산에서 안 겹침 — `pos = candidateIndex - 1 + (isSlot(result) and -result.Length:Get() or 1)`로 수정, 의도된 동작으로 확정. 남는 캐비엇(그 -Length가 outer `:List`의 reconcile 없이 나중에 바뀌면 `index`가 스냅샷인 -채로 안 갱신됨)은 이미 확정된 "`index`는 raw, 실시간 반응은 `updateFn` -몫" 원칙의 당연한 연장이라 새 문제로 취급 안 함. - -**반영된 파일**: `base/slot-plan.md`(요소 타입 제약 절 갱신, "반응형 raw -요소" 신규 절, `:Single`의 `updateFn` 선택 인자화, `reconcile`의 `pos` -커밋 공식 수정)/`ROADMAP.md`(M6)/`.claude/README.md`/`.claude/question.md`. - -**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 -우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. +## 세션 히스토리 + +전체 설계는 여러 세션에 걸쳐 대화로 확정됐음. **아래는 탐색용 압축 요약이고, +지금 유효한 설계의 소스는 항상 `.claude/base/`** — 여기 요약이나 세션 원문과 +`base/`가 어긋나면 `base/`가 맞음(더 최근 결정이 반영돼 있음). 각 항목의 +원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용)은 +`.claude/session/`에 그대로 보존돼 있음 — 결정의 배경/논쟁 과정이 궁금할 +때만 열어볼 것. + +**이 절을 갱신하는 방법(중요, 반복 방지용)**: 세션이 끝나면 (1) 전체 +서술은 `.claude/session/YYYY-MM-DD-NN-slug.md`로 새로 저장하고, (2) 여기엔 +2~4줄 압축 요약 + 그 파일로의 링크만 추가할 것. **절대 여기에 전체 논의 +과정을 그대로 쌓지 말 것** — 그게 바로 이 CLAUDE.md가 3000줄 넘게 불어나 +성능 저하를 유발했던 원인(2026-08-11 정리 세션에서 발견, 상세는 +`.claude/session/2026-08-11-08-claude-md-restructure.md` 참고). base/ +research/archive 반영이 그 세션 안에서 끝났는지 먼저 확인 후에 이 절에 +추가할 것 — 반영 안 된 게 있으면 archive/session 이전보다 그 반영이 항상 +먼저. + +**2026-08-04 — 로드맵 인수인계** (`session/2026-08-04-01-roadmap-handoff.md`) +Modifier 메커니즘 전체 확정(immutable clone 체이닝), 컴포넌트화 논의 완결 +(modifier/Ref는 named parameter로 컴포넌트 경계 통과, "멀티루트" 개념 폐기), +`.claude/` 코퍼스 전체 감사·정리, quad-base 테스트 mock 방향 확정, +`ROADMAP.md` 신설 — 설계 단계 종료, 다음 세션부터 M0 착수 예정. + +**2026-08-06 세션 — quad-debug 설계** (`session/2026-08-06-01-quad-debug-design.md`) +런타임 디버깅 플러그인 `quad-debug` 설계(백로그, "quad 개발 상당 부분 끝난 +뒤" 착수). Studio 플러그인↔Play 중 게임 간 BindableEvent 채널이 실제로 +작동함을 사용자가 직접 실측 검증. UICorner/UIPadding/UIScale 인라인 숏핸드, +Attribute 타입 파라미터화 논의 신설. + +**2026-08-06 후속 세션 — 이벤트 self 관습, rbvm GC, 코퍼스 정리** +(`session/2026-08-06-02-event-self-rbvm-corpus.md`) +이벤트 핸들러가 v1처럼 self(Instance)를 받는 관습은 채택 안 하기로 확정 +(Ref가 이미 커버, 이중 쓰기 경로 방지). `Store:Emit`/`:Compute`의 `previous` +인자/`state:Observer(fn)`/Ref 일반화(범용 값 박스로 확장) 확정. Observer +이름 확정, 생성자 스타일(`Type(args)`) 통일, "독립 프리미티브 vs 파생 +데이터" 원칙 신설. + +**2026-08-06 세 번째 세션 — 문서 사이트 구조, 프레임워크 비교, Source⊇State** +(`session/2026-08-06-03-docsite-source-state.md`) +문서 사이트 4축(초심자/api/심화/`quadnomicon`) 구조 확정. quad vs +Fusion/Vide/react-lua 정직 비교 완료. **핵심**: `Source`가 구조적으로 +`State`를 만족하는 서브타입 재구성 — `StoreSource` 프록시 폐기, +`store.key = value` 대입 문법 폐기하고 `:Set()`으로 전환. + +**2026-08-06 네 번째 세션 — M0 착수 직전 크리티컬 감사** +(`session/2026-08-06-04-pre-implementation-audit.md`) +`.claude/base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재감사, +`research/pre-implementation-audit.md` 신설(우선순위1 11개 등). 대부분은 +이후 세션에서 해소됨 — 현재 상태는 `.claude/question.md` 2번 참고. + +**2026-08-07 세션 — `:With`도 새 State 노드** (`session/2026-08-07-01-with-new-node.md`) +`:With(...)`는 clone 빌더가 아니라 매번 새 State 노드를 만드는 것으로 확정 +— 디버그 그래프가 코드 호출 체인과 1:1 대응해야 함, `:With(a,b,c)` 가변인자로 +노드 남발 문제는 해소. + +**2026-08-07 두 번째 세션 — `Modifier:Apply`** (`session/2026-08-07-02-modifier-apply.md`) +Jetpack Compose류 커스텀 확장 함수 패턴을 콤비네이터로 흉내낸 +`mod:Apply(factory)` sugar 채택 — `function(self,factory) return factory(self) end`. + +**2026-08-07 세 번째 세션 — `PreRef` 신설** (`session/2026-08-07-03-preref.md`) +base 디스패치 드라이버가 "배열(children/Ref) 먼저, 해시(프로퍼티/이벤트) +나중" 두 패스를 명시적으로 계약. `CreatedRef`의 `phase` 옵션 폐기(배열 +위치로 공짜 표현). 이벤트보다도 먼저 채워져야 하는 self-ref 전용으로 +`PreRef` 신설(호이스팅, 동적 경로 도착 시 error). + +**2026-08-07 네 번째 세션 — 코퍼스 전반 재편** (`session/2026-08-07-04-corpus-reorg.md`) +`reference/` 폴더 신설(v1 스냅샷/Fusion·Vide 비교를 온디맨드 자료로 분리), +`ui-shorthand-plan.md`를 `base/`로 승격, `Blocker`/`Effect` 확정 승격(`Batch`/ +`Context`는 기각→archive), archive 제목 컨벤션을 `[역전됨]`/`[기각됨]`으로 분화. + +**2026-08-07 다섯 번째 세션 — `Overridden`/`Peek`/`isState`, FuncSource 기각** +(`session/2026-08-07-05-override-peek-isstate.md`) +`Modifier.Override(mod1,mod2,...)`(뒤 인자가 필드 단위로 이김) 확정, +`:Peek(key)`(raw union 그대로 반환)/`isState` 신설. FuncSource(람다로 +계산+self-emit하는 Source) 기각 — 이미 확정된 원칙들의 논리적 귀결. + +**2026-08-07 여섯 번째 세션 — Ref/PreRef API, Tween GC 구조** +(`session/2026-08-07-06-ref-preref-api.md`) +Ref API를 `.Value`+`:Set`/`:Callback`/`:Wait`(전부 self 반환)로 확정, +Ref/PreRef 파일 분리. Tween per-instance 저장소가 GC-안전함을 확인. +Effect/Observer 관계 해소 시작(`state:Observer(fn)`는 등록 즉시 1회 실행). + +**2026-08-07 일곱 번째 세션 — `:Compute` 커링, `state:Apply`, 이중 바인딩 금지** +(`session/2026-08-07-07-state-apply-effect-subscribe.md`) +`:Compute`/Effect/Observer의 `fn`은 커링 스타일 권장. `state:Apply(factory)` +sugar 확정(`Modifier:Apply`와 동형). `EffectHandle:Subscribe`/`:Unsubscribe` +신설. Observer/Effect 이중 바인딩은 `Bound` 플래그로 즉시 error. + +**2026-08-07 여덟 번째 세션 — `None` 센티널, Dispatch 네이밍, `Brand`** +(`session/2026-08-07-08-none-sentinel-dispatch-brand.md`) +인라인 필드를 명시적으로 지우는 `None` 센티널 확정 — `NoneHandler`가 +`process(inst,k,nil)`로 재귀 재디스패치(Tween store-bind와 같은 패턴, 새 +메커니즘 아님). `Dispatch.getHandler`/`.process`/`.addHandler`/`.drive` 이름 +공식화. `Tag`/`Attribute` 전용 문서 신설. `Brand` 통합 판별 메커니즘(`isState`를 +10종으로 일반화) 신설. + +**2026-08-07 아홉 번째 세션 — 코퍼스 정합성 감사, `CreatedRef` 폐기** +(`session/2026-08-07-09-corpus-audit-createdref.md`) +stale 참조 12개 수정, `archive/agent-mistake.md` 신설(에이전트 자체 개념 +혼동 정정 이력 전용 카테고리). `CreatedRef` 이름 완전 폐기 — `Ref(default)`/ +`PreRef(default)` 인스턴스를 children 배열에 직접 놓으면 되므로 별도 래퍼 +불필요했음이 드러남. `PreRef` pre-pass 위치/순서/동적 경로 가드 확정. + +**2026-08-07 열 번째 세션 — 소진 슬롯은 `None`, Ref 콜백 배열은 별개** +(`session/2026-08-07-10-none-vs-nil-order.md`) +사용자가 Luau REPL로 "정수 키가 촘촘하지 않으면 순회 순서가 안 보장됨"을 +직접 실증 — 순서가 중요한 배열(PreRef pre-pass)은 `None`으로 소진. +`props.Modifier or None`/`props.Ref or None` 필수 관용구 확정. + +**2026-08-08 세션 — `Relate` 신규 프리미티브** (`session/2026-08-08-01-relate-bindlifetime.md`) +`bindLifetime`/`canExecute`(inst,value) 탑레벨 함수로 확정, 그 저장소로 +`inst`를 weak 키로 하는 범용 릴레이션 `Relate` 신설(구 `perInstanceState` +placeholder 대체). `retract` 필드는 no-op이어도 생략 불가로 확정. + +**2026-08-08 두 번째 세션 — Dispatch는 싱글톤, 네이밍 케이싱 컨벤션** +(`session/2026-08-08-02-dispatch-singleton-naming.md`) +Dispatch는 프리미티브가 아니라 탑레벨 싱글톤으로 확정(재귀 호출 배관 +비용 때문). children 배열의 Ref/Observer/PreRef leaf Handler는 +`Dispatch/Leaf.luau`(quad-base)로 확정. Handler는 "독립 프리미티브 vs +파생 데이터" 분류의 세 번째 카테고리로 명문화. 네이밍 케이싱 컨벤션 +(생성자/메소드=대문자, 탑레벨 유틸=소문자) 문서화. + +**2026-08-08 세 번째 세션 — Tag 재설계, Dispatch 체인+`retractUnder`** +(`session/2026-08-08-03-tag-redesign-dispatch-chain.md`) +`Tag`를 해시 파트 boolean 키에서 array-part 값 객체(`Tag(...)`, `:Added`/ +`:Removed`/`:Contains`/`:Apply`/`Merged`)로 재설계 — 상호배타 스타일 +상태 표현이 쉬워짐. 이 재설계로 "retract가 실제로 필요한 첫 사례"가 +드러나, Dispatch가 `(inst,k)`별 핸들러 체인을 직접 소유하고 +`Dispatch.retractUnder`가 꼬리부터 정리하는 모델로 확정(다단 재귀 +위임의 retract 전파 문제 해결). + +**2026-08-08 네 번째 세션 — `Overridden` 이름 확정** (`session/2026-08-08-04-overridden-naming.md`) +`Override`→`Overridden`(불규칙 과거분사)으로 이름 확정 — `-ed`/분사 어미가 +"즉시 커밋 뮤테이션이 아니라 계산되어 반환되는 새 값"을 신호한다는 +`Tag`의 `Added`/`Removed` 관례와 통일. + +**2026-08-08 다섯 번째 세션 — 용어 정리 라운드** (`session/2026-08-08-05-terminology-round.md`) +`Ref`/`PreRef`/`Peek`/`isState`/`None`/`NoneHandler`/"프로바이더"→`Handler` +전부 이름 확정. `DI`→`D`, `canExecute`→`isAlive`는 계속 미정으로 재확인. + +**2026-08-09 세션 — `canBound`, Modifier 핸들러 값 UB→error** +(`session/2026-08-09-01-canbound-modifier-error.md`) +`Bound`→`canBound(handle)` 탑레벨 함수로 확정. `:Compute`의 `previous` +인자는 오버엔지니어링 의심 기각(현재 설계 유지), 스코핑을 결과 State +노드 자신에 귀속으로 명확화. Modifier 필드/`State`에 핸들러 +계층 값(Ref/PreRef/Observer/Effect/Slot/Modifier)이 들어오면 UB 대신 +즉시 error로 전환(Slot만 예외 — 정상 dispatch 참가자라 계속 허용). +Tween `initValue`/`useTween` 논의 신설(미확정). + +**2026-08-09 두 번째 세션 — 코퍼스 stale 감사, 무효화 서사 archive 이전** +(`session/2026-08-09-02-corpus-audit-archive-move.md`) +`.claude/` 전체 stale 마커/모순 7개 파일 수정. 뒤집힌 설계가 정정 표시만 +붙은 채 본문에 전체 서술로 남아있던 4곳을 `archive/*-rejected.md`로 이전 +(quad2-try 리서치 전문, Observer cleanup 계약 기각, 키드 컬렉션 State +메소드 기각, quad-debug ReplicatedStorage 채널 기각). + +**2026-08-09 세 번째 세션 — Slot CRUD 완전 확정, `Slot:List`** +(`session/2026-08-09-03-slot-crud-list.md`) +`Add`/`Remove`/`Extract`/`Clear` CRUD 확정(당시엔 element 레퍼런스 기준 — +이후 열한 번째 세션에서 인덱스 기준으로 재정정됨). 키 기반 동적 컬렉션 +재조정이 `Slot:List(data, updateFn, keyFn?) -> Slot` 메소드로 통합·승격 +(Fusion `ForPairs`/`ForKeys`/`ForValues` 3분할을 하나로). `Move`/`Swap` +CRUD 추가, Slot 요소 타입 제약(`nil`/핸들러 계층 값 금지) 확정. +`renderFn`→`updateFn` 개명, `Source` 생성 권한을 `:List`가 아니라 +`userdata`로 이전. + +**2026-08-09 여섯 번째 세션 — Length/Offset, `unbindLifetime`** +(`session/2026-08-09-06-length-offset-unbindlifetime.md`) +여러 Slot이 형제로 섞일 때 순서 보장을 "각 위치가 앞 형제 개수 누적합만 +알면 됨" 모델로 완전히 풂 — `Dispatch.setLength`/`Dispatch.setOffsetSource` +신설, 각 원소 `LayoutOrder`는 기존 store-bind 재실행 모델에 얹힘(새 +메커니즘 없음). `bindLifetime`의 조기 해제를 위한 `unbindLifetime` 신설. + +**2026-08-09 일곱 번째 세션 — `Slot:List` 구독도 lazy `bindLifetime`** +(`session/2026-08-09-07-list-observer-lazy.md`) +`data:Observer(fn)` 구독이 `:List()` 호출 즉시 만들어져 `inst`를 몰라 +`bindLifetime`이 안 걸려있던 gap 발견·수정 — Slot 컨테이너 마운트 시점에 +lazy하게 구독하도록 변경, Destroy 후 재실행 gap 해소. + +**2026-08-09 여덟 번째 세션 — `base/` 전체 중간검토(질문 모드)** +(`session/2026-08-09-08-midreview-defects.md`) +서브에이전트로 `base/` 전체를 그라운딩된 리스팅으로 뽑아 6배치로 나눠 +사용자가 직접 확인 — 24개 질문 중 1/3에서 실제 설계 결함 발견·즉시 수정 +(Ref 콜백 배열 소진을 `None`→`nil`로 되돌림, Slot CRUD를 인덱스 기준으로 +재정정, `isRef`/`isPreRef` 포함관계 재정정 등). 상세는 파일 참고. + +**2026-08-09 열두 번째 세션 — `.claude/luau-test/` 신설** +(`session/2026-08-09-12-luau-test-spikes.md`) +M0가 검증해야 할 스파이크 항목을 독립 실행 스크립트 15개로 미리 작성 — +사용자가 `luau`/`luau-analyze`/`luau-lsp`/Studio로 직접 돌려보기로 함. +**아직 결과 미확인** — M0 착수 전 최우선 확인 대상. + +**2026-08-10 세션 — `Slot:Add`가 삽입 인덱스 반환** +(`session/2026-08-10-01-slot-add-return-index.md`) +`Slot:Add(element, index?): number`로 확정(계산된 위치를 공짜로 반환). +범위 밖 `index`는 clamp 대신 즉시 error. + +**2026-08-10 세션 — 동적 자식 추가/제거는 `Slot`/`state`만 정당** +(`session/2026-08-10-02-dynamic-children-ub.md`) +quad가 마운트한 부모 Instance에 `Slot`/store-bind 경로를 안 거치고 직접 +`.Parent =` 대입하는 것은 `Length`/`offset` 계산을 조용히 깨뜨리는 UB — +기존 문서에 빠져있던 갭을 명문화만 함(새 방어 로직 없음). + +**2026-08-10 두 번째 세션 — Tween 전면 재설계** (`session/2026-08-10-03-tween-redesign.md`) +독립 Dispatch 핸들러(우선순위 경쟁하는 특수 bind key) 모델을 폐기하고, +`Tween(opts) -> Tween` 값-레벨 래퍼로 전환 — PropertyHandler가 +`realv`를 다 풀어낸 뒤 `isTween(realv)`로 직접 분기. 이걸로 +"일반 반응형 바인딩도 Tween 파일을 거쳐가는가"라는 오래된 구조적 모호함 +(`pre-implementation-audit.md` 1-1)이 구조적으로 해소됨. 3-상태 릴레이션 +슬롯으로 진입 애니메이션 버그 방지, `T'=T|Tween` 타입 치환만으로 해결. + +**2026-08-10 세 번째 세션 — `OnChange` 특수 키** (`session/2026-08-10-04-onchange-key.md`) +`GetPropertyChangedSignal` 바인딩용 `OnChange(name)` DI 키 신설 — `Attribute`와 +달리 제네릭 타입 파라미터 없음(콜백 타입은 인라인 명시). 전부 quad-roblox. + +**2026-08-11 세션 — `:Compute(fn, ...)` trailing-args sugar** +(`session/2026-08-11-01-compute-trailing-args.md`) +`:Compute`가 trailing args로 추가 의존성을 바로 구독하는 sugar 채택(이미 +만들 노드에 엣지만 얹는 진짜 공짜 최적화). `Effect`/`Observer`는 의도적으로 +제외 — 다중 의존성 병합은 실제 새 노드가 필요해 `:With`를 명시적으로 +남겨야 함. + +**2026-08-11 두 번째 세션 — trailing deps를 `fn`에 위치 인자로 노출** +(`session/2026-08-11-02-trailing-deps-positional.md`) +trailing args `a,b,c`를 `fn(self,a,b,c)`처럼 값 자체로도 노출하는 안 채택 +— 커링 시 중복/드리프트 위험 해소. `previous`와의 타입 팩 순서 충돌 발견, +`.claude/luau-test/15` 실측 항목 신규. + +**2026-08-11 세 번째 세션 — `previous`는 팩 앞** (`session/2026-08-11-03-previous-before-pack.md`) +직전 세션의 "`previous`는 팩 뒤" 순서가 Luau 문법 제약(제네릭 팩은 맨 끝만 +가능)과 부딪힐 가능성이 높다는 걸 발견 — `fn(self, previous?, ...deps)`로 +정정(구조적으로 유일하게 안전한 순서). + +**2026-08-11 네 번째 세션 — `Slot:List`는 `LayoutOrder`를 자동 세팅 안 함** +(`session/2026-08-11-04-slot-list-layoutorder-index.md`) +"Handler가 마운트 시점에 `LayoutOrder`를 자동 바인딩해준다"는 원 서술이 +매직이라는 지적으로 정정 — `Slot.Offset`/`index`(raw number)만 `updateFn`에 +전달하고 실제 반영은 전부 `updateFn` 몫. `candidateIndex` 트릭으로 +"버림/다시 그림/source만 갱신" 세 갈래를 단일 forward pass로 정리, 이중 +write 제거. + +**2026-08-11 다섯 번째 세션 — Slot 문서화 프레이밍** (`session/2026-08-11-05-slot-doc-framing.md`) +Slot을 "동적 렌더링을 가능하게 하는 도구"로 문서화하기로 확정(순수 톤 결정, +새 런타임 설계 없음). + +**2026-08-11 여섯 번째 세션 — `Slot:Single`, Slot-in-Slot 중첩** +(`session/2026-08-11-06-slot-single-nesting.md`) +`Slot():Single(state, updateFn?)`을 `:List` 위의 sugar로 확정. Slot을 다른 +Slot 안에 넣을 수 있는 Slot-in-Slot 중첩 확정 — `Dispatch.setLength`/ +`setOffsetSource`를 Slot 자신을 owner 키로 재귀 호출하는 것만으로 풀림(새 +프리미티브 불필요). `Dispatch.drive`의 `recompute` off-by-one 버그(offset이 +자기 자신을 포함해 누적되던 것)를 실측 시뮬레이션 중 발견·수정. + +**2026-08-11 일곱 번째 세션 — 반응형 raw 요소** (`session/2026-08-11-07-reactive-raw-elements.md`) +`Slot:Add`가 `State`/`Source`도 요소로 받도록 확장 — 내부적으로 +`Slot():Single(element)`를 대신 삽입하는 순수 sugar(최초 검토한 별도 +position-keyed 구독 안은 `None`/Length/Move-Swap 문제로 기각). nested +Slot을 반환하는 `:List` 아이템은 `.Length`만큼 다음 형제 `index`를 +건너뛰도록 `reconcile`의 `pos` 커밋 공식도 같이 수정. + +**2026-08-11 여덟 번째 세션 — CLAUDE.md 재구조화(3000줄+ → 세션 로그 분리)** +(`session/2026-08-11-08-claude-md-restructure.md`) +CLAUDE.md가 세션 로그 누적으로 3196줄까지 불어나 컨텍스트 성능 저하를 +유발한다는 사용자 지적 — 세션별 전체 원문을 `.claude/session/`(38개 +파일)으로 이전하고, CLAUDE.md엔 세션당 2~4줄 요약+링크만 남기는 구조로 +재편. `base/`/`research/`/`question.md`가 이미 매 세션 반영을 성실히 +해왔음을 `README.md`/`question.md` 대조로 확인(반영 누락 없음 — archive +이전 전 처리 불필요). 새 서사가 CLAUDE.md에 다시 쌓이지 않도록 "이 절을 +갱신하는 방법" 절을 세션 히스토리 맨 위에 명문화. diff --git a/ROADMAP.md b/ROADMAP.md index 9243e7f..e5475f3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -482,7 +482,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 대체·해소됨) — `.claude/question.md` 1번, 최종 이름 확정되는 대로 아무 시점에나 - [ ] 각 마일스톤 완료 시 `.claude/qa-request/`/`.claude/archive/`에 기록, - 필요하면 `CLAUDE.md` "최근 세션 요약"도 갱신 + 필요하면 `CLAUDE.md` "세션 히스토리"도 갱신(전체 원문은 + `.claude/session/`에, CLAUDE.md엔 2~4줄 요약+링크만 — 2026-08-11 + 재구조화 세션 참고) ## 백로그 (스코프 밖 — 필요성이 실제로 드러나면 그때 설계)