6차 라운드 + 컴포넌트화/Modifier 논의, 문서 코퍼스 전체 정리 결과 반영

- 6차 라운드: 태그 네임스페이싱(Ref로 충분), Store가 Store를 담지 않음 확정
- Modifier 메커니즘 전체 확정(정적 merge, immutable+clone 체이닝, State
  필드 지원, "관측해야 실체화된다" 전역 원칙) — base/modifier-plan.md 신설
- 컴포넌트화 논의 시작(research/component-composition-plan.md) — 컴포넌트=
  플레인 함수, State/Source 읽기·쓰기 경계, StoreSource 프록시까지 수렴,
  modifier/Ref의 컴포넌트 경계 통과 방식은 열린 채로 남김
- .claude/ 코퍼스 전체(약 15개 문서)를 서브에이전트로 감사해 여러 라운드에
  걸쳐 쌓인 모순/중복/stale 마커/끊긴 참조 다수 수정
- purity-and-effects-plan.md를 research/에서 base/로 승격
- CLAUDE.md: 라운드별 인수인계 메모 3개를 하나로 통합, 오래된 "더 이상 열린
  질문 없음" 모순 제거
- question.md: 시간순도 우선순위순도 아니던 구조를 "지금 열려있는 것" 중심
  으로 재정리, 용어 정리 제안(State/DI/PerInstanceState 등 우선순위) 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-04 15:49:28 +09:00
parent c00e2e67d6
commit c19e82f661
Signed by: qwreey
GPG key ID: D28DB79297A214BD
16 changed files with 585 additions and 382 deletions

View file

@ -14,7 +14,7 @@
| `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 | | `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 |
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 |
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 | | `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + PA님 실 코드(`artworks/`, 4차 라운드 교차검증 근거) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `base/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
`research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시 `research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시
`qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만 `qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만
@ -26,20 +26,22 @@
|---|---| |---|---|
| `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서) | | `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서) |
| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 | | `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 |
| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료 | | `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료(전파 모델 등 일부 서술은 이후 라운드에서 뒤집혔으니 `bind-system-plan.md` 쪽을 최신으로 볼 것) |
| `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 | | `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 |
| `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` | | `store-semantics.md` | Store는 부작용 허용이 기본. State는 Store 위의 조합 가능한 캐시 레이어로 실제로 필요함(2026-08-04 정정) — 온톨로지 핵심 메커니즘은 2026-08-04 2차 라운드에서 확정, 최신 상세는 `base/bind-system-plan.md` |
| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드) | — | | `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, Store/State/Source 온톨로지 + 인체공학 질문 전부 확정. 디스패치 엔진은 `quad-base`가 인터페이스로 소유(2026-08-04 5차 라운드) |
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 | — | | `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 |
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 | — | | `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 |
| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정, getter 이름만 남음 |
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 |
## `research/` — 아직 착수 전, 상의 필요 ## `research/` — 아직 착수 전, 상의 필요
| 문서 | 내용 | 우선순위 | | 문서 | 내용 | 우선순위 |
|---|---|---| |---|---|---|
| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 | | `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 |
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | 하 — 문서화 성격, 급하지 않음 |
| `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 |
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, `StoreSource` 프록시 — 핵심 골격 수렴, modifier/Ref가 컴포넌트 경계를 어떻게 통과하는지만 남음 | 상 — 사용자가 "가장 문제되는 부분"으로 직접 지목 |
## 참고 ## 참고

View file

@ -31,17 +31,36 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
내장되어 store 컴퓨티드 바인드도 가능해야 함. 내장되어 store 컴퓨티드 바인드도 가능해야 함.
5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/ 5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/
`Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유. `Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유.
네임스페이싱 문제는 있지만(`.claude/question.md` 참고) 별도 네임스페이스 네임스페이싱 문제는 있지만 별도 네임스페이스 개념을 추가하면 라이브러리
개념을 추가하면 라이브러리 복잡도가 너무 올라간다고 판단 — 당장은 복잡도가 너무 올라간다고 판단 — 당장은 TagService 그대로 사용. **대신
TagService 그대로 사용. **대신 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가 아니라 "외부에서 이미
아니라 "외부에서 이미 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/ 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/래핑하기 위해
래핑하기 위해 직접 참조를 얻는 것"(`base/bind-system-plan.md`의 Ref 절 직접 참조를 얻는 것"(`base/bind-system-plan.md`의 Ref 절 참고) — 둘을
참고) — 둘을 혼동하지 말 것. 혼동하지 말 것.
- **2026-08-04 6차: 네임스페이싱 충돌을 심각하게 안 보는 이유 확정.**
충돌을 피해야 하는 단위는 보통 컴포넌트 단위로 나오고, 그 경우는 Ref로
직접 참조를 얻으면 되므로 태그 자체의 전역 네임스페이스가 굳이 필요
없음. 태그는 원래 주로 스타일링(스타일시트 셀렉터) 용도인데, 스타일시트는
적용 위치가 트리 상위에 존재해야 하고 사용자가 직접 그 위치에 심어야
하는 등 스크립팅으로 구성하기 어려워 quad 같은 UI 라이브러리에서는 잘
안 쓰는 접근 — 그래서 스타일시트 대신 modifier kit을 제공하는 것(아래
7번 항목의 modifier 우선순위 규칙 참고).
6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말 6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말
편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양. 편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양.
7. **Style(Default) 시스템 폐기.** Roblox 자체 스타일시트를 쓰는 게 낫다고 판단. 7. **Style(Default) 시스템 폐기.** 대신 modifier(spread되는 값, `...`으로
대신 modifier(spread되는 값, `...`으로 풀리는 것)를 지향 — 함수형 modifier가 풀리는 것)를 지향 — 함수형 modifier가 store 바인드를 받을 수도 있음.
store 바인드를 받을 수도 있음. (초기 근거였던 "Roblox 자체 스타일시트를 쓰는 게 낫다"는 6차 라운드에서
갱신됨 — 위 5번 항목의 6차 추가분 참고: 스타일시트는 적용 위치 제약과
스크립팅 난이도 때문에 오히려 안 쓰기로 하고 modifier kit으로 대체함.)
- **2026-08-04 세션: modifier 메커니즘 전체 확정, 상세는
`research/modifier-plan.md`로 분리.** 요지만: 런타임 pluggable 핸들러가
아니라 디스패치 이전에 정적으로 flatten되는 값(핸들러 레지스트리 미참여,
CSS cascade 문제 회피). Merge 우선순위는 "배열 순서상 나중 modifier가
우선"과 "인라인 키는 modifier보다 무조건 우선"이라는 독립된 두 규칙(Lua
테이블 리터럴이 배열/해시 파트 간 소스 순서를 보존 안 하므로 하나로 합칠
수 없음). 값은 immutable — 체이닝 메소드(`:FontSize(...)`류)는 항상
`table.clone` 후 반환, 원본 mutate 금지(형제 서브트리 오염/재렌더 드리프트
방지, 비용은 무시 가능한 수준으로 확인됨).
8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""` 8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""`
같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로 같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로
구현(`base/bind-system-plan.md`). 구현(`base/bind-system-plan.md`).
@ -59,9 +78,10 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
확정으로 재확인 — 더 이상 열린 질문 아님.) 확정으로 재확인 — 더 이상 열린 질문 아님.)
12. **멀티 타겟(pluggable 백엔드) — 특히 GTK 지원까지 염두.** Roblox 전용 렌더 12. **멀티 타겟(pluggable 백엔드) — 특히 GTK 지원까지 염두.** Roblox 전용 렌더
기술(react.lua, Fusion)은 결국 외부 개발자 유인이 없어 발전이 더딜 거라는 기술(react.lua, Fusion)은 결국 외부 개발자 유인이 없어 발전이 더딜 거라는
문제의식. 결과적으로 `plug/roblox`, `plug/base` 정도로 나뉠 전망 — base가 문제의식. 결과적으로 `quad-base`/`quad-roblox`로 나뉨(5차 라운드에서 확정된
가상돔 없이도 프로바이더 패턴으로 백엔드를 받는 인터페이스만 정의하고, 정확한 패키지 이름, 아래 "구현 착수" 절 참고) — base가 가상돔 없이도
실제 Roblox 구현은 `quad-roblox` 격 서브패키지가 담당. 프로바이더 패턴으로 백엔드를 받는 인터페이스만 정의하고, 실제 Roblox 구현은
`quad-roblox`가 담당.
13. **모듈은 기본 싱글톤, `New()`는 나중에.** 한 Lua 스레드에서 Roblox/비-Roblox 13. **모듈은 기본 싱글톤, `New()`는 나중에.** 한 Lua 스레드에서 Roblox/비-Roblox
프로바이더를 동시에 쓸 일이 거의 없을 거라 판단 — 필요해지면 그때 `New()` 프로바이더를 동시에 쓸 일이 거의 없을 거라 판단 — 필요해지면 그때 `New()`
추가. 추가.
@ -137,21 +157,22 @@ quad/
Tween/purity/existing-instance-bind는 여전히 `research/`에 남아있고 이 Tween/purity/existing-instance-bind는 여전히 `research/`에 남아있고 이
구조 확정을 막지 않음. 구조 확정을 막지 않음.
## 아직 미정 (research/로 분리됨) ## Store/State/Source 온톨로지 — 확정됨 (요약)
Tween 플러깅, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 — Store는 source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할
`.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`. 바인드 때마다 그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를
디스패치/Slot/모듈 라이프사이클은 위 "구현 착수" 섹션대로 확정되어
`.claude/base/`로 승격됨(`bind-system-plan.md`/`module-lifecycle-plan.md`/
`slot-plan.md`).
**Store/State/Source 온톨로지(2026-08-04 두 라운드에 걸쳐 확정)**: Store는
source(실제 값이 존재하는 단일 지점) 집합체이고, `store.key`로 접근할 때마다
그 source를 감싸는 새 State(자기 고유 value 없는 조합 가능한 캐시)를
반환한다. 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) — 반환한다. 전파는 push-invalidate(신호만)/pull-recompute(`Get()` 시점) —
Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는 Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는
쓰기 대상이 아니고(값 쓰기는 항상 Store의 `__newindex`), 값 하나만 다룰 땐 쓰기 대상이 아니고(값 쓰기는 항상 Store의 `__newindex`), 값 하나만 다룰 땐
Store와 별개인 가벼운 `Source` 프리미티브를 씀. 남은 건 정확한 API 이름과 Store와 별개인 가벼운 `Source` 프리미티브를 씀. `store.key` dot-access를 타입
"`store.key` dot-access를 타입 추론 1급 경로로 삼는다"는 제안의 정식 확인 추론 1급 경로로 삼는 것도 3차 라운드에서 정식 확정됨 — **더 이상 열린 질문
뿐 — `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절, 아님**, 남은 건 정확한 API 이름뿐. 상세는 `base/bind-system-plan.md`
`.claude/question.md` 참고. "Store/State/Source 온톨로지" 절 참고.
## 아직 미정 (research/로 분리됨)
Tween 플러깅, 이미 생성된 인스턴스에 대한 바인드, 컴포넌트가 modifier/Ref를
경계 너머로 어떻게 전달하는지 — `.claude/research/` 각 문서 참고, 전체 색인은
`.claude/README.md`. 바인드 디스패치/Slot/모듈 라이프사이클은 위 "구현 착수"
섹션대로 확정되어 `.claude/base/`로 승격됨(`bind-system-plan.md`/
`module-lifecycle-plan.md`/`slot-plan.md`).

View file

@ -122,10 +122,14 @@ Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract`
아니면 아예 다른 값으로 둬야 하는가? table/number 같은 프리미티브 타입이나 아니면 아예 다른 값으로 둬야 하는가? table/number 같은 프리미티브 타입이나
ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플러그를 만드는 걸로." ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플러그를 만드는 걸로."
이 문서의 제안: "Store 안의 값이 Store"인 경우도 그냥 하나의 (key,value) 쌍일 **2026-08-04 6차 확정: 그런 경우는 없다고 본다.** 위에 적힌 "재실행 래핑으로
뿐이고, 그 값 타입(Store)을 인식하는 핸들러가 pluggable 레지스트리에 등록되어 기계적으로는 커버 가능하다"는 제안은 메커니즘상 틀리지 않지만, 실제 설계
있으면 됨 — 위 "재실행 래핑" 방식과 동일한 메커니즘으로 커버됨. 별도 특수 의도와 안 맞음 — Store는 Source에 준하는 존재로 모든 반응형 값의 "시작점"
케이스 코드 불필요. 역할만 함. 시작점은 다른 변화하는 무언가에 연결되는 것을 제공하고자 하지
않음(= Store가 다른 Store/State를 값으로 담아 자동으로 따라가게 하는 용도로
쓰지 않음). Store에서 값을 꺼내 State를 옵저빙하다가 콜백으로 다른 Store 값을
바꾸는 식의 수동 연결은 있을 수 있지만, 잘 짜인 UI에서 실사용 사례를 거의
보지 못했다는 게 사용자 판단 — 그래서 이 케이스를 위해 별도로 신경 쓰지 않음.
## Ref — 도입 확정, 단 용도는 재정의됨 ## Ref — 도입 확정, 단 용도는 재정의됨
@ -167,29 +171,29 @@ ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플
**채택 방향**: `:With(...)`로 필요한 의존성을 모으고, 그 뒤 `:Compute(function() **채택 방향**: `:With(...)`로 필요한 의존성을 모으고, 그 뒤 `:Compute(function()
... end)`에서 **`with`한 값을 포지셔널 인자로 받지 않고 클로저로 직접 읽는다** ... end)`에서 **`with`한 값을 포지셔널 인자로 받지 않고 클로저로 직접 읽는다**
(정확히 어떤 방식으로 "직접 읽는지"는 아래 열린 질문 — `:fromState` 후보 참고). (정확히 어떤 방식으로 "직접 읽는지"는 2차 라운드에서 확정 — self/with 값 둘 다
lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/
`:Compute`" 부분 참고).
## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 기술적 난이도 미확정 ## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨
**중요한 배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐. **배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
이상적으로는 store에서 한 값을 추적(track)하면 "State"가 나오고, 거기에 이상적으로는 store에서 한 값을 추적(track)하면 "State"가 나오고, 거기에
`compute`를 적용하면 또 다른 "State"가 나오는 식 — Unix의 `(cat a; cat b) | while `compute`를 적용하면 또 다른 "State"가 나오는 식 — Unix의 `(cat a; cat b) | while
read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최종 목표. `:With` read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최종 목표. `:With`
두 번째 인자(`b`)도 다른 `:Compute`의 결과물(State)을 그대로 받을 수 있어야 두 번째 인자(`b`)도 다른 `:Compute`의 결과물(State)을 그대로 받을 수 있어야
이상적. 이상적.
**미해결 긴장 관계**: 이걸 구현하는 두 갈래 방식이 있고 어느 쪽이 맞는지 아직 **해소됨(2차 라운드) — 두 갈래 방식 중 실질적으로 옵션 2 방향으로 정리됨**:
결정 안 됨: 당시엔 (1) Compute 체인이 자기 자신을 mutable하게 바꾸는 방식(엔지니어링 비용
1. **Compute 체인이 항상 자기 자신을 mutable하게 바꾼다** — 엔지니어링 비용은 낮지만 공유/합성이 깨짐) vs (2) 명시적 `State:fromState(state)`류 비-mutating
낮지만, 다른 코드가 나중에 그 체인 뒤에 새 compute를 붙이는(다른 소비자가 생성자(합성은 안전, 비용 미확정) 둘로 긴장이 있었으나, 실제 확정된 모델은
동일 State에 독립적으로 파생값을 추가하는) 것이 불가능해짐 — 공유/합성이 아래 "Store/State/Source 온톨로지" 절의 **`state(state)`로 기존 state의
깨짐. 결과를 받아 새 state를 만드는 조합**임 — 매번 새 State를 만든다는 점에서
2. **명시적 `State:fromState(state)`류의 비-mutating 생성자** — 합성은 옵션 2와 같은 축(비-mutating)이고, 별도 `fromState`/`Pipe` 콤비네이터 타입
안전해지지만 엔지니어링 비용이 더 큼(정확히 얼마나 큰지 미확정). 없이도 `state(state)` 하나로 충분하다는 게 최종 결론(`Pipe` 후보는 폐기).
`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 같은
이건 `base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 축의 해법.
같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가
아직 검증 안 됨.
## Store/State/Source 온톨로지 — 핵심 메커니즘 확정 (2026-08-04 2차 라운드) ## Store/State/Source 온톨로지 — 핵심 메커니즘 확정 (2026-08-04 2차 라운드)
@ -228,6 +232,22 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦**
- `emit`은 이 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함 - `emit`은 이 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함
("emit 필요 여부" 열린 질문은 이걸로 해소). ("emit 필요 여부" 열린 질문은 이걸로 해소).
**전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)**
위 pull-recompute 규칙을 State 하나의 재계산 메커니즘으로만 읽지 말고,
프로젝트 전역에 적용되는 원칙으로 명시함: **어떤 파생값도 `.value`/`Get()`로
직접 읽히기(관측) 전까지는 계산되지 않는다.** 이 원칙은 State 자체뿐 아니라,
State를 필드 값으로 담고 있는 다른 구조(예: `base/modifier-plan.md`
Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 읽으면 그
순간이 바로 관측이고, 그 순간 계산이 확정됨.
**주의 — 구조적 복사는 관측이 아님.** `table.clone`처럼 테이블 레퍼런스만
복사하는 연산은 안에 담긴 State 핸들을 그대로 옮길 뿐 `.value`/`Get()`을
호출하지 않으므로 관측이 아니고, 계산을 트리거하지 않음. Modifier 체이닝
메소드가 `table.clone` 후 필드를 덮어쓰는 것(위 "Immutable 값 + clone 기반
체이닝")과 이 원칙이 충돌하지 않는 이유가 바로 이것 — clone은 그저 참조
복사라 State 필드는 클론 이후에도 여전히 살아있는 lazy 핸들로 남음.
**`:With`/`:Compute` — self 인자도 lazy 핸들로 통일** **`:With`/`:Compute` — self 인자도 lazy 핸들로 통일**
- 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제 - 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제
@ -278,9 +298,10 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦**
레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열 레코드 타입으로 지으면 일반 구조적 필드 타이핑으로 자동 해결되고, 문자열
리터럴 narrowing 문제 자체가 안 생김. `store "key"` 문자열 커링은 동적 리터럴 narrowing 문제 자체가 안 생김. `store "key"` 문자열 커링은 동적
키가 필요할 때 쓰는 미타입(`State<any>`) 폴백으로 격하. 키가 필요할 때 쓰는 미타입(`State<any>`) 폴백으로 격하.
- 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성(`DI.Frame`)/이벤트 - 이 패턴은 Store에만 국한되지 않고 **인스턴스 생성까지 관통하는 프로젝트
(`On.EventName`)까지 관통하는 프로젝트 전역 관습으로 확정**됨 — 아래 전역 관습으로 확정**됨 — 단 이벤트는 이후 4차 라운드에서 이 관습의
"인스턴스 생성 / 이벤트 네이밍 인체공학" 절 참고. **유일한 예외**로 빠졌음(PA님 방식인 문자열 키+런타임 리플렉션으로 전환).
아래 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절이 최신 확정 내용.
**`Pipe`(quad2-try 후보)는 폐기 확정** — 별도 `Pipe` 타입에 소유권/버전 **`Pipe`(quad2-try 후보)는 폐기 확정** — 별도 `Pipe` 타입에 소유권/버전
가드를 넣어 재설계하는 대신, State 자체가 파이핑 결합체이고 가드를 넣어 재설계하는 대신, State 자체가 파이핑 결합체이고
@ -359,15 +380,16 @@ State/스트림)를 다뤘던 이전 시도가 있었음. 조사 결과 요약:
**건질 만한 것 (인체공학/아이디어만, 코드는 아님):** **건질 만한 것 (인체공학/아이디어만, 코드는 아님):**
- **`store:Pipe(key):Compute(fn)` 같은 왼쪽에서 오른쪽으로 읽히는 파이프 - **`store:Pipe(key):Compute(fn)` 같은 왼쪽에서 오른쪽으로 읽히는 파이프
문법 자체**는 목표로 유지할 가치가 있음. 문법 자체**는 목표로 유지할 가치가 있음.
- **`Pipe`가 mutate-vs-`fromState` 긴장 관계에 제시 절충안** — "체이닝된 - **`Pipe`가 mutate-vs-`fromState` 긴장 관계에 제시했던 절충안** — "체이닝된
`Compute`/`Add`/... 호출은 자신이 액션 리스트의 유일한 '끝(tip)'일 때만 공유 `Compute`/`Add`/... 호출은 자신이 액션 리스트의 유일한 '끝(tip)'일 때만 공유
배열에 그대로 append(뮤테이션), 이미 다른 코드가 그 지점 이후로 체인을 배열에 그대로 append(뮤테이션), 이미 다른 코드가 그 지점 이후로 체인을
확장해버렸다면 배열을 복사한 뒤 새 `Pipe` 객체를 반환"하는 **copy-on-write 확장해버렸다면 배열을 복사한 뒤 새 `Pipe` 객체를 반환"하는 **copy-on-write
방식** — 이건 이 문서의 "mutate-in-place vs `fromState`" 긴장을 실제로 방식** — 한때는 이 문서의 "mutate-in-place vs `fromState`" 긴장을 풀어보려
풀어보려 한 유일한 시도라 **quad-v2에서 제대로 다시 설계해볼 만한 후보**. 한 유일한 시도로서 다시 설계해볼 후보였으나, **아래 "종합"에서 최종적으로
단, 원본은 "내가 지금 유일한 tip인가" 체크에 소유권/버전 관리가 전혀 없어서 폐기됨** — `state(state)` 조합 모델이 소유권/버전 가드 없이도 같은 문제를
경쟁 상황에 취약했고 테스트/실사용 검증도 없었음 — **그대로 베끼지 말고, 더 간단히 풀어서 이 절충안 자체가 불필요해짐. 원본이 갖고 있던 진짜 결함
같은 아이디어를 소유권 가드를 제대로 넣어 재설계할 것.** (소유권/버전 관리 없이 경쟁 상황에 취약, 테스트/실사용 검증도 없었음)은
기록으로만 남김.
- **`Depend(...)` 액션** — 계산값에는 관여하지 않고 오직 "이 소스가 바뀌면 - **`Depend(...)` 액션** — 계산값에는 관여하지 않고 오직 "이 소스가 바뀌면
다시 계산하라"는 추가 의존성만 등록하는 값-투명(value-transparent) no-op 다시 계산하라"는 추가 의존성만 등록하는 값-투명(value-transparent) no-op
액션. 작지만 깔끔한 아이디어라 이름 그대로 채택할 만함. 액션. 작지만 깔끔한 아이디어라 이름 그대로 채택할 만함.
@ -472,7 +494,7 @@ Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근
인덱스라 지금 quad-v2 스코프 밖 — Instance가 아닌 데이터에 태깅이 필요해질 인덱스라 지금 quad-v2 스코프 밖 — Instance가 아닌 데이터에 태깅이 필요해질
미래 시나리오를 위한 참고 자료로만 기록. 미래 시나리오를 위한 참고 자료로만 기록.
- **Store/State 전파 모델, 라이프사이클 — 둘 다 재검토 후 기존 확정 유지** - **Store/State 전파 모델, 라이프사이클 — 둘 다 재검토 후 기존 확정 유지**
(아래 "Store/State/Source 온톨로지" 절의 "PA님 코드와의 교차검증" 참고). ( "Store/State/Source 온톨로지" 절의 "PA님 코드와의 교차검증" 참고).
## 남은 열린 질문 (`.claude/question.md`에도 취합) ## 남은 열린 질문 (`.claude/question.md`에도 취합)
@ -493,9 +515,12 @@ Service` 기반으로 구현)로 두면 됨 — 별도 `On` 모듈/필드 접근
검증 대상). 검증 대상).
**해소된 것**: "Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 **해소된 것**: "Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가
필요한가"는 재검토 결과 질문 자체가 성립 안 함으로 결론 — State/Source 필요한가"는 재검토 결과 질문 자체가 성립 안 함으로 결론 — 두 가지 독립적인
그래프 구독이 전부 weak-keyed GC-native(명시적 `dispose()` 호출이 아예 없음, 이유로 이중 해소됨. (1) 애초에 그런 경우를 만들지 않기로 확정(위 "Store가
`base/lifecycle-pattern.md`의 GC 위임 원칙 재사용)라 "같은 걸 두 번 해제"할 Store를 저장 가능한가" 절, 2026-08-04 6차 — Store는 Source에 준하는 "시작점"
행위 자체가 존재하지 않음(GC는 멱등). "`:Compute`가 with한 값을 어떻게 이라 다른 반응형 값을 담아 자동 연결되는 용도로 안 씀). (2) 설령 발생해도
State/Source 그래프 구독이 전부 weak-keyed GC-native(명시적 `dispose()` 호출이
아예 없음, `base/lifecycle-pattern.md`의 GC 위임 원칙 재사용)라 "같은 걸 두 번
해제"할 행위 자체가 존재하지 않음(GC는 멱등). "`:Compute`가 with한 값을 어떻게
읽는가"/"emit 필요 여부"도 전파 모델 확정으로 해소, `RobloxFactory` 중복 읽는가"/"emit 필요 여부"도 전파 모델 확정으로 해소, `RobloxFactory` 중복
호출/충돌 시나리오·인스턴스 생성/이벤트 네이밍도 위 절에서 전부 확정. 호출/충돌 시나리오·인스턴스 생성/이벤트 네이밍도 위 절에서 전부 확정.

View file

@ -49,7 +49,7 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자
| 축 | Fusion | Vide | quad-v2 시사점 | | 축 | Fusion | Vide | quad-v2 시사점 |
|---|---|---|---| |---|---|---|---|
| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | "Store는 값 자체에 항상 eager 발화, cleanup이 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-cleanup 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. | | 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | ⚠️ **[정정] 아래 서술은 리서치 당시(2026-08-03 이전) 검토 방향이며 이후 뒤집힘 — 최종 확정은 `base/bind-system-plan.md`의 "전파 모델 확정" 절 참고**(push-invalidate는 신호만 쏘고 값은 안 실음, 재계산은 `Get()` 시점 pull-recompute로만, Fusion식 eager 노드·생성순 정렬은 아예 채택 안 함 — quad엔 그런 다단계 즉시 재계산이 필요한 소비자가 없다는 판단). 당시 스냅샷 원문: "Store는 값 자체에 항상 eager 발화, retract(구 cleanup)가 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-retract 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. |
| 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). | | 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). |
| 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`base/bind-system-plan.md`). | | 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`base/bind-system-plan.md`). |

View file

@ -1,6 +1,6 @@
# 라이프사이클 패턴 — rbvm의 `Connected` + GC 관용구 채택 # 라이프사이클 패턴 — rbvm의 `Connected` + GC 관용구 채택
**상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(cleanup) 전략의 원본. **상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(`retract`) 전략의 원본.
완료 개념 없음, 구현하면서 세부 조정 있을 수 있음. 완료 개념 없음, 구현하면서 세부 조정 있을 수 있음.
## 배경 ## 배경
@ -42,7 +42,7 @@ rbvm은 실제 Roblox Instance의 파괴를 감지하는 지점을 단 하나로
플래그를 그 콜백에서만 true로 뒤집음. `AncestryChanged`나 폴링 방식은 안 씀. 플래그를 그 콜백에서만 true로 뒤집음. `AncestryChanged`나 폴링 방식은 안 씀.
quad-v2도 동일: 인스턴스 라이프사이클 훅 지점은 `Destroying` 하나로 통일. quad-v2도 동일: 인스턴스 라이프사이클 훅 지점은 `Destroying` 하나로 통일.
### 3. 정리(cleanup)는 기본적으로 GC에 위임, 예외적으로만 즉시(eager) ### 3. 정리(`retract`)는 기본적으로 GC에 위임, 예외적으로만 즉시(eager)
rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private 데이터를 rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private 데이터를
저장 — 홀더 객체를 아무도 안 들고 있으면 그 private 레코드도 자동으로 사라짐. 저장 — 홀더 객체를 아무도 안 들고 있으면 그 private 레코드도 자동으로 사라짐.
@ -51,13 +51,17 @@ rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private
전체가 통째로 죽을 때의 순서 있는 dispose 훅. **quad-v2 원칙: 기본은 GC 위임, 전체가 통째로 죽을 때의 순서 있는 dispose 훅. **quad-v2 원칙: 기본은 GC 위임,
즉시 정리는 "안 끊으면 죽은 참조를 순회하게 되는 작은 포인터"류에만 국한.** 즉시 정리는 "안 끊으면 죽은 참조를 순회하게 되는 작은 포인터"류에만 국한.**
### 4. Signal 자체는 커스텀 구현체를 그대로 재사용 가능 ### 4. (참고 기록) rbvm의 Signal 자체는 재사용 가능한 범용 emitter였음 — 실제로는 채택 안 함
`signal.luau``Signal`/`Connection` 클래스는 rbvm 프록시 시스템에 의존하지 `signal.luau``Signal`/`Connection` 클래스는 rbvm 프록시 시스템에 의존하지
않는 범용 이벤트 emitter임 (`Connect`/`Once`/`Wait`/`Fire`/`Destroy`, 않는 범용 이벤트 emitter임 (`Connect`/`Once`/`Wait`/`Fire`/`Destroy`,
`IsInited`/`OnInit`/`OnUninit` 지연 활성화 훅 포함). **단, 사용자 원 메모에는 `IsInited`/`OnInit`/`OnUninit` 지연 활성화 훅 포함). 사용자 원 메모에는
"시그널 자체 구현은 아닌듯... 콜백 정도로도 충분"이라고 되어 있어 서로 상충함** "시그널 자체 구현은 아닌듯... 콜백 정도로도 충분"이라는 언급이 있어 한때
— 아래 열린 질문 참고. 이 문서 초안 단계에서 상충하는 것처럼 보였으나, **이 질문은 2026-08-04
검증 라운드에서 최종 확정으로 재확인됨 — 더 이상 열린 질문 아님**
(`base/architecture.md` 11번 항목도 동일하게 명시). 결론은 아래 "확정: Signal
클래스는 안 만든다" 절 참고 — 커스텀 `Signal`/`Connection` 클래스는 만들지
않고, 콜백 + `Connected` 계산 속성만 채택한다.
### 5. rbvm에서 그대로 가져오면 안 되는 것 (버그 발견됨) ### 5. rbvm에서 그대로 가져오면 안 되는 것 (버그 발견됨)
@ -97,8 +101,9 @@ Destroy되면 그 대상에 묶인 것들(Tween 등)도 자연히 죽은 상태
이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전 이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전
소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/ 소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/
tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 전부 `retract` tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 대부분 `retract`
갱신됨(이름 변경 근거는 아래). 갱신됨(이름 변경 근거는 아래) — 잔여 표기 확인은 진행 중, 해당 문서들은
각자 별도로 정리될 예정.
## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요 ## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요
@ -180,4 +185,5 @@ Roblox 엔진 자체가 Destroy 시 Tag/Attribute/실행 중인 Tween을 전부
쉬움) — 실제 의미는 "이전에 적용한 처리를 무른다/멈춘다"이므로 **`retract`** 쉬움) — 실제 의미는 "이전에 적용한 처리를 무른다/멈춘다"이므로 **`retract`**
로 통일. (`revert`, `rescind`도 검토했으나 `retract`가 "이전에 취한 조치를 로 통일. (`revert`, `rescind`도 검토했으나 `retract`가 "이전에 취한 조치를
철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를 철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를
이룸.) 모든 문서에서 이 이름으로 갱신. 이룸.) 대부분의 문서에서 이 이름으로 갱신됨 — 잔여 "cleanup" 표기가 남은
문서가 있을 수 있으며, 그 확인/정리는 진행 중.

View file

@ -0,0 +1,145 @@
# Modifier 설계 (정적 merge, immutable 체이닝)
**상태**: base — 핵심 메커니즘(런타임 plug 아님/정적 merge, immutable
값+clone 기반 체이닝, 이중 setter)은 2026-08-04 세션 채팅 논의로 확정. 남은
건 getter 정확한 이름뿐(구현 단계). Modifier가 컴포넌트 경계를 어떻게
통과하는지(다중 루트, 상속 방식)는 별개 문제로
`research/component-composition-plan.md`의 열린 질문에 남음 — 이 문서는
"Modifier 값 자체가 어떻게 동작하는가"만 다룸.
## 문제
`base/architecture.md` 7번 항목("Style(Default) 시스템 폐기, modifier
지향")이 방향만 정하고, 실제 메커니즘은 미정이었음: 핸들러 레지스트리에
넣을 것인가, 여러 modifier가 같은 키를 건드리면 어떻게 되는가, 트리를
타고 내려가며 조금씩 변형되는 modifier(예: 문서 뷰어의 TextStyle 상속)를
어떻게 안전하게 다룰 것인가.
## 확정된 결론
### 1. 런타임 pluggable 핸들러 아님 — 정적 merge
Modifier는 `isHandlable`/`priority`/`process`/`retract` 핸들러 레지스트리에
안 들어감. 그냥 평범한 테이블(데이터)을 보유하는 값이고, 디스패치 들어가기
전에 한 번 평탄화(flatten)돼서 최종 props 테이블에 합쳐짐. 이유: 런타임
pluggable로 만들면 여러 modifier가 반응형으로 같은 키를 계속 다투는 CSS
cascade 문제가 그대로 오는데, 이건 이미 확정된 "Store 바인드 변경은 전체
교체, 부분 오버레이 없음"(`base/architecture.md` 3번) 원칙과 충돌함.
### 2. Merge 우선순위: 배열 순서와 인라인은 독립된 두 규칙
`Frame { modifier1, modifier2, Name = ... }` 평탄화 시:
(a) 배열에 나열된 modifier들끼리는 순서상 나중 것이 우선.
(b) 명시적 키(인라인)는 modifier가 뭘 하든 무조건 우선.
Lua 테이블 리터럴은 배열 파트/해시 파트 사이에 소스 텍스트 순서를 보존하지
않으므로 "순서상 나중이 이긴다"는 단일 규칙만으로는 구현 불가 — 반드시 두
규칙으로 쪼개야 함.
### 3. Immutable 값 + clone 기반 체이닝
컴포지션 트리를 타고 내려가며 조금씩 변형되는 modifier(문서 뷰어에서 상위
TextStyle을 상속해 타이틀만 1.2배 키우는 경우 — Jetpack Compose의
`TextStyle.merge()`/`CompositionLocal`과 동일한 use case)는 특히 위험함 —
mutable하게 구현하면 같은 modifier 레퍼런스를 공유하는 형제 서브트리가
오염되거나(한쪽이 mutate하면 다른 쪽도 영향받음), 재렌더 시 값이 누적
드리프트하는 버그가 생김(`.claude/question.md` 초기 논의의 "원본 테이블
덮어쓰기/루프 깨짐" 우려와 동일 클래스).
**해결**: 모든 변환 메소드(`:FontSize(...)`류 체이닝)는 내부에서
`table.clone(self)`로 새 테이블을 만든 뒤 필드만 덮어써 반환 — 원본은
절대 mutate하지 않음. 별도의 제네릭 clone 콤비네이터 타입
(`modifier<<Frame>>(modifier):Set` 류 아이디어)은 기각 — 그런 타입을 만들면
`base/architecture.md` 3번의 "복사 구현 지양, 필요한 곳만 팩토리 함수로
명시적 복사" 원칙을 다시 재작업하는 셈이라, 각 변환 메소드 자체가 그 원칙을
따라 알아서 최소한만 복사하면 충분.
**성능**: Luau `table.clone`은 native shallow-copy라 modifier 크기(보통
한 자리~여남은 개 필드) 기준 비용 무시 가능, 렌더/컴포지션 타임에만
발생(프레임마다 도는 게 아님). State가 이미 `:With`/`:Compute`마다 새
노드를 할당하는 것과 같은 급의 비용이라 일관되고, mutable+문서화 경고보다
오염 버그를 원천 차단하는 쪽이 라이브러리 복잡도/사용자 편의 양쪽에서
낫다고 판단 — **immutable 기본으로 확정**.
### 4. Setter는 리터럴 값과 변환 함수 둘 다 받음
`:FontSize(value)`(리터럴) / `:FontSize(function(current) return
current*1.2 end)`(변환 함수) 둘 다 지원 — 한 줄로 끝내고 싶을 때는 콜백,
여러 줄로 풀어쓰고 싶을 때는 현재 값을 getter로 꺼내 계산 후 리터럴로
다시 넣는 스타일 둘 다 인체공학상 필요하다고 판단.
변환 함수는 State의 `:Compute`처럼 lazy State 핸들을 넘길 필요가 없음(*필드가
순수 데이터인 일반적인 경우에 한해* — 필드가 State일 때의 예외는 아래 참고).
계산 비용 자체가 없는 순수 데이터라면 콜백엔 그냥 raw 현재 값을 즉시 넘기면
충분(State의 self-lazy-핸들 문제와는 다른 카테고리).
**내부 구현**: `__real` 같은 별도 래퍼는 불필요해 보임 — 데이터를 테이블에
직접 두고 메소드는 공유 메타테이블 `__index`로 붙이면, `table.clone`
메타테이블까지 그대로 복사해주는 Luau 동작 덕분에 클론해도 체이닝이 안
끊김. flatten도 그 테이블 필드를 직접 읽으면 됨.
**Getter 정확한 모양은 미정** — `mod:Get("FontSize")` 같은 전용 메소드로
할지, 아니면 Store/DI 관습처럼 dot-access(`mod.fontSize`) 자체가 읽기
경로를 겸하게 해서 별도 `:Get()`이 아예 불필요하게 할지는 구현 단계에서
확정. **다만 getter의 동작 자체은 확정**: 필드가 State면 getter 호출이
곧 관측이라 그 순간 계산되어 확정된(더 이상 반응하지 않는) 값이 반환됨 —
`base/bind-system-plan.md`의 "관측해야 실체화된다" 전역 원칙 그대로 적용
(아래 참고).
### 4-1. 필드가 State일 수도 있음 — Setter가 State/plain 여부로 분기
`architecture.md` 7번 항목이 "함수형 modifier가 store 바인드를 받을 수도
있음"이라고 이미 언급한 대로, Modifier 필드는 plain 값뿐 아니라 State일
수도 있음(예: 상위에서 내려온 테마 색상이 Store에 바인드된 반응형 값).
이 경우 위 4번의 setter가 그대로 통하려면, **현재 저장된 필드 값이 State냐
plain이냐에 따라 setter 내부 동작이 갈려야 함** — 새 개념이 아니라 State에
이미 있는 lazy/`:Compute` 체이닝을 그대로 재사용하는 것뿐:
| 현재 필드 | 인자 | 동작 |
|---|---|---|
| plain | 리터럴 | clone 후 그 값으로 덮어씀 |
| plain | 함수 | clone 후 즉시 호출해 나온 값으로 덮어씀(현재 값이 raw로 넘어감) |
| **State** | **리터럴** | clone 후 **State를 통째로 리터럴로 덮어씀 — 의도적으로 반응성이 끊김**(Store의 "부분 오버레이 없음, 전체 교체" 원칙과 같은 결) |
| **State** | **함수** | clone 후 `field:Compute(fn)`으로 **새 파생 State**를 만들어 대입 — 반응성 유지, State의 기존 `:Compute` 메커니즘에 그대로 위임 |
즉 함수형 셋터는 필드가 State일 때 반응성을 보존하고, 리터럴 셋터(혹은
getter로 꺼내 계산 후 리터럴로 다시 넣는 멀티라인 스타일)는 그 순간 값을
확정시켜 반응성을 끊음 — 이 차이는 사용자가 인지하고 골라 쓰는 것으로
문서화.
### 4-2. Modifier는 소유권/유일성 제약이 없음
Modifier는 자식(child)을 담지 않음 — 마운트 정체성이 없는 순수 값. 그래서
어떤 컴포넌트가 특정 modifier를 실제로 적용하든 안 하든, 또 같은 modifier를
트리 여러 곳에 반복 적용하든 에러가 나지 않고 상관없음(Ref나 Slot 자식처럼
"정확히 한 곳에만 마운트돼야 한다"는 소유권 제약이 이들에게는 있지만
Modifier에는 없음).
### 5. 타입 출처는 이미 확정된 dot-access 관습 재사용
"누가 modifier에 타입을 붙여주냐"는 새 문제가 아니라, Store/인스턴스 생성에
이미 적용한 "정적으로 알려진 건 dot-access, 동적인 건 문자열 폴백" 프로젝트
전역 관습(`base/bind-system-plan.md` "타입 추론 문제" 절)을 그대로 적용하면
됨 — `Modifier.Rounded(8)`/`Modifier.FontSize(...)`처럼 DI 쪽 "제네릭
생성자 함수 하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용.
(주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은
PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/bind-system-plan.md`
"이벤트 바인딩 정정" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스
생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.)
### 6. State/Pipe 쪽엔 영향 없음 — 이미 있던 결정의 재확인일 뿐
Modifier가 immutable해야 하는 이유(변환마다 clone)와 State가 이미
"`:With`/`:Compute`마다 새 노드를 만든다"(`base/bind-system-plan.md` 2차
라운드 확정)로 확정해둔 이유는 같은 클래스의 문제(공유 mutable 상태로 인한
오염 방지)임을 이번 논의에서 재확인했을 뿐 — State/Source 온톨로지 자체엔
변경 사항 없음. 파이프 분기(`:With(...):Compute(fn)`)는 이미 코드에
명시적으로 쓰는 구조라 "암묵적 분기"가 애초에 존재하지 않음 — 새로 결정할
것 없음.
## 열린 질문 (`.claude/question.md`에도 취합)
- Getter 정확한 이름/모양(`:Get(key)` vs dot-access 겸용) — 후순위, 구현
단계에서 다른 세부 API 이름들과 같이 확정 가능.
- Modifier가 컴포넌트 경계를 어떻게 통과하는지(다중 루트, 상속 방식)는
`research/component-composition-plan.md`에서 계속 다룸 — 이 문서가 다루는
"값 자체의 동작"과는 별개 문제.

View file

@ -53,9 +53,15 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
- **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은 - **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은
이름 붙은 체이닝 연산(named modifier)은 명시적으로 안 만들기로 확정, 대신 이름 붙은 체이닝 연산(named modifier)은 명시적으로 안 만들기로 확정, 대신
일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한 일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한
연산들은 오히려 일관성을 해친다"는 게 이유. 연산들은 오히려 일관성을 해친다"는 게 이유. (주의: 아래의 v2 `:With(...)`
이름만 같을 뿐 여기서 안 만들기로 한 v1의 `:With`와는 다른 연산임 — v1은
"함수/테이블에서 값을 가져오는" 가공 연산이었고, v2는 그냥 "여러 State를
의존성으로 모으는" 수집 연산.)
- **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency - **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency
array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `base/bind-system-plan.md`의 남은 열린 질문 참고. array)은 있으면 좋겠다는 요청이었고 — **API 시그니처도 확정됨**:
`:With(...)`로 의존성을 모으고 `:Compute(fn)`으로 파생 State를 만드는
형태, 상세는 `base/store-semantics.md`의 "여러 스토어 값을 묶어 처리하는
것" 절 참고.
- `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는 - `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는
잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무 잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무
처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이 처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이
@ -93,6 +99,6 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류
유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. **중복 호출 유틸은 인터페이스, 실제 구현은 백엔드 팩토리가 주입" 절 참고. **중복 호출
가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로 가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로
재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 — 재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 —
바로 아래 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()` 바로 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()`
생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로 생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로
스코핑됨, 별도 재설계 불필요. 스코핑됨, 별도 재설계 불필요.

View file

@ -1,8 +1,9 @@
# 컴포넌트 순수성이 아니라 "이식성" 문제 (재정의됨) # 컴포넌트 순수성이 아니라 "이식성" 문제 (재정의됨)
**상태**: research — 사용자 확인 완료로 문제 자체는 명확해짐, 남은 건 문서화 **상태**: base — 확정됨(2026-08-04 세션에 `research/`에서 승격). 남은 건
강도 정도. 원본: `.claude/initreq/raw-userinput.md` "순수함수에 대한 범위를 가이드 문서 내 배치 위치 정도로 기술적 결정 사항은 없음. 원본:
정할 필요가 있음" / "진짜 부작용은 외부에 만들어버린다" 절. `.claude/initreq/raw-userinput.md` "순수함수에 대한 범위를 정할 필요가 있음" /
"진짜 부작용은 외부에 만들어버린다" 절.
## 정정: "순수함수 여부"가 아니라 "이식성(portability)" 문제였다 ## 정정: "순수함수 여부"가 아니라 "이식성(portability)" 문제였다

View file

@ -24,7 +24,8 @@ Mount(ScreenGui, Frame {...})
`Class.Extend()`로 재사용 컴포넌트(`Init/Render/AfterRender/Getter/Setter/ `Class.Extend()`로 재사용 컴포넌트(`Init/Render/AfterRender/Getter/Setter/
UpdateTriggers/Unload`) 정의 가능. `Store.GetObject(id)`류 id 기반 전역 조회는 UpdateTriggers/Unload`) 정의 가능. `Store.GetObject(id)`류 id 기반 전역 조회는
v2에서 태그 시스템으로 대체 예정(`base/store-and-tags.md` 참고). v2에서 대체될 예정 — Ref 도입과 네임스페이싱 판단까지 포함해 최신 상세는
`base/architecture.md` 5번 항목 참고.
## 핵심 내부 동작 요약 ## 핵심 내부 동작 요약

View file

@ -65,14 +65,24 @@ Slot은 하나의 instance 안에 여럿 존재할 수 있다. 전부 하나의
편하다는 방향. **기울어진 결론**: 별도 "Named Slot" 개념 없이, store나 편하다는 방향. **기울어진 결론**: 별도 "Named Slot" 개념 없이, store나
파라미터로 넘기고 그게 그냥 ref처럼 바인드되는 모양. 파라미터로 넘기고 그게 그냥 ref처럼 바인드되는 모양.
**상태 확인(2026-08-04 문서 정리 시점)**: 이 방향은 아래 "열린 질문" 절이나
`.claude/question.md`의 확정 목록 어디에도 명시적으로 흡수된 흔적이 없음 —
아직 정식 확정 절차(`AskUserQuestion` 등)를 거치지 않은 것으로 보임. **열린
질문으로 유지**, `.claude/question.md`에도 반영 필요.
## Slot과 Store 바인드의 관계 (`retract` 순서) ## Slot과 Store 바인드의 관계 (`retract` 순서)
Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract`(구 cleanup, Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract`(구 cleanup,
`base/lifecycle-pattern.md` 참고) 핸들러가 필요함 — 한번 넘어간 slot 요소가 `base/lifecycle-pattern.md` 참고) 핸들러가 필요함 — 한번 넘어간 slot 요소가
나중에 `retract`되면 삭제되는지, 아니면 "부모의 소유이니 부모가 처리"해야 나중에 `retract`되면 삭제되는지, 아니면 "부모의 소유이니 부모가 처리"해야
하는지 검토 필요. **기울어진 결론**: 부모가 정리 정도만 미리 수행하고 다시 하는지 검토 필요. **기울어진 결론(잠정안, 이후 정정됨)**: 부모가 정리 정도만
`process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 스스로 정리를 미리 수행하고 다시 `process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가
실행하는 게 아니라). 스스로 정리를 실행하는 게 아니라).
> **정정(2026-08-04 검증 라운드)**: 위 "부모 위임" 잠정안은 이후 **폐기**
> 쪽으로 정정됨 — 아래 "확정" 절과 `.claude/question.md`("Slot의 `retract`
> 동작이 '부모 위임' 잠정안에서 '폐기(옮기지 않음)'로 확정") 참고. 이 문단은
> 검토 과정의 히스토리로만 남겨둠, 현재 유효한 동작 아님.
이건 `base/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정 이건 `base/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정
모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot 모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot

View file

@ -1,7 +1,8 @@
# Store 의미론 — 부작용 허용, State는 Store 위의 조합 가능한 캐시 레이어 # Store 의미론 — 부작용 허용, State는 Store 위의 조합 가능한 캐시 레이어
**상태**: base — 부작용 허용/Store 문법 부분은 확정. State/Source 온톨로지는 **상태**: base — 전부 확정. State/Source 온톨로지는 2026-08-04 검증
2026-08-04 검증 라운드에서 새로 열린 진행 중인 설계 스레드(`base/bind-system-plan.md` 참고). 원본: `.claude/initreq/raw-userinput.md` 라운드에서 새로 열려 같은 세션 2~4차 라운드에 걸쳐 확정까지 마침 — 최신
상세는 `base/bind-system-plan.md` 참고. 원본: `.claude/initreq/raw-userinput.md`
"store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절. "store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절.
## Store는 부작용을 허용하는 게 기본 디자인 ## Store는 부작용을 허용하는 게 기본 디자인
@ -12,7 +13,7 @@
다만 한 가지는 명확히 구분: **렌더 리턴 위에서 무언가를 observe하는 것은 그냥 다만 한 가지는 명확히 구분: **렌더 리턴 위에서 무언가를 observe하는 것은 그냥
부작용**이다 (`useEffect`와 유사한 것으로 문서화). 이건 "허용되는 부작용"이 아니라 부작용**이다 (`useEffect`와 유사한 것으로 문서화). 이건 "허용되는 부작용"이 아니라
"당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`research/ "당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`base/
purity-and-effects-plan.md`와 연결됨). purity-and-effects-plan.md`와 연결됨).
**보강(2026-08-04 검증 라운드): 부작용은 심각도가 다른 두 갈래로 나뉜다.** **보강(2026-08-04 검증 라운드): 부작용은 심각도가 다른 두 갈래로 나뉜다.**
@ -23,7 +24,7 @@ purity-and-effects-plan.md`와 연결됨).
2. **경계를 넘는 부작용** — globalStore처럼 컴포넌트 바깥의 전역 상태를 2. **경계를 넘는 부작용** — globalStore처럼 컴포넌트 바깥의 전역 상태를
다루는 경우. 게임 UI 특성상(스킬/주변 환경에 영향받는 UI 등) 완전히 다루는 경우. 게임 UI 특성상(스킬/주변 환경에 영향받는 UI 등) 완전히
막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을 막을 수는 없지만, 라이브러리로 재사용하려는 컴포넌트가 이런 부작용을
가지면 이식성이 떨어짐(`research/purity-and-effects-plan.md`와 연결). 가지면 이식성이 떨어짐(`base/purity-and-effects-plan.md`와 연결).
**해소됨(2026-08-04 2차 라운드)**: state를 옵저빙해서 나온 결과로 slot에 **해소됨(2026-08-04 2차 라운드)**: state를 옵저빙해서 나온 결과로 slot에
`clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면 `clear`/`add` 같은 연산을 할 때, 그 시점에 대상 slot이 이미 죽어있으면
@ -60,14 +61,14 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신 방향으로 좁혀짐 — 별도 `Pipe` 타입을 만들어 소유권/버전 가드를 넣는 대신
State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임 State 자체가 "파이핑 결합체"이고 `state(state)`로 분기하면 될 걸로 보임
(`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `base/bind-system-plan.md` (`Pipe` 후보는 사실상 폐기 쪽으로 기움). 상세는 `base/bind-system-plan.md`
"Store/State/Source 온톨로지" 절 참고 — **아직 완전히 결론난 설계는 아니고, "Store/State/Source 온톨로지" 절 참고 — **이 절 이후 2~4차 라운드에 걸쳐
구현 단계에서 더 다뤄야 할 진행 중인 스레드.** 전부 확정됨, 더 이상 진행 중인 스레드 아님.**
미해결로 남은 것: `:Compute` 캐싱/무효화 전략(값이 바뀌었는데 듣는 소비자가 과거 "미해결로 남은 것"으로 적었던 두 항목도 모두 해소됨: `:Compute`
없으면 연산을 미루는 dirty-flag 방식 등), Luau 타입 시스템에서 `store "key"` 캐싱/무효화 전략은 push-invalidate(신호만)/pull-recompute(`Get()` 시점)로
같은 커링 호출이 오버로드 함수 타입으로 `state<T>`를 정확히 추론하기 어려운 확정(`base/bind-system-plan.md` "전파 모델" 절), `store "key"` 커링의 타입
문제(문자열 리터럴이 as-const로 좁혀지지 않는 문제) — 둘 다 열린 채로 추론 문제는 `store.key`(dot-access)를 1급 경로로 확정하며 해소(같은 문서
`base/bind-system-plan.md`에서 계속 다룰 것. "타입 추론 문제" 절, 3차 라운드).
## Store 값 설정 문법 — v1 인체공학 유지 (확정) ## Store 값 설정 문법 — v1 인체공학 유지 (확정)
@ -92,8 +93,11 @@ pull-recompute)·`:Compute` 인자 규칙·State 쓰기 금지·`Source` 독립
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는 `useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 — 요구가 있었음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 —
`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의 `base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). **v1의
`:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않음** — `:Add`/`:With`/`:Tween`처럼 값을 직접 가공하는 이름 붙은(named) 체이닝 연산은
대신 일반 함수를 받아 처리. 최종 형태는 `:With(...)`로 의존성을 모으고 만들지 않음** — 대신 일반 함수를 받아 처리. (주의: 아래의 v2 `:With(...)`
이름만 같을 뿐 v1의 `:With`와는 다른 연산임 — v1은 "함수/테이블에서 값을
가져오는" 가공 연산이었고, v2는 그냥 "여러 State를 의존성으로 모으는" 수집
연산.) 최종 형태는 `:With(...)`로 의존성을 모으고
`:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b}, `:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b},
fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다
State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고.

View file

@ -1,194 +1,89 @@
# 확인/결정 필요 목록 (전체 취합) # 확인/결정 필요 목록
각 plan 문서에 흩어진 "사용자 확인 필요" 절의 취합본. **막고 있는 항목은 **2026-08-04 세션 말미에 전체 재정리함.** 예전엔 라운드(1차~6차)별로 문서가
거의 없음** — 대부분 합리적 기본값/방향을 잡아두고 research 단계에 머물러 계속 쌓이면서 순서가 시간순도 우선순위순도 아니게 됐고, 이미 해소된 라운드
있음. 사용자가 Lua/Roblox 엔진에 대해 깊이 아는 사람이라는 전제로, 우선순위 기록이 새로 열린 질문보다 위에 있는 등 혼동을 유발했음(문서 감사에서 발견).
높은 것부터 정렬. 그 상세 히스토리는 지우지 않았음 — git log로 이 파일의 이전 버전을 보거나,
`base/`/`research/` 문서 안의 라운드 표시("2026-08-04 3차 라운드" 등)를
따라가면 그대로 남아있음. 이 문서는 이제 **"지금 열려있는 것" 우선으로만**
구성.
## 2026-08-04 5차 라운드 완료 — 소스 트리 구조 확정 ## 지금 열려있는 것 (우선순위순)
`.claude/base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고. ### 1. [최우선] 컴포넌트 경계에서 modifier/Ref가 어떻게 전달되는가
`base/bind-system-plan.md`/`base/module-lifecycle-plan.md`/`base/slot-plan.md`가
이 라운드에서 `research/`에서 승격됨.
- **패키징**: 최종 목표는 다중 wally 패키지지만, 지금 Luau 툴링(wally 타입 사용자가 "지금 quad에서 가장 문제되는 부분"으로 직접 지목. 컴포넌트가
단절, `luau-lsp` 심볼릭 링크 해석 문제)이 불안정해서 당장은 모놀리식 — 플레인 함수이고 반환하는 루트가 여러 개(혹은 Slot으로 갈라지는 구조)일 때,
`Sleitnick/RbxUtil` 패턴(루트 통합 개발/테스트, 서브폴더마다 자체 호출부가 넘긴 modifier/Ref가 "어느 루트로 가야 하는지" 모호해지는 케이스가
`wally.toml`) 채택. `.luaurc` alias는 런타임 require에서 아직 엔진 미지원 — 있음. Jetpack Compose는 언어 강제가 아니라 "컴포저블은 `modifier` 파라미터를
편집기 경험용으로만 사용, 런타임 require는 상대경로. 받아 루트에 적용해야 한다"는 순수 관례(+린트)로 풂 — quad도 비슷한 관례
- **패키지 경계**: `quad-base` = Store/State/Source 온톨로지+전파 **+** 기반으로 갈 수 있어 보이나 다중 루트 케이스는 미정.
pluggable 디스패치 엔진(`process`/`retract`, 핸들러 계약, `LifetimeHandle`/
`PerInstanceState` 인터페이스, Ref, Slot 코어 재조정 로직) — 전부
"인터페이스"로, 다른 엔진(GTK 등)에서도 재사용 가능해야 한다는 전제.
`quad-roblox` = 위 인터페이스의 실제 구현체(`RobloxFactory`, Property/Event/
Attribute/Tag/Tween/Slot 적용 핸들러, `DI` 인스턴스 생성자) — 이유: 엔진마다
큰 구현을 중복하지 않기 위함(rbvm의 relation 통합 시도와 같은 동기).
- **Slot 패키지 경계**: 재조정 로직(add/remove/clear)은 base, 실제 Instance
`Parent`/`Destroy` 조작은 roblox의 핸들러가 담당 — `base/slot-plan.md`
"base/roblox 패키지 경계" 절.
- **새 핸들러 필요성 확인**: `k:number, v:Instance`(중첩 인스턴스를 직접
자식으로 넣는 경우, `Frame { Frame {} }`)를 위한 `InstanceChild` 핸들러가
Slot과 별개로 필요 — `quad-roblox/src/Handlers/InstanceChild.luau`.
## 2026-08-04 검증 라운드 완료 **주의**: modifier "값 자체"가 어떻게 동작하는지(정적 merge, immutable+clone
체이닝, State 필드 지원)는 이미 완전히 확정됨(`base/modifier-plan.md`) — 이
질문은 그것과 별개로 "경계를 어떻게 통과하느냐"만 다룸, 혼동하지 말 것.
아래 "확정됨" 절 전체(architecture.md 14개 항목, lifecycle-pattern.md, → 상세/배경: `research/component-composition-plan.md`.
store-semantics.md, bind-system-plan.md, module-lifecycle-plan.md,
slot-plan.md, tween-plan.md)를 `AskUserQuestion`으로 하나씩 예/아니오 재검증
완료 — 대부분 그대로 확인됐지만, 아래는 검증 과정에서 실제로 문서가 수정된
항목:
- **`State` 프리미티브는 "안 만든다"가 아니라 실제로 필요함** — 정정 완료, ### 2. 용어 정리 (사용자 요청, 진행 중)
`base/store-semantics.md` 참고. **이 결과로 Store/State/Source 온톨로지
전체가 새로운 열린 설계 스레드로 떠올랐음** — 아래 "최우선 새 열린 질문"
참고.
- Slot의 `retract` 동작이 "부모 위임" 잠정안에서 "폐기(옮기지 않음)"로 확정
`base/slot-plan.md`.
- quad2-try의 `Pipe` copy-on-write 후보는 사실상 폐기, `state(state)` 조합
모델로 대체 — `base/bind-system-plan.md`.
- `Connected` 체크/GC 위임/`Destroying` 훅 관련 뉘앙스 보강(엔진별 인터페이스
주입, quad는 rbvm보다 즉시정리 필요성이 낮음) — `base/lifecycle-pattern.md`.
- base 유틸(per-instance 저장소, 생명 바인드)은 인터페이스만, 실제 구현은
`RobloxFactory(BaseModule)`류 백엔드 팩토리가 주입 — `base/bind-system-plan.md`.
## 최우선 새 열린 질문 (검증 라운드에서 새로 터져나옴) 사용자 원 메모: "quad는 register라던가 좀 부정확하거나 느낌이 바로 와닿지
않던 용어들이 많음 — 전체적 용어를 보고 생각해볼래? 제안을 줘, 나도 같이
볼게." 1차 제안 완료, 아래는 우선순위순 요약 — 최종 판단은 사용자와 계속
논의 필요:
**전부 확정됨** — 아래 "2026-08-04 3차 라운드" 절 참고. 이 섹션에 새 항목이 - **`State`(1순위, 위험도 높음)**: 지금 정의는 "읽기 전용, 파생/캐시 뷰"인데
생기면 여기 추가. React/Vue 등 업계 전반에서 "state"는 거의 항상 "쓸 수 있는 로컬 슬롯"을
뜻함 — 처음 보는 사람이 정반대로 오해할 위험이 큼. `Computed`/`Derived`
(Vue `computed()`, Svelte 5 `$derived`가 정확히 같은 의미로 씀)가 실제
의미에 더 맞아 보임. 단, v1의 "register"를 이미 한 번 "State"로 리네임한
지 얼마 안 됐다는 점 고려 필요.
- **`DI`(Declarative Instance, 1순위)**: "Dependency Injection"의 업계
표준 축약어와 완전히 겹침 — 4차 라운드에서 이미 한 번 실제로 오해가
있었던 전례(`base/bind-system-plan.md`의 "인스턴스 생성" 절 참고).
- **`PerInstanceState`(2순위)**: 핵심 프리미티브 `State`와 이름이 겹쳐서
실제로는 완전히 무관한 유틸(인스턴스별 weak-keyed 저장소)인데 혼동
유발 가능 — `PerInstanceStorage`/`InstanceData` 등 대안.
- **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가
다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이
헷갈릴 수 있음.
- **`CreatedRef`/`canExecute`(3순위, 사소함)**: `CreatedRef`는 과거분사형이라
생성자처럼 안 읽힘. `canExecute`는 실제로 "이 핸들이 아직 살아있나"
확인인데 이름이 범용 권한 체크처럼 들림 — `isAlive` 쪽이 더 직접적.
- **이미 지나간 사례로 참고**: `register`(v1) → `State`(v2) 리네임은
"모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든
셈 — 이번 정리에서 같은 패턴을 조심할 것.
- `Store`/`Source`/`Modifier`/`Ref`/`process`/`retract`/`isHandlable`은
업계 선례와 잘 맞거나 이미 신중하게 결정된 이름들이라 특별한 문제 없음.
## 2026-08-04 4차 라운드 완료 — PA님 실 코드(`initreq/artworks`) 교차검증 ### 3. 낮은 우선순위
사용자가 실제 참고 코드를 공유(`.claude/initreq/artworks/`, PA님 작성) — - `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현
아래 두 항목이 3차 라운드 잠정안에서 정정됨, 나머지는 재검토 후 기존 확정 착수를 막지 않음.
유지: - **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요**
`base/quad-v1-architecture.md`에 남겨진 v1 내부 동작 확인 사항, 마이그레이션
가이드 작성 시점에 필요. 지금은 그냥 백로그로만 기록.
- **"DI" = Declarative Instance**(Dependency Injection 아님) — 3차 라운드의 ## 참고: 지금까지 확정된 것 (요약)
오해 정정.
- **이벤트 바인딩 정정**: `On.EventName` 도트액세스 안 씀 — PA님 방식(평범한
문자열 키 + `ReflectionService` 기반 자동 판별, `Frame { MouseButton1Click
= fn }`)으로 전환. Store의 `store.key`는 실질적 타입 이득이 있어 dot-access
유지, 이벤트만 예외.
- **인스턴스 생성**: 2트랙(`DI.Frame`/`DI.New<<Frame>>`) 대신 PA님 코드처럼
제네릭 생성자 함수 하나 + 자주 쓰는 클래스만 정적 필드로 미리 바인딩하는
더 단순한 모양으로 정정.
- **전파 모델(push-invalidate/pull-recompute)·라이프사이클(GC-native)은
재검토 후 기존 확정 유지** — PA님 코드가 반례처럼 보였으나(전자는 push-값
단순 pub-sub, 후자는 전부 수동 해제) 대등한 비교가 아니었거나(파생/합성
개념 자체가 없음) 지금 필요성이 없다는 게 사용자 판단. 라이프사이클은
나중에 하이브리드로 확장 가능한 여지만 기록.
- OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 보일러플레이트를
보여 오히려 보강됨. Instance 태그는 CollectionService 직접 사용 유지.
→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 전부 `base/`에 문서화되어 더 이상 열려있지 않음 — 상세 근거/논의 과정이
인체공학" 절, "Store/State/Source 온톨로지"의 "PA님 코드와의 교차검증" 절, 필요하면 아래 문서를 열어볼 것(라운드별 세부 히스토리는 각 문서 안에
`base/lifecycle-pattern.md`의 "교차검증" 절. "2026-08-04 O차 라운드" 식으로 표시돼 있음):
## 2026-08-04 3차 라운드 완료 — dot-access 관습 확정, RobloxFactory 가드 확정 (일부 4차 라운드에서 정정됨) | 주제 | 문서 |
|---|---|
- **dot-access를 프로젝트 전역 관습으로 확정**: "정적으로 알려진 것=필드 | 전체 아키텍처 결정(디스패치 모델, DOMless, 태그/Ref, Signal 미채택 등) | `base/architecture.md` |
접근, 동적인 것=문자열 호출 폴백"이 Store(`store.key`/`store "key"`)와 | Store/State/Source 온톨로지, 인스턴스 생성/이벤트 인체공학, Ref, 남은 API 이름 | `base/bind-system-plan.md` |
인스턴스 생성에 적용됨 — **이벤트는 4차 라운드에서 예외로 정정**(위 참고). | Store 부작용 허용, `:With`+`:Compute`, dot-access 문법 | `base/store-semantics.md` |
- **`RobloxFactory` 재호출 가드 확정**: 같은 팩토리로 재호출 시 무시 | 프로바이더 패턴, bind/store 구현 책임 분리 | `base/module-lifecycle-plan.md` |
(no-op, hot-reload 안전), 다른 팩토리로 재호출 시 에러(유일 슬롯 충돌). | Slot 재조정, 재마운트 시 throw, retract=폐기 | `base/slot-plan.md` |
`New()`와는 인스턴스별 테이블 분리로 자연히 공존 — 재설계 불필요. (4차 | `Connected`+GC 라이프사이클 패턴 | `base/lifecycle-pattern.md` |
라운드에서 변경 없음) | Modifier(정적 merge, immutable 체이닝, State 필드 지원) | `base/modifier-plan.md` |
| 컴포넌트 이식성(전역 store 참조 시 재사용성 문제) | `base/purity-and-effects-plan.md` |
→ 상세: `base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 | Fusion/Vide 비교 리서치(주의: 일부 서술은 이후 라운드에서 뒤집힘, 문서 내 정정 표시 참고) | `base/comparison-fusion-vide.md` |
인체공학" 절, "base 유틸은 인터페이스..." 절의 재호출 가드 부분. | v1 내부 동작 스냅샷 | `base/quad-v1-architecture.md` |
| 트윈 오버라이드(기본값 Cancel), 세부 옵션만 남음 | `research/tween-plan.md` |
## 2026-08-04 2차 라운드 완료 — Store/State/Source 온톨로지 핵심 메커니즘 | quad2-try(폐기된 이전 시도) 리서치 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW 전부 죽은 접근으로 확인, 반복 조사 금지 | `base/bind-system-plan.md` |
위 최우선 질문 중 "Store/State/Source 온톨로지 전체"와 "부작용이 slot 생존
여부와 어떻게 연관되는가"는 `AskUserQuestion`으로 확인 완료, 더 이상 열려있지
않음:
- **전파 모델**: push-invalidate(신호만, 값 안 실음) / pull-recompute(`Get()`
시점에만 재계산) — Fusion식 eager 노드·생성순 정렬은 안 만듦.
- **`:Compute`의 self 인자**: raw 값이 아니라 State 핸들 자체를 넘겨서 self도
with한 값과 동일하게 lazy하게(`.value`를 실제로 읽을 때만 계산) 처리 —
별도 `ComputeWithout` 불필요.
- **State는 쓰기 대상이 아님**: `.value`는 읽기 전용, 값 쓰기는 항상 Store의
`__newindex`로만. `Source`는 Store 내부 디테일이 아니라 값 하나만 다룰 때
쓰는 별도의 가벼운 공개 프리미티브로 격상.
- **Slot 생존 확인**: 별도 메커니즘 없이 기존 "생명 바인드 유틸"의
`canExecute`로 통일 게이트.
- **`store.key` dot-access 타입 추론 제안**: 3차 라운드에서 정식 확정됨(위
"2026-08-04 3차 라운드 완료" 절 참고).
→ 상세: `base/bind-system-plan.md`의 "Store/State/Source 온톨로지 —
핵심 메커니즘 확정" 절, `base/store-semantics.md`, `base/lifecycle-pattern.md`.
## 확정됨 (2026-08-03 질의응답 라운드, 더 이상 열려있지 않음)
- **Store 책임 분리**: base가 `LifetimeHandle` 추상화 + store-bind의 재실행
로직(`process(inst,k,realv)` 재귀)을 소유, provider는 "언제 죽었다고
판단할지"(Roblox `Destroying` 등)만 결정. → `base/module-lifecycle-plan.md`,
`base/bind-system-plan.md`
- **Signal 클래스**: 안 만듦 — 콜백 + `Connected` 계산 속성만. → `base/
lifecycle-pattern.md`
- **핸들러 계약**: `isHandlable`+`priority`+`process`+`retract` 4종 유지,
tbox식 세분화는 지금 안 함. → `base/bind-system-plan.md`
- **Ref**: 도입하되 용도는 "id 조회 대체"가 아니라 "외부 관리 instance를
점진적으로 마이그레이션/래핑하기 위한 직접 참조 획득". Tween 등 어떤
핸들러도 대상 획득에 Ref가 필요하지 않음(항상 `inst`를 직접 받음).
`base/bind-system-plan.md`
- **`retract`(구 cleanup) 호출 시점**: 값 교체 시에만 호출, Destroy 시엔
호출 안 함(quad는 자신이 만든 instance의 생명주기 중간에 있지 않으므로
destroy-time 정리 자체가 불필요/불가능). → `base/lifecycle-pattern.md`
- **핸들러 내부 상태 저장**: base가 범용 weak-keyed per-instance 저장 유틸
제공(모든 핸들러 재사용). → `base/bind-system-plan.md`,
`base/lifecycle-pattern.md`
- **Store 값 설정 문법**: `__newindex`(`myStore.key = v`) 유지, 괄호 생략
커링/`:` 체이닝 인체공학도 유지 — 바뀌는 건 내부 구현(팩토리 함수)뿐.
`base/store-semantics.md`
- **Store의 named modifier(`:Add`/`:Mul` 등)**: 안 만듦 — 일반 함수를 받는
형태로 통일. → `base/store-semantics.md`
## 추가 확정됨 (2번째 라운드)
- **트윈 오버라이드 기본값**: 멈춤(Cancel), 새 트윈은 현재 보간된 값에서 시작.
나머지 세 동작(오버라이드/삭제후재시작/끝점이동후재시작)은 옵션으로 선택
가능. → `research/tween-plan.md`
- **Slot 재마운트 에러**: 즉시 throw. → `base/slot-plan.md`
- **`CreatedRef` 콜백 타이밍**: 생성 시점/마운트 시점 둘 다 옵션으로 지원.
`base/bind-system-plan.md`
- **여러 store 값 묶기**: `Store.Combine`류 포지셔널 인자 방식과 Vide식 암묵적
추적 둘 다 기각 — `:With(...)` + `:Compute(fn)`(fn은 with한 값을 포지셔널
인자가 아니라 클로저로 읽음) 방식으로 확정. Unix 파이프에서 영감받은 완전
합성 가능한 State 스트림이 이상향이나 기술적 난이도 미확정 — 과거 시도
(`quad2-try/quad-core`) 리서치 진행 중. → `base/bind-system-plan.md`
## quad2-try(이전 폐기된 시도) 리서치 완료 — 추가 확정
- **OOP 상속/`--&` 커스텀 파서/Slot 스텁은 확인대로 죽은 접근** — 절대 반복
금지, Slot은 from-scratch 설계 그대로 진행(재조사 불필요).
- **mutate-vs-`fromState` 긴장 관계**: quad2-try의 `Pipe` copy-on-write
절충안(유일한 tip일 때만 뮤테이션, 아니면 복사)이 한때 유력 후보였으나
**2026-08-04 검증 라운드에서 사실상 폐기로 재평가됨** — 별도 `Pipe` 타입
대신 State 자체가 파이핑 결합체이고 `state(state)`로 분기하는 쪽이 더
간단하다는 판단(위 "최우선 새 열린 질문"의 Store/State/Source 온톨로지
절로 흡수됨).
- **`Depend(...)` 액션, `:With` 네이밍**은 이전 시도에서도 지향했던 것과 일치
— 그대로 채택. → `base/bind-system-plan.md`
## 순수성/이식성, 기존 인스턴스 바인드 — 확인 완료, 낮은 우선순위로 유지
- **"순수함수" 문제는 실제로는 "이식성" 문제였음** — 재사용 의도 컴포넌트가
전역 store를 직접 참조하면 이식성이 깨짐(단일 페이지용 컴포넌트나 라이브러리
내부 전용 공유 상태는 문제 없음). 기술적 강제 안 함, 문서 경고 수준으로
확정. → `research/purity-and-effects-plan.md`
- **이미 생성된 인스턴스 재바인드**: 실제 요청한 사용자를 본 적 없지만
`retract` 인프라가 이미 있어 미래에 자연스럽게 가능해질 여지가 있음 —
"미지원" 확정도, 착수도 안 함, 진짜 열린 가능성으로만 유지. → `research/
existing-instance-bind-plan.md`
## 급하지 않음, 여유 있을 때만
- 태그 시스템의 네임스페이싱 부재(라이브러리 간 충돌 가능성)를 얼마나
심각하게 볼지 — 지금은 "별도 네임스페이스 개념은 복잡도 대비 이득이 적다"는
판단으로 보류 중. → `base/architecture.md` 5번 항목.
- Store가 Store를 담는 경우 이중 해제(double-dispose) 방지가 실제로 필요한
상황이 있는지 — 구현 단계에서 실사례로 재검증. → `base/bind-system-plan.md`
--- ---
전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이 전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이

View file

@ -0,0 +1,110 @@
# 컴포넌트화 (Roblox 기본 오브젝트 이외의 사용자 정의 컴포넌트)
**상태**: research — 2026-08-04 세션 채팅 논의에서 핵심 골격 수렴, 세부
API 이름/modifier·Ref passthrough는 미정. 사용자가 "지금 quad에서 가장
문제되는 부분"으로 직접 지목한 주제. `base/bind-system-plan.md`
Store/State/Source 온톨로지가 먼저 확정된 뒤에야 이 논의가 열림 — 그
문서가 선행 컨텍스트.
## 문제
v1의 `Class.Extend()`(Init/Render/AfterRender/Getter/Setter/UpdateTriggers,
`base/quad-v1-architecture.md` 참고)는 이미 OOP 상속 스타일이라 폐기
방향이지만, 그게 제공하던 실제 편의 기능(컴포넌트가 자기 store를 자동으로
가짐, props로 넘어온 State를 자동 흡수, `self:Default`/`self "key"`로
기본값·바인딩)까지 같이 버려도 되는지가 미결이었음. `MyComp {...}` 형태로
호출되는 사용자 정의 컴포넌트를 v2에서 어떤 모양으로 작성하게 할지가 핵심
질문.
## v1 실제 메커니즘 (조사 완료, `quad.qwreey.kr` 튜토리얼 + `initreq/quad/src/` 소스로 교차검증)
- `myStore "key"` → register(현재 State에 해당) 반환. `:Default(v)`/
`:With(fn)`/`:Add(v)`/`:Tween(opts)` 체이닝 가능(`store.lua:433-457`).
- `Class.Extend()``:Init(props)`에서 `props:Default("Size", v)`로 기본값
설정, props 테이블 자체가 store 인스턴스로 변신(`class.lua:365-379`,
`storeNew(prop,nil)`).
- props로 넘어온 값이 State(`quad_register`)면 `initStoreRegisterBinding`
(`store.lua:394-431`)이 자동으로 감지해 컴포넌트 자신의 store 키에 재귀
연결 — **자동 흡수 매직**이 실제로 존재했음.
- `self(name)` linker가 **두 가지 역할**을 겸함: (1) `self "_button"`
자식 자리에 넣으면 렌더링된 인스턴스를 `self._button`에 즉시 잡아둠(Ref
역할) (2) `[Event.Prop "Text"] = self "Text"`로 인스턴스 프로퍼티 변경을
다시 컴포넌트 store로 역방향 전파(양방향 바인딩, `EmitPropertyChangedSignal`
자동 연결과 동일) — quad.qwreey.kr 튜토리얼 `11_extend/` 문서 원문 확인.
이 두 역할이 v2 온톨로지에서는 이미 갈라져 있음: (1)은 확정된 **Ref**가
대체, (2)는 이번 논의에서 다루는 Source 양방향 프록시가 대체.
## 수렴된 결론
### 1. 컴포넌트 = 그냥 함수, "자기 store 자동 소유" 매직은 폐기
`MyComp = function(props) return Frame {...} end`, 호출 규약은
`Frame{...}`와 동일(`MyComp{...}` → `MyComp(propsTable)`). v1의 Extend
자동-store-생성+자동-흡수 매직은 재현하지 않음 — 대신 React식으로 호출부가
State/raw/Source/콜백 중 뭘 넘길지 명시적으로 고름. 이유: 자동 흡수는
매 컴포넌트 호출마다 "이 prop이 State인가?" 타입 분기를 프레임워크가
암묵적으로 수행해야 하는 매직이고, 명시적 전달이 더 단순·예측 가능(React가
Vue/Svelte 대비 내세우는 강점과 동일 논리) — **사용자 확정**("마법 안쓴다
그것도 동의함").
### 2. State/Source 경계 규칙: 파생이면 읽기전용, 원본이면 쓰기 가능
State는 `:With`/`:Compute`로 만들어진 파생값일 수 있어 쓰기가 정의 자체가
안 됨. Source(독립이든 Store 소속 `StoreSource` 프록시든)는 파생이 아니라
항상 원본 슬롯 하나를 직접 가리키므로 쓰기가 의미 있음 — **사용자 확정**
("맞음. 확실해").
### 3. `StoreSource`: Source를 인터페이스+구현체로 두고, Store 키에서 그 인터페이스를 구현하는 얇은 프록시를 받음
- **Source = 인터페이스이자 구현체**: 독립 생성자 `Source(initial)`가 기본
구현체, `store:GetSource("key")`(가칭)류 접근자가 반환하는 값은 같은
인터페이스를 구현하는 별도의 얇은 프록시(`StoreSource`) — 읽기는
`store.key`로, 쓰기는 `store.key = v`로 위임. **내부 Source 객체를 그대로
노출하지 않음** — 그러면 "쓰기는 오직 Store의 `__newindex`뿐"이라는 기존
확정과 새 쓰기 경로가 충돌하게 됨.
- **캐시 안 함**: State가 이미 "매번 새로 만듦, store에 캐시 안 됨"으로
확정돼 있어 일관성 + 엔지니어링 비용 둘 다 이쪽이 쌈 — **사용자 확정**
("그냥 엔지니어링적으로 비용이 싼거 택해").
### 4. Source 직접 전달(양방향)은 핸들러 계약 확장 없이 타입 유니온으로 처리 — 단, 실사용 범위는 좁음
- 핸들러가 값을 받을 때 `Source<T> | State<T>` 유니온으로 받고, 내부에서
타입 체크만 하면 됨(Source면 인스턴스 변경 이벤트에 걸어 역방향 쓰기까지
처리, State면 읽기만) — `isHandlable`/`priority`/`process`/`retract` 4종
계약에 5번째 항목을 추가할 필요 없음. Source 자체가 계산이 없는 원천이라
가능한 단순화 — **사용자 확정**("그냥 타입 상 source를 받거나 state를
받거나 하면 됨. source 자체는 원천이라 컴퓨팅 같은거 없어").
- **하지만 실사용은 좁을 것으로 예상**: `isEnabled`처럼 여러 조건에 영향
받는(=파생된) 값은 애초에 State지 Source가 아니므로 이 경로로 못 넘김.
즉 Source 직접 전달이 통하는 건 진짜 단순한 1:1 원본-토글 케이스뿐이고,
일반적인 경우엔 React식 `value(State) + onChange(callback)` 패턴이 기본
**사용자 확정**("isenabled가 여러 조건에 영향 받으면 바로 문제가
생기는거지. 따라서 실제 사용은 제한적일듯. callback을 쓰는게
일반적이여 보이긴 해. 타입으로도 편하기도 하고 디버깅도 편함").
### 5. 리프(Roblox 프로퍼티) 바인딩엔 원칙적으로 State만
계산된 최종값만 실제로 인스턴스에 반영되어야 하므로, 리프 바인딩은 State가
일반 경로. Source는 리프 바인딩용 프리미티브가 아니라, 아주 단순한 구조에서
콜백 보일러플레이트를 줄이기 위한 좁은 용도의 예외 — **사용자 확정**
("리프 바인딩엔 state만 쓰이지 않을까... source는 그냥 아주 단순한
구조에서 콜백을 넣고 하는 복잡함을 줄이기 위함일 뿐임").
## 아직 열린 질문 (`.claude/question.md`에도 취합)
- **modifier/Ref가 컴포넌트 경계를 어떻게 통과하는가**: 컴포넌트가 플레인
함수이고 반환하는 루트가 여러 개(혹은 Slot으로 갈라지는 구조)일 때, 호출부가
넘긴 modifier/Ref가 "어느 루트로 가야 하는지" 모호해지는 케이스가 있음.
Jetpack Compose는 언어 강제가 아니라 "컴포저블은 `modifier` 파라미터를
받아 루트에 적용해야 한다"는 순수 관례(+린트)로 풂 — quad도 비슷한 관례
기반으로 갈 수 있어 보이나 다중 루트 케이스는 미정. **주의: 이건 "경계를
어떻게 통과하는가"의 문제이고, "modifier 값 자체가 어떻게 동작하는가"(정적
merge, immutable 체이닝)는 `research/modifier-plan.md`로 이미 별도 확정됨
— 둘을 혼동하지 말 것.**
- **정확한 API 이름**: `Component`(플레인 함수 규약이라 별도 래퍼가 필요한지
자체도 불확실 — 아마 불필요), `GetSource` 계열 접근자 이름, `Source`
독립 생성자 이름은 전부 가칭. `base/bind-system-plan.md`의 "남은 열린
질문" 절(정확한 함수/생성자 이름 미정)과 같은 급의 후순위 항목.
- **`quad2-try`는 확인 불필요로 재확인** — 진행이 중단된 상태라 이 논의와
무관.

View file

@ -6,12 +6,12 @@
## 문제 ## 문제
이미 생성된 Roblox Instance에 새로운 `{k=v}` 프롭 테이블을 나중에 바인드하는 이미 생성된 Roblox Instance에 새로운 `{k=v}` 프롭 테이블을 나중에 바인드하는
걸 허용할지. 허용하려면 이전 바인드를 끊는 처리가 필요한데, cleanup이 걸 허용할지. 허용하려면 이전 바인드를 끊는 처리가 필요한데, retract가
구현되어 있어도 바로 지원하는 건 엔지니어링 비용이 높음. 구현되어 있어도 바로 지원하는 건 엔지니어링 비용이 높음.
## 기울어진 방향 ## 기울어진 방향
**UB로 두거나, 마일스톤(추후 구현)으로 미룬다.** cleanup이 이미 있고 store **UB로 두거나, 마일스톤(추후 구현)으로 미룬다.** retract가 이미 있고 store
바인드도 우선순위 높은 플러그라면 이론적으로는 가능해 보이지만(핸들러 바인드도 우선순위 높은 플러그라면 이론적으로는 가능해 보이지만(핸들러
레지스트리가 이미 "우선순위 스캔 후 bind" 구조라 재바인드도 같은 경로를 타면 레지스트리가 이미 "우선순위 스캔 후 bind" 구조라 재바인드도 같은 경로를 타면
됨), 초기 구현에서 **우선순위를 낮게** 잡아야 함 — 문제 유무가 많을 수 있어서. 됨), 초기 구현에서 **우선순위를 낮게** 잡아야 함 — 문제 유무가 많을 수 있어서.

View file

@ -1,7 +1,9 @@
# Tween / 애니메이션 플러깅 (착수 전, 사용자와 상의 필요) # Tween / 애니메이션 플러깅 (기본값 확정, 옵션 키 이름만 남음)
**상태**: research — 방향은 뚜렷하게 잡혀 있으나(라이브러리가 트윈을 직접 **상태**: research — 방향은 뚜렷하게 잡혀 있고(라이브러리가 트윈을 직접
구현하지 않는다) cleanup 순서/오버라이드 시맨틱은 미확정. 원본: 구현하지 않는다), `retract` 순서/오버라이드 기본값(Cancel)도 확정됨. 남은 건
기본값 외 나머지 오버라이드 동작을 고르는 옵션 키의 정확한 이름/시그니처
정도. 원본:
`.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은 `.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은
항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이 항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이
반응 그래프 안에 있는 설계는 명시적 반면교사 — `base/comparison-fusion-vide.md` 반응 그래프 안에 있는 설계는 명시적 반면교사 — `base/comparison-fusion-vide.md`
@ -79,7 +81,7 @@ Destroy 시점엔 아무 것도 안 함(라이프타임 `Connected` 체크로
## 네임스페이스드 객체 (성능상 이유로 보류) ## 네임스페이스드 객체 (성능상 이유로 보류)
트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 — 트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 —
TagService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페이스가 따로 있을 CollectionService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페이스가 따로 있을
필요가 있는지는 미정(단, 위 정정으로 이 절 자체의 필요성이 낮아짐 — 핸들러가 필요가 있는지는 미정(단, 위 정정으로 이 절 자체의 필요성이 낮아짐 — 핸들러가
이미 대상을 직접 받으므로 "나중에 이름으로 찾아서 트윈"할 필요 자체가 잘 이미 대상을 직접 받으므로 "나중에 이름으로 찾아서 트윈"할 필요 자체가 잘
없을 수 있음). 없을 수 있음).

177
CLAUDE.md
View file

@ -20,48 +20,33 @@ Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터
길게 잡음. 길게 잡음.
**지금은 설계/계획 단계이고 구현은 아직 시작 전** — 저장소 루트에 실제 소스 **지금은 설계/계획 단계이고 구현은 아직 시작 전** — 저장소 루트에 실제 소스
코드(`src/` 등)가 없음. 2026-08-03에 확정됐던 핵심 아키텍처 결정들(Store 코드(`src/` 등)가 없음. 핵심 아키텍처(Store 책임 분리, `process`/`retract`
책임 분리, `process`/`retract` 디스패치 모델, Signal 미채택, Ref 역할, Store 디스패치 모델, Store/State/Source 온톨로지, 소스 트리 구조, Modifier 메커니즘,
문법 인체공학, 트윈 기본 오버라이드, Slot 재마운트 에러 처리, 순수성→이식성 컴포넌트=플레인 함수)는 전부 `.claude/base/`에 문서로 확정돼 있음 — 먼저
재정의 등)은 2026-08-04에 `AskUserQuestion`으로 하나씩 재검증까지 마쳐서 `.claude/base/architecture.md`를 읽을 것. **단, "핵심 설계 질문이 더 이상
확정 상태 — `.claude/question.md`의 "확정됨" 절 참고. 이전에 시도했다 폐기한 없다"는 뜻은 아님** — 컴포넌트화(특히 modifier/Ref가 컴포넌트 경계를 어떻게
v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치 완료 — OOP 상속/커스텀 통과하는지)는 사용자가 직접 "지금 quad에서 가장 문제되는 부분"으로 지목한
파서/Slot 스텁은 확인된 죽은 접근이라 반복 금지, `Pipe`의 copy-on-write 채 아직 열려있음, 아래 "지금 할 일" 참고.
절충안은 한때 살려볼 후보였으나 **2026-08-04에 사실상 폐기로 재평가**됨(State
자체가 `state(state)`로 분기하는 쪽으로 대체).
**Store/State/Source 온톨로지 및 관련 인체공학 질문은 2026-08-04 네 라운드에 이전에 시도했다 폐기한 v2 재작성 시도(`.claude/initreq/quad2-try`)도 리서치
걸쳐 전부 확정됨**(사용자가 공유해준 실제 참고 코드 `.claude/initreq/ 완료 — OOP 상속/커스텀 파서/Slot 스텁/`Pipe` copy-on-write 절충안은 확인된
artworks/`, PA님 작성, 로 4차 교차검증까지 마침) — push-invalidate/ 죽은 접근이라 반복 조사 금지(`base/bind-system-plan.md` 참고).
pull-recompute 전파 모델, `:Compute` self/with 인자를 둘 다 lazy State
핸들로 통일, State는 쓰기 불가(값 쓰기는 항상 Store의 `__newindex`),
`Source`는 Store와 별개인 독립 프리미티브로 격상, Slot 생존 확인은 기존
canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추론 1급
경로로(인스턴스 생성도 같은 관습, 단 이벤트는 PA님 방식인 평범한 문자열
키+런타임 리플렉션으로 예외), `RobloxFactory` 재호출 가드(같은 팩토리=무시,
다른 팩토리=에러)까지 확정. 남은 건 정확한 API 표면 이름뿐 —
`.claude/base/bind-system-plan.md` 전체, `.claude/question.md`
"2026-08-04" 절들 참고.
**소스 트리 구조도 확정됨(2026-08-04 5차 라운드)**: `bind-system-plan.md`/
`module-lifecycle-plan.md`/`slot-plan.md` 모두 `research/`에서 `base/`
승격 완료. 모노레포(`quad-base`/`quad-roblox` 서브폴더, RbxUtil 패턴)로
당장은 모놀리식 진행, 패키지 경계(디스패치 엔진까지 base가 인터페이스로
소유)까지 확정 — `base/architecture.md`의 "구현 착수: 소스 트리 구조 확정"
절 참고. 아래 "지금 할 일" 1번이 다음 단계(실제 스캐폴딩)를 명시.
## 계획 문서 구조 ## 계획 문서 구조
`.claude/README.md`가 색인. 요약: `.claude/README.md`가 색인. 요약:
- `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저 - `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저
`.claude/base/architecture.md`를 읽을 것. `.claude/base/architecture.md`를 읽을 것.
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. - `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의. 지금은
`tween-plan.md`(세부 옵션만 남음), `existing-instance-bind-plan.md`(급하지
않음), `component-composition-plan.md`(**사용자가 최우선으로 지목한 열린
주제**) 세 개뿐.
- `.claude/qa-request/`, `.claude/archive/`, `.claude/feedback/` — 구현 - `.claude/qa-request/`, `.claude/archive/`, `.claude/feedback/` — 구현
시작되면 쓰기 시작함, 지금은 비어있음. 시작되면 쓰기 시작함, 지금은 비어있음.
- `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox, - `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox,
code-docker) + 원본 요청. **읽기 전용, `.gitignore`로 커밋 제외됨** — 내용을 code-docker) + PA님 실 코드(`artworks/`) + 원본 요청. **읽기 전용,
다른 곳으로 옮기지 말고 항상 원본 그대로 둘 것. 리서치가 더 필요하면 이 `.gitignore`로 커밋 제외됨** — 내용을 다른 곳으로 옮기지 말고 항상 원본
폴더를 다시 파고들 것. 그대로 둘 것. 리서치가 더 필요하면 이 폴더를 다시 파고들 것.
- `.claude/question.md` — 사용자가 답해야 할 질문 전체 취합(우선순위순). - `.claude/question.md` — 사용자가 답해야 할 질문 전체 취합(우선순위순).
- 루트 `HUMAN_TODO.md` — 사람만 할 수 있는 일(로컬 GUI 조작, 스케줄/루프 - 루트 `HUMAN_TODO.md` — 사람만 할 수 있는 일(로컬 GUI 조작, 스케줄/루프
설정 등). 설정 등).
@ -72,7 +57,9 @@ canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추
컨텍스트 보호. 이미 완료된 v1/rbvm/tbox/Fusion/Vide 리서치 결과는 컨텍스트 보호. 이미 완료된 v1/rbvm/tbox/Fusion/Vide 리서치 결과는
`.claude/base/`에 정리되어 있으니 중복 조사하지 말고 먼저 그걸 볼 것. `.claude/base/`에 정리되어 있으니 중복 조사하지 말고 먼저 그걸 볼 것.
- **병렬화 가능한 작업은 Agent 여러 개를 한 메시지에 동시 호출.** 서로 독립적인 - **병렬화 가능한 작업은 Agent 여러 개를 한 메시지에 동시 호출.** 서로 독립적인
파일/주제를 다루는 리서치나 구현 조사가 여기 해당. 파일/주제를 다루는 리서치나 구현 조사, 또는 서로 다른 문서 파일을 고치는
문서 정리 작업이 여기 해당(단, 같은 파일을 동시에 고치는 에이전트를 병렬로
띄우지 말 것 — 충돌함).
- **크리티컬한 설계 결정은 구현으로 밀어붙이지 말고 plan을 research/에 남긴 채 - **크리티컬한 설계 결정은 구현으로 밀어붙이지 말고 plan을 research/에 남긴 채
연기.** 사용자는 Lua/Roblox 엔진을 깊이 아는 사람 — 근거와 선택지를 문서에 연기.** 사용자는 Lua/Roblox 엔진을 깊이 아는 사람 — 근거와 선택지를 문서에
정리해두면 사용자가 깨어있을 때 훑어보고 답해줄 것. `.claude/question.md` 정리해두면 사용자가 깨어있을 때 훑어보고 답해줄 것. `.claude/question.md`
@ -81,6 +68,12 @@ canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추
조사하게 되는 재작업을 막기 위함. `.claude/base/`로 승격, `.claude/qa-request/` 조사하게 되는 재작업을 막기 위함. `.claude/base/`로 승격, `.claude/qa-request/`
이동, 또는 문서 자체를 갱신. code-docker/webmanager의 `.claude/` 관리 방식이 이동, 또는 문서 자체를 갱신. code-docker/webmanager의 `.claude/` 관리 방식이
좋은 예시(`.claude/initreq/code-docker/webmanager/.claude/README.md` 참고). 좋은 예시(`.claude/initreq/code-docker/webmanager/.claude/README.md` 참고).
- **문서가 쌓이면서 모순/중복/stale 마커가 생기기 쉬움 — 주기적으로 감사할
것.** 2026-08-04 세션에 실제로 전체 `.claude/` 코퍼스에서 이런 문제가
다수 발견되어 정리함(아래 "최근 세션 요약" 참고) — 여러 라운드에 걸쳐
같은 문서를 계속 고치다 보면 "정정됨" 표시가 원래 문장에 안 반영되고
방치되는 패턴이 반복되니, 큰 방향 전환이 있을 때마다 관련 문서 전체를
훑어 확인할 것.
- **Roblox Studio MCP 연결 시 주의**: Studio는 잘 죽는 편 — 죽었을 때 살리려고 - **Roblox Studio MCP 연결 시 주의**: Studio는 잘 죽는 편 — 죽었을 때 살리려고
위험한 명령을 반복 시도하지 말 것. 그런 상황이면 MCP 없이 할 수 있는 작업만 위험한 명령을 반복 시도하지 말 것. 그런 상황이면 MCP 없이 할 수 있는 작업만
하거나 대기. 연결 방법은 `HUMAN_TODO.md` 1번 항목 참고(사용자가 Studio에서 하거나 대기. 연결 방법은 `HUMAN_TODO.md` 1번 항목 참고(사용자가 Studio에서
@ -94,77 +87,59 @@ canExecute 유틸 재사용으로 해소, `store.key` dot-access를 타입 추
## 지금 할 일 (우선순위순) ## 지금 할 일 (우선순위순)
1. **[다음 세션 최우선] 실제 스캐폴딩.** 소스 트리 구조는 문서로 이미 확정됨 1. **컴포넌트화 논의 계속 — 사용자가 직접 "가장 문제되는 부분"으로 지목.**
(`base/architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절) — 다음 `research/component-composition-plan.md` 참고. 핵심 골격(컴포넌트=플레인
세션에서 실제로 `quad-base/`, `quad-roblox/` 폴더, 각각의 `wally.toml`, 함수, State/Source 읽기·쓰기 경계, `StoreSource` 프록시)과 Modifier
루트 `default.project.json`, `.luaurc`를 만들 것. 이 시점부터 `qa-request/`/ 메커니즘 자체(`base/modifier-plan.md`, 완전 확정)는 수렴됨 — 남은 건
`archive/` 폴더가 실제로 쓰이기 시작함. **modifier/Ref가 컴포넌트 경계(특히 다중 루트)를 어떻게 통과하는가**라는
2. 남은 세부 시그니처(`CreatedRef`/`state()`/`Source()`/`DI`류 정확한 진짜 설계 질문(이름 문제가 아님). 다음 세션에서 이걸 이어서 파고들 것.
이름)는 위 항목과 자연스럽게 같이 확정 가능 — PA님 실 코드(`.claude/ 2. **실제 스캐폴딩.** 소스 트리 구조는 문서로 이미 확정됨(`base/
initreq/artworks/`)를 이미 받아서 교차검증 완료(아래 인수인계 메모 architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절) — `quad-base/`,
참고), `On` 모듈은 이벤트 바인딩 방식이 바뀌며 아예 불필요해짐. `quad-roblox/` 폴더, 각각의 `wally.toml`, 루트 `default.project.json`,
3. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만 `.luaurc`를 만들 것. 1번의 컴포넌트 경계 논의가 `DI`/`Modifier` 모듈
설계에 영향을 주므로, 그 결론이 안 나온 상태에서도 나머지 구조(Store/
State/Source, 디스패치 엔진, Slot)는 그대로 스캐폴딩 가능 — 막을 필요
없음(`architecture.md`에도 명시). 이 시점부터 `qa-request/`/`archive/`
폴더가 실제로 쓰이기 시작함.
3. **용어 정리 — 사용자가 별도로 요청, 진행 중.** "register"(v1) 같이
부정확한 이름들을 전체적으로 재검토하자는 요청 — 1차 제안 완료(우선순위
순: `State`가 React/Vue식 "쓸 수 있는 로컬 상태"라는 통상 의미와 반대라
가장 위험, `DI`가 Dependency Injection 축약어와 충돌, `PerInstanceState`
핵심 프리미티브 `State`와 이름 충돌 — 세부는 `.claude/question.md` 참고),
사용자와 같이 계속 논의 필요.
4. `research/existing-instance-bind-plan.md`는 급하지 않음 — 스코프 논의만
필요, 구현 착수를 막지 않음. 필요, 구현 착수를 막지 않음.
4. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목). (`HUMAN_TODO.md` 2번 항목).
## 인수인계 메모 (2026-08-04 세션 종료 시점, 5차 라운드까지 반영) ## 최근 세션 요약 (2026-08-04, 6차 라운드 이후)
**5차 라운드(소스 구조 확정)**: 4차 라운드 종료 시점에 서브에이전트로 먼저 **6차 라운드**: 남아있던 "급하지 않음" 질문 두 개 해소 — 태그 네임스페이싱
계획 문서 전체의 정합성을 점검(차질 없음 확인) 후 진행. 패키징 방식은 충돌은 컴포넌트 단위로는 Ref가 대신 해결해줘서 심각하게 안 봄(`architecture.md`
서브에이전트 웹 리서치로 확인(`.luaurc` alias 런타임 미지원, wally 심볼릭 5번), Store가 Store를 담는 경우는 없음으로 확정(Store는 Source에 준하는
링크/타입 문제, `Sleitnick/RbxUtil`의 모노레포+개별 wally.toml 선례, "시작점"이라 다른 반응형 값에 자동 연결되지 않음, `bind-system-plan.md`).
`pesde`는 아직 이름) — 모노레포로 당장 진행, 나중에 실제 분리 결정.
패키지 경계는 사용자가 "base=인터페이스, roblox=구현"이라는 원칙을 명확히
해서 확정 — Store/State/Source 온톨로지뿐 아니라 `process`/`retract`
디스패치 엔진, `LifetimeHandle`/`PerInstanceState` 인터페이스, Ref, Slot
코어 재조정 로직까지 전부 `quad-base`가 소유(다른 엔진에서도 재사용
가능해야 한다는 전제, 엔진마다 큰 구현 중복 방지가 목적). Slot도 같은
원칙 적용 확정, 그 과정에서 `k:number,v:Instance` 중첩 인스턴스 자식용
`InstanceChild` 핸들러가 추가로 필요하다는 게 밝혀짐. `bind-system-plan.md`/
`module-lifecycle-plan.md`/`slot-plan.md` 세 문서 모두 `research/`에서
`base/`로 승격 완료, `base/architecture.md`에 전체 소스 트리가 문서화됨 —
실제 폴더/파일 스캐폴딩은 다음 세션(위 "지금 할 일" 1번).
## 인수인계 메모 (2026-08-04 세션 종료 시점, 4차 라운드까지 반영) **그 이후 채팅에서 세 가지 큰 스레드가 새로 열림/정리됨**:
- **Modifier 메커니즘 전체 확정** — 런타임 pluggable 핸들러가 아니라 정적
merge, immutable+`table.clone` 기반 체이닝, 필드가 State일 수도 있는
경우의 setter/getter 동작까지 전부 확정(`base/modifier-plan.md`, 새로
base 승격). 이 논의에서 "관측해야 실체화된다"는 프로젝트 전역 원칙도
명문화(`bind-system-plan.md`).
- **컴포넌트화 논의 시작, 아직 미완** — v1의 `Class.Extend()` 자동-store
매직은 폐기하고 React식으로 값을 명시적으로 전달하는 방향으로 수렴,
`StoreSource`(Source를 인터페이스+구현체로 보고 Store 키에서 얇은
프록시로 얻는 것) 아이디어까지 나왔지만 modifier/Ref의 컴포넌트 경계
통과 방식은 미해결(`research/component-composition-plan.md`, 위 "지금
할 일" 1번).
- **문서 전체 감사 및 정리**`.claude/` 코퍼스 전체(약 15개 문서)를
서브에이전트로 감사해 여러 라운드에 걸쳐 쌓인 모순/중복/stale 마커를
대거 발견하고 수정(예: 이벤트 dot-access 확정 여부가 문서 내에서 서로
모순, 이미 해소된 질문이 "미해결"로 방치, 존재하지 않는 문서/섹션을
가리키는 끊긴 참조 다수, `TagService`/`CollectionService` 혼용 등).
`research/purity-and-effects-plan.md`도 내용이 이미 확정 상태라 `base/`
승격. **이 CLAUDE.md 자체도 이번에 오래된 라운드별 인수인계 메모 3개를
이 요약 하나로 통합하며 정리함** — 라운드별 상세 히스토리가 필요하면
git log와 각 `base/`/`research/` 문서 안의 라운드 표시(예: "2026-08-04
3차 라운드")를 참고할 것, 여기서 전부 반복하지 않음.
2026-08-03에 확정됐다고 표시된 결정 전체(architecture.md 14개 + lifecycle- 용어 정리 제안 진행 중인 점은 위 "지금 할 일" 3번 참고.
pattern/store-semantics/bind-system-plan/module-lifecycle-plan/slot-plan/
tween-plan)를 `AskUserQuestion`으로 하나씩 예/아니오 검증 완료 — 상세는
`.claude/question.md`의 "2026-08-04 검증 라운드 완료" 절. 검증 과정에서
사용자가 실시간으로 설계를 더 전개하면서 **"State 프리미티브는 안 만든다"는
기존 결정이 틀렸다는 게 밝혀짐** — Store/State/Source 온톨로지 전체가 이
세션에서 새로 열린 가장 중요한 설계 스레드로 떠올랐음.
**같은 날 이어진 2차/3차 라운드에서 그 온톨로지와 인체공학 질문 전부를
확정함**: push-invalidate/pull-recompute 전파 모델(Fusion식 eager 노드/생성순
정렬 불필요), `:Compute`의 self/with 인자를 둘 다 lazy State 핸들로 통일
(별도 `ComputeWithout` 불필요), State는 쓰기 불가(값 쓰기는 Store의
`__newindex`로만) 확정, `Source`는 Store 내부 디테일이 아니라 값 하나만
다룰 때 쓰는 독립 공개 프리미티브로 격상, Slot 생존 확인 문제는 새 메커니즘
없이 기존 canExecute 유틸 재사용으로 해소(부수 효과로 "Store가 Store를 담을
때 이중 해제 방지 필요한가" 백로그 항목도 "명시적 dispose가 없어 질문 자체가
성립 안 함"으로 닫힘), `store.key` dot-access를 타입 추론 1급 경로로 삼는
관습을 인스턴스 생성까지 프로젝트 전역으로 확정, `RobloxFactory` 재호출
가드(같은 팩토리=무시, 다른 팩토리=에러, `New()`와는 인스턴스별 테이블
분리로 자연히 공존)까지 확정.
**4차 라운드에서 사용자가 실제 참고 코드(`.claude/initreq/artworks/`, PA님
작성 — UI 포함 전반적 설계 패턴을 시범 적용한 데모 모듈)를 공유해줘서
교차검증**: "DI"는 Dependency Injection이 아니라 Declarative Instance였음
(정정). 인스턴스 생성은 2트랙 구상보다 단순한 "제네릭 생성자 함수 하나 +
자주 쓰는 클래스만 정적 필드로 미리 바인딩" 모양으로 정정. **이벤트
바인딩은 `On.EventName` 도트액세스를 접고 PA님 방식(평범한 문자열 키 +
`ReflectionService` 기반 자동 판별)으로 전환** — Store의 dot-access는 실질적
타입 이득이 있어 그대로 유지, 이벤트만 예외. 전파 모델(push-invalidate/
pull-recompute)과 라이프사이클(GC-native)은 PA님 코드가 반례처럼 보였으나
(각각 파생 개념이 없는 단순 pub-sub, 전부 수동 해제) 재검토 후 **기존
확정 유지** — 라이프사이클은 나중에 하이브리드로 확장 가능한 여지만 기록.
OOP 회피 결정은 PA님의 `class.luau`도 같은 체이닝 상속 문제를 보여 오히려
보강됨. **더 이상 열려있는 핵심 설계 질문은 없음** — 남은 건 API 표면 이름뿐
(위 "지금 할 일" 참고). 그 외 자잘한 정정들(Slot retract=폐기 확정, Pipe COW
후보 폐기 등)은 각 문서에 바로 반영해둠 — 재조사 불필요.
이전 세션(2026-08-03) 종료 시점 메모: `.claude/` 전체 스캐폴드 + 대부분의
핵심 아키텍처 결정을 완료, 로컬 git 저장소 초기화+첫 커밋(원격 없음,
`SAFETY.md` 참고 — 원격은 사용자가 제한 계정을 마련해줘야 추가 가능).