decide(base): Slot CRUD(Add/Remove/Extract/Clear/Move/Swap)·요소 타입 제약 확정, Slot:List 신설
- Slot CRUD를 Add/Remove/Extract/Clear/Move/Swap 6종으로 확정, get/set 드롭 - "원시 연산 최소화" 원칙 뒤집고 Move/Swap 추가 — Extract+Add 기반 리오더가 Parent 조작 두 번(detach+reattach)이라 무겁고, :List 없이 수동 구성한 Slot엔 리오더 수단 자체가 없었음. Swap은 element 아닌 index 기준(element면 위치 조회에 2n 들어 O(1) 약속이 깨짐) - isMounted 이중 추적 분리(Slot 컨테이너 self._mounted vs 개별 element 전역 weak-set), pre-implementation-audit.md 1-7/1-8 해소 - 요소 타입 제약 신설 — nil/None 둘 다 raw 요소로 금지, 핸들러 계층 값 (Ref/PreRef/Observer/Effect/Modifier)은 self-ref 컨텍스트가 없어 의미 불성립이라 즉시 error(Modifier 필드와 같은 판별 메커니즘 재사용). Slot<T>() 제네릭화 - Slot:List(data, updateFn, keyFn?) 신설 — 키 기반 동적 컬렉션 재조정, research/additional-primitives-plan.md에서 승격·통합. keyFn 생략 시 index를 key로 사용(80% 케이스 커버, 캐스케이드 갱신 트레이드오프 명시) - updateFn<UD>(item, index, userdata, prev)이 매 reconcile 사이클마다 호출 — filter/toggle이 Visible 토글이 아니라 실제 파괴/재생성이 되도록 재설계(200+ 항목에서 lazy하지 않은 문제 회피), prev 재사용이 저비용 경로 - :List가 Source를 더 이상 안 만들고 userdata로 그 권한을 updateFn에 위임 — result/userdata 반환값 커플링 제거, 정리 루프는 mounted가 아니라 직전 keyIndex 전체를 순회해야 함(userdata만 살아남는 케이스 커버) - userdata는 GC-native 값만 허용, 명시적 cleanup 필요한 값은 UB로 명문화 — item=nil 정리 훅 추가안은 부모 Destroy 경로에서 안 불려 절반만 동작하므로 기각(retract가 Destroy 시 안 불리는 것과 같은 이유) - question.md/ROADMAP.md/README.md 동기화, slot-plan.md 자체 정합성 재감사 (stale 백로그 섹션 제거 등) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYfAz3BsaTrmMM8hnzn6mj
This commit is contained in:
parent
911ab559ea
commit
baa004ad42
8 changed files with 801 additions and 160 deletions
|
|
@ -30,7 +30,7 @@
|
|||
| `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차 라운드) |
|
||||
| `module-lifecycle-plan.md` | 프로바이더 패턴, bind/store 구현 책임 분리 — 확정 |
|
||||
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정 |
|
||||
| `slot-plan.md` | 뮤터블 자식 배열, 엄격한 단일 마운트 소유권, 재마운트 시 throw, base/roblox 패키지 경계까지 확정. **[2026-08-09 세 번째 세션]** `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD(복잡도 표기 포함), `isMounted` 이중 추적 분리, 요소 타입 제약(`nil`/`None`/핸들러 계층 값 금지, `Slot<T>()` 제네릭), 키 기반 동적 컬렉션 재조정(`Slot:List(data, updateFn, keyFn?)`, `keyFn` 생략 시 index 기본값, `updateFn<UD>(item, index, userdata, prev)`가 매 사이클 호출되며 `prev` 재사용/`nil` 반환으로 filter 지원, `Source` 생성은 `updateFn`이 `userdata`로 직접 관리, `userdata`는 GC-native 값만 허용·명시적 cleanup 필요한 값은 UB)까지 전부 확정 통합, base/roblox 경계에 reposition 훅 추가 |
|
||||
| `modifier-plan.md` | Modifier는 런타임 plug 아닌 정적 merge, immutable+clone 기반 체이닝 — 메커니즘 확정. **[2026-08-07 다섯 번째 세션 추가]** `:Apply(factory)` 팩토리 체이닝, `Overridden`(구 `Merge`→`Override`, 2026-08-08 세션에서 이름까지 확정) 값 결합+성능 기준, `:Peek`/`isState` 필드 읽기까지 전부 확정(`Peek`/`isState`는 이름만 용어 정리 라운드까지 잠정) |
|
||||
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 |
|
||||
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 |
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
| `documentation-plan.md` | 문서 사이트 구조(초심자/api/심화/`quadnomicon` 4축, 백엔드별 트랙 분리) + UI 네이밍 컨벤션·Store 부작용 패턴·권장 이벤트 핸들링 3개 세부 문서 뼈대 | 하 — 착수 시점 미정, 구조/스코프만 합의된 상태 |
|
||||
| `documentation-content-map.md` | 위 4축에 실제로 뭘 채울지 `base/` 전체를 초심자/api/심화/skip으로 서베이한 콘텐츠 맵 — 초심자 core loop 목차 초안 포함 | 하 — 문서화 착수 시점의 목차/우선순위표로 쓸 것 |
|
||||
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 진짜 불리한 점 중 고칠 만한 것 3개, 못 고치는 트레이드오프 정리 | 하 — 사용자 검토 후 반영 여부 결정 대기 |
|
||||
| `additional-primitives-plan.md` | **[2026-08-07 범위 축소]** 확정/기각된 Effect·Blocker·Batch·Context는 `base/blocker-plan.md`·`base/effect-plan.md`·`archive/`로 분리됨 — 이제 **키 기반 동적 컬렉션 재조정**(Fusion `ForPairs`/Vide `indexes()`류에 대응하는 프리미티브가 quad엔 없음) 하나만 다룸 | 상 — 사용자 판단 대기, 착수 전 M0/M1 스코프에 영향 줄 수 있음 |
|
||||
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
||||
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1(M0~M4 착수 전 확인 권장) + 11개 우선순위2 + 2개 단순화후보 | 상 — M0 착수 전 최소 우선순위1 항목 확인 권장 |
|
||||
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ architecture.md`의 "구현 착수: 소스 트리 구조 확정" 절 참고).
|
|||
`.claude/initreq/raw-userinput.md` "slot을 구현하도록 하기로 했음" 절. Fusion의
|
||||
`Children` SpecialKey와 Vide의 mount 무가드 비교는 `reference/comparison-fusion-vide.md`
|
||||
참고 — 결론: **두 라이브러리 어디에도 이런 엄격한 단일 마운트 가드가 없음,
|
||||
quad의 진짜 개선점.**
|
||||
quad의 진짜 개선점.** **[2026-08-09 세 번째 세션]** CRUD 의미론
|
||||
(`pre-implementation-audit.md` 1-7/1-8) 완전 확정, `research/
|
||||
additional-primitives-plan.md`가 다루던 키 기반 동적 컬렉션 재조정도
|
||||
`Slot:List(...)` 메소드로 이 문서에 승격·통합 완료 — 아래 참고.
|
||||
|
||||
## base/roblox 패키지 경계 (2026-08-04, 5차 라운드 확정)
|
||||
|
||||
|
|
@ -17,6 +20,12 @@ Handlers/Slot.luau`가 그 위에서 적용/해제만 담당 — 다른 모든
|
|||
분리와 동일한 패턴(`base/architecture.md`의 소스 트리 참고). Slot 자체는
|
||||
당연히 Instance들을 담게 될 것으로 취급.
|
||||
|
||||
**[2026-08-09 세 번째 세션 보강]** 이 경계가 담당하는 훅은 mount(`Add`)/
|
||||
unmount(`Remove`) 둘이 아니라 **reposition(`Move`/`Swap`)까지 셋** —
|
||||
아래 "CRUD API 확정" 절 참고. reposition은 **Parent를 건드리지 않는다는
|
||||
계약만 base가 강제**하고, quad-roblox가 이걸 `SetSiblingIndex`로 구현할지
|
||||
(`LayoutOrder` 기반 정렬이라) 사실상 no-op으로 둘지는 구현 선택.
|
||||
|
||||
**추가로 필요해진 핸들러**: Slot과는 별개로, `k`가 number이고 `v`가 이미
|
||||
만들어진 Instance인 경우(중첩 인스턴스를 자식으로 직접 넣는 경우, 예:
|
||||
`Frame { Frame {} }`)를 위한 핸들러도 필요 — `quad-roblox/src/Handlers/
|
||||
|
|
@ -25,9 +34,47 @@ InstanceChild.luau`. Slot은 "뮤터블 배열"을 다루고 이 핸들러는 "
|
|||
|
||||
## 개념
|
||||
|
||||
`add`/`remove`/`clear`/`get`/`set` 등 뮤터블 연산을 지원하는 메타 배열. 실제
|
||||
바인드가 일어나면 child로 풀리고, 이 메타 배열에 CRUD를 하면 실제 children이
|
||||
적절히 제어됨.
|
||||
뮤터블 자식 배열. `Slot<T>()`(빈 인스턴스, 인자 없는 바닥 생성자 — 다른
|
||||
독립 프리미티브의 `Type(args)` 관습과 동일하되, 무인자라 `T`를 추론할
|
||||
수 없어 tbox 명시적 제네릭 적용 `Slot<<Instance>>()`로 지정)로 만들고,
|
||||
`Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD로 조작하면 실제
|
||||
바인드된 children이 그에 맞춰 갱신됨 — 정확한 시그니처는 아래 "CRUD API
|
||||
확정" 절 참고(`get`/`set`은 드롭).
|
||||
|
||||
### 요소 타입 제약 (2026-08-09 세 번째 세션)
|
||||
|
||||
- **`nil`/`None` 둘 다 금지 — Slot의 raw 요소는 오직 실제 마운트 가능한
|
||||
`T` 값만.** [정정, 같은 세션 후속] 처음엔 "배열 파트는 `nil` 대신
|
||||
`None`" 원칙을 그대로 가져와 `None`을 Slot 요소로 허용했었는데,
|
||||
`:List`의 필터링 요구사항을 구체화하며 재검토한 결과 불필요했음이
|
||||
드러남 — `updateFn`이 "이번엔 렌더 안 함"을 표현하는 건 아래 `:List`
|
||||
절에서 **`updateFn`의 반환값을 해석하는 `:List` 자신의 내부 로직**으로
|
||||
처리되고, 그 경우 `rawAdd` 자체가 아예 호출되지 않음(즉 `None`이 실제로
|
||||
Slot 배열에 들어갈 일이 없음) — 그래서 raw `Add`가 굳이 `None`을
|
||||
허용해야 할 이유가 없어짐. `element == nil`뿐 아니라 `element == None`도
|
||||
`Add`(및 내부 `raw*`)에서 즉시 `error` — "Slot 안엔 실제로 마운트
|
||||
가능한 값만 들어간다"는 단일 규칙으로 단순화.
|
||||
- **핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier) 금지, 즉시
|
||||
`error`** — `Modifier` 필드가 이 값들을 담으면 즉시 `error`로 확정했던
|
||||
것(`modifier-plan.md` 7번)과 같은 판별 메커니즘(`isRef`/`isPreRef`/
|
||||
`isObserver`/`isEffect`/`isModifier` Brand predicate)을 그대로 재사용.
|
||||
근거: `Dispatch/Leaf.luau`가 처리하는 "children 배열에 `Ref`/`Observer`/
|
||||
`PreRef`가 직접 놓이는" 케이스는 **그 컴포넌트가 지금 만들고 있는
|
||||
Instance 자기 자신을 가리키는 self-ref 캡처**(`Frame { PreRef():Callback(fn) }`가
|
||||
그 Frame 자신을 잡는 것)라 `inst`가 "지금 생성 중인 바로 그 하나의
|
||||
Instance"로 고정돼 있어야 의미가 성립하는데, **Slot은 특정 컴포넌트
|
||||
호출 하나에 묶여있지 않고 이미 존재하는 부모에 나중에 독립적으로
|
||||
붙는 동적 리스트라 이 전제 자체가 없음** — Slot 안의 Ref가 "무엇"을
|
||||
가리켜야 하는지 정의가 안 됨. 대체 경로도 이미 있어 능력 손실 없음 —
|
||||
특정 child에 ref가 필요하면 그 child를 만드는 컴포넌트 호출 자체에
|
||||
Ref를 넘기면 됨(`slot:Add(Frame { Ref = myRef })`).
|
||||
- **`T`의 실제 의미**: 위 배제 덕에 "이 Slot이 실제로 담을 수 있는 최종
|
||||
마운트 가능한 값의 타입" 그 자체로 단순해짐 — quad-roblox엔 사실상
|
||||
`T = Instance` 하나뿐(컴포넌트 호출 결과도 결국 Instance)이라
|
||||
`D.InstSlot = Slot<<Instance>>`가 사실상 "그" Slot 타입. `Slot<T>()`가
|
||||
기본값(`T` 생략 시) 없이 항상 명시를 요구하는지, `quad-base`에선
|
||||
`any`로 기본값을 두는지는 tbox 제네릭 적용 문법 확정 시 같이 정할 것
|
||||
(이 문서 "자식으로 넘기는 클래스 스토어" 절의 기존 미결과 같은 갈래).
|
||||
|
||||
## 핵심 제약: 소유권 귀속과 단일 마운트
|
||||
|
||||
|
|
@ -43,6 +90,26 @@ Fusion의 `Children` SpecialKey는 이걸 "특정 SpecialKey 하나의 내부
|
|||
같은 target에 두 번 `mount()`하면 조용히 두 개의 독립 루트가 생김 — 둘 다
|
||||
반면교사.
|
||||
|
||||
### `isMounted` 이중 추적 분리 (1-8 해소, 2026-08-09 세 번째 세션)
|
||||
|
||||
"한 인스턴스가 다중 마운팅 절대 안 됨"이라는 위 원칙과 아래 "재마운트 시
|
||||
즉시 throw"가 원래 하나의 `isMounted`로 뭉뚱그려 서술돼 있었는데, 실제로는
|
||||
서로 다른 두 대상을 추적해야 함 — 명시적으로 분리:
|
||||
|
||||
- **Slot 컨테이너 자신**: `self._mounted: boolean`(Slot 인스턴스 필드
|
||||
하나). **트리거 시점은 `Dispatch.process(inst,k,self)`가 이 Slot
|
||||
객체에 대해 실제로 호출된 순간**(핸들러 매치 시점) — Instance
|
||||
`Parent` 대입 완료를 기다리지 않음. 다른 모든 "마운트됨" 판정(PreRef
|
||||
소진, Ref 콜백 fire 등)이 전부 dispatch-process 시점 기준이라 여기만
|
||||
post-effect 기준으로 가면 일관성이 깨짐. 컴포넌트가 Slot을 prop으로
|
||||
받아 저장만 하고 실제 트리에 안 놓는 경로는 `process`가 애초에 안
|
||||
불려서 이 정의로도 오탐 없음.
|
||||
- **개별 element**: Slot 안에 담기는 각 element(Instance/컴포넌트 결과 등)
|
||||
마다 전역 weak-set 멤버십으로 추적 — 특정 Slot 인스턴스에 안 묶임
|
||||
("한 인스턴스가 어디에도 중복 마운트 안 됨"이 라이브러리 전역 불변식이라서).
|
||||
`Add`가 이 weak-set을 확인(이미 참이면 error)/설정, `Remove`/`Extract`
|
||||
둘 다 여기서 제거(둘의 차이는 파괴 여부일 뿐, "마운트 해제"라는 점은 같음).
|
||||
|
||||
## 여럿 존재 가능, 부모가 실제 데이터 테이블만 다루면 됨
|
||||
|
||||
Slot은 하나의 instance 안에 여럿 존재할 수 있다. 전부 하나의 children으로
|
||||
|
|
@ -117,6 +184,351 @@ Slot은 바인딩되는 순간 그 안의 요소를 전부 own해버리는 데
|
|||
**이번 마일스톤에서는 오버엔지니어링으로 판단, 하지 않음** — 필요성이 명확해지면
|
||||
그때 별도로 다시 논의.
|
||||
|
||||
> **범위 명확화(2026-08-09 세 번째 세션)**: 위 "폐기, 옮기지 않음"은
|
||||
> **프레임워크가 store-bind 재실행으로 Slot 값 전체를 통째로 갈아치울
|
||||
> 때**(retract)만의 얘기 — **사용자가 직접 `Slot:Extract(element)`를
|
||||
> 부르는 CRUD 경로는 이것과 다른 시나리오**다. Extract로 뺀 element는
|
||||
> 파괴되지 않고 호출부가 소유권을 되찾으며, **임의의 다른 Slot으로
|
||||
> 자유롭게 다시 `Add`할 수 있다**(아래 "CRUD API 확정" 절) — retract가
|
||||
> "옮기지 않는다"고 확정한 건 프레임워크가 알아서 옮겨주는 자동 portal을
|
||||
> 안 만든다는 뜻이지, 사용자가 명시적으로 두 번 호출(`Extract` 후
|
||||
> `Add`)해서 옮기는 것 자체를 막는 게 아니다.
|
||||
|
||||
## CRUD API 확정 (2026-08-09 세 번째 세션, 1-7 해소)
|
||||
|
||||
`get`/`set`은 드롭 — 최종 표면(`Move`/`Swap`은 같은 세션 후속 논의에서
|
||||
추가, 아래 "원시 최소화 원칙 정정" 참고):
|
||||
|
||||
| 연산 | 시그니처 | 복잡도 | 의미 |
|
||||
|---|---|---|---|
|
||||
| `Add` | `Slot:Add(element, index?)` | O(n) | 삽입(뒤 요소 밀림), `index` 생략 시 끝에 추가 |
|
||||
| `Remove` | `Slot:Remove(element)` | O(n) | 제거 **+ 파괴**(retract/Destroy) |
|
||||
| `Extract` | `Slot:Extract(element)` | O(n) | 제거, **파괴 안 함** — 호출부가 소유권 회수, 임의의 다른 Slot에 재삽입 가능 |
|
||||
| `Clear` | `Slot:Clear()` | O(n) | 전체 `Remove`(전부 파괴) — 빈 Slot에 호출해도 no-op |
|
||||
| `Move` | `Slot:Move(element, newIndex)` | **O(n)** | 제자리 재배치 — 옛/새 위치 사이 요소들이 밀림/당겨짐(배열 splice와 동일 의미), **Parent 안 건드림** |
|
||||
| `Swap` | `Slot:Swap(indexA, indexB)` | **O(1)** | 두 **인덱스**의 요소를 맞교환, 나머지 안 건드림, **Parent 안 건드림** |
|
||||
|
||||
- **식별은 기본적으로 element 레퍼런스 기준**(`Add`/`Remove`/`Extract`/
|
||||
`Move`) — element는 컴포넌트 호출/Instance 생성이 만들어낸 구체 값이라
|
||||
항상 아이덴티티로 구분 가능. 인덱스 기준으로 하면 add/remove가 반복될
|
||||
때 호출부가 들고 있던 인덱스가 곧바로 stale해짐.
|
||||
- **`Swap`만 예외 — 인덱스 기준(`indexA`/`indexB`), element 레퍼런스
|
||||
아님(2026-08-09 세 번째 세션 정정).** element 레퍼런스로 받으면 Slot이
|
||||
element→index 역방향 맵을 안 갖고 있는 이상 두 element 각각의 현재
|
||||
위치를 찾는 데 O(n)씩(총 2n) 들어서 **`Swap`이 약속하는 O(1)이 깨짐**
|
||||
— `Move`는 어차피 시프트 자체가 O(n)이라 element 조회 비용이 묻히지만,
|
||||
`Swap`은 조회 비용이 곧 전체 비용이라 이 차이가 그대로 드러남. 호출부는
|
||||
보통 "지금 몇 번째 항목과 몇 번째 항목을 바꿀지"를 이미 알고 있는
|
||||
상황(예: 드래그 리오더 UI)이라 인덱스로 받는 게 자연스럽기도 함.
|
||||
- **열거(iteration)/`Get` API는 지금 안 만듦** — `Slot:List(...)`(아래)는
|
||||
자기 자신의 key→element 맵을 따로 들고 있어 Slot의 내부 상태를 조회할
|
||||
필요가 없음. 다른 실사용이 나오면 그때 추가(YAGNI).
|
||||
- **에러 조건 — 전부 즉시 `error()`, no-op 없음**(기존 "재마운트 시 throw"와
|
||||
같은 fail-fast 톤):
|
||||
- `Add`: element가 이미 어딘가(같은 Slot이든 다른 Slot이든) 마운트돼
|
||||
있으면 에러 — "라이브러리 차원에서 다중 마운팅 절대 금지" 원칙을
|
||||
CRUD 경로에도 동일 적용. `element`가 `nil`/`None`이거나 핸들러 계층
|
||||
값(Ref/PreRef/Observer/Effect/Modifier)이면 에러 — 위 "요소 타입 제약" 절.
|
||||
- `Remove`/`Extract`: 그 element가 지금 이 Slot의 멤버가 아니면 에러.
|
||||
- `Move`: `element`가 이 Slot의 멤버가 아니면 에러.
|
||||
- `Swap`: `indexA`/`indexB` 중 하나라도 범위 밖(1..현재 개수)이면 에러 —
|
||||
단 `Swap(i, i)`(같은 인덱스)는 위치가 안 바뀌므로 에러 없이 no-op.
|
||||
- **`Move`/`Swap`은 반환값 없음(void)** — 내부 재배치만 수행, 멤버십
|
||||
weak-set을 안 건드림(요소가 Slot을 떠난 적이 없으므로) — 그래서 `Add`/
|
||||
`Remove`/`Extract`보다 저렴함.
|
||||
- **모든 공개 CRUD는 "가드 확인 + `raw*` 위임"의 얇은 wrapper** — `Add`/
|
||||
`Remove`/`Extract`/`Clear`/`Move`/`Swap` 전부 `self._listed`(`:List`가
|
||||
설치돼 있으면 수동 CRUD 금지)만 확인하고 실제 로직은 `rawAdd`/
|
||||
`rawRemove`/`rawExtract`/`rawClear`/`rawMove`/`rawSwap`에 있음 — 이
|
||||
`raw*` 함수들이 `:List`의 reconcile이 가드 없이 직접 호출하는 바로
|
||||
그 함수(아래 "`Slot:List`" 절의 "구현" 참고). 공개 메소드에 로직이
|
||||
따로 있는 게 아니라 전부 이 한 세트를 공유.
|
||||
- **재진입성**(Observer/store-bind 재실행 콜백 안에서 `Add`/`Clear`를
|
||||
다시 호출) — 별도 가드 불필요. CRUD는 평범한 동기 테이블 뮤테이션 +
|
||||
Dispatch 호출일 뿐이라 "일반적 무한루프는 방어 안 함, provider 버그로
|
||||
간주"라는 기존 원칙이 그대로 적용됨.
|
||||
- **`Slot()` 생성자**: 인자 없는 빈 생성자로 확정 — 초기 children을
|
||||
가변인자로 받는 옵션도 검토했으나, "명시적으로 `Add`해야 들어간다"
|
||||
쪽이 이 프로젝트의 "매직 없이 명시적" 기조와 더 맞음.
|
||||
|
||||
### 원시 최소화 원칙 정정 — `Move`/`Swap` 공개 API로 추가 (같은 세션 후속)
|
||||
|
||||
`:List`의 리오더 메커니즘을 구체화하던 중, 처음엔 `Extract`+`Add(index)`
|
||||
조합으로 충분하다고 봐서 "원시 연산 최소화" 원칙에 따라 별도 `Move`/`Swap`을
|
||||
안 만들기로 했었는데 — 실제로는 두 가지 공백이 드러나 **뒤집음**:
|
||||
|
||||
1. **`Extract`+`Add`는 리오더치고 너무 무겁다.** `Extract`의 계약이 "제거,
|
||||
파괴 안 함, 소유권 회수"라 백엔드가 곧이곧대로 구현하면 실제 Parent
|
||||
조작이 두 번(detach+reattach) 일어남 — Roblox에서 `AncestryChanged`
|
||||
발화, 잠재적 깜빡임, 불필요한 재바인딩 비용까지 딸려올 수 있음.
|
||||
순서만 바뀌는, 매 `:List` 재계산마다 흔히 일어나는 케이스치고 과함.
|
||||
2. **`:List` 없이 수동으로 Slot을 구성하는 사용자에겐 리오더 수단이
|
||||
아예 없었다** — `Extract`+`Add`도 결국 위 1번 비용을 그대로 지므로
|
||||
대체제가 못 됨.
|
||||
|
||||
둘 다 원시 최소화보다 우선하는 실사용 공백이라 판단, `Move`(O(n), 배열
|
||||
splice 의미)와 `Swap`(O(1), 순수 페어 교환)을 공개 CRUD에 추가 — 시간복잡도
|
||||
차이를 문서화해서 사용자가 상황에 맞게 고를 근거를 줌. `:List`의 reconcile
|
||||
자체는 키 기반 diff가 "이 키는 이제 절대 위치 i다"를 산출하지 "A랑 B를
|
||||
맞바꿔라"를 산출하지 않으므로 내부적으로는 계속 `Move`(의 가드 없는 버전)만
|
||||
사용 — `Swap`은 순수하게 수동 Slot 사용자를 위한 편의 API.
|
||||
|
||||
## `Slot:List(data, updateFn, keyFn?)` — 키 기반 동적 컬렉션 재조정 (2026-08-09 세 번째 세션, `research/additional-primitives-plan.md`에서 승격·통합)
|
||||
|
||||
Fusion `ForPairs`/`ForKeys`/`ForValues`, Vide `indexes()`/`values()`, React
|
||||
`key` prop에 대응하는 프리미티브 — 데이터 배열을 정체성(key) 기준으로
|
||||
diff해서 변경분만 생성/갱신/파괴한다. **독립 타입이 아니라 `Slot`의
|
||||
콜론 메소드**로 확정(아래 "왜 자유 함수/새 타입이 아닌가" 참고) — 자기
|
||||
자신을 변경하고 자신을 반환, `Ref():Callback(fn)`류의 기존 체이닝 패턴과
|
||||
동일:
|
||||
|
||||
```
|
||||
Slot():List(data, updateFn, keyFn?) -> Slot -- self
|
||||
```
|
||||
|
||||
**파라미터 순서 정정, `keyFn` 선택 인자화 (같은 세션 후속).** 원래
|
||||
`(data, keyFn, updateFn)`이었는데, 실사용 대부분(사용자 추정 80%)이
|
||||
"item 자체의 정체성 추적 없이 그냥 순번을 key로 써도 충분한" 단순 목록
|
||||
(재정렬·중간 삽입/삭제로 인한 identity 보존이 필요 없는 경우)이라
|
||||
`keyFn`을 매번 명시하게 하는 게 불필요한 보일러플레이트였음 — `updateFn`을
|
||||
필수 인자 자리(두 번째)로, `keyFn`을 선택 인자(세 번째, 생략 시 인덱스를
|
||||
그대로 key로 사용하는 `function(item, index) return index end`)로 재배치.
|
||||
**tradeoff는 명시적으로 문서화 필요**: 인덱스를 key로 쓰면 중간 삽입/삭제
|
||||
시 그 뒤 모든 항목이 "다른 item인데 같은 key"로 오인돼 캐스케이드 갱신이
|
||||
일어남(파괴/재생성은 없음, 단지 identity 보존이 없을 뿐) — 흔한 업계
|
||||
관행(React `key` 생략 시 index 기본값, Vue `v-for` key 없이 쓰는 경우)과
|
||||
같은 트레이드오프라 새로 설명할 개념은 아님, 재정렬/중간 삽입이 실제로
|
||||
일어나는 목록엔 진짜 `keyFn`을 넘기라고 안내하는 정도로 충분.
|
||||
|
||||
**이름 정정 — `renderFn` → `updateFn` (같은 세션 후속).** 아래 서술하는
|
||||
호출 계약이 "새 key가 나타났을 때 1회 렌더"에서 "매 사이클 재호출되어
|
||||
갱신 여부를 스스로 판단"으로 바뀌면서, "render"보다 "update"가 실제
|
||||
역할을 더 정확히 반영한다고 판단해 이름도 같이 바꿈.
|
||||
|
||||
- `data: {[K]:V} | State<{[K]:V}> | Source<{[K]:V}>` — plain이면 최초
|
||||
1회 배치만 하고 이후 추적 안 함(다시는 안 바뀌므로), State/Source면
|
||||
아래 메커니즘이 계속 동작. 기존 leaf 프로퍼티의 "리터럴 또는 State
|
||||
둘 다 받는" 폴리모픽 컨벤션 재사용.
|
||||
- `keyFn(item, index) -> key`(선택, 생략 시 `index`를 그대로 key로 사용) —
|
||||
아이템 값과 인덱스 둘 다 받음.
|
||||
- **`updateFn<UD = any>(item, index, userdata: UD?, prev: T?): (T | nil, UD?)`
|
||||
— 매 reconcile 사이클마다 모든 key에 대해 호출됨.** `:List`는 더 이상
|
||||
item을 위해 `Source`를 대신 만들어주지 않음(아래 "왜 `Source`를
|
||||
`:List`가 안 만드는가" 참고) — `item`/`index`는 매번 그 사이클의 raw
|
||||
현재값 그대로 넘어감, 반응형으로 쓸지는 `updateFn`이 알아서 결정.
|
||||
- **`userdata: UD?`** — 이 key에 대해 지난 호출에서 `updateFn` 자신이
|
||||
반환해둔 두 번째 값을 그대로 돌려받음(첫 호출은 `nil`). 완전히
|
||||
opaque — `:List`는 안을 전혀 안 들여다봄. `updateFn`이 원하는 걸
|
||||
아무거나 담아도 됨(item의 `Source`, 여러 파생 State, 로컬 UI
|
||||
상태 등).
|
||||
- **`prev: T?`** — 이 key에 대해 지금 실제로 마운트돼 있는
|
||||
element(없으면 `nil`, 첫 호출을 포함해 언제든 가능).
|
||||
- **반환값 두 개는 서로 완전히 독립** — `:List`가 `result`와 `userdata`
|
||||
사이에 어떤 커플링도 안 둠(예: `result`가 `nil`이라고 `userdata`를
|
||||
자동으로 지우지 않음), 그대로 기록만 함. **[정정, 같은 세션 후속]**
|
||||
처음엔 "`result`가 `nil`이면 `userdata`도 같이 버림"이었으나, 이러면
|
||||
"인스턴스는 파괴하되 다시 나타날 때 재사용하려고 캐시는 남겨두고
|
||||
싶다" 같은 정당한 패턴 자체가 원천 봉쇄됨 — 그럴 이유가 없어 커플링을
|
||||
없앰. 흔한 경우(둘 다 리셋)는 그냥 `return nil` 하나로 충분(Lua가
|
||||
안 받은 반환 슬롯을 알아서 `nil`로 채움), 캐시를 남기고 싶으면
|
||||
명시적으로 `return nil, ud`.
|
||||
- `updateFn`은 매번 다음 중 하나를 반환:
|
||||
- **`prev`를 그대로 반환** — "지금 마운트된 걸 계속 쓴다"는 뜻.
|
||||
관용구: `if prev and (필터 통과) then ...update ud...; return prev,
|
||||
ud end`. 실제 마운트/파괴가 없는 **저렴한 경로**.
|
||||
- **새 값(또는 다른 값)을 반환** — 첫 렌더(이 key 최초 등장) 또는
|
||||
의도적 교체. `prev`가 있었다면 그건 파괴되고 새 값이 그 자리를
|
||||
대신함.
|
||||
- **첫 번째 값으로 `nil`을 반환** — "지금 이 key는 렌더 안 함"(filter
|
||||
탈락 등). `prev`가 있었다면 실제로 파괴됨(단순 `Visible = false`
|
||||
아님 — 아래 참고). `None`을 반환해도 동일 취급(둘 다 허용, 편의상
|
||||
`nil` 권장 — 반환값이 raw Slot 요소로 직접 들어가는 게 아니라
|
||||
`:List`의 reconcile이 해석만 하므로 "요소 타입 제약"의 raw
|
||||
`nil`/`None` 금지와 안 부딪힘).
|
||||
- `userdata = userdata or {}`류 lazy-init 관용구가 `UD`가 완전히 자유
|
||||
제네릭인 상태에서도 Luau 타입 시스템이 매끄럽게 좁혀주는지는 **실측
|
||||
필요**(M0/M6 착수 시 확인 항목, 지금 단정 안 함).
|
||||
|
||||
### 왜 매 사이클 호출로 바뀌었는가 — filter/toggle 문제
|
||||
|
||||
사용자가 제기한 문제: item이 State 변경으로 "더 이상 렌더되면 안 되는"
|
||||
상태가 될 수 있는데(예: 검색 필터에서 탈락), 기존 "1회만 호출" 모델엔
|
||||
이걸 표현할 방법이 없었음. 실무에서 흔한 회피책은 실제로 제거하지 않고
|
||||
`Visible = false`만 토글하는 것 — 하지만 이건 **lazy하지 않음**: 필터링된
|
||||
항목도 여전히 완전히 살아있는 Instance라 애니메이션/이벤트 연결/재계산이
|
||||
계속 돎. 리스트가 200개+가 되면 "보이는 건 20개인데 200개가 전부 계속
|
||||
돌아가는" 문제가 실제 비용으로 드러남.
|
||||
|
||||
**해법**: `updateFn`을 매 사이클 호출하되, `prev`를 줘서 "바꿀 게 없으면
|
||||
그대로 돌려주기만 하면 되는" 저렴한 경로를 만들고, filter 탈락은 `nil`
|
||||
반환으로 **진짜 파괴**되게 함 — Visible 토글이 아니라 실제 Remove.
|
||||
200개 중 20개만 통과하는 필터면 20개만 실제로 살아있고 나머지 180개는
|
||||
정말로 존재하지 않음(애니메이션도 안 돎).
|
||||
|
||||
**"이전 상태를 다음 호출에 어떻게 넘기냐" 문제는 `userdata`가 그 채널** —
|
||||
item이 plain table이라 매번 `Source`를 새로 안 만들고 재사용하려면 그
|
||||
`Source`를 어딘가 저장해야 하는데, `:List`가 그걸 대신 안 만들어주는
|
||||
대신(아래 참고) `userdata`라는 전용 채널로 `updateFn`이 직접 관리하게
|
||||
함 — filter 탈락 후 재등장해도(Instance는 파괴됐다 새로 만들어져도)
|
||||
`userdata`를 살려뒀다면 그대로 이어짐(위 "반환값 두 개는 서로 독립" 참고).
|
||||
|
||||
**sort는 이 재설계와 무관, 기존 메커니즘으로 이미 커버됨** — 호출부가
|
||||
`data`의 순서를 바꾸면 `keyIndex[key] ~= i` 감지 → `Move`가 그대로
|
||||
처리, 새 메커니즘 필요 없음(사용자가 filter와 같이 물었던 것 중 sort는
|
||||
원래도 문제가 없었음).
|
||||
|
||||
### 왜 `Source`를 `:List`가 안 만드는가 — item/index를 raw로 넘기는 이유
|
||||
|
||||
이전 초안은 `:List`가 `itemState`/`indexState`(내부 `Source`)를 강제로
|
||||
만들어 `updateFn`에 넘겨줬는데, 재검토 결과 이건 **`:List`가 굳이 강요할
|
||||
필요 없는 결정**이었음 — 반응형 바인딩이 필요 없는 단순한 행(예: 매번
|
||||
그냥 새로 계산해도 싼 텍스트 하나)까지 전부 `Source` 생성 비용을 억지로
|
||||
지게 됨. `userdata`로 이 권한을 완전히 `updateFn` 쪽에 넘기면, 원하는
|
||||
item만 자기 `Source`를 만들어 `userdata`에 담고, 나머지는 매번 raw
|
||||
`item`에서 그냥 다시 계산해도 됨 — 어느 쪽이 나은지는 케이스 by 케이스라
|
||||
`:List`가 미리 정할 이유가 없음.
|
||||
|
||||
**부수 효과 — 이전 "item 값은 무조건 재전파, index는 실제 변경시만"
|
||||
비대칭 백로그가 사라짐.** `:List`가 더 이상 `Source`를 안 만드므로 그
|
||||
문제 자체가 `:List` 소관이 아니게 됨 — item/index를 반응형으로 감쌀지,
|
||||
매번 무조건 `:Set()`할지 조건부로 할지는 전부 `updateFn` 작성자의 선택.
|
||||
|
||||
### `userdata`의 생명주기 제약 — GC-native만 허용, 명시적 cleanup이 필요한
|
||||
값은 UB (같은 세션 후속)
|
||||
|
||||
**검토했다가 기각한 대안**: `item`을 `T?`(nilable)로 바꿔서, key가 최종
|
||||
제거될 때 `updateFn(nil, index, userdata, prev)`를 한 번 더 불러 "정리할
|
||||
기회"를 주는 안 — `if not item then <userdata 안의 구독 해제 등> return
|
||||
end` 관용구로 `userdata` 안에 담긴 리소스(예: `Observer:Subscribe()`한
|
||||
구독)를 정리할 수 있게 하자는 아이디어. **기각 — 사용자가 스스로 반례를
|
||||
찾음**: 이 훅은 `data`에서 key가 빠져 `reconcile`이 다시 도는 정상
|
||||
경로에서만 발화함 — 하지만 **Slot을 담고 있는 부모 Instance 자체가
|
||||
`Destroy`되는 경로**(가장 흔한 소멸 경로)는 `reconcile`을 다시 안 돌기
|
||||
때문에 이 훅이 전혀 안 불림. 절반만 동작하는 정리 메커니즘은 없는 것보다
|
||||
나쁨 — 사용자가 "정리가 보장된다"고 오해하고 `Subscribe`류를 `userdata`에
|
||||
넣었다가 Destroy 경로에서 조용히 새는 게 실제로 훨씬 위험한 결과.
|
||||
`retract`가 Destroy 시엔 절대 안 불린다는 기존 원칙(`base/
|
||||
lifecycle-pattern.md` "quad는 라이프사이클 중간에 있지 않다")과 정확히
|
||||
같은 이유로, `:List`에 새 반쪽짜리 예외를 만들 이유가 없음.
|
||||
|
||||
**대신 명시적 제약으로 문서화**: **`userdata`에는 반환된 element(또는
|
||||
Slot 자신)보다 명시적으로 오래 살아야 하는 값을 담으면 안 됨 — GC만으로
|
||||
자연히 정리되는 값만 담을 것(plain 값, `Source`/`State` 등), `:Subscribe()`한
|
||||
`Observer`/`Effect`류처럼 명시적 `:Unsubscribe()`가 필요한 값을 담는 건
|
||||
UB.** `:List`가 어떤 teardown 경로도 보장 안 하므로, `userdata` 안의
|
||||
무언가가 GC 하나만으로 안 죽는다면 그건 곧 leak. 이건 quad 전역
|
||||
GC-native 원칙(`lifecycle-pattern.md`)을 `:List`라는 구체적 지점에 그대로
|
||||
적용한 것뿐 — 새 원칙 아님.
|
||||
|
||||
### 구현
|
||||
|
||||
```lua
|
||||
function Slot:List(data, updateFn, keyFn)
|
||||
assert(not self._listed, "Slot already has :List installed")
|
||||
self._listed = true
|
||||
keyFn = keyFn or function(_, index) return index end
|
||||
|
||||
local mounted, userdata, keyIndex = {}, {}, {}
|
||||
|
||||
local function reconcile(items)
|
||||
local newKeyIndex, seen = {}, {}
|
||||
for i, item in ipairs(items) do
|
||||
local key = keyFn(item, i)
|
||||
newKeyIndex[key] = i
|
||||
seen[key] = true
|
||||
|
||||
local prev = mounted[key]
|
||||
local result, ud = updateFn(item, i, userdata[key], prev)
|
||||
if result == None then result = nil end -- 편의: None도 nil과 동일 취급
|
||||
|
||||
if result ~= prev then
|
||||
if prev ~= nil then rawRemove(self, prev) end -- 파괴
|
||||
if result ~= nil then rawAdd(self, result, i) end -- 새로 배치
|
||||
mounted[key] = result
|
||||
elseif prev ~= nil and keyIndex[key] ~= i then
|
||||
rawMove(self, prev, i) -- 그대로 쓰되 위치만 이동
|
||||
end
|
||||
|
||||
userdata[key] = ud -- result와 무관, 그대로 기록
|
||||
end
|
||||
for key in pairs(keyIndex) do -- 직전 사이클에 존재했던 전체 key
|
||||
if not seen[key] then
|
||||
local prev = mounted[key]
|
||||
if prev ~= nil then rawRemove(self, prev) end
|
||||
mounted[key], userdata[key] = nil, nil
|
||||
end
|
||||
end
|
||||
keyIndex = newKeyIndex
|
||||
end
|
||||
|
||||
if isState(data) then
|
||||
data:Observer(function() reconcile(data:Get()) end)
|
||||
-- Observer는 등록 즉시 1회 실행 확정 -> 최초 population도 공짜
|
||||
else
|
||||
reconcile(data)
|
||||
end
|
||||
return self
|
||||
end
|
||||
```
|
||||
|
||||
- **`data:Observer(fn)`**: 새 구독 프리미티브 아님 — 2026-08-07 여섯 번째
|
||||
세션에 이미 "등록 즉시 1회 실행" 확정된 그 메소드를 그대로 씀.
|
||||
`reconcile`은 매번 **현재 전체 스냅샷을 받아 O(n) 단일 패스로 diff**
|
||||
— 트리 전체를 비교하는 비싼 diff가 아니라 `seen` 셋 하나로 "새 key
|
||||
목록에 없는 건 지운다"만 판정하는 React/Vue/Solid류의 표준 key 기반
|
||||
방식, `data`가 참조를 유지한 채 뮤테이션+`Emit()`되는 경로도 지원해야
|
||||
하는 이상 최소 한 번은 훑어야 하는 게 불가피함.
|
||||
- **`updateFn`을 매번 부르는 게 비싼 게 아닌 이유** — 흔한 경로(`prev`
|
||||
그대로 반환)는 함수 호출 하나뿐, 실제 Instance 생성/파괴가 있는 건
|
||||
key가 새로 나타나거나/사라지거나/filter로 구조가 바뀌는 경우뿐.
|
||||
200개 중 값만 갱신되는 사이클엔 200번의 값싼 함수 호출이 있을 뿐,
|
||||
200번의 재구성이 있는 게 아님.
|
||||
- **`mounted`/`userdata`를 정리하는 루프가 `mounted`가 아니라 이전
|
||||
사이클의 `keyIndex`를 순회하는 이유** — `userdata`가 이제 `result ==
|
||||
nil`이어도 살아남을 수 있어서(위 "반환값 두 개는 서로 독립"), 어떤
|
||||
key가 `mounted[key] == nil`인 채로(필터 탈락 상태) `data`에서 완전히
|
||||
사라지면 `pairs(mounted)`로는 그 key가 아예 안 잡혀서 `userdata`가
|
||||
못 치워지고 샘 — 직전 사이클에 실제로 존재했던 **전체** key 집합
|
||||
(`keyIndex`, 매 사이클 모든 key에 대해 채워짐)을 순회해야 이 케이스를
|
||||
놓치지 않음.
|
||||
- **`mounted`/`userdata`/`keyIndex`**: 이 Slot 인스턴스의 평범한 로컬
|
||||
필드(클로저 업밸류) — 별도 전역 weak table(`Relate` 등) 불필요, `self`가
|
||||
살아있는 동안만 존재하면 되고 Slot이 죽으면 클로저도 같이 GC됨.
|
||||
- **`reconcile`이 직접 호출하는 건 `rawAdd`/`rawRemove`/`rawMove`뿐** —
|
||||
`rawExtract`/`rawSwap`/`rawClear`도 (위 "모든 공개 CRUD는 가드+위임"
|
||||
구조상) 당연히 존재하지만, `:List`의 reconcile 알고리즘 자체가 그
|
||||
셋을 쓸 일이 없을 뿐(제거는 항상 파괴 확정이라 `Extract` 아닌 `Remove`
|
||||
경로, 리오더는 항상 절대 위치 이동이라 `Swap` 아닌 `Move` 경로,
|
||||
`Clear`는 reconcile 단위가 아니라 Slot 전체 단위 연산이라 무관).
|
||||
- **리오더는 `Move`(의 가드 없는 버전)** — Parent를 안 건드리는 진짜
|
||||
저비용 경로. 최소-이동 알고리즘(LIS 기반 등) 자체는 구현 시점 최적화로
|
||||
미룸, 여기선 계약(파괴 없이 위치만 바뀜)만 확정.
|
||||
|
||||
### 왜 자유 함수/새 타입이 아닌가
|
||||
|
||||
처음엔 `List(data, updateFn, keyFn?) -> Slot` 같은 자유 함수(또는 `Slot`을
|
||||
구조적으로 만족하는 새 타입 `List`)로 검토했으나 둘 다 기각:
|
||||
|
||||
- **자유 함수 기각**: `Source(default)`/`Ref(default)`/`Store({defaults})`가
|
||||
지켜온 "`Type(args)` 팩토리 이름 = 반환 타입"이라는 컨벤션이 깨짐 —
|
||||
`List(...)`이 `Slot`을 반환하면 이름과 실제 타입이 안 맞음.
|
||||
- **새 서브타입(`List extends Slot`, Source⊇State 같은 구조적 서브타이핑)
|
||||
기각**: Source가 State의 서브타입이어야 했던 이유는 Source가 State보다
|
||||
진짜로 더 많은 공개 메소드(`:Set`/`:Emit`)를 갖기 때문 — 반면 이
|
||||
프리미티브는 Slot이 이미 가진 것(`Add`/`Remove`/`Extract`/`Clear`/
|
||||
`Move`/`Swap`) 위에 새 공개 메소드를 얹지 않음. 그냥 "자동으로 채워지고
|
||||
관리되는 Slot"일 뿐이라 별도 타입일 이유가 없음.
|
||||
- **결론: `Slot`의 콜론 메소드.** "원천에 종속된 파생 데이터는 자유 함수
|
||||
생성자가 없고 메소드로만 얻어진다"(State/Observer)는 기존 분류 원칙과
|
||||
같은 모양 — 다만 여기 원천은 Source가 아니라 이미 만들어진 Slot 자신.
|
||||
Fusion의 `ForPairs`/`ForKeys`/`ForValues` 3분할도 이 재구성으로 통합
|
||||
방향이 자연스러워짐(단일 `:List`가 이미 Slot 메소드 이름공간 안에
|
||||
있으니 여러 진입점을 나열할 이유가 약해짐) — **통합 확정**.
|
||||
- 이름 후보로 검토됐던 `Render`/`Draw`도 이 재구성으로 더 이상 "타입
|
||||
이름"이 아니라 "메소드 이름" 문제가 됐지만, `List`가 여전히 가장
|
||||
낫다고 판단(`Render`는 quad의 "렌더 주기 없음" 원칙과 메소드 이름으로
|
||||
써도 충돌 소지가 남고, `Draw`는 즉시모드 GUI 뉘앙스) — **`List`로 확정**.
|
||||
|
||||
## 자식으로 넘기는 클래스 스토어
|
||||
|
||||
자식에게 내려주는 클래스 스토어는 부모 쪽에서 미리 만들어서 내려보내는 게
|
||||
|
|
@ -131,13 +543,9 @@ Slot은 바인딩되는 순간 그 안의 요소를 전부 own해버리는 데
|
|||
정도 — 설계 방향 자체는 더 이상 열려있지 않음.
|
||||
- "클래스가 슬롯을 받는 방법"(Named Slot 없음)도 확정됨(위 "클래스가 슬롯을
|
||||
받는 방법" 절 참고).
|
||||
- **[해소됨, 2026-08-09 세 번째 세션]** `add`/`remove`/`clear` CRUD 의미론,
|
||||
`isMounted` 이중 추적 분리, 키 기반 동적 컬렉션 재조정(`Slot:List`) —
|
||||
위 "CRUD API 확정"/"`isMounted` 이중 추적 분리"/"`Slot:List`" 절 참고.
|
||||
- **여러 Slot이 형제로 섞일 때 순서 보장**은 아직 열려있음(위 "여러 Slot이
|
||||
섞일 때 순서 보장" 절 참고) — Roblox 단일 백엔드로는 급하지 않음, Slot
|
||||
코어 로직 구현 시점에 재검토.
|
||||
- **`add`/`remove`/`clear` CRUD 의미론 자체가 아직 정의 안 됨** — 위
|
||||
"개념" 절이 이 세 연산을 뮤터블 메타 배열에 지원되는 것처럼 나열만
|
||||
하고 정확한 동작(예: `add`가 위치를 지정하는지, `remove`가 값 동등성
|
||||
기준인지 참조 기준인지, `clear`가 재마운트 가능한 자리를 남기는지)은
|
||||
정의돼 있지 않음. `research/pre-implementation-audit.md`가 이미 지적한
|
||||
갭이고, 2026-08-07 아홉 번째 세션에서 사용자가 직접 다루기로 보류함 —
|
||||
Slot 코어 로직 구현 착수 전 반드시 확정 필요.
|
||||
|
|
|
|||
|
|
@ -20,16 +20,12 @@
|
|||
`archive/batch-rejected.md`, Context(+레이어드 Store) → `archive/
|
||||
context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** 남김.
|
||||
|
||||
- **키 기반 동적 컬렉션 재조정(유일하게 아직 완전히 열려있음, 최우선)**:
|
||||
Fusion `ForPairs`/`ForKeys`/`ForValues`, Vide `indexes()`/`values()`,
|
||||
React `key` prop에 대응하는 프리미티브가 quad엔 전혀 없음 확인 —
|
||||
`pre-implementation-audit.md` 1-7번(Slot CRUD 미정의)과 같은 지점이라
|
||||
같이 정의해야 함. **자유 함수**(plain data 또는 State 둘 다 받는
|
||||
폴리모픽 시그니처, State 메소드 프레이밍은 Source 안 쓰는 컴포넌트가
|
||||
못 쓴다는 반례로 철회됨), Slot에 파괴 없이 빼내는 `Extract` 연산 추가
|
||||
필요 — 최종 이름만 미정(아래 "용어 정리" 절에 후보 추가). **사용자가
|
||||
"작업 전에 모든 정의를 마치고 싶다"고 명시** — M0 이전 완전 확정 목표.
|
||||
상세는 `research/additional-primitives-plan.md`(이제 이 주제 전용).
|
||||
- **[해소됨, 2026-08-09 세 번째 세션]** 키 기반 동적 컬렉션 재조정 —
|
||||
`Slot:List(data, updateFn, keyFn?) -> Slot` 콜론 메소드로 완전히 확정
|
||||
(자유 함수/새 타입 둘 다 기각, "Slot이 이미 가진 것 위에 새 공개
|
||||
메소드를 안 얹으니 별도 타입일 이유가 없다"는 게 근거). Slot의
|
||||
`Extract`/`Add(index)` CRUD와 같이 확정됨, 상세는 `base/slot-plan.md`
|
||||
"`Slot:List(...)`" 절.
|
||||
- **[해소됨, 2026-08-07 여섯 번째 세션]** Effect/Observer 관계 — Effect는
|
||||
자유 함수로 확정(`state` 인자를 받으면 내부적으로 `state:Observer(...)`를
|
||||
조합해 재실행+자동 cleanup 배선, React `useEffect`와 동형). `state:Observer(fn)`도
|
||||
|
|
@ -80,13 +76,9 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
질문이라 `is`보다 `can` 계열 접두를 유지하는 쪽이 낫다는 방향으로 사용자가
|
||||
기욺 — 여전히 미확정, 다음에 `can`으로 시작하는 구체 대안(예: `canRun`)을
|
||||
같이 검토할 것.
|
||||
- **키 기반 동적 컬렉션 재조정 프리미티브 이름(3순위, 2026-08-06 추가)**:
|
||||
`Keyed`는 타이핑이 어색하고 "Slot을 렌더한다"는 느낌과 안 맞는다는
|
||||
사용자 피드백으로 탈락. 후보: `Render`(가장 직접적이지만 "quad엔 렌더
|
||||
주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만
|
||||
즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직
|
||||
미정, `research/additional-primitives-plan.md`의 "키 기반 동적 컬렉션
|
||||
재조정" 절 참고.
|
||||
- **[해소됨, 2026-08-09 세 번째 세션]** 키 기반 동적 컬렉션 재조정 이름 —
|
||||
`List`로 확정(`Slot:List(...)` 메소드, `Render`/`Draw`는 기각). 상세는
|
||||
`base/slot-plan.md` "`Slot:List(...)`" 절.
|
||||
- **[해소됨, 2026-08-09 세션]** `Bound` — **`canBound(handle): boolean`
|
||||
탑레벨 함수로 확정**, `canExecute`와 같은 결(raw 필드를 직접 노출하는
|
||||
대신 predicate 함수로 감쌈). `base/bind-system-plan.md` "이중 바인딩
|
||||
|
|
@ -178,9 +170,10 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
`pre-implementation-audit.md` 3-1). **[해소됨]** UI shorthand의 기존
|
||||
UICorner 매칭 기준도 `base/ui-shorthand-plan.md`에 이미 확정 반영돼
|
||||
있던 것을 이번에 `pre-implementation-audit.md` 2-11에도 해소 표시로
|
||||
동기화. **아직 실제로 열려있는 건 두 개뿐** — Slot CRUD 의미론
|
||||
(`add`/`remove`/`clear`) 미정의(1-7), 우선순위 스캔 동률/매치실패
|
||||
처리(1-3) — `pre-implementation-audit.md` 본문 참고.
|
||||
동기화. **[해소됨, 2026-08-09 세 번째 세션]** Slot CRUD 의미론
|
||||
(`add`/`remove`/`clear`) 미정의(1-7)/`isMounted` 이중 추적 혼용(1-8) —
|
||||
`base/slot-plan.md` 참고. **아직 실제로 열려있는 건 하나** — 우선순위
|
||||
스캔 동률/매치실패 처리(1-3) — `pre-implementation-audit.md` 본문 참고.
|
||||
- **[해소됨, 2026-08-08 두 번째 세션]** `Frame { ref }`/`Frame { observer }`처럼
|
||||
children 배열 숫자 슬롯에 직접 놓는 leaf 값을 매칭·바인드하는 Handler
|
||||
(`(i:number, v=Ref/Observer/PreRef)`)의 패키지 배치 — 원래 제안대로
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@
|
|||
**2026-08-07 문서 정리에서 확정/기각된 항목을 분리**: Effect/Blocker →
|
||||
`base/blocker-plan.md`/`base/effect-plan.md`, Batch(lexical) → `archive/
|
||||
batch-rejected.md`, Context(+레이어드 Store 대안) → `archive/
|
||||
context-rejected.md`. 이 문서에는 **아직 완전히 열려있는 것 하나만** 남음
|
||||
— 키 기반 동적 컬렉션 재조정. 사용자가 "작업 전에 모든 정의를 마치고
|
||||
싶다"고 명시 — M0 전 완전 확정이 목표.
|
||||
context-rejected.md`. **[2026-08-09 세 번째 세션]** 마지막으로 남아있던
|
||||
키 기반 동적 컬렉션 재조정도 `Slot:List(...)` 메소드로 완전히 확정되어
|
||||
`base/slot-plan.md`로 승격됨(아래 절은 요약+포인터만 남기고 상세는 그쪽
|
||||
참고) — **이 문서에 새로 열려있는 설계 질문은 더 이상 없음**, 아래 표/
|
||||
"빈 자리 아닌 것"/"문서화 백로그"/"참고 소스" 절은 배경 리서치 기록으로만
|
||||
유지.
|
||||
|
||||
## 배경
|
||||
|
||||
|
|
@ -29,7 +32,7 @@ Vide/v1/artworks 소스 근거 조사, Context 구현 난이도 판정) + 그
|
|||
|
||||
| 후보 | 판정 | 현재 위치 |
|
||||
|---|---|---|
|
||||
| 키 기반 동적 컬렉션 재조정 | **진짜 빈 자리, 최우선** — 아직 열려있음 | 이 문서(아래) |
|
||||
| 키 기반 동적 컬렉션 재조정 | **채택, 확정** — `Slot:List(...)` 메소드로 통합 | `base/slot-plan.md`(2026-08-09 세 번째 세션) |
|
||||
| Effect(leaf 죽음에 확정 정리 + `state` 있으면 재실행) | **채택, 확정** — Observer와의 관계도 해소 | `base/effect-plan.md` |
|
||||
| Blocker(값 기반 emit 지연/합치기) | **채택** — Batch의 대안 | `base/blocker-plan.md` |
|
||||
| Batch(함수/코루틴 스코프 lexical block) | **기각** | `archive/batch-rejected.md` |
|
||||
|
|
@ -40,121 +43,21 @@ Vide/v1/artworks 소스 근거 조사, Context 구현 난이도 판정) + 그
|
|||
| Error Boundary | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 |
|
||||
| Readonly wrapper | 빈 자리 아님 | 아래 "빈 자리 아닌 것" 절 |
|
||||
|
||||
## 키 기반 동적 컬렉션 재조정 — 최우선, 설계 진행 중
|
||||
## 키 기반 동적 컬렉션 재조정 — 확정, `base/slot-plan.md`로 승격 (2026-08-09 세 번째 세션)
|
||||
|
||||
**무엇인가**: 데이터 배열(인벤토리, 리더보드, 채팅로그처럼 삽입/삭제/
|
||||
재정렬되는 목록)을 UI로 렌더링할 때, 이전 렌더 결과와 새 데이터를
|
||||
**정체성(key) 기준으로 diff**해서 변경분만 생성/갱신/파괴하는 프리미티브.
|
||||
React `key` prop, Vue `v-for :key`, Solid `<For>`, Fusion `ForPairs`/
|
||||
`ForKeys`/`ForValues`, Vide `indexes()`/`values()`가 이 층위. `base/
|
||||
slot-plan.md`의 `Slot`은 CRUD 껍데기일 뿐 diff 엔진이 아니라서 이 프리미티브가
|
||||
quad엔 없음(확인 완료, 근거는 문서 하단 소스 목록 참고).
|
||||
`ForKeys`/`ForValues`, Vide `indexes()`/`values()`에 대응하는 프리미티브 —
|
||||
데이터 배열을 정체성(key) 기준으로 diff해서 변경분만 생성/갱신/파괴한다.
|
||||
**최종 확정 형태는 자유 함수도 새 타입도 아니라 `Slot`의 콜론 메소드**
|
||||
(`Slot():List(data, updateFn, keyFn?) -> Slot`) — 상세 시그니처/구현
|
||||
의사코드/왜 자유 함수·새 타입이 아닌지/`Move` 기반 리오더/`userdata` 기반
|
||||
`Source` 관리 위임은 전부 `base/slot-plan.md`의 "`Slot:List(...)`" 절
|
||||
참고, 여기서 반복 안 함.
|
||||
|
||||
### 왜 "매핑 함수" 직관이 안 통하는가
|
||||
|
||||
React류는 매 렌더마다 새 가상 트리를 통째로 새로 만들고 reconciler가
|
||||
old/new를 diff한다 — 그래서 "그냥 다시 매핑"이 성립한다. quad는 컴포넌트가
|
||||
**한 번만 실행**되므로 그 "매 렌더"가 아예 없다. 그래서 이건 매핑 함수가
|
||||
아니라 **한 번 설치되면 스스로 diff-and-patch를 도는 Observer 변형**이다 —
|
||||
전달한 `renderFn`은 새 key가 나타났을 때 딱 한 번만 불리고, 그 이후로는
|
||||
값이 바뀌어도 절대 다시 안 불린다.
|
||||
|
||||
### 메커니즘 스케치
|
||||
|
||||
```
|
||||
key가 새로 나타남 → renderFn(key, itemState) 호출, itemSource:Set(초기값), Slot에 삽입
|
||||
key가 사라짐 → Slot에서 제거(파괴)
|
||||
key가 유지, 값만 변경 → renderFn 재호출 없음, itemSource:Set(새값)만 → itemState 구독한 리프만 갱신
|
||||
key가 유지, 순서만 변경 → renderFn 재호출 없음, Slot 위치만 조정(파괴/재생성 없음)
|
||||
```
|
||||
|
||||
`itemState`는 이 프리미티브 내부 소유의 Source이고, `renderFn`엔 그 State
|
||||
뷰만 노출한다(Source 자체를 주면 renderFn이 실수로 `:Set()`해서 diff
|
||||
엔진과 경쟁할 수 있음).
|
||||
|
||||
### 폼 팩터 — 자유 함수로 정정 (State 메소드 프레이밍 철회)
|
||||
|
||||
`state:Keyed(...)`처럼 State의 메소드로 두려던 초안은 "Source를 안 쓰는
|
||||
컴포넌트가 접근 못 함" 반례로 철회됨 — 상세 경위는
|
||||
`archive/keyed-collection-state-method-rejected.md` 참고.
|
||||
|
||||
재검토 결과 — quad는 이미 **leaf 프로퍼티가 "리터럴 값 또는 State" 둘 다
|
||||
받는 폴리모픽 컨벤션**을 갖고 있다(`BackgroundColor3 = someColor`도
|
||||
`BackgroundColor3 = someState`도 같은 자리에서 됨, 정적이면 한 번만 세팅,
|
||||
State면 구독). 이 프리미티브도 같은 컨벤션을 따르는 게 자연스럽다 —
|
||||
**자유 함수**로 두고 `data` 인자가 plain array/table이든 `State<array>`/
|
||||
`Source<array>`든 둘 다 받게 한다. Plain이면 diff 로직 자체가 발동 안 하고
|
||||
(다시는 안 바뀌므로 최초 1회 배치만 하면 끝), State/Source면 위 메커니즘이
|
||||
동작한다. 이름은 아직 미정이지만 시그니처 형태:
|
||||
|
||||
```
|
||||
<이름>(data: {[K]: V} | State<{[K]: V}>, keyFn: (V, K) -> Key, renderFn: (Key, State<V>) -> Child) -> Slot
|
||||
```
|
||||
|
||||
### Slot 확장 — `Extract`, 그리고 확정해야 할 소유권 모델
|
||||
|
||||
순서 변경(파괴 없이 위치만 옮기기)을 처리하려면 Slot에 **"파괴하지 않고
|
||||
빼내기"** 연산이 필요하다. 기존 확정 사항(`slot-plan.md`):
|
||||
|
||||
> retract되는 slot은 옮겨지지 않고 그냥 폐기된다 ... React의 portal류로
|
||||
> 나중에 옮길 수 있게 하는 것도 검토됐으나 이번 마일스톤에서는
|
||||
> 오버엔지니어링으로 판단, 하지 않음.
|
||||
|
||||
이건 **"다른 Slot으로 옮기기"(portal)를 안 한다**는 결정이지 **"같은 Slot
|
||||
안에서 위치만 바꾸기"**를 막는 결정이 아니다 — 순서 재조정은 후자만
|
||||
필요하므로 이 결정과 충돌하지 않는다.
|
||||
|
||||
**사용자가 명확히 한 최종 소유권 모델(확정)**:
|
||||
- **Slot 자체의 바인딩은 귀속·불가역** — 한 번 마운트되면 그 Slot 컨테이너
|
||||
자체를 다른 곳에 다시 바인드할 수 없음(기존 "재마운트 시 throw"와 동일).
|
||||
- **Slot 안에 있는 개별 요소의 입출력은 자유** — 넣고 빼고 다시 넣는 것에
|
||||
제약 없음.
|
||||
- **단, 요소의 `.Parent`를 Slot API를 거치지 않고 직접 만지는 건 UB.**
|
||||
|
||||
제안:
|
||||
|
||||
```
|
||||
Slot:Extract(key) -> element -- 파괴 없이 빼냄
|
||||
Slot:Add(element, index?) -- 기존 그대로, Extract로 뺀 것도 다시 넣을 수 있음
|
||||
```
|
||||
|
||||
리오더는 `Extract` + `Add(index)` 조합으로 구현(별도 `Move`/`Swap` 원시
|
||||
연산은 새로 안 만듦 — 원시 개수를 최소화). 덤으로 이게
|
||||
`pre-implementation-audit.md` 1-8번("isMounted가 element 단위와 Slot
|
||||
컨테이너 단위를 뭉뚱그려 서술됨")을 자연스럽게 푼다 — `Extract`가 있으면
|
||||
"element의 mounted 여부는 가변, Slot 컨테이너 자체의 mounted 여부는
|
||||
불변"으로 두 개념이 명확히 분리된다.
|
||||
|
||||
### 이름 후보 (확정 아님, 용어 정리 라운드 대상)
|
||||
|
||||
`Keyed`는 탈락 — 타이핑이 어색하고 "Slot을 렌더한다"는 느낌과도 안 맞는다는
|
||||
사용자 피드백. 후보:
|
||||
- `Render(data, keyFn, renderFn) -> Slot` — 가장 직접적("슬롯을 렌더한다").
|
||||
단, quad는 "렌더 주기가 없다"를 아키텍처 원칙으로 강조해왔는데 이 이름만
|
||||
"Render"를 쓰면 혼동 가능성 있음.
|
||||
- `Draw(data, keyFn, renderFn) -> Slot` — 짧음, "Render"와의 충돌은 피하지만
|
||||
즉시모드 GUI(Dear ImGui류) 뉘앙스를 가져올 수 있어 quad의 유지형(retained)
|
||||
모델과 어긋나 보일 수 있음.
|
||||
- `List(data, keyFn, renderFn) -> Slot` — 중립적, 결과가 "리스트"라는 것만
|
||||
전달, 메커니즘을 과다 암시 안 함.
|
||||
|
||||
세 후보 다 트레이드오프가 있어 확정 안 함 — `.claude/question.md`의 용어
|
||||
정리 라운드에 후보로 올려둠.
|
||||
|
||||
### 남은 열린 질문
|
||||
|
||||
- 최종 이름(위 후보 중 또는 새 후보).
|
||||
- `Extract`/`Add(index)` 조합의 정확한 시그니처(에러 조건: 이미 다른 Slot에
|
||||
있는 요소를 Add하면? 존재 안 하는 key를 Extract하면?).
|
||||
- Fusion의 `ForPairs`/`ForKeys`/`ForValues` 3종 분리를 안 따르고
|
||||
`renderFn(key, itemState)` 1종으로 통합하는 안이 여전히 유력(단순화
|
||||
후보, `pre-implementation-audit.md`의 "단순화 후보" 렌즈와 같은 결) —
|
||||
최종 확정 아님.
|
||||
- **목표: M6(Slot) 착수 전, 가급적 M0 스파이크 이전에 이 전체를 완전히
|
||||
정의**(사용자 명시적 요청) — 이 프리미티브가 Slot 자체의 CRUD 시맨틱
|
||||
(`pre-implementation-audit.md` 1-7, 지금 미정)과 얽혀 있어서, Slot을
|
||||
먼저 정하고 나중에 여기를 끼워맞추면 재작업이 날 가능성이 높음. Slot
|
||||
CRUD 시맨틱을 정의할 때 이 프리미티브의 요구사항을 같이 고려할 것.
|
||||
이 아래 있던 "왜 매핑 함수 직관이 안 통하는가"/"메커니즘 스케치"/
|
||||
"이름 후보"/"남은 열린 질문" 절은 전부 그 문서로 흡수·확정되어 제거함 —
|
||||
State 메소드로 두려던 초기 폼팩터가 기각된 경위만 여전히
|
||||
`archive/keyed-collection-state-method-rejected.md`에 별도 보존.
|
||||
|
||||
## 빈 자리 아닌 것으로 확인된 것들
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,14 @@ v1 폐기 API/버그/구조 결함 전부 v2 설계를 정당화하는 내부
|
|||
- 초심자: Modifier 기본 체이닝+merge 우선순위 규칙 실제 예시 / Slot 기본 개념(children 배열)+클래스가 슬롯 받는 방법(Named Slot 없음) / 마운트된 slot 재마운트 시 즉시 throw
|
||||
- api: Setter가 리터럴/변환 함수 둘 다 받음(→심화: getter 없는 이유) / 필드가 State일 수 있는 4가지 조합 표(→심화: 반응성 유지/끊김 이유) / `mod:UICorner(8)` dot-access 생성자 관습 / Slot은 인스턴스당 여럿 가능 / 중첩 인스턴스 자식 처리 / retract 시 slot 내용 폐기(→심화: portal 없는 이유) / `:Apply(factory)` 기본 체이닝 관용구(→심화: 언제 `Apply` vs `Overridden`인지 성능 기준) / `:Peek<<T>>(key)` + `isState`(→심화: `Get`과 이름을 다르게 한 이유)
|
||||
- 심화: 정적 merge vs 런타임 pluggable 기각 이유(CSS cascade) / immutable+clone 체이닝 이유(형제 오염 방지) / getter 미채택 이유 / `__index` 런타임 구현 통찰 / Modifier가 핸들러 계층을 모르는 이유 / base/roblox 패키지 경계(Dispatch/Slot vs Handlers/Slot) / Slot 단일 마운트 소유권이 v1/Fusion/Vide 대비 개선인 이유 / retract=폐기 확정 히스토리(portal 검토 후 기각) / **왜 `Apply`가 기본이고 `Overridden`는 최적화 특수 케이스인가**(계산 의존성 있는 조합 vs 독립적 재사용 가능 조각의 병합 — 2026-08-07 다섯 번째 세션, `modifier-plan.md` 9번) / 왜 `Apply`가 clone 대신 mutate하지 않는가(형제 오염 방지가 개별 clone 비용 절감보다 우선)
|
||||
- 열린 질문(문서화 보류): 여러 Slot이 형제로 섞일 때 순서 보장 — 미확정
|
||||
- 열린 질문(문서화 보류): 여러 Slot이 형제로 섞일 때 순서 보장 — 미확정.
|
||||
**[2026-08-09 추가]** `Slot:List`의 `prev`/`userdata` 재사용 최적화를
|
||||
getting-started에서 "항상 파괴 후 재생성" 단순 버전만 가르치고 나중에
|
||||
최적화 단계에서 별도로 알려줄지, 아니면 Slot이 학습 순서상 core loop
|
||||
후반부라 어차피 Source/State를 다 아는 시점이니 처음부터 완전한 형태로
|
||||
한 번에 가르칠지 — 사용자가 직접 제기, 미결. 제 의견은 후자(후반부
|
||||
배치라 단계적으로 나눌 이득이 적어 보임)로 기울지만 확정 아님, 실제
|
||||
콘텐츠 작성 시점에 결정.
|
||||
- skip: 세션 날짜/확정 이력, 문서 승격/정정 안내
|
||||
|
||||
### store-semantics.md / tween-plan.md / ui-shorthand-plan.md
|
||||
|
|
|
|||
|
|
@ -198,6 +198,10 @@ eager cleanup을 택했다"는 구체적 위험을 지적하며 "quad는 rbvm
|
|||
|
||||
### 1-7. Slot의 `add`/`remove`/`clear` CRUD 의미론이 정의돼 있지 않음
|
||||
|
||||
**[해소됨, 2026-08-09 세 번째 세션]** `base/slot-plan.md`의 "CRUD API
|
||||
확정" 절에 `Add`/`Remove`/`Extract`/`Clear` 시그니처·에러 조건·재진입성까지
|
||||
전부 확정 반영됨(`get`/`set`은 드롭). 아래는 당시 지적 원문, 참고용으로만 남김.
|
||||
|
||||
**위치**: `base/slot-plan.md` "개념" 절 — "`add`/`remove`/`clear`/`get`/
|
||||
`set` 등 뮤터블 연산을 지원하는 메타 배열"이라고만 서술.
|
||||
|
||||
|
|
@ -214,6 +218,11 @@ eager cleanup을 택했다"는 구체적 위험을 지적하며 "quad는 rbvm
|
|||
|
||||
### 1-8. Slot "재마운트 시 throw"가 두 가지 다른 추적 대상을 혼용해서 서술됨
|
||||
|
||||
**[해소됨, 2026-08-09 세 번째 세션]** `base/slot-plan.md`의 "`isMounted`
|
||||
이중 추적 분리" 절에 Slot 컨테이너(`self._mounted`, dispatch-process 시점
|
||||
트리거)와 개별 element(전역 weak-set)를 명시적으로 분리 반영됨. 아래는
|
||||
당시 지적 원문, 참고용으로만 남김.
|
||||
|
||||
**위치**: `base/slot-plan.md` "핵심 제약: 소유권 귀속과 단일 마운트" +
|
||||
"마운트된 Slot의 재마운트는 즉시 throw" 절.
|
||||
|
||||
|
|
|
|||
295
CLAUDE.md
295
CLAUDE.md
|
|
@ -44,8 +44,9 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
- `.claude/research/` — 아직 착수 전, 사용자와 상의 필요한 설계 논의.
|
||||
`tween-plan.md`/`existing-instance-bind-plan.md`/`debug-tooling-plan.md`/
|
||||
`documentation-plan.md`/`documentation-content-map.md`/
|
||||
`framework-comparison-findings.md`/`additional-primitives-plan.md`(키 기반
|
||||
동적 컬렉션 재조정만 남음)/`pre-implementation-audit.md`/`v1-compat-plan.md`
|
||||
`framework-comparison-findings.md`/`additional-primitives-plan.md`(2026-08-09
|
||||
세 번째 세션에 마지막 열린 항목까지 전부 해소, 이제 배경 자료용)/
|
||||
`pre-implementation-audit.md`/`v1-compat-plan.md`
|
||||
— 전부 후순위(급한 건 `tween-plan.md` 세부 옵션 정도). 최신 목록·우선순위는
|
||||
`.claude/README.md`가 소스, 여기서 개수 반복 안 함(과거에 "두 개뿐"이라
|
||||
적어놨다가 새 문서 추가될 때마다 안 갱신되는 패턴이 반복돼서 아예 안
|
||||
|
|
@ -1858,3 +1859,293 @@ Modifier만 예외인 건 Modifier가 애초에 dispatch 경로 자체를 안
|
|||
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, 위 "다음 세션 예고"
|
||||
Slot/키 기반 컬렉션 재조정도 그대로) — 이번 세션은 순수 문서 위생
|
||||
작업이라 설계 우선순위엔 영향 없음.
|
||||
|
||||
## 2026-08-09 세 번째 세션 — Slot CRUD 완전 확정, 키 기반 동적 컬렉션
|
||||
재조정이 `Slot:List(...)` 메소드로 통합·승격
|
||||
|
||||
위에서 예고된 "다음 세션 주제"(Slot과 키 기반 동적 컬렉션 재조정)를
|
||||
실제로 다룬 세션. `pre-implementation-audit.md` 1-7/1-8과
|
||||
`research/additional-primitives-plan.md`의 마지막 열린 항목이 전부
|
||||
`base/slot-plan.md`에 흡수·확정됐음 — 상세는 그 문서 본문이 소스,
|
||||
여기는 요지만:
|
||||
|
||||
- **Slot CRUD 최종 확정**: `Add(element, index?)`/`Remove(element)`(제거+파괴)/
|
||||
`Extract(element)`(제거, 파괴 안 함)/`Clear()`(전체 `Remove`) — `get`/`set`은
|
||||
드롭(YAGNI). 식별은 항상 element 레퍼런스 기준(인덱스 아님). 에러 조건
|
||||
전부 즉시 `error()`(이미 다른 곳에 마운트된 element를 `Add`, 멤버 아닌
|
||||
element를 `Remove`/`Extract`) — fail-fast 톤 유지. 재진입성은 별도 가드
|
||||
불필요(기존 "무한루프 방어 안 함" 원칙 재사용). `Slot()`은 인자 없는
|
||||
빈 생성자로 확정.
|
||||
- **`isMounted` 이중 추적 분리(1-8 해소)**: Slot 컨테이너 자신은
|
||||
`self._mounted`(트리거는 `Dispatch.process`가 이 Slot 객체에 실제로
|
||||
호출된 시점 — 다른 모든 "마운트됨" 판정과 동일하게 dispatch-process
|
||||
기준), 개별 element는 전역 weak-set(라이브러리 전역 다중 마운트 금지
|
||||
불변식이라 특정 Slot에 안 묶임).
|
||||
- **`Extract` 후 portal 범위 — 임의의 다른 Slot으로 자유 이동 확정.**
|
||||
기존 "retract되는 slot은 폐기되지 옮겨지지 않는다"는 확정은 **프레임워크가
|
||||
store-bind 재실행으로 값을 통째로 갈아치우는 시나리오**에만 해당하고,
|
||||
사용자가 명시적으로 `Extract`→`Add` 두 번 호출해서 옮기는 것과는 다른
|
||||
얘기라는 걸 명확히 구분(사용자 확인).
|
||||
- **키 기반 동적 컬렉션 재조정 — `Slot:List(data, keyFn, renderFn) -> Slot`로
|
||||
확정, 자유 함수/새 타입 둘 다 기각.** 처음엔 `List(...) -> Slot` 자유
|
||||
함수를 검토했으나, "타입 이름=반환 타입"이라는 `Source(default)`류
|
||||
팩토리 컨벤션이 깨진다는 문제를 사용자가 직접 지적 — Source⊇State식
|
||||
구조적 서브타입도 검토했으나 List가 Slot 위에 새 공개 메소드를 안
|
||||
얹으므로(그냥 "자동으로 채워지는 Slot") 별도 타입일 근거가 약해 기각.
|
||||
최종적으로 "원천에 종속된 파생 데이터는 메소드로만 얻어진다"(State/
|
||||
Observer와 같은 원칙, 여기 원천은 Slot 자신)로 수렴 — `Ref():Callback(fn)`
|
||||
체이닝과 같은 패턴. Fusion `ForPairs`/`ForKeys`/`ForValues` 3분할도
|
||||
단일 `:List`로 통합 확정.
|
||||
- **구현 메커니즘은 전부 기존 프리미티브 재사용, 새 개념 없음** — 사용자가
|
||||
"너무 마법같다"고 지적해 의사코드까지 구체화해서 검증: `data:Observer(fn)`
|
||||
(2026-08-07 확정된 "등록 즉시 1회 실행"), `Source(item)`, 방금 확정한
|
||||
Slot CRUD의 비공개(가드 안 거치는) 버전 세 개의 조합일 뿐. `itemSources`/
|
||||
`elements`/`order`는 Slot 인스턴스의 평범한 클로저 업밸류(별도 전역
|
||||
저장소 불필요). 리오더는 `Extract`+`Add(index)` 조합, 최소-이동
|
||||
알고리즘 자체는 구현 시점 최적화로 미룸.
|
||||
- **`renderFn(key, itemState)`의 `itemState`는 내부 `Source`를 그냥
|
||||
`State`로 다운캐스트해서 넘김 — 별도 `ReadOnlySource` 타입 안 만듦**
|
||||
(사용자 확인: "그걸 위해 ReadOnlySource 같은 걸 만들 이유가 있냐 하면
|
||||
아니다, 이미 그게 State다"). 타입 레벨 힌트만, 런타임 강제 없음(`Peek`/
|
||||
Modifier UB와 같은 "규율 위반은 방어 안 함" 기조) — 나중에 진짜
|
||||
런타임 강제가 필요해지면 `src:Compute(function(v) return v end)`(항등
|
||||
함수 Compute)로 `:Set` 없는 State를 만드는 가벼운 대안이 있다는 것만
|
||||
메모.
|
||||
- **백로그, 착수 안 함(연구만) — reconcile의 무조건 `:Set()` 재전파.**
|
||||
`data`가 테이블 뮤테이션+`:Emit()`으로 오는 경로도 지원해야 해서 이전
|
||||
값과 동등성 비교를 할 방법이 없고, 그래서 값이 실제로 안 바뀐 item도
|
||||
매 재계산마다 재전파됨 — 사용자 판단: "이 재계산 비용은 우리가 핸들해야
|
||||
할 부분은 아닌 것 같다", `Blocker`류 값-동등성 기반 전파 억제도 검토했으나
|
||||
"확정 안 하면 이전 값 자체가 없어서 비교가 안 된다"는 근본적 어려움이
|
||||
있어 기술적으로 더 논의해볼 만한 주제로만 `research/
|
||||
additional-primitives-plan.md`에 백로깅.
|
||||
- **`research/additional-primitives-plan.md` 사실상 전부 해소** — 마지막
|
||||
열린 항목(키 기반 컬렉션)까지 없어져서, 이 문서엔 이제 새로 열린 설계
|
||||
질문이 없음(배경 자료로만 유지). `question.md`/`ROADMAP.md`(M6 체크박스)/
|
||||
`README.md` 전부 동기화 완료.
|
||||
|
||||
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — Slot/키 기반 컬렉션
|
||||
재조정이 이번 세션에서 완결됐으므로 더 이상 "다음 세션 예고" 대상 아님.
|
||||
남은 열린 것은 여전히 `question.md`의 `DI`→`D`/`canExecute`→`isAlive`/
|
||||
`Brand` 이름, `pre-implementation-audit.md` 1-3(우선순위 스캔 동률 처리),
|
||||
"여러 Slot이 형제로 섞일 때 순서 보장"(Roblox 단일 백엔드론 급하지 않음)
|
||||
정도.
|
||||
|
||||
**같은 세션 후속 — quad-roblox 구현 관점에서 재검토, `Move`/`Swap` 공개
|
||||
CRUD로 추가(원시 최소화 원칙 뒤집음), `renderFn`에 `indexState` 추가.**
|
||||
사용자가 "Slot 값 변경을 quad-roblox가 실제로 어떻게 따라가나"를 구체적으로
|
||||
캐물으며 세 가지가 드러남:
|
||||
- **`renderFn(key, itemState) -> element`에 위치 정보가 빠져있었음** —
|
||||
Roblox는 순서를 `LayoutOrder`로 표현하므로 `renderFn`이 그걸 반응형으로
|
||||
바인딩하려면 위치도 State로 받아야 함. `itemState`와 독립된
|
||||
`indexState: State<number>`를 추가(`renderFn(key, itemState,
|
||||
indexState)`) — 값 변경/위치 변경은 서로 독립 신호라는 게 근거, Slot이
|
||||
`LayoutOrder`를 대신 관리해주는 마법은 안 둠.
|
||||
- **`Extract`+`Add(index)`로 리오더를 구현하면 백엔드에서 진짜 Parent
|
||||
조작이 두 번(detach+reattach) 일어난다는 게 드러남** — Roblox
|
||||
`AncestryChanged` 발화, 잠재적 깜빡임, 불필요한 재바인딩 비용까지
|
||||
딸려올 수 있어 매 `:List` 재계산마다 흔한 케이스치고 과함.
|
||||
**`Move`(O(n), 배열 splice 의미)/`Swap`(O(1), 순수 페어 교환)을 공개
|
||||
CRUD로 추가** — 둘 다 Parent를 안 건드림. `:List` 없이 수동으로 Slot을
|
||||
구성하는 사용자에게 애초에 리오더 수단이 아예 없었다는 것도 같이
|
||||
드러난 공백 — "원시 연산 최소화" 원칙보다 이 두 실사용 공백이 우선한다고
|
||||
판단해 뒤집음(같은 세션 내 정정이라 별도 archive 없이 `slot-plan.md`
|
||||
본문에 "원시 최소화 원칙 정정" 절로 직접 반영).
|
||||
- **base/roblox 패키지 경계에 mount/unmount 둘로는 부족, reposition
|
||||
훅이 세 번째로 필요함** — `Dispatch/Slot.luau`/`Handlers/Slot.luau`가
|
||||
이제 "Parent 조작(mount/unmount)"뿐 아니라 "Parent 안 건드리는 재배치
|
||||
(reposition, `Move`/`Swap`)"까지 계약해야 함. quad-roblox가 이걸
|
||||
`SetSiblingIndex`로 구현할지, `LayoutOrder` 기반 정렬이라 사실상 no-op
|
||||
으로 둘지는 구현 선택으로 열어둠.
|
||||
- **item 값 전파(무조건, 백로그)와 index 전파(실제 변경시만)가 비대칭인
|
||||
이유도 명확해짐** — item 값은 외부 뮤테이션+`Emit()` 경로 때문에 "이전
|
||||
값"을 비교할 방법이 없지만, `:List`가 전적으로 소유하는 `keyIndex`는
|
||||
"실제로 위치가 바뀌었는지"를 정확히 알 수 있어 index 쪽엔 같은 문제가
|
||||
없음 — 그래서 index 전파는 처음부터 조건부로 구현.
|
||||
|
||||
전부 `base/slot-plan.md`(CRUD 표, "원시 최소화 원칙 정정" 신규 절, `:List`
|
||||
구현 스케치·설명 갱신)/`ROADMAP.md`(M6)/`README.md` 반영 완료. `question.md`엔
|
||||
새로 열린 항목 없음 — 이번 후속도 순수 확정/구현 세부 명확화.
|
||||
|
||||
**같은 세션 두 번째 후속 — `Swap`을 element 레퍼런스가 아니라 인덱스
|
||||
기준으로 정정, "공개 CRUD는 가드+`raw*` 위임" 구조 명문화.** 사용자가
|
||||
`Swap(elementA, elementB)`를 바로 잡음 — element 레퍼런스로 받으면 Slot이
|
||||
element→index 역방향 맵을 안 갖고 있는 이상 두 element의 현재 위치를 각각
|
||||
찾는 데 O(n)씩(총 2n) 들어서, `Swap`이 약속한 O(1)이 그 자리에서 깨짐.
|
||||
`Move`는 시프트 자체가 O(n)이라 조회 비용이 묻히지만 `Swap`은 조회 비용이
|
||||
곧 전체 비용이라 이 차이가 그대로 드러남 — `Slot:Swap(indexA, indexB)`로
|
||||
정정(호출부가 이미 "몇 번째와 몇 번째를 바꿀지"를 아는 상황, 예: 드래그
|
||||
리오더 UI, 이라는 것도 자연스러움의 근거). 이어서 사용자가 "`Slot:Move`
|
||||
구현은 결국 락(`_listed`) 확인만 하고 실제 로직은 `rawMove`류에 다 있는
|
||||
구조 아니냐"고 확인 요청 — 맞다고 답하며 이걸 여섯 CRUD 전체에 적용되는
|
||||
일반 구조로 명문화: `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` 전부
|
||||
"`self._listed` 확인 + `raw*` 위임"뿐인 얇은 wrapper, 실제 로직은 전부
|
||||
`raw*` 함수 세트 하나에 있고 `:List`의 reconcile도 그 세트를 가드 없이
|
||||
직접 호출. 전부 `base/slot-plan.md`/`ROADMAP.md` 반영 완료.
|
||||
|
||||
**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터).
|
||||
|
||||
**같은 세션 세 번째 후속 — Slot 요소 타입 제약 신설: `nil` 금지/`None`
|
||||
허용/핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier) 금지, `Slot<T>()`
|
||||
제네릭화.** 사용자가 "Slot 안에 뭐가 들어갈 수 있는지 정해진 바 없다"고
|
||||
지적하며 시작 — 처음엔 제가 "Ref/Observer/PreRef도 Slot 요소로 허용,
|
||||
`D.InstSlot = Slot<<Instance>>`류 백엔드 별칭으로 좁히자"고 제안했으나,
|
||||
사용자가 바로 반박: Slot이 동적으로 다뤄지는데 그 안에 Ref/Observer가
|
||||
들어가면 quad-roblox가 그걸 처리할 방법이 없고(특수 대응을 새로 만들어야
|
||||
해서 오버엔지니어링),애초에 왜 필요한지도 불명확하다는 지적 — 검증해보니
|
||||
정확히 맞았음:
|
||||
- `Dispatch/Leaf.luau`가 처리하는 "children 배열에 Ref/Observer/PreRef가
|
||||
직접 놓이는" 케이스는 **그 컴포넌트가 지금 만들고 있는 Instance 자기
|
||||
자신을 가리키는 self-ref 캡처**라(`Frame { PreRef():Callback(fn) }`가
|
||||
그 Frame 자신을 잡음), `inst`가 "지금 생성 중인 바로 그 하나의 Instance"로
|
||||
고정돼 있어야 의미가 성립함. Slot은 특정 컴포넌트 호출 하나에 안 묶이고
|
||||
이미 존재하는 부모에 나중에 독립적으로 붙는 동적 리스트라 이 전제
|
||||
자체가 없음 — Slot 안의 Ref가 "무엇"을 가리켜야 하는지 정의가 안 됨.
|
||||
- 대체 경로가 이미 있어 능력 손실도 없음 — 특정 child에 ref가 필요하면
|
||||
그 child를 만드는 컴포넌트 호출 자체에 Ref를 넘기면 됨
|
||||
(`slot:Add(Frame { Ref = myRef })`).
|
||||
- 사용자가 직접 대비시킨 반례도 정확함: `State<Slot>`(Slot 자체가 State의
|
||||
값)은 retract 시 통째로 버려지고 다시 채워지는 굵은 단위 교체라 이미
|
||||
확정된 모델(폐기, 재구성)과 맞지만, Slot **요소 하나하나**로
|
||||
Ref/Observer가 들어가는 건 그런 굵은 단위 교체가 아니라 세밀한 CRUD
|
||||
대상이라 성격이 다름.
|
||||
- **결론**: `Modifier` 필드가 핸들러 계층 값을 담으면 즉시 `error`로
|
||||
확정했던 것과 같은 판별 메커니즘(`isRef`/`isPreRef`/`isObserver`/
|
||||
`isEffect`/`isModifier` Brand predicate)을 Slot에도 재사용 — 새
|
||||
메커니즘 없이 그대로 막음. 덕분에 `Slot<T>`의 `T`도 "실제로 마운트
|
||||
가능한 최종 값의 타입"으로 단순해짐 — quad-roblox엔 사실상 `T =
|
||||
Instance` 하나뿐이라 `D.InstSlot = Slot<<Instance>>`가 사실상 "그"
|
||||
Slot 타입. `nil`은 기존 배열 파트 `None` 원칙을 그대로 적용해 금지,
|
||||
`None`은 `:List`의 `renderFn`이 "이 item은 이번엔 스킵"을 표현하는
|
||||
용도로 허용 — `renderFn`의 반환 타입도 `T | None`으로 갱신.
|
||||
- `Slot<T>()`가 무인자 생성자라 `T` 추론이 안 되므로 tbox 명시적 제네릭
|
||||
적용(`Slot<<Instance>>()`)이 필요하다는 것도 같이 반영 — 정확한 문법은
|
||||
"자식으로 넘기는 클래스 스토어" 절의 기존 tbox 참고 미결과 같은 갈래로
|
||||
묶어 열어둠.
|
||||
|
||||
전부 `base/slot-plan.md`(신규 "요소 타입 제약" 절, CRUD 에러 조건,
|
||||
`renderFn` 반환 타입) 반영 완료.
|
||||
|
||||
**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터).
|
||||
|
||||
**같은 세션 네 번째 후속 — `Slot:List`의 `renderFn`을 "1회 호출"에서
|
||||
"매 사이클 호출 + `before` 재사용"으로 재설계, filter/toggle 문제 해결.**
|
||||
사용자가 두 가지를 연달아 제기: (1) `renderFn`이 `None`을 반환해 "지연
|
||||
렌더"를 표현하는 아이디어는 좋지만, State 변경으로 이미 렌더된 필드를
|
||||
나중에 다시 지워야 하는 경우(filter)는 기존 "1회만 호출" 모델로 안 풀림.
|
||||
(2) filter/sort를 Slot에서 어떻게 구현할지가 문제 — 흔한 회피책인
|
||||
"`Visible`만 토글"은 필터링된 item도 여전히 완전히 살아있는 Instance로
|
||||
남겨서(애니메이션/이벤트 연결 계속 돎) 200개+ 리스트에서 lazy하지 않다는
|
||||
실질적 비용이 됨.
|
||||
|
||||
**해법 — 사용자가 직접 제시**: `renderFn(itemState, before: inst?): inst?`
|
||||
모양으로 바꿔 **매 reconcile 사이클마다 호출**하되, 이전에 마운트된
|
||||
element(`before`, 없으면 `nil`)를 받아서 `if before then return before
|
||||
end`(바꿀 거 없으면 그대로 반환, 값 갱신은 이미 물려있는 반응형 바인딩이
|
||||
자동으로 함)로 저비용 재사용 경로를 만듦 — filter 탈락 시엔 `nil` 반환으로
|
||||
**진짜 파괴**(Visible 토글 아님). 편의상 `renderFn`이 raw `nil`을
|
||||
던지는 게(Lua에서 자연스러운 관용구) `None`보다 편하다는 것도 사용자가
|
||||
지적 — 검토 결과 `renderFn`의 반환값은 raw Slot 요소로 직접 들어가는
|
||||
게 아니라 `:List`의 reconcile이 해석만 하는 것이라, `nil`을 받아도 위
|
||||
"요소 타입 제약"(raw Slot 요소는 `nil` 금지)과 전혀 안 부딪힘 — `nil`/
|
||||
`None` 둘 다 "스킵" 신호로 동일하게 받아들이기로 정리.
|
||||
|
||||
**부수적으로 드러난 것 — "이전 상태를 다음 렌더에 어떻게 넘기냐" 문제는
|
||||
이미 해소돼 있었음.** 사용자가 "item이 보통 plain table이라 매 렌더마다
|
||||
Source/Store를 새로 안 만들려면 이전 상태를 어딘가 저장해야 하는데 그게
|
||||
어렵다"고 우려했으나, 확인해보니 `itemSources[key]`/`indexSources[key]`가
|
||||
`renderFn` 호출 여부와 무관하게 **처음부터 `:List` 자신이 계속 소유**하고
|
||||
있어서(원래 설계 그대로) — `renderFn`이 매 사이클 불려도 이 부분은 전혀
|
||||
안 바뀜, item이 filter 탈락 후 재등장해 Instance가 파괴됐다 새로 만들어져도
|
||||
반응형 Source는 안 끊기고 그대로 이어짐. 이 부분은 재설계가 아니라 기존
|
||||
설계가 이미 답이었다는 걸 확인한 것.
|
||||
|
||||
**sort는 이번 재설계와 무관** — 호출부가 `data` 순서를 바꾸면 기존
|
||||
`keyIndex`/`Move` 메커니즘이 이미 처리, 새로 손댈 것 없음(사용자가 filter와
|
||||
같이 물었던 것 중 이건 원래도 문제 없었음).
|
||||
|
||||
전부 `base/slot-plan.md`(요소 타입 제약 절 "None 허용" → "nil/None 둘 다
|
||||
금지"로 정정, `:List`의 `renderFn` 시그니처·구현 스케치·"왜 매 사이클
|
||||
호출로 바뀌었는가" 신규 절)/`ROADMAP.md`(M6)/`README.md` 반영 완료.
|
||||
|
||||
**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터).
|
||||
|
||||
**같은 세션 다섯 번째 후속 — `renderFn` → `updateFn` 개명, `:List`가
|
||||
`Source` 생성을 그만두고 `userdata`로 그 권한을 통째로 넘김.** 사용자가
|
||||
"`renderFn`이 아니라 `updateFn`이 맞고, `itemState`도 `:List`가 강제로
|
||||
만들지 말고 원문 item + `userdata: UD?` + `prev: T?`를 주는 게 낫다"고
|
||||
제안 — 검토 후 채택, 근거:
|
||||
- **`itemState`/`indexState`를 `:List`가 강제로 만드는 건 불필요한 강요였음**
|
||||
— 반응형이 필요 없는 단순한 행까지 전부 `Source` 생성 비용을 지게
|
||||
했음. `userdata`로 권한을 넘기면 필요한 item만 자기 `Source`를 만들어
|
||||
`userdata`에 담고, 나머지는 매번 raw `item`에서 다시 계산해도 됨 —
|
||||
`:List`가 미리 정할 이유가 없는 선택.
|
||||
- **"이전 상태를 다음 호출에 넘기는" 문제, 원래 걱정했던 것과 달리
|
||||
`userdata`라는 명시적 채널로 완전히 해소됨** — item이 plain table이라
|
||||
매번 `Source`를 새로 안 만들려면 어딘가 저장해야 한다는 우려가 있었는데,
|
||||
`userdata`가 정확히 그 저장소.
|
||||
- **`prev`(구 `before`)와 `userdata`가 원래 비일관적이었음** — 사용자가
|
||||
직접 지적: 하나(`prev`)는 `:List`가 자동 관리하는데 다른
|
||||
하나(`userdata`)만 수동 반환을 요구했음. 해법은 **둘 사이 커플링을
|
||||
완전히 제거** — `result`가 `nil`이라고 `:List`가 `userdata`를 자동으로
|
||||
안 지움, 그대로 기록만 함. 흔한 경우(둘 다 리셋)는 `return nil` 하나로
|
||||
Lua가 나머지 반환 슬롯을 알아서 `nil`로 채워주고, "파괴하되 캐시는
|
||||
남기고 싶다"는 정당한 패턴은 `return nil, ud`로 명시적으로 표현
|
||||
가능해짐 — 이전 설계(result nil이면 userdata 자동 삭제)로는 이 패턴이
|
||||
원천 봉쇄돼 있었음.
|
||||
- **제가 놓칠 뻔한 버그를 사용자와의 논의 과정에서 직접 잡음**: `userdata`가
|
||||
이제 `mounted`(실제 element)보다 오래 살 수 있게 되므로, 정리 루프가
|
||||
`pairs(mounted)`만 순회하면 "필터 탈락 상태(mounted=nil)로 `userdata`만
|
||||
살아있던 key가 `data`에서 완전히 사라지는" 케이스를 못 잡고 새서
|
||||
— 직전 사이클의 전체 key 집합(`keyIndex`, 매 사이클 모든 key에 대해
|
||||
채워짐)을 순회하도록 정정.
|
||||
- **부수 효과 — "item 값 무조건 재전파" 백로그가 사라짐**: `:List`가
|
||||
더 이상 `Source`를 안 만드므로 그 문제 자체가 `:List` 소관이 아니게
|
||||
됨, `updateFn` 작성자의 선택으로 넘어감.
|
||||
- `userdata = userdata or {}`류 lazy-init 관용구가 `UD`가 자유 제네릭인
|
||||
채로 Luau 타입 시스템에서 잘 좁혀지는지는 실측 필요 항목으로 명시적으로
|
||||
남김(사용자가 직접 이 불확실성을 짚음) — M0/M6 착수 시 확인.
|
||||
|
||||
전부 `base/slot-plan.md`(`:List` 절 전면 재작성 — `updateFn` 시그니처/구현/
|
||||
"왜 `Source`를 `:List`가 안 만드는가" 신규 절)/`ROADMAP.md`(M6)/
|
||||
`README.md` 반영 완료.
|
||||
|
||||
**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터).
|
||||
|
||||
**같은 세션 여섯 번째 후속 — `keyFn` 선택 인자화(파라미터 순서 정정),
|
||||
`userdata` cleanup 훅 검토 후 기각·GC-native 제약 명문화, 문서화 순서
|
||||
질문은 백로그로 이관.** 사용자가 세 가지를 짧게 제기:
|
||||
|
||||
1. **`Slot:List(data, keyFn, updateFn)` → `Slot:List(data, updateFn,
|
||||
keyFn?)`로 파라미터 순서 정정, `keyFn` 선택 인자화.** 실사용 대부분
|
||||
(사용자 추정 80%)이 item identity 추적 없이 순번을 key로 써도 충분한
|
||||
단순 목록이라 매번 `keyFn`을 명시하게 하는 게 불필요한 보일러플레이트 —
|
||||
생략 시 `function(item, index) return index end` 기본값. tradeoff(중간
|
||||
삽입/삭제 시 그 뒤 항목들이 "다른 item인데 같은 key"로 오인돼 캐스케이드
|
||||
갱신 — identity 보존 없음, 파괴/재생성 자체는 없음)는 React `key` 생략
|
||||
시 index 기본값 등 업계 흔한 관행과 같은 결이라 새로 설명할 개념 아님.
|
||||
2. **`updateFn(item?, ...)`로 바꿔 최종 제거 시 "정리용 1회 추가 호출"을
|
||||
주는 안 — 검토 후 기각, 사용자가 직접 반례를 찾음.** 이 훅은 `data`에서
|
||||
key가 빠져 `reconcile`이 다시 도는 정상 경로에서만 발화하는데, **Slot을
|
||||
담은 부모 Instance 자체가 `Destroy`되는(가장 흔한) 경로는
|
||||
`reconcile`이 다시 안 돌아서 이 훅이 전혀 안 불림** — 절반만 동작하는
|
||||
정리 메커니즘은 없는 것보다 위험(사용자가 "정리가 보장된다"고 오해하고
|
||||
`Subscribe`류를 `userdata`에 넣었다가 Destroy 경로에서 조용히 샘).
|
||||
`retract`가 Destroy 시 절대 안 불린다는 기존 원칙(`lifecycle-pattern.md`
|
||||
"quad는 라이프사이클 중간에 있지 않다")과 정확히 같은 이유로 기각.
|
||||
**대신 `userdata`엔 GC-native 값만 담고, `:Subscribe()`한 Observer류처럼
|
||||
명시적 cleanup이 필요한 값을 담는 건 UB로 명문화** — quad 전역
|
||||
GC-native 원칙을 `:List`라는 구체 지점에 그대로 적용한 것뿐, 새 원칙
|
||||
아님.
|
||||
3. **문서화 순서(getting-started에서 단순 버전만 가르치고 나중에
|
||||
`prev`/`userdata` 최적화를 알려줄지, 아니면 Slot이 학습 순서상 후반부라
|
||||
처음부터 완전한 형태로 가르칠지)는 결정 안 함** — `research/
|
||||
documentation-content-map.md`의 modifier/slot 절에 백로그로 추가,
|
||||
제 의견(후자 쪽으로 기욺)만 메모, 실제 콘텐츠 작성 시점 결정 사항이라
|
||||
지금 확정 안 함.
|
||||
|
||||
전부 `base/slot-plan.md`(`:List` 시그니처/코드 재정렬, `keyFn` 기본값
|
||||
설명, "`userdata`의 생명주기 제약" 신규 절)/`ROADMAP.md`(M6)/`README.md`/
|
||||
`research/documentation-content-map.md` 반영 완료.
|
||||
|
||||
**다음 세션이 할 일**: 여전히 안 바뀜(`ROADMAP.md` M0부터).
|
||||
|
|
|
|||
46
ROADMAP.md
46
ROADMAP.md
|
|
@ -170,14 +170,44 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
|
||||
- [ ] "여러 Slot이 형제로 섞일 때 순서 보장" 열린 질문 확인(`slot-plan.md`) —
|
||||
Roblox 단일 백엔드로는 급하지 않으면 스킵하고 진행 가능
|
||||
- [ ] **Slot의 `add`/`remove`/`clear` CRUD 의미론 확정 — 착수 전 필수.**
|
||||
`research/pre-implementation-audit.md` 우선순위1이 지적한 갭, 아직
|
||||
미해결(2026-08-07 아홉 번째 세션에서 "사용자가 다음 세션에서 직접
|
||||
다루기로 보류"로 확인, 2026-08-09 세션 말미에도 다음 세션 예고로
|
||||
다시 지목됨). "재마운트 시 즉시 throw"가 개별 element 기준인지 Slot
|
||||
컨테이너 전체 기준인지도 이 논의에서 같이 확정할 것.
|
||||
- [ ] base `Dispatch/Slot.luau`(추상 재조정) + quad-roblox `Handlers/Slot.luau`
|
||||
(실제 Parent 조작)
|
||||
- [x] **Slot의 `Add`/`Remove`/`Extract`/`Clear`/`Move`/`Swap` CRUD 의미론
|
||||
확정** (2026-08-09 세 번째 세션) — `get`/`set` 드롭, 에러 조건까지
|
||||
전부 확정(`base/slot-plan.md` "CRUD API 확정"). "재마운트 시 즉시
|
||||
throw"도 `isMounted` 이중 추적 분리로 개별 element/Slot 컨테이너
|
||||
기준이 명확히 갈림(같은 문서 "`isMounted` 이중 추적 분리" 절).
|
||||
`Move(element, newIndex)`(O(n))/`Swap(indexA, indexB)`(O(1), element
|
||||
아닌 인덱스 — element면 위치 조회에 2n 들어 O(1) 약속이 깨짐)은
|
||||
리오더 전용, Parent를 안 건드림. 공개 6개 메소드 전부 "가드 확인 +
|
||||
`raw*` 위임" 얇은 wrapper. base/roblox 경계에 mount/unmount 외
|
||||
reposition 훅 추가됨. **`Slot<T>()` 제네릭화, 요소 타입 제약 확정**
|
||||
— `nil`/`None` 둘 다 raw 요소로 금지(Slot 안엔 실제 마운트 가능한
|
||||
`T`만), 핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier)은
|
||||
self-ref 컨텍스트가 없어 의미 불성립이라 즉시 error(`Modifier`
|
||||
필드와 같은 판별 메커니즘 재사용) — `D.InstSlot = Slot<<Instance>>`가
|
||||
quad-roblox의 사실상 유일한 Slot 타입.
|
||||
- [ ] `Slot:List(data, updateFn, keyFn?)` — 키 기반 동적 컬렉션 재조정,
|
||||
`keyFn` 생략 시 index를 그대로 key로 사용(중간 삽입/삭제 시 identity
|
||||
보존 안 됨, 캐스케이드 갱신 — 흔한 업계 관행과 같은 트레이드오프).
|
||||
`updateFn<UD=any>(item, index, userdata: UD?, prev: T?): (T|nil, UD?)`가
|
||||
**매 reconcile 사이클마다 호출**(filter/toggle 지원 — 첫 반환값
|
||||
`nil` 시 실제 파괴, `Visible` 토글 아님, 200+ 항목에서 lazy하지 않은
|
||||
문제 회피), `prev` 그대로 반환하면 저비용 재사용 경로. `:List`가
|
||||
`Source`를 대신 안 만듦 — item/index를 반응형으로 감쌀지는
|
||||
`updateFn`이 `userdata`에 직접 관리(반환값 두 개는 서로 독립,
|
||||
`result`가 `nil`이어도 `userdata`는 명시적으로 반환 안 하는 한 안
|
||||
지워짐). 정리 루프는 `mounted`가 아니라 직전 사이클 `keyIndex`
|
||||
전체를 순회해야 함(`userdata`만 살아있는 채로 key가 완전히 사라지는
|
||||
케이스 커버). `userdata = userdata or {}` lazy-init 패턴이 Luau
|
||||
제네릭에서 잘 좁혀지는지 실측 필요. **`userdata`는 GC-native 값만
|
||||
허용, `:Subscribe()`한 Observer류 명시적 cleanup 필요한 값은 UB** —
|
||||
`item`을 nilable로 바꿔 최종 제거 시 정리 훅을 한 번 더 부르는 안은
|
||||
기각(Slot 부모 자체가 Destroy되는 경로에선 이 훅이 전혀 안 불려서
|
||||
절반만 동작, `retract`가 Destroy 시 안 불리는 것과 같은 이유).
|
||||
(2026-08-09 세 번째 세션 확정,
|
||||
`base/slot-plan.md` "`Slot:List(...)`" 절) 구현
|
||||
- [ ] base `Dispatch/Slot.luau`(추상 재조정, mount/unmount/reposition 3훅) +
|
||||
quad-roblox `Handlers/Slot.luau`(실제 Parent 조작 + reposition —
|
||||
`SetSiblingIndex` 또는 `LayoutOrder` 기반이면 no-op, 구현 선택)
|
||||
|
||||
## M7 — Modifier
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue