quad-v2 재작성 계획 초기 스캐폴드

quad(Roblox DOMless UI 렌더러) v2 재작성을 위한 .claude/ 계획 구조를 세우고
핵심 아키텍처 결정을 정리함:

- quad v1 / rbvm / tbox / Fusion / Vide / 폐기된 quad2-try 프로토타입 리서치
- Store 책임 분리(base vs provider), process/retract 핸들러 디스패치 모델,
  Ref 역할, Slot/Tween 설계 방향 등 핵심 결정 확정
- .claude/{base,research,qa-request,archive,feedback}, question.md, README.md
  구조 마련 (code-docker/webmanager 패턴 참고)
- 루트 CLAUDE.md/HUMAN_TODO.md 작성

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-04 00:07:40 +09:00
commit 0c9b8584ee
Signed by: qwreey
GPG key ID: D28DB79297A214BD
17 changed files with 1348 additions and 0 deletions

49
.claude/README.md Normal file
View file

@ -0,0 +1,49 @@
# .claude/ — quad-v2 계획/설계 문서 색인
이 레포 전체가 quad-v2(재작성) 프로젝트이므로, webmanager류 서브프로젝트 구분 없이
`.claude/` 바로 아래에 전부 있음. **루트 `CLAUDE.md`가 현재 상태+TODO 색인의
최종 소스** — 먼저 그걸 보고, 특정 결정의 자세한 근거/논의가 필요할 때만 아래
개별 문서를 열어볼 것.
## 폴더 기준
| 폴더 | 기준 |
|---|---|
| `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식 |
| `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 |
| `qa-request/` | 구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음 — 지금은 구현 자체가 시작 전이라 비어있음 |
| `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 — 지금은 비어있음 |
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — 지금은 비어있음(구현 시작 전) |
| `initreq/` | 프로젝트 착수 시 클론해둔 참고 레포(quad v1, fusion, vide, rbvm, tbox, code-docker) + 원본 요청(`req.md`, `raw-userinput.md`) + `quad2-try`(이전에 시도했다 폐기한 v2 재작성 시도 — 리서치 완료, 결론은 `research/bind-system-plan.md`) — 읽기 전용 리서치 소스, 여기 내용을 옮기지 말고 항상 원본 그대로 유지 |
`research/`의 문서가 설계 확정되면 `base/`로 승격(또는 구현 착수 시
`qa-request/`행). 지금은 구현 라운드 전(설계 단계)이라 전부 `base/`/`research/`에만
있음.
## `base/` — 결정된 것, 프로젝트 전체 컨텍스트
| 문서 | 내용 |
|---|---|
| `architecture.md` | quad-v2 전체 아키텍처 확정 사항 요약(제일 먼저 볼 문서) |
| `quad-v1-architecture.md` | v1(`initreq/quad`) 내부 동작 스냅샷 — "이 문제를 안 반복하려면"의 기준선 |
| `comparison-fusion-vide.md` | Fusion/Vide 아키텍처 비교 리서치 — 설계 결정 근거 자료 |
| `lifecycle-pattern.md` | rbvm의 `Connected`+GC 관용구를 quad-v2가 채택하는 방식 |
| `store-semantics.md` | Store는 부작용 허용이 기본, 별도 State 프리미티브는 안 만듦 |
## `research/` — 아직 착수 전, 상의 필요
| 문서 | 내용 | 우선순위 |
|---|---|---|
| `bind-system-plan.md` | pluggable key/value 핸들러 레지스트리 — `process`/`retract` 디스패치 모델, Ref, quad2-try 리서치 결과. 핵심은 확정, 세부 시그니처만 남음 | 최상 — 다른 모든 설계가 이 위에서 조립됨 |
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정됨 | 최상 — 확정, 구현 착수 시 API 세부만 조정 |
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw | 상 — bind-system 확정 후 |
| `tween-plan.md` | 트윈을 Store 밖 특수 bind key로 처리, 기본 오버라이드는 Cancel | 중 — 세부 옵션만 남음 |
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 | 하 — 문서화 성격, 급하지 않음 |
| `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 |
## 참고
- **저장소 소유자가 답해야 할 질문 전체 취합**: `.claude/question.md`
- **사람만 할 수 있는 일(로컬 조작/결정)**: 루트 `HUMAN_TODO.md`
- **원본 브레인스토밍(raw chain-of-thought)**: `.claude/initreq/raw-userinput.md`,
`.claude/initreq/req.md` — 위 문서들로 나누기 전의 원본, 참고용 백업이니 그대로 둘 것

View file

@ -0,0 +1,77 @@
# quad-v2 전체 아키텍처 (현재 상태 요약)
**상태**: base — 횡단 결정의 최종 상태 요약. 특정 기능 plan이 아니라 프로젝트
전체에 걸친 결정이라 완료 개념 없음. 근거가 된 원본 브레인스토밍은
`.claude/initreq/raw-userinput.md`(안 옮기고 그대로 둠 — 이 문서들로 나누기 전의
raw chain-of-thought 백업 역할). 현재 v1 구조는 `base/quad-v1-architecture.md`,
비교 리서치는 `base/comparison-fusion-vide.md`, `base/lifecycle-pattern.md` 참고.
## 한 줄 요약
quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblox UI 렌더러라는
정체성은 유지하되, 내부를 확장 가능하게 재구현한다. 프로덕트 하나를 빨리 내는 게
목표가 아니라 코드 퀄리티/지속 가능성이 목표 — 빠른 이터레이션보다 정확성이
우선.
## 확정된 결정
1. **DOMless 유지, 하지만 pluggable 하게.** 가상 DOM 없이 즉시 Roblox Instance를
만드는 기존 방식은 유지. 대신 key/value 바인드 디스패치, 렌더 백엔드를
pluggable하게 만들어 확장성 확보(아래 4, 5번).
2. **Class는 이제 "특정 상태의 store를 받는 함수"** — v1의 `Class.Extend()`
OOP 스타일(메서드 체이닝, Getter/Setter) 대신 함수형이 기본. 체이닝은 store
바인드처럼 정말 체인이 자연스러운 곳에만(`:` 문법) 남김. 타입 작성 난이도가
OOP 스타일에서 너무 커진다는 게 이유.
3. **복사(clone) 구현 지양, 팩토리 함수로 대체.** v1의 metatable 체이닝(1-필드
테이블을 계속 쌓는 방식, `base/quad-v1-architecture.md` 참고)은 폐기.
store 바인드에 대한 변경은 "전체 변경"으로 간주(UB 아님, 문서화된 의미론) —
부분 복사/오버레이가 필요하면 팩토리 함수로 필요한 곳만 명시적으로 복사.
4. **PA님 스타일 DI 키 계속 지원**: `[Attribute "Name"]`, `[Tag ""] = true` 같은
특수 바인드 키. Tag는 `retract`(구 cleanup, `base/lifecycle-pattern.md` 참고)가
내장되어 store 컴퓨티드 바인드도 가능해야 함.
5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/
`Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유.
네임스페이싱 문제는 있지만(`.claude/question.md` 참고) 별도 네임스페이스
개념을 추가하면 라이브러리 복잡도가 너무 올라간다고 판단 — 당장은
TagService 그대로 사용. **대신 Ref가 도입됨** — 단 Ref의 용도는 "id로 조회"가
아니라 "외부에서 이미 관리되고 있는 instance를 quad로 점진적으로 마이그레이션/
래핑하기 위해 직접 참조를 얻는 것"(`research/bind-system-plan.md`의 Ref 절
참고) — 둘을 혼동하지 말 것.
6. **함수지향 디폴트, `:` 체이닝은 예외적으로만.** 스토어 바인드처럼 체인이 정말
편한 경우만 `:` 사용, 나머지는 외부 함수가 인스턴스를 인자로 받는 모양.
7. **Style(Default) 시스템 폐기.** Roblox 자체 스타일시트를 쓰는 게 낫다고 판단.
대신 modifier(spread되는 값, `...`으로 풀리는 것)를 지향 — 함수형 modifier가
store 바인드를 받을 수도 있음.
8. **특수 이벤트는 특수 플러깅으로.** `PropertyChangedSignal`, `PropertyChangedEvent ""`
같은 것들은 일반 이벤트 바인드가 아니라 pluggable 바인드 핸들러 중 하나로
구현(`research/bind-system-plan.md`).
9. **Tracker 미구현.** v1의 소스 변경 감지 자동 재렌더 기능(hot-reload watcher,
실제로는 `.claude/initreq/quad/src/tracker.lua` — v1에서도 이미 `exports.lua`
연결 안 된 죽은 코드였음, `base/quad-v1-architecture.md` 참고)은 렌더
라이브러리 범위 밖으로 판단. 스토리북 구현체(https://ui-labs.luau.page/docs/getstarted)가
이미 존재하므로 대체.
10. **lang 모듈 미구현, 분리.** 로케일/문자열 처리는 리액터블 라이브러리와
별개 라이브러리로 존재해야 함(v1 `lang.lua`의 전역 스코프 버그 등은
`base/quad-v1-architecture.md` 참고 — 애초에 반면교사).
11. **커스텀 Signal 클래스 미구현.** 콜백 정도로 이벤트 바인드 뒤에 함수를
넣는 것만으로 충분하다고 판단(단, `base/lifecycle-pattern.md`의 rbvm 리서치
결과 rbvm의 커스텀 Signal이 실제로는 재사용 가능해 보여서 상충 — 열린 질문으로
`.claude/question.md`에 있음).
12. **멀티 타겟(pluggable 백엔드) — 특히 GTK 지원까지 염두.** Roblox 전용 렌더
기술(react.lua, Fusion)은 결국 외부 개발자 유인이 없어 발전이 더딜 거라는
문제의식. 결과적으로 `plug/roblox`, `plug/base` 정도로 나뉠 전망 — base가
가상돔 없이도 프로바이더 패턴으로 백엔드를 받는 인터페이스만 정의하고,
실제 Roblox 구현은 `quad-roblox` 격 서브패키지가 담당.
13. **모듈은 기본 싱글톤, `New()`는 나중에.** 한 Lua 스레드에서 Roblox/비-Roblox
프로바이더를 동시에 쓸 일이 거의 없을 거라 판단 — 필요해지면 그때 `New()`
추가.
14. **pluggable 초기화는 팩토리 함수로.** rbvm처럼 네임스페이스 하나하나 수동
init 하는 방식(`base/lifecycle-pattern.md` 5번 항목 참고)은 피하고,
`InitRoblox(Module)` 같은 팩토리 함수가 생성된 모듈을 뮤테이션하는 도구를
주는 방식.
## 아직 미정 (research/로 분리됨)
바인드 시스템 디스패치, Slot 설계 세부, Tween 플러깅, 모듈 라이프사이클/누가
Store를 구현하는가, 순수함수 범위, 이미 생성된 인스턴스에 대한 바인드 —
`.claude/research/` 각 문서 참고, 전체 색인은 `.claude/README.md`.

View file

@ -0,0 +1,63 @@
# Fusion / Vide 아키텍처 비교 — quad-v2 설계 근거
**상태**: base — 리서치 스냅샷(참고용 근거 자료), "완료" 개념 없음. quad-v2의
Store/Slot/Tween/bind-dispatch 설계 결정에 인용되는 원본 비교 자료.
## Fusion (`.claude/initreq/fusion/`)
- **반응 그래프**: push(무효화) + pull(재계산) 하이브리드. `Value:set()``change()`
통해 `dependentSet`을 BFS로 훑으며 `invalid`로 마킹하지만, 실제 재계산은
`timeliness="eager"`인 노드(Observer/Tween/Spring)만 즉시 동기 실행 — `Computed`/
`Value``use()`될 때만 lazy하게 재계산. 글리치 방지를 위해 eager 집합을
`createdAt` 순으로 정렬 후 처리.
- **Scope 정리 모델**: `Scope`는 사실 그냥 배열 + 메타테이블로 생성자들을 주입한
것. 생성자마다 자기 destroy 클로저를 배열에 `insert`. `doCleanup`은 다형적
(Instance→Destroy, Connection→Disconnect, 함수→호출, 테이블→역순 순회) 티어다운.
**완전히 eager/수동 — GC에 의존하지 않음.**
- **프로퍼티/자식/이벤트 디스패치**: `SpecialKey`라는 열린 "모양"(shape)은
누구나 만들 수 있지만, 우선순위 축이 `self/descendants/ancestor/observer` 4단계로
하드코딩되어 있어 5번째 우선순위 도입이 불가능 — quad가 원하는 완전 개방형
priority 레지스트리보다 약함.
- **Tween/Spring이 State그래프 안의 1급 노드** — 매 프레임 틱하는 외부
Stopwatch/ExternalTime 소스에 의존, 애니메이션-입력 간 별도 lifetime 체크
기계장치 필요. **quad가 트윈을 Store 밖(특수 bind key)으로 빼려는 이유의
반면교사**: Fusion처럼 그래프 안에 넣으면 Computed의 입력으로 자유롭게 합성
가능해지지만, 그 대가로 프레임 클럭 통합 + eager 노드 + 교차 lifetime 체크
3중 복잡도를 떠안음.
## Vide (`.claude/initreq/vide/`)
- **반응 그래프**: SolidJS류 순수 push. `source()`를 쓰면 즉시, 동기적으로,
깊이우선으로 모든 의존 노드를 재평가(lazy/pull 경로 없음). **저자들 스스로
`todo.md`에 "복잡한 다이아몬드 그래프에서 중복 재평가 방지" 를 미해결로 남겨둠**
— quad Store가 이 naive BFS 방식을 그대로 베끼면 안 되는 이유.
- **정리 모델**: 의존성 엣지(`parents`)와 구조적 소유(`owner`/`owned`)를 같은
`Node`에서 두 개의 별도 관계로 분리 — CHANGELOG 0.2.0에서 "destroy가 더 이상
reactive dependent까지 타고 내려가지 않고 owned만" 으로 명시적으로 고침(초기
설계 실수를 나중에 수정한 이력). 0.4.0에서 "활성 스코프는 destroy 불가" 하드
가드 추가. **역시 완전 eager/수동 — GC 의존 없음**(오히려 `root.luau`가 GC로부터
루트를 보호하는 `refs` 테이블까지 둠).
- **디스패치**: 대부분 Luau 키 타입으로 닫힌 하드코딩. 유일한 열린 확장점은
`action(callback, priority)` — 등록 없이 private 메타테이블 태그로 인식되는
값을 던지면 우선순위 순으로 실행. 단 key/value 쌍이 아니라 콜백+우선순위만
전달 — quad가 원하는 "key와 value를 함께 받는 핸들러"보다 좁음.
- **`mount()`에 단일-마운트 가드가 전혀 없음** — 같은 target에 두 번 mount하면
독립된 두 루트가 생겨 자식이 중복됨. **quad의 Slot "엄격한 단일 마운트
소유권"이 두 라이브러리 어디에도 없는 진짜 개선점**이라는 근거.
## 종합 비교표
| 축 | Fusion | Vide | quad-v2 시사점 |
|---|---|---|---|
| 전파 모델 | push+pull 하이브리드, eager 집합만 즉시 재계산, 생성순 정렬로 글리치 방지 | 순수 push, 즉시 동기 재평가, 다이아몬드 중복 재평가 미해결(저자 인정) | "Store는 값 자체에 항상 eager 발화, cleanup이 key/value를 먼저 확인" 요구사항은 Vide의 push 모델 + eval-전-cleanup 패턴에 더 가까움. 단 Vide의 naive BFS 대신 Fusion의 생성순 정렬 글리치 방지 규율은 채택할 것. |
| 정리/스코프 | 배열+메타테이블, dependency-agnostic, bind 시점에만 lifetime soft-check | dependency edge와 구조적 owner를 분리한 2중 관계, destroy는 owned만 cascade, 활성 스코프 destroy 하드 가드 | 둘 다 GC 비의존 eager 수동 정리 — rbvm의 Connected+GC 관용구와 정반대 축. quad의 Slot은 Vide처럼 "마운트 소유권"과 "반응 의존성"을 별개 관계로 분리하는 게 안전해 보임(`base/lifecycle-pattern.md`의 rbvm 패턴과는 다른 층위 — rbvm은 인스턴스 파괴 감지, 이건 Slot 내부 소유권 모델). |
| 키/값 디스패치 개방성 | SpecialKey 모양은 열려있으나 우선순위 4단계 하드고정 | action()은 등록 없는 태그 인식 방식이지만 key/value 버림, 콜백+우선순위만 | quad는 Fusion의 "디스패처가 key+value+target을 다 받는" 풍부함과 Vide의 "등록 없이 태그로 인식" 인체공학을 합치되, 우선순위 축은 열린 숫자 공간으로 일반화해야 함(`research/bind-system-plan.md`). |
## 추가로 기록해둘 것
- Vide의 암묵적(ambient stack) 의존성 추적 vs Fusion의 명시적 `use()` 축은
push/pull 축과 독립적인 별개 결정 — quad는 아직 미정(`research/bind-system-plan.md`
열린 질문 참고). Fusion의 명시적 `use()``checkLifetime` 같은 bind-time
체크를 가능하게 하는 부수 효과가 있음.
- 두 라이브러리 다 mount 시 단일 소유권 가드가 없다는 것 자체가 quad Slot의
차별점이라는 근거로 재사용 가능.

View file

@ -0,0 +1,132 @@
# 라이프사이클 패턴 — rbvm의 `Connected` + GC 관용구 채택
**상태**: 결정됨(base) — quad-v2가 채택할 라이프사이클/정리(cleanup) 전략의 원본.
완료 개념 없음, 구현하면서 세부 조정 있을 수 있음.
## 배경
`raw-userinput.md`(이하 사용자 원 메모): "레지스터의 실행은 내 생각엔 그냥 Destroy
되었는지를 라이프타임 홀더의 Connected 상태로 보는게 맞는듯", "여전히 connection
으로 해키한 방법 쓸듯. destroying으로 자료구조 계속 바꿔가는거 비용 큼. gc 네이티브
에 맡겨야함. 대신 이젠 Connected 필드로 이 연결이 살아 있는지 보고 설정 가능함.
이게 rbvm 쪽에서 구현되어있음."
rbvm(`.claude/initreq/rbvm/`)을 조사한 결과, 정확히 이 패턴이 존재함. 아래는
그 조사 결과 요약(전체는 이 문서 작성에 쓰인 리서치 세션 참고, 소스는
`.claude/initreq/rbvm/src/signal.luau`, `src/proxy/base.luau`, `src/namespace.luau`).
## 채택할 패턴
### 1. `Connected`는 저장되는 bool이 아니라 계산된 속성
rbvm의 `Connection` 타입(`signal.luau:21-24`)은 `Connected`를 실제 필드로 두지 않고
`__index` 메타메소드에서 계산함:
```luau
function ConnectionMeta.__index(self: Connection, key: string): any
if key == "Connected" then
local data = Connection.GetPrivate(self)
return data.Signal ~= nil
end
end
```
연결 해제 시 `data.Signal = nil`만 하면 됨(`Connection.Dispose`) — 자료구조를
바로 지우거나 재구성하지 않음. quad-v2도 이 모양을 그대로 채택: 라이프타임
홀더는 "내가 아직 살아있게 하는 뒷받침 참조"가 nil인지만 확인하면 됨.
### 2. Instance 파괴는 `Instance.Destroying` 훅 하나로만 관측
rbvm은 실제 Roblox Instance의 파괴를 감지하는 지점을 단 하나로 좁혀둠 —
`inst.Destroying:Connect(...)` (`proxy/base.luau:150-156`), `Destroyed` 같은
플래그를 그 콜백에서만 true로 뒤집음. `AncestryChanged`나 폴링 방식은 안 씀.
quad-v2도 동일: 인스턴스 라이프사이클 훅 지점은 `Destroying` 하나로 통일.
### 3. 정리(cleanup)는 기본적으로 GC에 위임, 예외적으로만 즉시(eager)
rbvm 전역에 약한 테이블(weak table, `__mode = "k"/"v"/"kv"`)로 private 데이터를
저장 — 홀더 객체를 아무도 안 들고 있으면 그 private 레코드도 자동으로 사라짐.
즉시 처리하는 예외는 딱 두 가지뿐이었음: (a) 가상 트리의 부모/자식 포인터처럼
방치하면 죽은 참조를 계속 순회하게 되는 "작고 유계(bounded)"인 것, (b) 네임스페이스
전체가 통째로 죽을 때의 순서 있는 dispose 훅. **quad-v2 원칙: 기본은 GC 위임,
즉시 정리는 "안 끊으면 죽은 참조를 순회하게 되는 작은 포인터"류에만 국한.**
### 4. Signal 자체는 커스텀 구현체를 그대로 재사용 가능
`signal.luau``Signal`/`Connection` 클래스는 rbvm 프록시 시스템에 의존하지
않는 범용 이벤트 emitter임 (`Connect`/`Once`/`Wait`/`Fire`/`Destroy`,
`IsInited`/`OnInit`/`OnUninit` 지연 활성화 훅 포함). **단, 사용자 원 메모에는
"시그널 자체 구현은 아닌듯... 콜백 정도로도 충분"이라고 되어 있어 서로 상충함**
— 아래 열린 질문 참고.
### 5. rbvm에서 그대로 가져오면 안 되는 것 (버그 발견됨)
- `proxy/base.luau:72-78``Proxy.DisposeNamespace``signal.luau:401-408`
`SignalProxy.DisposeNamespace``Connected` 체크 방향이 서로 뒤집혀 있음
(하나는 "아직 연결되어 있으면 continue", 다른 하나는 정반대) — 후자가 맞는
방향(`not Connected`일 때만 skip, 살아있으면 Disconnect). quad-v2 구현 시
이 반전 버그를 복사하지 않도록 주의.
- `namespace.luau:5-6``ItemNamespaceMap``__mod = "k"`로 오타가 나 있어서
실제로는 weak table이 아님(`__mode`가 맞음) — 그대로 베끼면 메모리 누수.
- `InitNamespace`/`Registered`-가드/`NewLib` 3종 세트로 "라이브러리마다 하나하나
수동 init" 하는 방식은 정확히 사용자가 피하고 싶다고 한 패턴 — 순서 있는
dispose-hook 리스트 자체는 재사용해도, 수동 init 관례는 그대로 베끼지 말 것
(팩토리 함수로 대체 — `research/module-lifecycle-plan.md` 참고).
## 확정: Signal 클래스는 안 만든다
**사용자 확인 완료** — 콜백 + `Connected` 계산 속성만으로 간다. rbvm의 범용
`Signal`/`Connection` 클래스 전체는 가져오지 않음. rbvm에서 채용하는 건 오직
"`Connected`가 계산된 속성" 이라는 패턴 자체뿐.
## 확정: quad는 자신이 만든 Instance의 라이프사이클 "중간"에 있지 않다
이게 이 문서 전체의 핵심을 재정의하는 결정. rbvm의 proxy는 Instance와 소비자
사이에 자신을 끼워넣는(중간 계층) 설계라 "내가 사라질 때 무언가를 정리해야
하는가"라는 문제가 생기지만, **quad는 자신이 만든 Instance를 그 Instance의
생명주기 끝까지 그대로 들고 있는 소유자다 — 중간에 끼는 계층이 없음.**
결론: **Instance/바인드 전체가 Destroy될 때 실행해야 하는 정리(teardown) 로직은
없다.** 오히려 있으면 안 됨 — Destroy 이후에 그 Instance에 프로퍼티를 셋하거나
메서드를 호출하면(예: 이미 죽은 Tween에 `:Cancel()`) 그냥 에러남. 대상이
Destroy되면 그 대상에 묶인 것들(Tween 등)도 자연히 죽은 상태가 됨 — GTK 등
다른 백엔드도 마찬가지로 "run된 dispose를 관리"할 필요가 없는 구조로 봄.
**해야 할 일은 딱 하나: 생명주기가 끝난 뒤에 그 대상을 다시 건드리는 시도가
일어나지 않게 막는 것**(=처리를 그냥 멈춤) 뿐 — 자료구조 자체의 해제는
가능하면 GC에 맡김.
이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전
소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/
tween-plan.md`/`research/slot-plan.md`의 "cleanup" 표기는 전부 `retract`
갱신됨(이름 변경 근거는 아래).
## 함수 안에서 만든 옵저버도 GC 대상이 되어야 함 — 범용 "생명 바인드 유틸" 필요
사용자 원 메모: "함수 안에서 옵저버 만들어버린 거, 그것도 gc 대상 되어야
할 텐데, 이건 아래쪽에 생성하는 실 객체에 유저가 바인드 할 수 있게 하는 약간의
유틸이 있긴 해야 할듯. connect 트릭 그대로 들고 와서 쓰면 될 것 같고. 옵저버는
canExecute 같은 람다 함수 하나 달게 해서 Connected 상태 보게 하여 실행 안 될
수 있게 만들어도 될 듯."
즉, 핸들러가 처리 도중 만든 구독/옵저버 클로저는 그 자체로는 아무것도 자동으로
GC에 묶이지 않음 — v1이 여기저기서 `PropertyChangedSignal`에 연결해 참조를
붙잡아두던 "GC 방지 핫팩"(`base/quad-v1-architecture.md` 참고)과 같은 문제.
**base가 범용 유틸로 제공할 것: 임의의 클로저/구독을 실제 Roblox 객체의
생명주기에 바인드하는 도구** — 내부적으로 v1/rbvm이 쓰던 "connect 트릭"(어떤
신호에든 연결해서 참조를 죽을 때까지만 붙잡아두는 것)을 그대로 재사용. 이
도구로 바인드된 옵저버는 `canExecute: () -> boolean` 같은 predicate 람다를
가질 수 있어서, `Connected`가 false면 실행 자체를 건너뛸 수 있음(죽은 대상에
대한 처리 시도 방지, 위 원칙과 직결).
이건 `research/bind-system-plan.md`의 "핸들러 내부 상태 저장" 유틸과 짝을
이루는 별도 유틸 — 하나는 "상태를 어디에 저장할지"(weak-keyed per-instance
저장소), 다른 하나는 "언제까지 실행되어도 되는지"(생명 바인드 + canExecute)를
다룸. 둘 다 base가 제공하는 범용 유틸로 확정.
## 이름: `cleanup``retract`
"cleanup"이라는 이름은 부적절하다는 사용자 피드백(완전 소멸 정리로 오인되기
쉬움) — 실제 의미는 "이전에 적용한 처리를 무른다/멈춘다"이므로 **`retract`**
로 통일. (`revert`, `rescind`도 검토했으나 `retract`가 "이전에 취한 조치를
철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를
이룸.) 모든 문서에서 이 이름으로 갱신.

View file

@ -0,0 +1,84 @@
# quad v1 내부 구조 (재작성 이전 기준선)
**상태**: base — 참고용 스냅샷, "완료" 개념 없음. v1(`.claude/initreq/quad/`)이
실제로 어떻게 동작하는지 정리한 문서로, v2 설계 시 "이 문제를 안 반복하려면"의
기준선으로 계속 참조됨. 아래는 리서치 에이전트가 file:line까지 확인한 내용의 요약 —
정확한 인용이 필요하면 `.claude/initreq/quad/src/*.lua` 원본을 볼 것.
## 공개 API 개요
```lua
local Quad = require(path).Init(QuadId?) -- id 생략 시 격리된 인스턴스
local Class, Store, Mount, Event, Style, Signal, Lang, Tween = Quad.Class, ...
local Frame = Class "Frame"
Frame {
Name = "Wow!";
Frame { Name = "Child" }; -- [1] = child
[Event "Activated"] = function(self,...) end; -- 이벤트 바인드 키
BackgroundColor3 = myStore "color"; -- store 바인드
myStyle; -- style 오브젝트도 숫자 키로
}
Mount(ScreenGui, Frame {...})
```
`Class.Extend()`로 재사용 컴포넌트(`Init/Render/AfterRender/Getter/Setter/
UpdateTriggers/Unload`) 정의 가능. `Store.GetObject(id)`류 id 기반 전역 조회는
v2에서 태그 시스템으로 대체 예정(`base/store-and-tags.md` 참고).
## 핵심 내부 동작 요약
- **`class.lua``ProcessQuadProperty`**(하드코딩된 if/elseif 디스패처)가 사실상
전체 "키 핸들러"임 — 숫자 키(children/style), `quad_register`/`quad_linker`/
`quad_style` 같은 `__type` 문자열 태그가 붙은 테이블, 그리고 `"Event::"` 접두
문자열 세 가지를 런타임 `typeof`/`type` sniffing으로 구분. 새 특수 키를
추가하려면 이 중앙 함수 자체를 고쳐야 함 — **v2가 pluggable bind 시스템을
원하는 직접적인 이유**.
- **`store.lua`의 register 체이닝이 바로 사용자가 "별로였다"고 한 metatable
체이닝**: `:With`/`:Add`/`:Tween`/`:Default` 각각이 이전 register를 `__index`
가리키는 새 1-필드 테이블을 만드는 방식 — 매 호출마다 테이블+메타테이블 할당,
같은 메서드 두 번 호출하면 마지막 것만 남음(합성 안 됨), `Register`/`Observe`는
반대로 루트 스토어를 직접 mutate — 일관성 없는 순수/불순 혼합.
- **정리(cleanup)에 대한 통일된 모델이 없음** — 여러 곳에서 각자
`PropertyChangedSignal("ClassName")`에 연결해 참조를 붙잡아두는 "GC 방지 핫팩"이
중복 등장(`class.lua`에 2곳, `lang.lua`에 1곳). 대칭되는 해제(dispose) 경로가
없어서 weak table GC에만 의존. `Uninit(id)`도 실제 파괴 없이 참조만 끊는 스텁.
- **`mount.lua`는 실제로 부모/자식 부기(bookkeeping) + 라이프사이클 파괴까지
담당하는 무거운 모듈**(`rawget/rawset`로 Extend 내부 필드를 직접 건드림) —
사용자 원 메모의 "이전 quad는 mount가 별다른 행동 안 함"은 더 오래된 스냅샷
기준일 가능성.
- **`event.lua`는 이벤트 연결 후 해제(disconnect) 추적이 전혀 없음** — fire-and-forget.
`signal.lua`는 완전 커스텀 Signal 구현체(Roblox BindableEvent 미사용)이지만
class.lua/mount.lua의 정리 경로에 연결되어 있지 않음.
- **`style.lua`는 이름 매칭(문자열 패턴) 기반, 선언 순서 의존적** — 실행 순서가
꼬이면 스타일이 안 먹는 문서화된 함정.
- **`tracker.lua`는 실제로 `exports.lua`에서 require조차 안 되는 죽은 코드** —
Rojo 트리(`DescendantAdded`/`.Changed`) 변경을 감지해 debounce 후 "updated"를
쏘는 핫리로드 감시자였지만 현재 공개 API에 연결 안 됨. v2는 아예 구현 안 하기로
이미 결정됨(스토리북 라이브러리가 대체, `base/architecture.md` 참고).
- **`lang.lua`의 로케일 상태(`CurrentLocale`/`langList`)가 module-local 전역이라
`Quad.Init(id)`의 id 스코프를 무시함** — Store/Style은 id별로 스코프되는데 Lang만
전역 공유, 일관성 없는 스코핑. v2는 lang 모듈 자체를 분리해서 안 만들기로 결정됨.
- **문자열 DSL(`"a,b"`, `"a&amp;b,c"` 같은 콤마/앰퍼샌드 파싱)로 구현된 구조적 기능**들이
주석 처리된 죽은 코드(`__newIndex` 대문자 오타로 절대 안 불리는 메타메소드 등)와
섞여 있어 신뢰도가 낮음.
## v2가 명시적으로 피하려는 것 (이 문서에서 근거로 인용)
1. Metatable 체이닝으로 "불변 빌더" 흉내내기 → 대신 팩토리 함수로 필요한 곳만 복사
(`raw-userinput.md` "복사 구현은 지양" 항목, `.claude/initreq/raw-userinput.md:83-86`).
2. 하드코딩된 중앙 디스패처 → pluggable `isHandlable(key,value)` + 우선순위 핸들러
레지스트리 (`research/bind-system-plan.md`).
3. 흩어진 "GC 안 되게 참조 붙잡기" 핫팩 → rbvm 스타일 `Connected` 계산 속성 +
명시적 라이프타임 홀더 (`base/lifecycle-pattern.md`).
4. mount가 여러 책임(부모 부기+파괴+child 레지스트리)을 한 모듈에 다 지는 구조 →
Slot이 child CRUD를 전담, mount는 단일-마운트 강제만 전담
(`research/slot-plan.md`).
5. tracker.lua, lang.lua 내장 → 둘 다 라이브러리 범위 밖으로 분리(스토리북/
외부 로케일 라이브러리에 위임).
## 열려 있는 확인 사항
- `objectListClass.__newIndex`(오타, 항상 미발동)로 문서화된 "GetObjects() 리스트에
일괄 프로퍼티 설정" 기능이 실제로 동작하는지 v1에서 재현 테스트 필요 — 동작 안
했다면 v2 마이그레이션 가이드에서 "이 기능은 애초에 없었다"고 명시해야 함.

View file

@ -0,0 +1,54 @@
# Store 의미론 — 부작용 허용, State 프리미티브 없음
**상태**: base — 확정된 설계 결정 두 가지. 원본: `.claude/initreq/raw-userinput.md`
"store는 부작용을 허용함" / "state는 어떻게 구현하는가" 절.
## Store는 부작용을 허용하는 게 기본 디자인
부작용 없이(파라메터 패싱만으로) 쓰는 것도 물론 가능하지만, 라이브러리 차원에서
막지 않는다. 부작용 유무는 **사용자가 직접 문서화**하는 관례로 둔다 — 라이브러리가
순수성을 강제하지 않음.
다만 한 가지는 명확히 구분: **렌더 리턴 위에서 무언가를 observe하는 것은 그냥
부작용**이다 (`useEffect`와 유사한 것으로 문서화). 이건 "허용되는 부작용"이 아니라
"당연히 부작용"이라는 뜻 — 문서화 시 이 경계를 분명히 할 것 (`research/
purity-and-effects-plan.md`와 연결됨).
## 별도 `State` 프리미티브는 만들지 않는다 (기본값)
클래스 자신이 필요한 state가 있으면 그냥 클래스 안에서 `Store`를 만들면 됨 —
Store는 부분집합으로 쪼개 전달하는 것도 충분히 가능하다고 보기 때문에, 굳이
"단일 값 저장용" State를 별도로 만들 필요성을 못 느낌. 나누고 싶으면 사용자가
알아서 나누면 됨(사용자 자유).
**단서**: 구현하다가 실제로 State가 있는 게 더 편해지는 지점이 나오면 그때
추가할 수 있음 — 지금은 "필요성이 확인 안 됐다"는 판단이지 "절대 안 만든다"는
확정이 아님. 구현 라운드에서 이 판단이 바뀌면 이 문서를 갱신할 것.
## Store 값 설정 문법 — v1 인체공학 유지 (확정)
**사용자 확인 완료**: Store 값 설정은 `__newindex` 기반(`myStore.key = value`)을
그대로 유지 — ProfileService 등 Roblox 생태계에서 이미 익숙한 관용구라 바꿀
이유 없음. 마찬가지로 다음 두 인체공학도 유지:
- **괄호 생략(paren-less) 구조** — 필요 시 커링(`myStore "key"`처럼 문자열
하나로 register를 얻는 v1 스타일)을 계속 허용.
- **`:` 체이닝** — 값을 바꾸는 연산에 한해 체이닝 문법 허용(`base/
architecture.md`의 "함수지향 디폴트, `:`는 예외적으로만" 원칙과 일치 — 체이닝이
자연스러운 곳 중 하나가 바로 이 store 값 변경).
`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 함께 읽을
것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며 "불변 빌더"를
흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가 아니었음. v2는 문법
인체공학(사용자가 좋아하는 부분)은 유지하되 내부 구현(체이닝이 아니라 팩토리
함수)만 바꾼다.
## 여러 스토어 값을 묶어 처리하는 것 (dependency array) — 연구 필요
`useEffect`처럼 여러 store 값을 디펜던시로 묶어 파생값을 계산하고 싶다는
요구가 있음(v1의 `myStore "a,b"` 콤마-조인 문자열 방식은 폐기 대상 —
`base/quad-v1-architecture.md`의 "문자열 DSL" 문제점 참고). 단, **v1의
`:Add`/`:With`/`:Tween` 같은 이름 붙은(named) 체이닝 연산은 만들지 않기로
확정** — 대신 일반 함수를 받아 처리하는 쪽이 일관적이라는 판단. 구체적인 API
모양(`Store.Combine({a,b}, function(a,b) ... end)`류)은 아직 미정 —
`research/bind-system-plan.md` 참고.

79
.claude/question.md Normal file
View file

@ -0,0 +1,79 @@
# 확인/결정 필요 목록 (전체 취합)
각 plan 문서에 흩어진 "사용자 확인 필요" 절의 취합본. **막고 있는 항목은
거의 없음** — 대부분 합리적 기본값/방향을 잡아두고 research 단계에 머물러
있음. 사용자가 Lua/Roblox 엔진에 대해 깊이 아는 사람이라는 전제로, 우선순위
높은 것부터 정렬.
## 확정됨 (2026-08-03 질의응답 라운드, 더 이상 열려있지 않음)
- **Store 책임 분리**: base가 `LifetimeHandle` 추상화 + store-bind의 재실행
로직(`process(inst,k,realv)` 재귀)을 소유, provider는 "언제 죽었다고
판단할지"(Roblox `Destroying` 등)만 결정. → `research/module-lifecycle-plan.md`,
`research/bind-system-plan.md`
- **Signal 클래스**: 안 만듦 — 콜백 + `Connected` 계산 속성만. → `base/
lifecycle-pattern.md`
- **핸들러 계약**: `isHandlable`+`priority`+`process`+`retract` 4종 유지,
tbox식 세분화는 지금 안 함. → `research/bind-system-plan.md`
- **Ref**: 도입하되 용도는 "id 조회 대체"가 아니라 "외부 관리 instance를
점진적으로 마이그레이션/래핑하기 위한 직접 참조 획득". Tween 등 어떤
핸들러도 대상 획득에 Ref가 필요하지 않음(항상 `inst`를 직접 받음).
`research/bind-system-plan.md`
- **`retract`(구 cleanup) 호출 시점**: 값 교체 시에만 호출, Destroy 시엔
호출 안 함(quad는 자신이 만든 instance의 생명주기 중간에 있지 않으므로
destroy-time 정리 자체가 불필요/불가능). → `base/lifecycle-pattern.md`
- **핸들러 내부 상태 저장**: base가 범용 weak-keyed per-instance 저장 유틸
제공(모든 핸들러 재사용). → `research/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. → `research/slot-plan.md`
- **`CreatedRef` 콜백 타이밍**: 생성 시점/마운트 시점 둘 다 옵션으로 지원.
`research/bind-system-plan.md`
- **여러 store 값 묶기**: `Store.Combine`류 포지셔널 인자 방식과 Vide식 암묵적
추적 둘 다 기각 — `:With(...)` + `:Compute(fn)`(fn은 with한 값을 포지셔널
인자가 아니라 클로저로 읽음) 방식으로 확정. Unix 파이프에서 영감받은 완전
합성 가능한 State 스트림이 이상향이나 기술적 난이도 미확정 — 과거 시도
(`quad2-try/quad-core`) 리서치 진행 중. → `research/bind-system-plan.md`
## quad2-try(이전 폐기된 시도) 리서치 완료 — 추가 확정
- **OOP 상속/`--&` 커스텀 파서/Slot 스텁은 확인대로 죽은 접근** — 절대 반복
금지, Slot은 from-scratch 설계 그대로 진행(재조사 불필요).
- **mutate-vs-`fromState` 긴장 관계**: quad2-try의 `Pipe` copy-on-write
절충안(유일한 tip일 때만 뮤테이션, 아니면 복사)이 유력 후보로 좁혀짐 — 단
소유권/버전 가드를 제대로 설계해야 함(원본은 가드 없이 방치돼 있었음).
- **`Depend(...)` 액션, `:With` 네이밍**은 이전 시도에서도 지향했던 것과 일치
— 그대로 채택. → `research/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) 방지가 실제로 필요한
상황이 있는지 — 구현 단계에서 실사례로 재검증. → `research/bind-system-plan.md`
---
전체 순서/우선순위는 루트 `CLAUDE.md`가 최종 소스 — 위 표는 힌트일 뿐 그쪽이
바뀌면 이 문서도 갱신할 것.

View file

@ -0,0 +1,271 @@
# Bind 시스템 — pluggable key/value 핸들러 (핵심 모델 확정, 세부 사항만 남음)
**상태**: research — 핵심 디스패치 모델(`process`/`retract`, 핸들러 4종 계약,
Signal 미채택, Ref 역할)은 사용자 확인 완료로 사실상 확정. 남은 건 세부
시그니처(dependency array API, `CreatedRef` 모양) 뿐 — 이것들이 정리되면
`base/`로 승격 예정. 원본: `.claude/initreq/raw-userinput.md`
"key와 value에 대한 바인드 연산은 pluggable 하도록 구성하기" / "스토어는 스토어를
저장 가능한가" / "Ref는 고민중" 절. v1의 문제점은 `base/quad-v1-architecture.md`
("ProcessQuadProperty" 하드코딩 디스패처), 참고 패턴은 `.claude/initreq/tbox`
(레지스트리)와 Fusion/Vide 비교는 `base/comparison-fusion-vide.md` 참고.
## 문제
v1의 `ProcessQuadProperty`(`.claude/initreq/quad/src/class.lua:134-214`)는
숫자 키(children/style) vs 문자열 키(prop/event) vs `__type` 태그 테이블
(register/linker/style)을 하드코딩된 if/elseif 체인으로 구분한다. 새 특수 키
(`[Attribute "X"]`, `[Tag ""]`, `PropertyChangedEvent ""` 등)를 추가하려면 이
중앙 함수 자체를 고쳐야 한다 — 라이브러리로서 확장 불가능한 구조.
## 핸들러 계약 (확정 — 아래 "확정된 디스패치 모델" 절과 통합해서 읽을 것)
핸들러는 다음 4개를 제공하는 등록 가능한 객체:
- `isHandlable(key, value): boolean` — 이 핸들러가 이 key/value 쌍을 처리할
수 있는지 판별하는 predicate. **부작용 없이, 빠르게** — tbox의 type-check/
constraint-check 분리 원칙(`.claude/initreq/tbox/CLAUDE.md`의 "타입 체크는
분기 선택에 쓰이므로 순수해야 함")을 그대로 적용: `isHandlable`은 오직
"이 핸들러가 맞는가" 판별에만 쓰이고, 실제 유효성 검사는 핸들러가
선택된 *이후* 별도 단계에서.
- `priority: number` — 우선순위. 등록 순서(Fusion의 4단계 고정 stage, Vide의
action() 우선순위)보다 일반화된 **열린 숫자 공간**으로.
- `process(inst, key, value)` — 실제 처리 수행(아래 "확정된 디스패치 모델"
절 참고). v1/기존 논의에서 "bind"라 부르던 것과 동일한 역할.
- `retract(inst, key, value)` — 이전 처리를 무르는/멈추는 함수(아래 절,
`base/lifecycle-pattern.md` 참고). 모든 핸들러가 의미 있게 구현할 필요는
없음(예: 일반 프로퍼티 핸들러는 보통 no-op).
디스패치는 등록된 핸들러를 우선순위 순으로 스캔하며 `isHandlable`을 호출,
첫 매치가 처리(Fusion의 SpecialKey 우선순위 스캔과 유사하되 4단계 고정이 아니라
열린 레지스트리). tbox의 `TUnion` 런타임 체커가 이미 이 "순서대로 스캔, 첫 매치
반환, 실패 정보는 클로저로 지연 생성" 패턴을 구현해뒀음(`.claude/initreq/tbox/
src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들지 말고 매치
실패 시에만 클로저 호출.
## 확정된 디스패치 모델: `process(inst, k, v)` / `retract(inst, k, v)`
**사용자가 직접 준 구체적인 모델 — 이 문서의 이전 초안보다 우선함.** 아래가
실제로 구현할 모양:
- 모든 핸들러는 대상 **Instance를 직접, 항상** 받는다. quad는 "인스턴스를 생성하고
그 인스턴스를 처리하는" 라이브러리다 — 다른 라이브러리가 만든 값(예: Store)을
그 인스턴스에 적용하도록 돕는 역할에 가깝다. 그래서 핸들러가 "나중에 생길
대상"을 비동기로 기다릴 필요 자체가 없음(아래 Ref 절 참고 — Ref는 다른 이유로
존재).
- `process(inst, k, v)` — 우선순위 순으로 등록된 핸들러를 스캔, `isHandlable(k,v)`
만족하는 최상위 핸들러가 실제 처리를 담당.
- 예시: Tween의 store-bind 핸들러는 **`k`는 무엇이든 받고 `v`가 Store인 경우를
잡아내는, 우선순위가 매우 높은 핸들러** — `v`가 store이면 그 값을 처리(구독)함.
이 핸들러 안에서:
1. 지금 이 처리가 실행되어도 되는지 라이프타임(`Connected`)을 확인 —
확인 안 하면 이미 Destroy된 대상에 대해 처리가 실행되는 문제가 생김. GC가
결국 정리하긴 하지만, GC 되기 전에도 store 값이 업데이트될 수 있으므로
그 시점엔 그냥 `Connected`를 보고 무시(no-op).
2. 처리해도 되면, 사용자가 넘긴 함수들을 거쳐 실제 값(`realv`)을 계산.
3. **`realv`를 들고 다시 `process(inst, k, realv)`를 재귀 호출** — 이게 바로
"store 바인드는 pluggable 바인드를 재실행하는 래핑"이라는 이 문서 이전
초안의 결론과 일치. `realv`가 store가 아니라면 자연히 Tween의 store-bind
핸들러 `isHandlable`을 통과 못 하고 우선순위상 다음 핸들러(일반 프로퍼티
세터 등)로 흘러감 — 무한 재귀 걱정 없음.
- **`retract(inst, k, v)`** (이전 초안의 "cleanup", 이름 변경 근거는
`base/lifecycle-pattern.md` 참고) — 이전 처리를 무르는/멈추는 함수. **오직
"같은 key에 새 값이 들어와서 이전 처리를 갈아치우는" 시나리오에만 존재** —
인스턴스/바인드 전체가 Destroy될 때는 `retract`가 호출되지 않음(`base/
lifecycle-pattern.md`의 "quad는 라이프사이클 중간에 있지 않다" 원칙 참고).
- 일반 프로퍼티는 애초에 "unset" 개념이 없음(`nil`로 셋하는 것도 그냥 셋
동작) — 그래서 프로퍼티 핸들러는 보통 `retract`가 필요 없음.
- `retract`가 실제로 의미 있는 곳: **Tag를 지운다, Attribute 엔트리 자체를
지운다, 실행 중인 Tween을 멈춘다** 같은, "값을 새로 셋하는 것"과
"이전 상태를 명시적으로 되돌리는 것"이 다른 케이스.
- store bind가 새 값으로 넘어갈 때 이전 핸들러의 `retract(inst, k, v)`
한 번 호출해주면 됨.
- **핸들러 내부 상태 저장**: `retract`가 "이전에 생성한 것"(예: 실행 중이던
Tween 객체)에 접근하려면 상태를 어딘가에 저장해야 함 — **`inst`를 키로 하는
weak-keyed 테이블에 `k`별로 저장**(예: 생성된 Tween을 담아뒀다가 나중에
멈추거나 끝냄). **base가 이걸 범용 유틸로 제공**(`base.perInstanceState(inst)`
류, 정확한 이름/모양은 구현 단계에서 확정) — 모든 핸들러가 재사용, 각자
WeakMap을 새로 만들지 않음. `base/lifecycle-pattern.md`의 "생명 바인드 유틸"과
짝을 이루는 유틸.
- **다른 값 변경을 추적하는 것도 process 함수의 정상 범위**: 예를 들어 Slot
핸들러는 자기가 감시하는 값(배열/스토어)이 바뀌면 그에 따라 child를
갱신해야 함 — `retract` 시점엔 그 추적(구독)만 풀면 됨.
## Store 바인드는 특수 경우인가, 아니면 pluggable 바인드를 재실행하는 래핑인가
사용자 원 메모: "스토어 바인드는 특수 경우로 둘지, 아니면 다른 pluggable 바인드를
재실행하는 래핑으로 쓸지 생각해봐야함... 충분히 확장 가능하게 둘 수 있음."
**확정**: 래핑 쪽. 위 "확정된 디스패치 모델" 절 참고 — store 바인드 핸들러도
다른 핸들러와 동일한 `isHandlable`/`priority`/`process`/`retract` 계약을
따르되, `process`가 내부적으로 "실제 값이 바뀔 때마다 (원래 key, 새 value)로
`process(inst,k,realv)`를 재귀 호출"하는 식으로 구현됨. 이러면 store 값
자체가 어떤 타입이든(원시값, 인스턴스, 심지어 다른 store) 상관없이 동일한
재귀적 디스패치로 처리 가능 — 아래 "store가 store를 저장 가능한가"와 직결.
Slot이 store 바인드로 넘어오는 경우, pluggable 처리기에 `retract` 핸들러가
필요하다는 점(부모가 slot을 정리하고 다시 process하는 방식)도 이 래핑 방식과
자연스럽게 맞음 — `research/slot-plan.md` 참고.
## Store가 Store를 저장 가능한가
사용자 원 메모: "슬롯을 스토어처럼 생각 가능하다면 이건 가능하다고 봐야하는가?
아니면 아예 다른 값으로 둬야 하는가? table/number 같은 프리미티브 타입이나
ref 타입처럼 생각하는 게 맞는 거 같음 — 그걸 처리하는 플러그를 만드는 걸로."
이 문서의 제안: "Store 안의 값이 Store"인 경우도 그냥 하나의 (key,value) 쌍일
뿐이고, 그 값 타입(Store)을 인식하는 핸들러가 pluggable 레지스트리에 등록되어
있으면 됨 — 위 "재실행 래핑" 방식과 동일한 메커니즘으로 커버됨. 별도 특수
케이스 코드 불필요.
## Ref — 도입 확정, 단 용도는 재정의됨
**중요한 정정**: Ref는 Tween이 대상을 얻기 위해 필요한 게 아님(Tween 핸들러도
`process(inst,k,v)`처럼 항상 대상 Instance를 직접 받으므로 — 위 "확정된 디스패치
모델" 참고, `research/tween-plan.md`도 이에 맞춰 갱신됨). Ref의 진짜 용도는 다름:
- v1의 `Frame "id" {}` + `Store.GetObject(id)` 식 id 매핑은 폐기 확정
(`base/architecture.md` 5번 항목) — "비현실적"이라는 게 이유.
- 하지만 **"라이브러리가 자기 자신이 만들어낸 instance를 나중에 다루기 편하게"**
하는 용도로 Ref는 여전히 필요. 구체 시나리오: 기존에 다른 라이브러리로
관리되던 instance를 당장 quad로 옮기지 않고, ref를 따서 그 안에 자식을
`Parent`로 마운트한다든가, 점진적으로 마이그레이션한다든가, 래퍼를 만든다든가
하는 다양한 용도.
- Store는 이미 바깥에서 옵저빙 가능한 존재라 별도 취급 불필요 — Ref는 그와
달리 "원하는 객체 자체를 직접 얻어오는" 경로. **얻어진 뒤에 그 참조를 어디에
저장하고 어떻게 쓰는지는 라이브러리 책임 범위 밖**(사용자 자유).
- **바인드 방법**: 특수 처리 없이, children을 배열 아이템으로 넣듯 `CreatedRef`
같은 값을 숫자 키 슬롯에 넣는 방식(정확한 이름/시그니처는 미정, 예:
`[1] = CreatedRef(function(inst) ... end)`) — child와 동일한 층위에서
`process(inst,k,v)` 디스패치를 그대로 타게 함. 즉 Ref도 pluggable 핸들러
레지스트리의 평범한 참가자.
- 코루틴 기반 "채워질 때까지 대기" 지원 여부는 여전히 미정(별도 확인 필요 없이
구현 우선순위 낮음 — 필요성이 명확해지면 그때 추가).
- **콜백 호출 시점 확정**: 생성 직후(construction 시점)와 트리 마운트 후(Parent
세팅 완료) 둘 다 지원 — 옵션으로 선택(`CreatedRef(fn, {phase="created"|
"mounted"})`류, 정확한 API 이름은 구현 단계에서 확정).
## 여러 Store 값을 묶어 파생값 만들기 — `:With` + `:Compute`, 포지셔널 인자 지양
**사용자 확인 완료, 상세 방향 확정.** 후보로 검토했던 두 방식 모두 기각:
- **암묵적 자동 추적(Vide식 ambient stack)** 기각 — "함수 실행 중과 끝 사이를
확인하고 부작용이 필요"한 방식이라 Lua에서 깔끔한 방법이 아니라고 판단.
- **명시적 디펜던시 배열 + 포지셔널 인자**(`Store.Combine({a,b}, function(av,bv)
...)`)도 기각 — 두 가지 이유: (1) 팩토리 함수로 store-bind 처리기를 쉽게 못
만들어줌, (2) 여러 팩토리를 체이닝하면 인자 순서가 꼬일 수 있고, 타입 표기도
어려워짐.
**채택 방향**: `:With(...)`로 필요한 의존성을 모으고, 그 뒤 `:Compute(function()
... end)`에서 **`with`한 값을 포지셔널 인자로 받지 않고 클로저로 직접 읽는다**
(정확히 어떤 방식으로 "직접 읽는지"는 아래 열린 질문 — `:fromState` 후보 참고).
## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 기술적 난이도 미확정
**중요한 배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
이상적으로는 store에서 한 값을 추적(track)하면 "State"가 나오고, 거기에
`compute`를 적용하면 또 다른 "State"가 나오는 식 — Unix의 `(cat a; cat b) | while
read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최종 목표. `:With`
두 번째 인자(`b`)도 다른 `:Compute`의 결과물(State)을 그대로 받을 수 있어야
이상적.
**미해결 긴장 관계**: 이걸 구현하는 두 갈래 방식이 있고 어느 쪽이 맞는지 아직
결정 안 됨:
1. **Compute 체인이 항상 자기 자신을 mutable하게 바꾼다** — 엔지니어링 비용은
낮지만, 다른 코드가 나중에 그 체인 뒤에 새 compute를 붙이는(다른 소비자가
동일 State에 독립적으로 파생값을 추가하는) 것이 불가능해짐 — 공유/합성이
깨짐.
2. **명시적 `State:fromState(state)`류의 비-mutating 생성자** — 합성은
안전해지지만 엔지니어링 비용이 더 큼(정확히 얼마나 큰지 미확정).
이건 `base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과
같은 축의 문제 — 옵션 2가 그 원칙과 더 잘 맞아 보이지만, 실현 가능성 자체가
아직 검증 안 됨.
## quad2-try 리서치 결과 (완료) — 이전 시도에서 뭘 가져오고 뭘 버릴지
`.claude/initreq/quad2-try/out/quad-core`에 정확히 이 문제(Unix 파이프 영감의
State/스트림)를 다뤘던 이전 시도가 있었음. 조사 결과 요약:
**확인된 죽은 접근 — 절대 반복하지 말 것:**
- **OOP 상속(`Base:Extends`) 구조**가 `Source`/`State`/`Pipe`/`Store`/`Event`/
`Action`+8개 서브타입 전체에 퍼져 있었음 — 모든 서브클래스 생성자마다
`self._super._constructor(self, ...)`를 수동으로 호출해야 하고(빼먹기 쉬움,
컴파일러가 검증 안 함), private/protected는 `_` 접두사 관례일 뿐 실제
캡슐화가 전혀 없었으며, `Base:IsInstance`가 수동 유지되는 `_proto`/`_super`
연결 리스트를 순회하는 런타임 전용 타입 체크라 Luau 정적 타입 시스템이
전혀 못 봄. **사용자가 우려한 그대로 확인됨 — 상속 기반 설계 금지.**
- **`--&` 커스텀 파서 시도**는 완전히 죽은 코드였음 — 6개 파일에 156줄의
주석 기반 타입/가시성 어노테이션이 있었지만, 이걸 실제로 소비할 도구
(`quad-gen`, `quad-lang`)는 **둘 다 완전히 빈 디렉토리**였음. 오타(`@clsas`를
`@class`로 못 고침)가 안 잡힌 채 남아있었고, 같은 주석 마커 아래 전혀 다른
Lua5.1-호환 트랜스파일러 지시어까지 섞여 있었음 — 파서가 한 번도 제대로
동작한 적 없다는 명백한 증거. **확인대로 반복 금지.**
- **Slot은 이 시도에서도 사실상 빈 스텁**이었음 — `Insert`의 실제 구현부가
전부 주석 처리되어 있고, `Notify()`도 빈 함수. 심지어 구 v1(`quad-2`)의
`DEV_CHANGELOG.txt`에도 "TODO: slot 기능 구현"이 마지막까지 미완료로 남아있었음
**가져올 게 전혀 없음**, `research/slot-plan.md`의 from-scratch 설계를
그대로 진행하면 됨(재조사 불필요).
- 다른 서브패키지(`quad-roblox`/`quad-gtk`/`quad-lang`/`quad-gen`/`quad-compat`/
`quad-debug`/`quad-docs`)는 전부 파일이 0개인 빈 디렉토리 — `quad-core` 밖엔
참고할 게 없음.
- `Store:Pipe`/`Store:Value` 연동이 담긴 유일한 두 예제 콜사이트(`slot.luau:31-41`)조차
존재하지 않는 `Store:Value` 메서드를 호출하는 등 실제로 동작 검증된 적이
없는 죽은 스크래치 코드였음 — 이 프로토타입은 끝까지 실사용 검증을 통과한
적이 없음.
**건질 만한 것 (인체공학/아이디어만, 코드는 아님):**
- **`store:Pipe(key):Compute(fn)` 같은 왼쪽에서 오른쪽으로 읽히는 파이프
문법 자체**는 목표로 유지할 가치가 있음.
- **`Pipe`가 mutate-vs-`fromState` 긴장 관계에 제시한 절충안** — "체이닝된
`Compute`/`Add`/... 호출은 자신이 액션 리스트의 유일한 '끝(tip)'일 때만 공유
배열에 그대로 append(뮤테이션), 이미 다른 코드가 그 지점 이후로 체인을
확장해버렸다면 배열을 복사한 뒤 새 `Pipe` 객체를 반환"하는 **copy-on-write
방식** — 이건 이 문서의 "mutate-in-place vs `fromState`" 긴장을 실제로
풀어보려 한 유일한 시도라 **quad-v2에서 제대로 다시 설계해볼 만한 후보**.
단, 원본은 "내가 지금 유일한 tip인가" 체크에 소유권/버전 관리가 전혀 없어서
경쟁 상황에 취약했고 테스트/실사용 검증도 없었음 — **그대로 베끼지 말고,
같은 아이디어를 소유권 가드를 제대로 넣어 재설계할 것.**
- **`Depend(...)` 액션** — 계산값에는 관여하지 않고 오직 "이 소스가 바뀌면
다시 계산하라"는 추가 의존성만 등록하는 값-투명(value-transparent) no-op
액션. 작지만 깔끔한 아이디어라 이름 그대로 채택할 만함.
- **흥미로운 발견**: 스크래치 파일(`out/asdf`)에 남아있던 더 이전 버전의
파이핑 스케치가 정확히 `Pipe(store.background):With(store.transparency,
globalStore.test):Compute(fn)` 모양이었음 — 실제 구현으로 넘어가며
`:Depend()`+포지셔널 인자로 바뀌었지만, **`:With(...)` 네이밍은 사용자가
이번 라운드에서 다시 요청한 것과 정확히 일치** — 우연이 아니라 원래
지향점이었던 것으로 보임, `:With` 이름 채택에 힘을 실어줌.
**종합**: 이 프로토타입은 사실상 죽은 시도가 맞음(확인됨) — 다만 Pipe의
copy-on-write 절충안과 `Depend`/`:With` 네이밍은 quad-v2 설계에 그대로
살려볼 가치가 있는 아이디어로 남김.
## 확정된 것 (더 이상 열린 질문 아님)
- **핸들러 계약**: `isHandlable(k,v)` + `priority` + `process`(구 `bind`) +
`retract`(구 `cleanup`) 4종 조합으로 확정 — tbox식 6-hook 세분화는 지금은
안 함. 실제 구현하며 부족한 지점이 보이면 그때 hook 추가(점진적 확장).
- **Signal 클래스**: 안 만듦, 콜백 + `Connected` 계산 속성만(`base/
lifecycle-pattern.md`).
- **Ref**: 도입 확정(위 절 참고), 용도는 "id 기반 조회 대체"가 아니라 "외부
관리 instance를 점진적으로 다루기 위한 직접 참조 획득".
## 남은 열린 질문 (`.claude/question.md`에도 취합)
- **`:Compute``with`한 값을 정확히 어떻게 읽는가** — 클로저로 원본 store/
register를 직접 캡처하는 것인지, `:Compute`가 특수한 접근자를 몸체 함수에
넘겨주는 것인지 구체 시그니처 미정.
- **mutate-in-place vs `fromState` 긴장 관계** — quad2-try의 copy-on-write
절충안(위 절)이 유력한 후보로 좁혀짐. 실제 구현 시 "내가 유일한 tip인가"
판단에 제대로 된 소유권/버전 가드를 설계하는 게 핵심 과제 — 원본처럼
가드 없이 가면 안 됨.
- **`CreatedRef`(가칭)의 정확한 함수/옵션 이름** — children 배열에 아이템으로
넣는다는 방향과 생성/마운트 두 시점 모두 지원한다는 것은 확정, 정확한 API
이름만 남음.
- **매 `process()` 호출마다 우선순위 스캔 비용** — 실제 구현/벤치마크 단계에서
확인 필요(디자인 자체는 확정됐으므로 더 이상 사용자 확인 대상 아님, 구현
검증 대상).
- Store가 Store를 담는 경우의 실제 소유권(누가 내부 Store를 destroy하는가) —
이 문서의 "재실행 래핑" 제안이 맞다면 자연히 바깥 Store bind가 내부 Store의
라이프타임도 감싸게 될 텐데, 이중 해제(double-dispose) 방지가 필요한지 확인.
단, `base/lifecycle-pattern.md`의 "destroy 시점엔 아무것도 안 함" 원칙상 이중
해제 자체가 걱정할 필요 없는 개념일 수도 있음 — 재검토 필요.

View file

@ -0,0 +1,53 @@
# 이미 생성된 인스턴스에 대한 바인드 (후순위, UB 또는 마일스톤)
**상태**: research — 명시적으로 후순위/UB 후보. 원본:
`.claude/initreq/raw-userinput.md` "이미 생성된 객체에 대한 바인드?" 절.
## 문제
이미 생성된 Roblox Instance에 새로운 `{k=v}` 프롭 테이블을 나중에 바인드하는
걸 허용할지. 허용하려면 이전 바인드를 끊는 처리가 필요한데, cleanup이
구현되어 있어도 바로 지원하는 건 엔지니어링 비용이 높음.
## 기울어진 방향
**UB로 두거나, 마일스톤(추후 구현)으로 미룬다.** cleanup이 이미 있고 store
바인드도 우선순위 높은 플러그라면 이론적으로는 가능해 보이지만(핸들러
레지스트리가 이미 "우선순위 스캔 후 bind" 구조라 재바인드도 같은 경로를 타면
됨), 초기 구현에서 **우선순위를 낮게** 잡아야 함 — 문제 유무가 많을 수 있어서.
## Default 값과 얽히는 문제
Default를 넣으면 더 어려워짐 — Default로 쓰다가 실제 쓰인 값으로 되돌아가는
케이스를 생각해야 함. Modifier 설계와 맞물려 있는 문제로, 결과적으로 매번
테이블을 flattening 해야 할 수도 있음 — 그런데 그걸 위해 클론까지 해야 하나?
사용자 스스로도 "약간 애매" 하다고 남김.
**후보 아이디어(미확정)**: ref로만 다시 바인드 가능한 걸 얻게 하고, ref가 되면
복사(clone) 모드를 켜야 하나 — 근데 그러면 너무 복잡해질 것 같다는 우려까지만
기록. 결론 없음.
## 사용자 확인 결과: 진짜로 모르겠음 — 열린 가능성으로 유지
**사용자 확인 완료, 그러나 결론은 "미정 유지".** 실제로 이 기능을 원한다고
말한 사용자를 본 적은 없지만, 막상 만들어진다면 유용하게 쓸 수 있을 것 같다는
느낌은 있음. 근거:
- `retract`(구 cleanup)이 이미 존재한다면, store 바인드도 이미 `retract`되는
경로가 있는 셈 — 재바인드를 지원하기 위한 인프라가 어느 정도 이미 깔림.
- Modifier를 잘 설계하면 나중에 오버라이드가 자연스럽게 가능해질 수도 있음 —
미래에 어떤 방법을 생각해낼 여지가 있다는 것.
- **역사적 맥락**: quad는 원래 "script 스니펫"이라고 부를 정도로, react.lua
같은 당대 대안 대비 압도적으로 쉽고 단순해서 누구나 빠르게 이해해 쓸 수
있는 걸 의도적으로 지향한 도구였음. 라이브러리가 지금처럼 몸집이 커지는
후속 단계에선 이런 기능성을 충분히 고려할 만함.
**결론**: v2 초기 스코프에서 제외하되, "미지원"으로 확정 명문화하지는 않음 —
진짜 열린 가능성으로 남겨두고, 실사용 중 필요성이 드러나면 그때 설계.
`base/architecture.md`의 "복사 구현 지양, store 바인드 변경은 전체 변경"
원칙과 긴장 관계에 있다는 점은 여전히 유효 — 나중에 설계할 때 이 원칙과
어떻게 공존할지부터 다시 볼 것.
## 열린 질문 (`.claude/question.md`에도 취합)
- 구체적 설계는 완전히 미정 — 실사용 패턴이 쌓이기 전까지는 착수하지 않음.
급하지 않음.

View file

@ -0,0 +1,86 @@
# 모듈 라이프사이클 — 프로바이더 패턴, bind/store는 누가 구현하는가 (착수 전)
**상태**: research — 방향은 있지만 "누가 store를 구현하는가"는 사용자 스스로
"진짜 애매한 지점"이라고 남긴 미해결 항목. 원본:
`.claude/initreq/raw-userinput.md` "넘버 바인드는 누가 처리?" / "모듈은 스코핑
되는가" / "pluggable 하다면 해당 플러그를 초기화하는 건 누구 몫?" / "다시 돌아와서…
bind는 누가 어떻게 구현" / "스토어는 누가 구현해…" 절. 확정된 상위 결정은
`base/architecture.md` 12~14번 항목(멀티 백엔드, 싱글톤 모듈, 팩토리 초기화)
참고 — 이 문서는 그 안의 세부 미해결 사항만 다룸.
## 넘버 바인드(숫자 프로퍼티 등)는 누가 처리하는가
Slot과 맞물려서 잘 생각해서 구현해야 하는 부분. **기울어진 방향**: mount가
처리하는 게 맞아 보이지만, 그러면 확장성이 있을지가 문제. 결론: **표준 구현체는
인터페이스만 두고, 실제 구현은 `quad-roblox` 같은 백엔드 서브패키지가 해당
인터페이스를 구현**. 런타임에 프로바이더로 Roblox를 주입받는 방향(반대로
"프로바이더로 base를 받는" 게 아니라) — 이유: 여긴 가상돔이 없어서, base
쪽이 "누가 실제로 그려주는지" 모르는 채로 있다가 Roblox 프로바이더를 주입받는
모양이 더 자연스러워 보임.
## pluggable 플러그 초기화는 누구 몫인가
RBVM처럼 `init namespace` 하나하나 부르는 방식은 별로(`base/lifecycle-pattern.md`
5번 항목에서 실제로 rbvm이 이렇게 되어 있는 걸 확인함 — `InitNamespace`/
`Registered`-가드/`NewLib` 3종 세트를 라이브러리마다 반복). 대신 **적절한 팩토리
함수 제공**: `InitRoblox(Module)` 식으로, 생성된 모듈을 뮤테이션할 수 있는 도구를
주고 사용자가 호출하도록. `base/architecture.md` 14번 항목과 동일한 결정 —
여기서는 "왜"만 보강.
## Bind는 누가, 어떻게 구현하는가
인터페이스 상 `bind`를 두고 이것도 pluggable하게 할지 고민 — 단 **1개만 존재할
수 있는 형태**로 구현하는 게 맞다고 기울어짐: 이미 bind 구현체가 있는데 또
init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류. 즉 "pluggable
슬롯이지만 유일하게 채워질 수 있는 슬롯" — 위의 `research/bind-system-plan.md`
말하는 "여러 핸들러가 우선순위로 경쟁"하는 것과는 다른 층위: **핸들러
레지스트리 자체(그 배후의 실제 bind 구현/백엔드)는 유일해야 하고, 그 안에
등록되는 개별 핸들러들은 여럿+우선순위 경쟁이 맞는 모양.**
의존성을 부작용 식으로 주입해서 `quad-roblox` 바인드를 허용케 하는 건 괜찮아
보임(=`InitRoblox(Module)`가 하는 일이 바로 이 "유일 슬롯 채우기").
## Store는 누구 몫인가 — 상당 부분 확정됨
**사용자 확인 완료**: base가 `LifetimeHandle` 추상화(생명주기/`Connected`
계산 속성)를 소유하는 게 맞다고 확정. 추가로 명확해진 것 — **store 바인드가
수행하는 "처리된 값을 다시 `process(inst,k,realv)`로 넘기는" 재실행 로직
자체도 base가 한 번만 구현**해야 함(모든 백엔드/핸들러가 각자 재구현하면 안
됨). 근거: "모든 곳에서 다시 구현하는 건 나쁘니까." → `research/
bind-system-plan.md`의 "확정된 디스패치 모델" 절이 바로 이 base 제공 로직.
부수적으로 확인된 것:
- **Store 자체의 연산은 더 단순해져도 됨** — v1의 `:Add`/`:With`/`:Tween` 같은
이름 붙은 체이닝 연산(named modifier)은 명시적으로 안 만들기로 확정, 대신
일반 함수를 받는 형태로 통일(`base/store-semantics.md` 참고). "너무 verbose한
연산들은 오히려 일관성을 해친다"는 게 이유.
- **여러 store 값을 묶어 유연하게 처리하는 방법**(`useEffect`류 dependency
array)은 있으면 좋겠다는 요청 — API 시그니처는 미정, `research/
bind-system-plan.md`의 남은 열린 질문 참고.
- `can execute store bind` 후킹 자체는 `Connected` 계산 속성으로 대체된다는
잠정 제안이 그대로 유지되고, 여기에 더해 **완전 소멸(Destroy) 시점엔 아무
처리도 필요 없다**는 원칙까지 확정됨(`base/lifecycle-pattern.md`) — 즉 이
질문은 "필요한가?"에서 "확정된 Connected 체크 하나로 충분하다"로 정리됨.
- 여러 `isHandlable`이 되는 플러그를 매번 우선순위 순으로 스캔하는 비용은
여전히 실제 구현/벤치마크 단계에서 검증 필요 — 디자인 자체는 확정됐으므로
더 이상 사용자 자문 대상이 아니라 구현 검증 대상.
## 모듈 스코핑 (참고, 확정은 `base/architecture.md` 13번)
한 Lua 스레드에서 둘 이상의 모듈 분화체(Roblox+비Roblox 동시)를 쓸 일이
거의 없을 거라 판단, 지금은 싱글톤으로 두고 필요해지면 `New()` 추가.
## Quad는 스크립트인가 라이브러리인가 (확정, 참고용)
이전엔 Instance를 보조하는 역할이라 "스크립트"로 분류했지만, 지금은 확실히
"라이브러리" — 구조화되어 있고 데이터 타입이 존재함. 기능을 각자 따로 묶는 게
아니라 하나의 시스템으로 돌 수 있게(pluggable 하게 두자는 논리의 근거이기도
함). `base/architecture.md` 도입부와 동일 결정.
## 열린 질문 (`.claude/question.md`에도 취합)
- **Store 책임 분리(base vs provider)는 확정됨** — 위 절 참고. 남은 건 실제
구현 단계에서 base의 `LifetimeHandle`/재실행 유틸 API를 정확히 어떻게
노출할지 정도(설계 방향 자체는 더 이상 열려있지 않음).
- 넘버 바인드/프로바이더 인터페이스의 정확한 함수 시그니처(base가 요구하는
provider 인터페이스 계약)는 아직 미정 — 구현 착수 시 함께 확정.

View file

@ -0,0 +1,52 @@
# 컴포넌트 순수성이 아니라 "이식성" 문제 (재정의됨)
**상태**: research — 사용자 확인 완료로 문제 자체는 명확해짐, 남은 건 문서화
강도 정도. 원본: `.claude/initreq/raw-userinput.md` "순수함수에 대한 범위를
정할 필요가 있음" / "진짜 부작용은 외부에 만들어버린다" 절.
## 정정: "순수함수 여부"가 아니라 "이식성(portability)" 문제였다
**사용자 확인 완료 — 이전 초안의 프레이밍이 부정확했음.** quad는 vdom이
없으므로 컴포넌트(Class 함수)는 **딱 한 번만 실행**된다. 모든 부작용은 그
한 번의 실행에서 전부 등록됨 — store에 의해 렌더 함수 안 특정 부분이 다시
트리거될 순 있지만, 함수 자체가 반복 실행되는 구조가 아님. 이 전제 위에서
실제로 문제였던 것은 "순수함수냐 아니냐"가 아니라 **컴포넌트가 자신이 받은
파라미터(store) 대신 전역(global) store를 직접 참조하는 경우의 이식성**이었음.
### 구체적 문제 상황
컴포넌트가 특정 store를 받아서 렌더하도록 설계되어야 하는데, 그렇게 안 하고
전역 store를 직접 참조해버리는 경우:
- 그 컴포넌트가 **한 게임 안에서 한 번만 쓰이는 존재**(예: 특정 페이지에 해당하는
컴포넌트)라면 전혀 문제 없음 — 오히려 그게 자연스러울 수 있음.
- 하지만 **여기저기서 재사용하려고 만들어둔 컴포넌트**가 전역을 건드린다면
이식성이 망가짐 — 다른 프로젝트/다른 컨텍스트에 갖다 쓸 수 없게 됨.
- **라이브러리 내부적으로만 쓰는 공유 값**(라이브러리가 의도적으로 내부에서
전역 상태를 만들어 쓰는 경우)은 문제 없을 수도 있음 — 이식성 문제는 "재사용을
의도한 컴포넌트가 자기가 받은 입력 밖의 것에 은밀히 의존하는가"에 국한됨.
### 결론: 입력받은 store만 처리하는 함수가 좋은 컴포넌트
재사용/이식을 의도하는 컴포넌트는 파라미터로 받은 store만 처리하는 게
좋다는 게 결론 — 다만 **이건 기술적으로 막을 문제가 아니라 UB로 두고 사용자에게
경고해야 할 문서화 문제**. 라이브러리가 "전역 참조 금지"를 런타임/타입
시스템으로 강제하려는 시도는 좋은 접근이 아니라고 명시적으로 판단함(과도한
엔지니어링, 정당한 유스케이스까지 막을 위험).
## 문서화 방향
- `base/store-semantics.md`("Store는 부작용을 허용하는 게 기본 디자인")와
같은 결의 문제 — Store 자체의 부작용 허용 여부와는 별개로, **컴포넌트가
"자기 입력 밖의 상태"에 의존하면 이식성이 깨진다**는 원칙을 문서에 별도로
명시.
- 가이드 문서에 "재사용 가능한 컴포넌트를 만들 땐 store를 파라미터로만
받고 전역을 직접 참조하지 말 것 — 페이지/앱 최상위 컴포넌트처럼 애초에
재사용 의도가 없다면 상관없음"이라는 원칙과, 그 이유(이식성)를 예시와 함께
기술.
- 린트 규칙이나 런타임 경고 같은 기술적 강제는 하지 않음(확정) — 순수 문서
수준의 권장.
## 열린 질문
- 문서에 이 원칙을 얼마나 두드러지게(가이드 최상단 vs 각주 수준) 배치할지 —
급하지 않음, 실제 문서 작성 단계에서 결정.

View file

@ -0,0 +1,81 @@
# Slot — 뮤터블 자식 배열, 엄격한 단일 마운트 소유권 (착수 전)
**상태**: research — 설계 방향은 상당히 잡혀 있으나 세부(특히 소유권 이전/해제
시맨틱)는 사용자와 확인 필요. 원본: `.claude/initreq/raw-userinput.md` "slot을
구현하도록 하기로 했음" 절. Fusion의 `Children` SpecialKey와 Vide의 mount 무가드
비교는 `base/comparison-fusion-vide.md` 참고 — 결론: **두 라이브러리 어디에도
이런 엄격한 단일 마운트 가드가 없음, quad의 진짜 개선점.**
## 개념
`add`/`remove`/`clear`/`get`/`set` 등 뮤터블 연산을 지원하는 메타 배열. 실제
바인드가 일어나면 child로 풀리고, 이 메타 배열에 CRUD를 하면 실제 children이
적절히 제어됨.
## 핵심 제약: 소유권 귀속과 단일 마운트
Slot에 들어간 요소는 **ownership이 귀속**되며 다른 곳에 마운트할 수 없게 된다.
`isMounted`를 관리해서, **한 인스턴스에 대한 다중 마운팅이 라이브러리 차원에서
절대 일어나지 않도록 강제**하는 게 v1 대비 핵심 디자인 변화. v1의 `mount()`
별다른 강제를 안 했지만(`base/quad-v1-architecture.md`의 mount.lua 분석 참고 —
실제로는 부모/자식 부기까지 했지만 다중 마운트 방지는 없었음), v2는 mount
함수 자체가 이 강제를 담당.
Fusion의 `Children` SpecialKey는 이걸 "특정 SpecialKey 하나의 내부 부기"로만
구현했고(재사용 가능한 1급 프리미티브가 아님), Vide는 아예 이 개념이 없어서
같은 target에 두 번 `mount()`하면 조용히 두 개의 독립 루트가 생김 — 둘 다
반면교사.
## 여럿 존재 가능, 부모가 실제 데이터 테이블만 다루면 됨
Slot은 하나의 instance 안에 여럿 존재할 수 있다. 전부 하나의 children으로
들어가지만, 실제 렌더된 instance에서 `GetChildren()`을 직접 하지 않고도 부모가
생성한 "실제 slot 데이터 테이블"만 다루면 되게 해서 **추상화 수준을 낮은 직접
바인딩에서 한 단계 떼어냄**(간접화를 통한 추상화).
## 마운트된 Slot의 재마운트는 즉시 throw (확정)
**사용자 확인 완료**: 이미 사용된(마운트된) slot을 재마운트하려 하면 **즉시
`error()`로 중단** — warn+no-op 아님. 개발 중 바로 잡아낼 수 있게 강하게
실패하는 쪽 선택. 마운트되는 순간 slot의 실제 대상은 고정된다 — 따라서
**글로벌 스코프에서 slot을 쓰는 건 그다지 좋지 않을 수 있음**(재사용/재마운트가
막히므로).
## 클래스가 슬롯을 받는 방법
"네이밍된 슬롯"이 필요한가에 대한 사용자 자문: 그냥 슬롯 바인드 테이블을
값으로 넘기면 되는 것 아닌가 — 결국 array처럼 구현된 Store라고 생각하는 게
편하다는 방향. **기울어진 결론**: 별도 "Named Slot" 개념 없이, store나
파라미터로 넘기고 그게 그냥 ref처럼 바인드되는 모양.
## Slot과 Store 바인드의 관계 (`retract` 순서)
Slot이 store 바인드로 들어오는 경우, pluggable 처리기에 `retract`(구 cleanup,
`base/lifecycle-pattern.md` 참고) 핸들러가 필요함 — 한번 넘어간 slot 요소가
나중에 `retract`되면 삭제되는지, 아니면 "부모의 소유이니 부모가 처리"해야
하는지 검토 필요. **기울어진 결론**: 부모가 정리 정도만 미리 수행하고 다시
`process`하면 되므로, 부모에게 위임(자식 slot 요소 자체가 스스로 정리를
실행하는 게 아니라).
이건 `research/bind-system-plan.md`의 "Store 바인드는 재실행 래핑" 확정
모델과 맞물림 — slot이 store 값으로 오면, store 바인드 핸들러가 이전 slot
상태를 `retract`하고 새 slot 상태로 다시 `process`하는 사이클을 돈다는 뜻.
Slot 핸들러 자신이 감시 중인 값(배열/스토어)이 바뀔 때 child를 갱신하는
추적(구독)도 `research/bind-system-plan.md`가 말하는 "process 함수가 다른 값
변경을 추적해도 됨" 범위에 속하고, `retract` 시점엔 그 추적만 풀면 됨 —
Destroy 시점엔 `retract`가 호출되지 않는다는 원칙(`base/lifecycle-pattern.md`)도
동일하게 적용.
## 자식으로 넘기는 클래스 스토어
자식에게 내려주는 클래스 스토어는 부모 쪽에서 미리 만들어서 내려보내는 게
편할 것 같다는 방향 — `store<<ChildClass.Props>>` 형태로 구성된 스토어를 만들면
됨(타입 표기는 러프한 스케치, 실제 문법은 tbox의 명시적 제네릭 적용 패턴
`f<<T>>(...)``.claude/initreq/tbox/CLAUDE.md:40-41` — 참고해서 확정할 것).
## 열린 질문 (`.claude/question.md`에도 취합)
- 재마운트 에러 처리는 확정(throw). 남은 건 Slot 안 요소가 `retract`될 때
"부모가 정리 후 재`process`"가 정말 항상 올바른 기본 동작인지, 아니면 slot
자체가 일부 자기 정리를 해야 하는 케이스가 있는지 — 구현하면서 실제 사례로
재검증 필요.

View file

@ -0,0 +1,91 @@
# Tween / 애니메이션 플러깅 (착수 전, 사용자와 상의 필요)
**상태**: research — 방향은 뚜렷하게 잡혀 있으나(라이브러리가 트윈을 직접
구현하지 않는다) cleanup 순서/오버라이드 시맨틱은 미확정. 원본:
`.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은
항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이
반응 그래프 안에 있는 설계는 명시적 반면교사 — `base/comparison-fusion-vide.md`
"Animation(Tween/Spring) integration — 경고 신호" 절 참고.
## 확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다
렌더 라이브러리가 트윈을 직접 구현하는 건 처음부터 디자인이 잘못된 접근 —
렌더링 엔진(Roblox `TweenService`)의 애니메이션 관리를 전혀 안 따르게 되기
때문. 대신:
```
[Tween(key, tweenData...)] = storeValue
```
형태의 **특수 bind key**로 제공. 처음 실행될 때는 그냥 바인드로 필드를 쓰지만,
이후에는 store 값을 핸들해서 바뀔 때마다 트윈을 처리. 아니면 사용자가 직접
태그를 얻어 관리하게 둠(둘 다 허용 가능한 경로로 열어둘 것).
**왜 이게 중요한가(Fusion 리서치에서 확인된 근거)**: Fusion은 Tween/Spring을
`GraphObject`(1급 반응 노드, `timeliness="eager"`)로 만들어서 Computed의 입력으로
자유롭게 합성 가능하게 했지만, 그 대가로 (1) 매 프레임 틱하는 외부 클럭
소스(Stopwatch/ExternalTime)를 반응 그래프에 통합해야 했고, (2) eager 재계산
노드가 무효화/전파 로직과 경쟁하게 됐고, (3) 트윈-입력 간 별도의 교차 lifetime
체크 기계장치가 필요해졌다. quad가 트윈을 특수 bind key로 빼면 이 세 가지
복잡도를 전부 피할 수 있다 — 대신 트윈된 값이 Computed류의 추가 입력으로
자유롭게 합성되진 못한다는 걸 감수(Fusion 대비 유일한 손해).
## 정정: Ref 불필요 — 핸들러는 항상 대상 Instance를 직접 받는다
**이전 초안의 전제가 틀렸음.** Tween 핸들러도 `research/bind-system-plan.md`
"확정된 디스패치 모델"을 그대로 따르는 store-bind 핸들러 중 하나 — `process(inst,
k, v)`가 항상 대상 Instance(`inst`)를 직접 받으므로, 트윈 대상을 얻기 위해
Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind 핸들러는 "`k`는
무엇이든, `v`가 Store인 것"을 잡아내는 우선순위 매우 높은 핸들러로 등록되고,
`inst`는 이미 파라미터로 주어짐. (Ref 자체는 도입되지만 전혀 다른
용도 — `research/bind-system-plan.md`의 Ref 절 참고.)
## `retract`(구 cleanup)로 확정된 오버라이드 시맨틱
**스토어 값은 항상 먼저 캐치한다** — 그래야 `retract` 호출이 가능(이름 변경
근거는 `base/lifecycle-pattern.md`). 이전 트윈을 취소하고 새 트윈을 만드는 게
맞지만, "취소" 시점의 동작이 여러 갈래로 갈릴 수 있음:
1. 키 밸류를 받으면, 그로 인해 생성된 트윈을 얻어서 **멈춰버리기**.
2. 트윈 뒤에 **삭제하지 않고 오버라이드**(새 트윈이 이전 트윈의 현재 값에서
시작, 이전 트윈 자체는 그대로 재사용/대체).
3. **삭제** 후 새로 시작.
4. 트윈을 **끝 지점으로 옮기고** 새로운 트윈을 시작.
**확정된 기본값**: **멈춤(Cancel)** — 새 트윈은 현재 보간된 값에서 자연스럽게
시작. 근거: Roblox `TweenService``:Cancel()`은 프로퍼티를 되돌리지 않고
그 자리에서 멈추기만 하므로, 새 트윈이 시작될 때 이미 인스턴스 프로퍼티에
남아있는 현재 값에서 자연스럽게 이어짐 — 대부분의 UI 애니메이션이 기대하는
동작과 일치.
이 기본값 외 나머지 세 동작(오버라이드/삭제 후 재시작/끝점 이동 후 재시작)은
라이브러리가 강제하지 않고, `[Tween(key, tweenData, {onOverride=...})]`처럼
키 설정으로 사용자가 고를 수 있게 열어둠 — `retract(inst, k, v)`가 이전
값(v)을 받으므로 여기서 선택된 동작을 구현. `retract`가 접근해야 할 "이전에
생성한 실제 Tween 객체"는 `research/bind-system-plan.md`가 말하는 base 제공
범용 유틸(`inst`를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면
됨.
## `retract`는 완전 소멸(Destroy) 시엔 호출되지 않는다
`base/lifecycle-pattern.md`의 핵심 원칙: quad는 자신이 만든 Instance를 생명주기
끝까지 그대로 들고 있는 소유자라, Destroy 이후에 실행해야 할 정리 로직이 없다
— 오히려 Destroy된 대상에 `:Cancel()`/`:Stop()` 같은 메서드를 호출하면 에러남
(대상이 죽으면 그 대상에 묶인 Tween도 함께 죽은 상태가 되므로). 따라서
**`retract`는 "같은 key에 새 값이 들어와 이전 트윈을 갈아치울 때"만 호출되고,
Destroy 시점엔 아무 것도 안 함(라이프타임 `Connected` 체크로 처리 자체를
멈추는 것만으로 충분).**
## 네임스페이스드 객체 (성능상 이유로 보류)
트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 —
TagService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페이스가 따로 있을
필요가 있는지는 미정(단, 위 정정으로 이 절 자체의 필요성이 낮아짐 — 핸들러가
이미 대상을 직접 받으므로 "나중에 이름으로 찾아서 트윈"할 필요 자체가 잘
없을 수 있음).
## 열린 질문 (`.claude/question.md`에도 취합)
- 기본값(Cancel)은 확정됨. 남은 건 나머지 세 동작(오버라이드/삭제 후 재시작/
끝점 이동 후 재시작)을 선택하는 옵션 키의 정확한 이름/시그니처 — 구현
단계에서 확정.

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.claude/initreq

108
CLAUDE.md Normal file
View file

@ -0,0 +1,108 @@
# CLAUDE.md
## 언어/모델 관례 (기존 메모, 유지)
사용자는 한국 유저임을 유의해. 사용자가 보게 될 것은 한국어로 띄워주는 게
좋아. 너가 보는 것들(코드 주석 등)은 원하는 언어여도 되는 것(가장 성능이 좋을
영어를 쓰든 그래도 됨, 예를 들어 이 CLAUDE.md도 영어여도 무방하지만 지금은
한국어로 유지). 사용자가 검토해야 하는 것(plan, backlog 등)은 한국어로 써.
코드 안 주석은 공식성을 유지하기 위해 굳이 한국어일 필요 없음, 영어 가능.
또 토큰 맥싱에 유의해 — 아주 작은 테스크라 충분히 작은 모델이 쓸 수 있으면
haiku, 일반 작업은 sonnet. 특히 소스코드를 많이 읽어야 하는 리서치는 메인
컨텍스트를 부패시키니 Agent로 위임할 것(아래 "작업 방식" 참고).
## 이 프로젝트가 뭔지
Roblox 엔진에서 동작하는 DOMless UI 렌더러 **quad**를 처음부터 다시 짜는
프로젝트. 목표는 개별 프로덕트가 아니라 **라이브러리**로서의 코드 퀄리티와
지속 가능성 — 빠른 이터레이션보다 정확성/설계 정합성이 우선. 작업 기간은
길게 잡음.
**지금은 설계/계획 단계이고 구현은 아직 시작 전** — 저장소 루트에 실제 소스
코드(`src/` 등)가 없음. 2026-08-03 여러 질의응답 라운드를 거쳐 핵심 아키텍처
결정 대부분이 확정됨(Store 책임 분리, `process`/`retract` 디스패치 모델,
Signal 미채택, Ref 역할, Store 문법 인체공학, 트윈 기본 오버라이드, Slot
재마운트 에러 처리, 순수성→이식성 재정의) — `.claude/question.md`의 "확정됨"
절 참고. 이전에 시도했다 폐기한 v2 재작성 시도(`.claude/initreq/quad2-try`)도
리서치 완료 — OOP 상속/커스텀 파서/Slot 스텁은 확인된 죽은 접근이라 반복 금지,
`Pipe`의 copy-on-write 절충안은 살려볼 후보. 남은 건 세부 함수 시그니처
(`:Compute`가 의존값을 읽는 방법, `CreatedRef`/`Store.Combine`류 정확한 이름)
정도 — `.claude/question.md`의 "착수하면서 확인" 절 참고.
## 계획 문서 구조
`.claude/README.md`가 색인. 요약:
- `.claude/base/` — 확정된 아키텍처/컨텍스트, plan/done 개념 없음. 먼저
`.claude/base/architecture.md`를 읽을 것.
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의.
- `.claude/qa-request/`, `.claude/archive/`, `.claude/feedback/` — 구현
시작되면 쓰기 시작함, 지금은 비어있음.
- `.claude/initreq/` — 클론해둔 참고 레포(quad v1, Fusion, Vide, rbvm, tbox,
code-docker) + 원본 요청. **읽기 전용, `.gitignore`로 커밋 제외됨** — 내용을
다른 곳으로 옮기지 말고 항상 원본 그대로 둘 것. 리서치가 더 필요하면 이
폴더를 다시 파고들 것.
- `.claude/question.md` — 사용자가 답해야 할 질문 전체 취합(우선순위순).
- 루트 `HUMAN_TODO.md` — 사람만 할 수 있는 일(로컬 GUI 조작, 스케줄/루프
설정 등).
## 작업 방식
- **소스코드를 많이 읽어야 하는 리서치는 Agent(Explore)로 위임** — 메인
컨텍스트 보호. 이미 완료된 v1/rbvm/tbox/Fusion/Vide 리서치 결과는
`.claude/base/`에 정리되어 있으니 중복 조사하지 말고 먼저 그걸 볼 것.
- **병렬화 가능한 작업은 Agent 여러 개를 한 메시지에 동시 호출.** 서로 독립적인
파일/주제를 다루는 리서치나 구현 조사가 여기 해당.
- **크리티컬한 설계 결정은 구현으로 밀어붙이지 말고 plan을 research/에 남긴 채
연기.** 사용자는 Lua/Roblox 엔진을 깊이 아는 사람 — 근거와 선택지를 문서에
정리해두면 사용자가 깨어있을 때 훑어보고 답해줄 것. `.claude/question.md`
반드시 반영.
- **작업이 끝나면(또는 방향이 바뀌면) 항상 자기 문서화** — 완료된 걸 다시
조사하게 되는 재작업을 막기 위함. `.claude/base/`로 승격, `.claude/qa-request/`
이동, 또는 문서 자체를 갱신. code-docker/webmanager의 `.claude/` 관리 방식이
좋은 예시(`.claude/initreq/code-docker/webmanager/.claude/README.md` 참고).
- **Roblox Studio MCP 연결 시 주의**: Studio는 잘 죽는 편 — 죽었을 때 살리려고
위험한 명령을 반복 시도하지 말 것. 그런 상황이면 MCP 없이 할 수 있는 작업만
하거나 대기. 연결 방법은 `HUMAN_TODO.md` 1번 항목 참고(사용자가 Studio에서
베타 기능을 켜줘야 함).
- **`SAFETY.md` 반드시 지킬 것** — (1) GitHub 등 외부 git 호스팅에 이 레포를
push하지 말 것, 모델의 git 작업 공간은 사용자가 별도로 마련해줄 제한 계정
(예: git.qwreey.moe) 전용으로 국한됨 — 그 계정/원격이 설정되기 전까지는
**로컬 git 커밋까지만** 하고 원격 추가/푸시는 하지 말 것. (2) Roblox Studio는
메인 계정이 아닌 별도 계정으로만 사용 — 로그인 계정 전환을 사용자가 안 해줬다면
Studio 관련 작업(MCP 연결 등)을 진행하지 말고 대기.
## 지금 할 일 (우선순위순)
1. **[다음 세션 최우선] 확정된 설계 검증 라운드.** 2026-08-03에 여러 라운드에
걸쳐 확정한 아키텍처 결정들(`.claude/question.md`의 "확정됨" 절 전체 —
Store 책임 분리, `process`/`retract` 모델, Ref 역할, Store 문법, `:With`/
`:Compute`, 트윈 기본값, Slot 에러 처리, 순수성→이식성 재정의 등)을 **사용자가
명시적으로 요청한 방식으로 재검증할 것**: 각 결정을 작게 쪼개서 예/아니오로
답할 수 있는 질문으로 만들어 `AskUserQuestion`으로 하나씩 확인. 목적은 이
설계 라운드에서 내가(에이전트가) 잘못 이해했거나 성급히 확정한 부분을
찾아내 프로젝트 전반의 기틀과 정확성을 높이는 것 — 이미 답변받은 걸 다시
묻는 게 아니라, "정말 이렇게 이해한 게 맞는지"를 세분화해서 다시 짚는 것.
`.claude/base/`, `.claude/research/` 각 문서를 훑으며 검증 질문 목록을 먼저
만들고, 한 번에 다 던지지 말고 문서/주제 단위로 나눠서 진행할 것.
2. 검증 라운드가 끝나면 `research/bind-system-plan.md`/
`research/module-lifecycle-plan.md``base/`로 승격하고,
`base/architecture.md`에 "구현 착수" 섹션을 추가해 실제 소스 트리 구조
(어느 서브패키지가 뭘 갖는지)를 확정 — 이 시점부터 `qa-request/`/`archive/`
폴더가 실제로 쓰이기 시작함.
3. 남은 세부 시그니처(dependency 값 읽는 방법, `CreatedRef`/`Store.Combine`류
정확한 이름)는 검증 라운드 중 자연스럽게 같이 확정 가능.
4. `research/purity-and-effects-plan.md`, `research/existing-instance-bind-plan.md`
급하지 않음 — 스코프 논의만 필요, 구현 착수를 막지 않음.
5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중
(`HUMAN_TODO.md` 2번 항목).
## 인수인계 메모 (2026-08-03 세션 종료 시점)
이 세션에서 `.claude/` 전체 스캐폴드 + 대부분의 핵심 아키텍처 결정을 완료함.
사용자가 다음 세션 시작 시 이렇게 요청함: "각 디자인 부분을 작게작게 질문으로
만들어서, 이게 맞나요? 예/아니오로 대답할 수 있는 걸 던져가며 검증해보자.
프로젝트의 전반적 기틀 잡힘과 정확성을 올리기를 할 것이라 해둬." — 위 "지금
할 일" 1번이 이 요청을 그대로 반영한 것. 로컬 git 저장소는 이 세션에서 초기화
+ 첫 커밋까지 해둠(원격 없음, `SAFETY.md` 참고 — 원격은 사용자가 제한 계정을
마련해줘야 추가 가능).

65
HUMAN_TODO.md Normal file
View file

@ -0,0 +1,65 @@
# HUMAN_TODO — 사용자(사람)만 할 수 있는 일
에이전트가 못 하거나(로컬 GUI 조작, 외부 계정/기기 필요) 사용자의 결정이 필요해서
멈춰둔 것만 여기 모음. 설계 질문(디폴트 이미 잡아두고 진행 중, 급하지 않음)은
`.claude/question.md`에 따로 있음 — 그건 안 봐도 진행에 지장 없고, 이 문서는 실제로
사람이 뭔가 눌러/타이핑해야 풀리는 것만 담음.
## 1. Roblox Studio에 MCP로 연결 (테스트 자동화용)
Roblox가 2026-02부터 Studio에 **MCP 서버를 내장**했음 — 예전처럼 Rust로 직접
`studio-rust-mcp-server`를 빌드할 필요 없이 Studio 자체 베타 기능으로 켜면 됨.
**설정 방법** (사용자가 로컬에서 직접):
1. Roblox Studio → File → Studio Settings → Beta Features → **MCP Server** 활성화
2. 기본적으로 `localhost:3004`에서 리슨 시작함
3. Claude Code의 MCP 클라이언트 설정(`.mcp.json` 등)에 이 로컬 서버를 추가 —
이 설정 파일 자체는 내가 대신 만들어줄 수 있으니, Studio에서 베타 기능만 켜고
"여기 프로젝트에 연결해줘"라고 말해주면 이어서 진행함.
4. 노출되는 툴: `create_object`, `set_property`, `set_script_source`,
`execute_luau` 등 — Undo 히스토리를 존중해서 Ctrl+Z로 되돌릴 수 있음(안전망 있음).
**주의(사용자가 이미 말한 것)**: Roblox Studio는 잘 죽는 편 — 죽었을 때 살리려고
위험한 명령을 반복 시도하지 않을 것이고, 그런 날엔 MCP 없이 할 수 있는 작업만
하거나 대기함. 이 안전 원칙은 `CLAUDE.md`에도 적어둠.
**해야 할 일**: 테스트용 place 파일(빈 place 하나, 또는 `quad/test.project.json`
기반 rojo 싱크 대상)을 열어서 베타 기능만 켜주면 됨. 이후 MCP 서버 설정 파일
작성/연결 확인은 내가 진행 가능.
**`SAFETY.md` 제약**: Studio는 메인 계정이 아닌 별도 계정으로만 사용하기로
되어 있음 — 계정 전환 여부를 알려주기 전까지는 MCP 연결을 진행하지 않고 대기함.
## 0. (SAFETY.md) Git 원격 저장소 계정 마련
`SAFETY.md`에 따라 이 레포는 GitHub 등 외부 호스팅에 올리지 않기로 되어 있음 —
모델(나)의 git 작업 공간은 사용자가 마련해줄 제한 계정 전용이어야 함(예:
git.qwreey.moe에 제한된 계정 생성). 로컬 git 저장소는 이미 초기화 + 초기
커밋까지 해뒀음(원격 없음) — 원격을 추가하고 싶으면 그 계정 정보를 알려줄 것,
그 전까지는 로컬 커밋만 계속 쌓아둠.
## 2. 자율 작업 루프/스케줄 설정
사용자가 잠들어 있는 동안에도 계획된 TODO를 이어서 진행하길 원한다는 요청이 있었음
(`req.md` 참고). 이건 세션을 넘어 지속되는 자동 실행이라 다음 중 하나를 사용자가
직접 트리거해야 함(에이전트가 임의로 크론/무인 실행을 켜는 건 파급力이 커서 먼저
확인받는 게 맞다고 판단해 보류함):
- `/loop` — 지금 세션 안에서 일정 주기로 스스로 다음 작업을 이어가게 함(사용자
대화 종료 전까지). 간단한 자율 반복엔 이걸로 충분.
- `/schedule` — 진짜 cron 스케줄로 별도 클라우드 에이전트를 반복 실행(예: 매일
새벽에 큐에 있는 다음 plan 문서 하나씩 처리). 무인 상태로 더 오래/여러 날에
걸쳐 진행하고 싶다면 이쪽.
원하는 주기/범위를 알려주면 그에 맞춰 설정해줄 수 있음. 어떤 걸 골라도, 진행한
내용은 항상 `.claude/`에 자기 문서화(완료 표시, 다음 TODO 갱신)해서 다음 세션이나
사람이 바로 이어받을 수 있게 할 것.
## 3. `.claude/question.md` 검토 (급하지 않음)
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
진행하면서 `.claude/question.md`에 모아두는 중. 깨어있을 때 훑어보고 기본값이
마음에 안 드는 것만 답해주면 됨 — 막고 있는 항목은 없음.
---
Sources (MCP 리서치): [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server), [How to Connect Claude Code to Roblox Studio — Clauder Navi](https://www.clauder-navi.com/en/claude-roblox-studio)

2
SAFETY.md Normal file
View file

@ -0,0 +1,2 @@
- Git 레포지토에리 대해 접근을 주지 마세요: 모델이 어디로 튈 지 모르기에, github 에는 작업하지 않음. 그래서 모델의 작업 공간은 오직 git.qwreey.moe(예를 들어) 에 제한적 계정으로 계정을 생성해 주는것으로 사용.
- Code-docker 활용: 컨테이너 환경으로 한번 격리하여 사용할 수 있도록 유도. studio 환경은 메인 계정이 아닌 다른 계정을 사용하여야함