docs(base): PostRef 확정·OnRendered 채택, PreRef/PostRef 계열 안 순서 미보장으로 역전
- base/ref-plan.md: "PostRef" 절 신설(PreRef의 거울상 — pre-pass 공동 수집, ProcessedPostRef 센티널+전담 Handler, 동적 경로 가드, _fired, 타입 차단). 보장 범위를 명시: 자기 서브트리 완성은 보장하되 이 인스턴스가 부모에 붙기 전에 불림(React componentDidMount와 다름). - 복수 PreRef/PostRef의 계열 안 fire 순서를 "배열 index 순서 보장"에서 미보장으로 역전 — 구현이 아니라 계약만 좁힘 (archive/preref-order-guaranteed-reversed.md 신설). - research/lifecycle-hooks-plan.md → base/ 승격, OnRendered 채택 반영 (마지막 열린 항목이던 채택 여부/메커니즘/스코프/패키지 전부 확정). - dispatch-core-plan/brand/architecture/slot/modifier/typing-limits/README/ ROADMAP/question.md 전파. doc-check.py ERROR 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6dbce6cc7c
commit
bcd02f1cec
15 changed files with 657 additions and 149 deletions
|
|
@ -48,11 +48,12 @@
|
|||
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** 단일 키 `[AttributeKey<T> "Name"]`(구 `Attribute<T>`) — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐. **[2026-08-11 아홉 번째 세션]** 여러 Store를 한 번에 attribute로 묶는 그룹 `Attribute(...)` 프리미티브 신설(`Tag`와 동형 array-part 값 객체, `Merged`로 헤테로지니어스 Store 합성), 이름 충돌 방지로 단일 키를 `AttributeKey`로 리네임(잠정). **[같은 세션 후속]** `AttributeKey(name)`이 이름별 weak 캐시로 동등성 보장하도록 확정되며, 그룹 Handler는 자기 완결형 재구현 대신 메모이즈된 키로 기존 단일 키 경로에 재귀 위임하는 걸로 개정(중복 구현 제거). **[2026-08-12 열 번째 세션]** 그룹/직접 쓰기가 같은 이름을 동시에 관리하는 충돌을 막기 위해 그룹은 공개 캐시 대신 `rawNew(name)` 전용 키+소유권 `Relate`로 전환. **[열한 번째 세션]** `retract`가 store 재발행마다 항상 불린다는 정정에 맞춰 `AttributeKeyHandler.retract`를 손봄(이 시점엔 `v==nil` 가드 버전 — 아래 열여섯 번째 세션에서 최종 재정정됨), 그룹의 "남아있는 이름" 위임도 매번 `retractUnder`를 먼저 부르도록 정정(체인 누수 방지). **[2026-08-12 열여섯 번째 세션, 최종 재정정]** `retract`는 완전 no-op으로 굳어짐(`SetAttribute`는 오직 `process(inst,k,nil)`에서만) — Attribute는 명시적 `None`/`nil`로만 지워지고, 그룹 diff나 컴포넌트 언마운트로 이름이 조용히 사라져도 값은 자동으로 안 지워짐(`Ref`의 "Destroy 무관, 정리는 명시적으로" 철학과 통일), 단 사라진 이름의 *구독*은 끊어 자원 누수는 막음 — 위 "v==nil 가드" 버전은 이걸로 폐기. **[2026-08-13 세션, 전면 재정정]** `rawNew`+`owners` 수동 레지스트리 방식이 "그룹이 이름을 놓았다 다시 포함하면 자기 자신과 충돌"하는 실제 버그로 확인됨 — `AttributeGroupKeyHandler`라는 `isHandlable` 없는 순수 체크포인트 핸들러를 `Dispatch.processAs`로 명시 push하고 `Dispatch.retractSelfAndUnder`로 통째 철거하는 방식으로 전면 재설계, 소유권 충돌 감지도 별도 레지스트리 없이 기존 재진입 가드가 대신 잡아줌(`bind-system-plan.md` 참고). `AttributeKeyHandler`는 다시 완전 무상태로 단순화됨. **[2026-08-13 세션, 다섯 번째, 전면 재설계 — 체크포인트조차 불필요해짐]** `Dispatch`가 인덱스 기반으로 재설계되며 `AttributeGroupKeyHandler`/`processAs`/`retractSelfAndUnder`를 전부 걷어냄 — 그룹이 그냥 공개 `AttributeKey(name)`으로 항상 인덱스 1부터 `Dispatch.process`/`retractFrom`을 직접 부르면 끝(점유 체크 자체가 소유권 충돌 감지), `groupState` Relate도 필요 없어짐(반환 클로저가 이름 집합을 직접 캡처) — 중간 버전은 `archive/checkpoint-handler-pattern-reversed.md`. **[2026-08-13 감사, 정정]** 그런데 그 의사코드가 `process` 안에서 이름마다 `retractFrom(...,1,...)`을 먼저 부르고 있어 **인덱스 1이 무조건 비워지는 바람에 점유 체크가 전혀 작동하지 않았음**(그룹↔그룹 사이에서 조용한 last-write-wins가 그대로 남아 있었음) — `process`는 `Dispatch.process`만 부르고 철거는 반환 클로저가 자기가 등록한 이름 전부에 대해 하도록 정정. 그룹 Handler 시그니처가 계약과 안 맞던 것(`process(inst,index,v)` 3-인자)도 같이 수정 **[2026-08-13 열네 번째 세션, 0-Z 확정]** 그룹이 **자기 전용 키**(비공개 `GetKey`)로 위임하고 이름 소유권은 `AttributeKeyHandler`의 **이름 claim**(`nameClaims` Relate, 충돌 시 즉시 error)이 판정 — 하강 diff에선 두 그룹이 똑같이 `StoreBind`로 보여 점유 체크가 성립하지 않기 때문. 후보 (a)(그룹 안 claimant Relate)는 **그룹↔직접 쓰기를 못 잡아** 기각. 같은 세션에 **패키지 재배치**(값·알고리즘·단일 키 전부 quad-base, 백엔드는 `setAttribute(inst,name,v)`만 주입, 엔진 고유 타입 패밀리만 백엔드) |
|
||||
| `onchange-plan.md` | **[2026-08-10 세션 신설]** `OnChange(name)` — `GetPropertyChangedSignal` 바인딩 전용 DI 키, `Attribute`와 달리 제네릭 타입 파라미터 없음(콜백 타입은 인라인 명시, 이벤트 바인딩과 같은 급 트레이드오프). 전부 quad-roblox(`Handlers/OnChange.luau`), `State<function>`은 기존 이벤트 store-bind 메커니즘 재사용. **[2026-08-11 아홉 번째 세션 후속]** `AttributeKey`와 동일한 이름별 weak 캐시로 `OnChange(a) == OnChange(a)` 동등성 보장 |
|
||||
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘. **[2026-08-12 열세/열네 번째 세션]** 서로 다른 두 `Relate`가 서로의 키를 상대방 값으로 강하게 붙잡는 상호 순환 패턴 경고 신설 — Luau에 ephemeron 테이블이 없어(공식 확인, luau.org/compatibility) 이런 순환은 실제로 GC가 안 됨, `Slot`의 `kSlotMap`/`slotOwner`가 실제 사례이자 수정 사례 |
|
||||
| `ref-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Ref`/`PreRef` — 지연 없는 확정 값 박스. 용도 재정의(leaf 노드를 담는 용도로도, leaf 노드에 바인딩하는 용도로도), `.Value`+`:Set`/`:Callback`/`:Wait`(전부 self 반환), `Ref`의 retract가 `TagHandler`와 같은 `Relate` diff 패턴이라는 것, 이중 바인딩 금지(`canBound`), `PreRef` 호이스팅 pre-pass와 1회용 `_fired` 가드. **분리는 순수 이동 — 결정은 하나도 안 바뀜** **[2026-08-13 열네 번째 세션]** 하강 diff 반영(0-Z 배너 해소) — "이전 클로저가 언바인딩, 다음 `process`가 바인딩" 두 단계는 그대로이고 그걸 일으키는 주체만 `Dispatch.process`의 핸들러 선비교로 바뀜 |
|
||||
| `ref-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Ref`/`PreRef`/`PostRef` — 지연 없는 확정 값 박스. 용도 재정의(leaf 노드를 담는 용도로도, leaf 노드에 바인딩하는 용도로도), `.Value`+`:Set`/`:Callback`/`:Wait`(전부 self 반환), `Ref`의 retract가 `TagHandler`와 같은 `Relate` diff 패턴이라는 것, 이중 바인딩 금지(`canBound`), `PreRef` 호이스팅 pre-pass와 1회용 `_fired` 가드. **분리는 순수 이동 — 결정은 하나도 안 바뀜** **[2026-08-13 열네 번째 세션]** 하강 diff 반영(0-Z 배너 해소) — "이전 클로저가 언바인딩, 다음 `process`가 바인딩" 두 단계는 그대로이고 그걸 일으키는 주체만 `Dispatch.process`의 핸들러 선비교로 바뀜 **[2026-08-14 아홉 번째 세션]** `PostRef` 확정·편입 — `PreRef`의 거울상(같은 pre-pass가 수집만 하고 두 패스가 **전부 끝난 뒤** fire, `ProcessedPostRef` 센티널+전담 Handler까지 완전 대칭). 보장 범위는 "자기 서브트리 완성"이고 **자기가 부모에 붙는 것보다는 여전히 먼저**임에 주의. 같은 세션에 `PreRef`/`PostRef` **계열 안 fire 순서를 미보장으로 역전**(`archive/preref-order-guaranteed-reversed.md`) |
|
||||
| `event-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리 — 사용자가 직접 지목]** 이벤트 바인딩 — 핸들러가 self(Instance)를 **안** 받는다는 확정(Ref가 이미 커버, 이중 쓰기 경로 방지), 이벤트도 store-bind 가능하며 `false`를 넣으면 disconnect. 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 `bind-system-plan.md`에 남음, `GetPropertyChangedSignal`은 `onchange-plan.md`. **분리는 순수 이동** |
|
||||
| `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 10종 branded 타입 전부로 일반화. 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** |
|
||||
| `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 branded 타입 전부로 일반화(`isPostRef` 포함, 2026-08-14 아홉 번째 세션). 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** |
|
||||
| `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) |
|
||||
| `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary는 빈 자리 아님" 결론 위에 얹힘). `Fallback`은 `pcall` 기반(trace 없음), `Traceback`은 `xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 |
|
||||
| `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. `OnDestroyed` 이름만 `dispose()` 범위(0-B) 확정 시 재검토 여지(용어 대기열 3순위) |
|
||||
|
||||
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)
|
||||
|
||||
|
|
@ -73,7 +74,6 @@
|
|||
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
||||
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 |
|
||||
| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리, `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 **[2026-08-13 여섯 번째 세션]** `State<State<T>|T>` → `State<T>` 평탄화 항목 신설(백로그) — `State<State<T>>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 |
|
||||
| `lifecycle-hooks-plan.md` | **[2026-08-14 신설]** `OnCreated`/`OnDestroyed` 생명주기 훅 — `OnCreated(fn)`은 `PreRef():Callback(fn)`, `OnDestroyed(fn)`은 `Effect(function() return fn end)`를 반환하는 순수 팩토리 함수라 새 타입/Dispatch 메커니즘이 전혀 필요 없음(호출 즉시 평가돼 기존 `PreRef`/`EffectHandle` 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원. `OnRendered`는 현재 base에 없는 post-pass가 실제로 필요해 공짜가 아니라 **지금은 의도적으로 구현 안 함** — 거울상 `PostRef` 스케치(`PreRef`의 pre-pass와 대칭인 post-pass)만 백로그 후보로 남김 | 하 — 형제 백로그(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)와 동급, "quad 개발 상당 부분 끝난 뒤" |
|
||||
| `debounce-throttle-plan.md` | **[2026-08-14 신설]** 시간 기반 전파 게이트 `Debounce`/`Throttle` — 사용자 요청("`Blocker`와 유사하게")으로 신설. 요지: (1) `Blocker`가 이미 쓰는 게이트 노드의 **릴리스 트리거만 타이머로 바꾼 것**이라 새 전파 메커니즘이 아님 — `Blocker` 구현(M3) 시점에 게이트를 공용으로 빼두는 게 쌈, (2) 무효화 채널만 만지므로 laziness 안 깨짐, (3) **Debounce/Throttle의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐** — 공개 생성자는 둘, 구현은 하나(초안의 lodash식 `maxWait` 공식엔 trailing 통과 직후 이중 발화 버그가 있었음), (4) 알고리즘은 quad-base + 주입 op 2개 `setTimeout(func, delay) -> Timeout`/`clearTimeout`(Roblox `task.delay`/`task.cancel`로 배선 — **인자 순서 반대라 주의**). `os.clock()`은 Luau 표준 라이브러리라 주입 대상 아님(단 절대 시각이 아니라 **diff 전용**), 취소 없는 엔진도 래핑+유효 플래그로 대응 가능, `Timeout`은 `{ __type_timeout: true, _native: any }`. **부수 성과**: 이 설계 중 `source-state-plan.md`의 무효화 dedup 서술이 `Observer` 계약과 모순되는 게 발견돼 base 전면 정정(`archive/invalidate-dedup-propagation-reversed.md`) | 하 — M0 안 막음(코어 계약 변경 없음), 다만 순수 슈가가 아니라 실제 기능 갭이라 `operator-sugar-plan.md`보다는 위. 의존은 M3(State)+백엔드 주입. 남은 열린 질문: 이름(Roblox 관용 debounce와 충돌)/값 지연 의미론/제어 핸들 `Flush`/`Time=0` 허용 |
|
||||
| `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 코어 구현 시점까지 미결 |
|
||||
|
||||
|
|
@ -83,6 +83,7 @@
|
|||
|---|---|
|
||||
| `store-source-proxy-reversed.md` | [역전됨] 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 |
|
||||
| `ref-phase-option-reversed.md` | [역전됨] `CreatedRef`의 `phase` 옵션 — 위치 기반 순서 + `PreRef` 신설로 대체됨 |
|
||||
| `preref-order-guaranteed-reversed.md` | **[역전됨, 2026-08-14 아홉 번째 세션 신설]** 복수 `PreRef` 간 fire 순서 "배열 index 순서 그대로 보장"(2026-08-07 아홉 번째 세션) → **미보장**. 구현은 그대로이고 계약만 좁힌 것 — "같은 계열 내 등록 순서에 의존하는 코드가 생겨선 안 된다"는 사용자 판단, `PostRef`도 같은 규칙 |
|
||||
| `ui-shorthand-roundsize-dropped.md` | **[기각됨, 2026-08-07 신설]** v1 `RoundSize`(이미지 9-slice 라운드 트릭) — 네이티브 `UICorner`로 대체되어 포팅 불필요. 이 판단이 한 차례 "Corner/PaddingAll/Scale 숏핸드 전체가 불필요하다"로 과잉일반화됐다가 정정된 이력 포함 |
|
||||
| `batch-rejected.md` | **[기각됨, 2026-08-07 신설]** lexical `Batch(fn)` — 코루틴 yield 위에서 구조적으로 위험해 기각, 값 기반 `Blocker`(`base/blocker-plan.md`)로 대체 |
|
||||
| `context-rejected.md` | **[기각됨, 2026-08-07 신설]** `Context`(트리 하위 암묵 전파) + 대안이던 레이어드 Store 둘 다 기각 — 명시적 타입 강제 Store 전달로 충분하다는 판단 |
|
||||
|
|
|
|||
63
.claude/archive/preref-order-guaranteed-reversed.md
Normal file
63
.claude/archive/preref-order-guaranteed-reversed.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# [역전됨] 복수 `PreRef` 간 fire 순서 = "배열 index 순서 그대로 보장" → 미보장
|
||||
|
||||
**역전 일자**: 2026-08-14 아홉 번째 세션 (원 결정: 2026-08-07 아홉 번째
|
||||
세션). **현재 유효한 계약은 `base/ref-plan.md`의 "`phase` 옵션 폐기 →
|
||||
위치로 표현, `PreRef` 신설" 절**(복수 `PreRef` 간 순서 항목)과 같은
|
||||
문서의 "`PostRef`" 절 — 이 파일은 히스토리 보존용이라 여기 적힌 걸
|
||||
근거로 코드를 짜지 말 것.
|
||||
|
||||
## 원문 (2026-08-07 아홉 번째 세션 확정, `bind-system-plan.md` → `ref-plan.md`)
|
||||
|
||||
> - **복수 `PreRef` 간 순서(2026-08-07 아홉 번째 세션, 사용자 확인) —
|
||||
> 새 규칙 불필요, 배열 index 순서 그대로.** 같은 인스턴스에 `PreRef`가
|
||||
> 여럿 있으면, 이 pre-pass는 위 "props 순회 순서" 절이 이미 확정해둔
|
||||
> "배열 파트는 index 순서대로" 계약을 그대로 재사용해 리터럴 순서대로
|
||||
> fire하면 됨 — 서로 다른 우선순위/순서 개념을 별도로 만들 필요 없음
|
||||
> (호이스팅은 "PreRef 전체 대 나머지"에만 적용되는 규칙이지, "PreRef끼리"
|
||||
> 에는 적용될 게 없음 — PreRef끼리는 그냥 평범한 배열 순회).
|
||||
|
||||
## 무엇이 뒤집혔나
|
||||
|
||||
**구현이 아니라 계약이 뒤집힘.** pre-pass가 배열을 index 순서로 훑는다는
|
||||
구현은 그대로이고, 실제 fire 순서도 사실상 리터럴 순서일 것임 — 바뀐 건
|
||||
그걸 **사용자에게 보장하느냐**임.
|
||||
|
||||
| | 역전 전 | 현재 |
|
||||
|---|---|---|
|
||||
| `PreRef`끼리의 상대 순서 | **보장**(배열 index 순서) | **보장 안 함** |
|
||||
| `PostRef`끼리의 상대 순서 | (당시 `PostRef` 없음) | **보장 안 함** |
|
||||
| 호이스팅("`PreRef` 전체가 나머지보다 먼저") | 보장 | 보장(**변경 없음**) |
|
||||
| `PostRef`("두 패스가 전부 끝난 뒤") | (없음) | 보장 |
|
||||
|
||||
## 역전 이유 (사용자)
|
||||
|
||||
> "PreRef는 프로퍼티/자식 들어오기 전에 설정됨. 각 PreRef 간의 순서는
|
||||
> 보장 안함. PostRef는 프로퍼티/자식 들어온 후 설정됨, PostRef간 순서는
|
||||
> 보장 안함(실제로 하더라도, 안 한다고 두는게 버그를 차라리 덜 만든다고
|
||||
> 생각함. 같은 계열 내 Ref 등록 순서에 의존하는 무언가가 생겨선 안된다는
|
||||
> 생각, 그건 구조부터 잘못된거니까)"
|
||||
|
||||
정리하면 셋:
|
||||
|
||||
1. **순서에 의존하는 코드는 애초에 설계가 잘못된 것** — 두 훅의 상대
|
||||
순서가 중요할 정도면 훅을 둘로 나눌 게 아니라 하나 안에서 순서대로
|
||||
부르면 됨. 보장을 제공하면 그 잘못된 구조를 오히려 권장하게 됨.
|
||||
2. **보장하지 않으면 구현을 바꿀 자유가 남음** — pre-pass의 순회 방식을
|
||||
나중에 바꿔도(예: 최적화, 백엔드별 자료구조 차이) 계약 위반이 아님.
|
||||
`base/dispatch-core-plan.md`가 "다른 백엔드가 props를 Lua 테이블이
|
||||
아닌 자료구조로 표현할 수 있다"는 이유로 두 패스 순서를 **명시적
|
||||
계약**으로 고정했던 것과 같은 결의 판단 — 거기선 보장이 필요해서
|
||||
고정했고, 여기선 필요 없어서 안 고정함.
|
||||
3. **버그가 덜 생김** — 보장이 없으면 그 순서에 기대는 사용자 코드가
|
||||
애초에 안 생김.
|
||||
|
||||
## 영향 범위
|
||||
|
||||
- `base/ref-plan.md` — 해당 bullet 교체(역전 표시 + 이유), 새 "`PostRef`"
|
||||
절도 같은 규칙으로 서술.
|
||||
- `base/lifecycle-hooks-plan.md` — "다중 등록 가능" 절에 "같은 계열끼리
|
||||
순서 미보장" 경고 추가, ② 절의 "복수 `PostRef` 간 순서는 복수 `PreRef`와
|
||||
같은 원칙(배열 index 순서 그대로)" 문장 정정.
|
||||
- `ROADMAP.md` M8 — "복수 `PreRef`는 배열 index 순서 그대로(별도 규칙
|
||||
없음)" 체크리스트 문구 정정.
|
||||
- 코드 영향 없음(구현이 안 바뀌므로) — 바뀐 건 문서화 대상 계약뿐.
|
||||
|
|
@ -167,7 +167,7 @@ quad/
|
|||
│ │ ├── init.luau # process 엔진 — `chains`(inst,k별 인덱스 배열, 슬롯마다 {handler, retractor}) + 하강 diff(핸들러가 같으면 그 자리 클로저에 새 값을 넘기고 재process, 다르면 그 자리부터 retractFrom) + 3-인자 `retractFrom(inst,k,index)` (`dispatch-core-plan.md` "Dispatch 체인" 절, 2026-08-08 신설 → 2026-08-13 다섯 번째 세션 인덱스화 → 같은 날 열네 번째 세션 하강 diff)
|
||||
│ │ ├── Handler.luau # 핸들러 계약 타입(isHandlable/priority/process — process가 자기 retract 클로저를 반환)
|
||||
│ │ ├── StoreBind.luau # store 값 재귀 재실행 로직(범용, 엔진 무관)
|
||||
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/PreRef) children-array leaf 매칭 Handler, StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
||||
│ │ ├── Leaf.luau # (i:number, v=Ref/Observer/PreRef/PostRef) children-array leaf 매칭 Handler(일반 Ref 매치는 `isRef(v) and not isPreRef(v) and not isPostRef(v)`), StoreBind와 같은 층위(범용/엔진무관, 2026-08-08 두 번째 세션 확정)
|
||||
│ │ ├── Tag.luau # TagHandler — 이름별 참조 카운트(`tagNameMap`), 실제 호출은 주입된 addTag/removeTag(inst, {string}). HANDLER_PRIORITY_FALLBACK으로 등록(`base/tag-plan.md`, 2026-08-13 열네 번째 세션 base로 이동)
|
||||
│ │ ├── AttributeKey.luau # AttributeKeyHandler — 이름 claim(`nameClaims`, 소유권 충돌 즉시 error) + 주입된 setAttribute(inst,name,v) 호출, `None`→nil은 재디스패치로 자동(`base/attribute-plan.md` "이름 소유권" 절)
|
||||
│ │ ├── Attribute.luau # AttributeGroupHandler — 그룹 전용 키(비공개 GetKey)로 이름마다 AttributeKey 경로에 인덱스 1 위임, 클로저가 자기 키 전부 retractFrom(`base/attribute-plan.md` "메커니즘" 절)
|
||||
|
|
@ -176,6 +176,8 @@ quad/
|
|||
│ ├── LifetimeHandle.luau # `bindLifetime(inst,value)`/`unbindLifetime(value)`/`canExecute(value)` 탑레벨 함수 "인터페이스"(타입/계약만), 내부는 Relate 사용(`base/lifecycle-pattern.md`)
|
||||
│ ├── Ref.luau # 범용 값 박스(.Value 읽기 + :Set()/:Callback()/:Wait() 셋), `Ref(default)`를 children 배열 숫자 슬롯에 직접 놓으면 (v=Ref) 매치 핸들러가 바인드 — 별도 CreatedRef 래퍼 없음
|
||||
│ ├── PreRef.luau # Ref 런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단, 호이스팅되는 pre-pass 특수화(별도 파일, `ref-plan.md` "PreRef 신설" 절, 2026-08-07 여섯 번째 세션에서 분리)
|
||||
│ ├── PostRef.luau # PreRef의 거울상 — 같은 Ref 런타임/제약, 같은 pre-pass가 수집만 하고 두 패스가 전부 끝난 뒤 fire(`ref-plan.md` "`PostRef`" 절, 2026-08-14 아홉 번째 세션 확정)
|
||||
│ ├── LifecycleHooks.luau # OnCreated/OnRendered/OnDestroyed — PreRef/PostRef/Effect를 반환하는 순수 팩토리 슈가(`base/lifecycle-hooks-plan.md`), 새 타입/Dispatch 개념 없음
|
||||
│ └── init.luau
|
||||
└── quad-roblox/
|
||||
├── wally.toml
|
||||
|
|
@ -212,7 +214,7 @@ existing-instance-bind는 **[2026-08-14 세션] 기각되어 `archive/`로
|
|||
프리미티브 타입 자신의 공개 어휘**라는 것:
|
||||
1. 프리미티브 타입 생성자, `Type(args)` 스타일: `Source(default)`/
|
||||
`Ref(default)`/`Store({defaults})`/`Modifier()`/`Relate()`/
|
||||
`Effect(fn, state?)`/`PreRef(default)`.
|
||||
`Effect(fn, state?)`/`PreRef(default)`/`PostRef(default)`.
|
||||
2. 그 인스턴스의 콜론 메서드: `state:Get()`/`:With(...)`/`:Compute(fn)`/
|
||||
`:Observer(fn)`/`:Apply(factory)`/`:Peek(key)`, `source:Set(v)`/`:Emit()`,
|
||||
`ref:Set(v)`/`:Callback(fn)`/`:Wait(thread?)`, `observer:Subscribe()`/
|
||||
|
|
@ -225,7 +227,7 @@ existing-instance-bind는 **[2026-08-14 세션] 기각되어 `archive/`로
|
|||
`Modifier()` 생성자와 같은 이유로 대문자.
|
||||
- **소문자 시작(camelCase)** — 특정 프리미티브 타입 하나에 안 묶이고 여러
|
||||
타입을 넘나드는 범용 유틸(`isState`/`isSource`/`isRef`/`isPreRef`/
|
||||
`isModifier`/`isObserver`/... `Brand` 절), 생명주기 게이트(`canExecute`/
|
||||
`isPostRef`/`isModifier`/`isObserver`/... `Brand` 절), 생명주기 게이트(`canExecute`/
|
||||
`bindLifetime`, `base/lifecycle-pattern.md`), 그리고 **프리미티브가
|
||||
아닌** 내부 엔진/레지스트리의 네임스페이스 멤버(`Dispatch.process`/
|
||||
`getHandler`/`addHandler`/`drive`, `Brand.set`/`get`) — 이 셋은 "타입
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ Signal 미채택, Ref 역할)과 소스 트리 상 패키지 경계(디스패치
|
|||
*네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 아래 "인스턴스 생성 /
|
||||
이벤트 네이밍 인체공학" 절에 그대로 있음.
|
||||
- **`Brand`** — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`,
|
||||
`isState`를 10종 branded 타입 전부로 일반화) → **`base/brand-plan.md`**.
|
||||
`isState`를 branded 타입 전부로 일반화) → **`base/brand-plan.md`**.
|
||||
- **`Tag` / `Attribute` 특수 키** → **`base/tag-plan.md`** /
|
||||
**`base/attribute-plan.md`**. 이 문서가 예전에 다루던 타입 파라미터화 문제
|
||||
(`[AttributeKey<<boolean>> "name"]`(구 `Attribute<<boolean>>`) vs
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ function Brand.get(x) return registry[x] end -- nil이면 quad가 모르는 값
|
|||
|
||||
-- 각 브랜드는 고유 테이블(빈 테이블이어도 됨) — 문자열 리터럴 아님
|
||||
local ObserverTag, EffectTag, TagTag, AttributeTag, TweenTag, BlockerTag,
|
||||
StateTag, SourceTag, StoreTag, SlotTag, RefTag, PreRefTag, ModifierTag =
|
||||
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
|
||||
StateTag, SourceTag, StoreTag, SlotTag, RefTag, PreRefTag, PostRefTag,
|
||||
ModifierTag =
|
||||
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
|
||||
|
||||
-- 각 타입의 모든 생성 지점(Observer(...), Source(...), :With(...), Tag(...) 등)에서:
|
||||
Brand.set(newHandle, ObserverTag)
|
||||
|
|
@ -68,8 +69,12 @@ end
|
|||
local function isPreRef(x)
|
||||
return Brand.get(x) == PreRefTag
|
||||
end
|
||||
local function isPostRef(x) -- [2026-08-14 아홉 번째 세션] PostRef 확정
|
||||
return Brand.get(x) == PostRefTag
|
||||
end
|
||||
local function isRef(x)
|
||||
return isPreRef(x) or Brand.get(x) == RefTag -- PreRef가 Ref 런타임을 재사용 = Ref의 한 종류
|
||||
-- PreRef/PostRef가 Ref 런타임을 재사용 = 둘 다 Ref의 한 종류
|
||||
return isPreRef(x) or isPostRef(x) or Brand.get(x) == RefTag
|
||||
end
|
||||
```
|
||||
|
||||
|
|
@ -97,13 +102,20 @@ end
|
|||
PreRefTag`), **`isRef(x)`는 그 위에 `Brand.get(x)==RefTag`를 OR로
|
||||
얹은 상위 개념** — 즉 이제 **`isRef(preRefInstance)`는 `true`.**
|
||||
- **`(v=Ref)` children 배열 leaf 매치 핸들러(`Dispatch/Leaf.luau`)는
|
||||
이제 `isHandlable`을 `isRef(v) and not isPreRef(v)`로 명시적으로
|
||||
좁혀야 함** — 예전처럼 `isRef` 자체가 배타적이라 저절로 걸러지는 게
|
||||
아니라, "Ref이긴 한데 그 중 PreRef는 아니다"를 호출부가 명시적으로
|
||||
말해야 하는 모양으로 바뀜(PreRef pre-pass가 이미 소진시켜 정상 경로에선
|
||||
거의 안 걸리지만, 위 "PreRef" 절의 동적 경로 가드 Handler와 이 조합이
|
||||
같이 "일반 Ref 경로를 절대 타면 안 됨"을 보장). `isModifier`도 같은
|
||||
단순 항등(`Brand.get(x) == ModifierTag`, 상위 개념 없음).
|
||||
이제 `isHandlable`을 `isRef(v) and not isPreRef(v) and not isPostRef(v)`로
|
||||
명시적으로 좁혀야 함**(**[2026-08-14 아홉 번째 세션]** `PostRef` 확정으로
|
||||
제외 항이 하나 늘어남) — 예전처럼 `isRef` 자체가 배타적이라 저절로
|
||||
걸러지는 게 아니라, "Ref이긴 한데 그 중 Pre/Post는 아니다"를 호출부가
|
||||
명시적으로 말해야 하는 모양으로 바뀜(두 pre-pass 소진이 이미 걸러줘
|
||||
정상 경로에선 거의 안 걸리지만, `base/ref-plan.md`의 두 동적 경로 가드
|
||||
Handler와 이 조합이 같이 "일반 Ref 경로를 절대 타면 안 됨"을 보장).
|
||||
`isModifier`도 같은 단순 항등(`Brand.get(x) == ModifierTag`, 상위 개념
|
||||
없음).
|
||||
- **`PostRef`도 `PreRef`와 완전히 같은 포함 방향** — `Ref` 런타임을 그대로
|
||||
재사용하고 브랜드 태그만 다르므로 `isRef(postRefInstance)`도 `true`.
|
||||
즉 `isRef`는 이제 `{Ref, PreRef, PostRef}` 셋을 통과시키는 상위 개념이고,
|
||||
`isPreRef`/`isPostRef`가 각각 가장 구체적인 항등 — `PreRef`/`PostRef`
|
||||
사이엔 포함 관계가 없음(서로 배타적인 형제).
|
||||
|
||||
**같은 이유로 `isSlot`/`isEffect`도 명시(2026-08-09 세션)** —
|
||||
`Brand.get(x) == SlotTag`/`Brand.get(x) == EffectTag`인 단순 항등
|
||||
|
|
|
|||
|
|
@ -365,7 +365,11 @@ end
|
|||
pre-pass가 소진시킨 자리"는 이 규칙의 예가 아님** — 그 자리는 `None`이
|
||||
아니라 별도 센티널 `ProcessedPreRef`로 소진되고, `ProcessedPreRefHandler`
|
||||
(`base/ref-plan.md`의 "PreRef" 절)를 통해 정상 `Dispatch.process`
|
||||
경로를 그대로 탐(아래 "Length/Offset" 절 참고) — 예전엔 이 둘(원래부터
|
||||
경로를 그대로 탐(아래 "Length/Offset" 절 참고). **[2026-08-14 아홉 번째
|
||||
세션] `PostRef`가 소진시킨 자리(`ProcessedPostRef`)도 완전히 같은 취급**
|
||||
— 전용 센티널 + 전용 `ProcessedPostRefHandler`(`base/ref-plan.md`의
|
||||
"`PostRef`" 절), 즉 "정말 빈 자리인 `None`"만 두 패스 루프가 직접
|
||||
건너뜀 — 예전엔 이 둘(원래부터
|
||||
빈 자리 vs 한때 PreRef였다가 소진된 자리)이 똑같이 `None`으로 뭉뚱그려져
|
||||
`setLength`/`setOffsetSource` 등록 책임 소재가 불분명한 갭이 있었음
|
||||
(2026-08-14 첫 번째 세션 조사에서 발견), 지금은 서로 다른 센티널로
|
||||
|
|
@ -411,6 +415,13 @@ end
|
|||
flatten된 props 테이블을 받아 배열 파트(children/Ref) 먼저, 해시
|
||||
파트(프로퍼티/이벤트) 나중으로 두 패스 순회하며 각 `(k,v)`에
|
||||
`Dispatch.process(inst, k, v, 1)`을 호출하는 게 이 함수의 본체.
|
||||
**[2026-08-14 아홉 번째 세션] 이 두 패스 앞뒤에 `Ref` 계열 훅 처리가
|
||||
붙음** — 앞에는 `PreRef`/`PostRef`를 한 번에 훑는 pre-pass(`PreRef`는
|
||||
그 자리에서 fire, `PostRef`는 이 호출에만 로컬인 `postRefList`에 적재만
|
||||
하고 둘 다 전용 센티널로 소진), 뒤에는 그 `postRefList`를 순회하며 각
|
||||
`PostRef`를 fire하는 짧은 루프. 둘 다 배열 재순회가 아니라 pre-pass
|
||||
하나 + 실제 `PostRef` 개수만큼의 목록 순회라 비용이 작음 — 상세는
|
||||
`base/ref-plan.md`의 "`PostRef`" 절.
|
||||
**진입 인덱스는 항상 `1`**(2026-08-13 감사에서 명시화 — 인덱스 도입
|
||||
후에도 이 자리만 인자가 안 적혀 있었음) — `drive`는 그 키의 체인을
|
||||
처음 여는 자리이므로 "다른 키로 위임할 때는 그 키의 재귀 깊이와
|
||||
|
|
@ -959,6 +970,9 @@ Dispatch.setOffsetSource(inst, i, offset: Source<number> | None)
|
|||
ref-plan.md`의 "PreRef" 절)가 정상 매치 과정에서 직접 `setLength(0)`/
|
||||
`setOffsetSource(None)`을 등록** — "이 위치를 처음 매치한 Handler가
|
||||
등록 책임을 진다"는 위 원칙을 특수 취급 없이 그대로 만족.
|
||||
**[2026-08-14 아홉 번째 세션] `PostRef` 소진 자리도 동일** —
|
||||
`ProcessedPostRefHandler`(`base/ref-plan.md`의 "`PostRef`" 절)가 같은
|
||||
두 등록을 하는 거울상 Handler라, 새 규칙 없이 그대로 맞물림.
|
||||
|
||||
**해제(그 자리가 더 이상 기여하지 않게 될 때)는 `setOffsetSource(...,None)`
|
||||
→ `setLength(...,0)` 순서로 (2026-08-13 여섯 번째 세션, 사용자 지적).**
|
||||
|
|
@ -988,8 +1002,9 @@ lazy 생성.
|
|||
`nil`을 넣으면 (1) 그 자리가 "안 채워짐"과 구별이 안 되고 (2) 배열이
|
||||
구멍 나면서 순수 array 취급이 깨져 접근 비용이 올라감(해시 파트로 밀림)
|
||||
— `None`은 실재하는 값이라 자리를 "채워짐"으로 유지시켜줌, `flattened`
|
||||
배열이 진짜 빈 자리(`None`, 예: `props.Ref or None`)와 PreRef pre-pass
|
||||
소진 자리(`ProcessedPreRef`, 2026-08-14 두 번째 세션 이전엔 여기도 `None`)
|
||||
배열이 진짜 빈 자리(`None`, 예: `props.Ref or None`)와 pre-pass 소진
|
||||
자리(`ProcessedPreRef`/`ProcessedPostRef`, 2026-08-14 두 번째 세션
|
||||
이전엔 여기도 `None`)
|
||||
둘 다 실재하는 센티널로 채워 구멍을 피하는 것과 같은 원칙(`ref-plan.md`의
|
||||
"Ref 일반화" 절 "왜 `None`이 아니라 `nil`인가" 참고 — **단, 그 절에서
|
||||
최종적으로 `nil`로 되돌아간 건 Ref 콜백/대기자 배열 한정**이고
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
# 생명주기 훅 슈가 — `OnCreated`/`OnDestroyed`(+백로그 후보 `OnRendered`/`PostRef`)
|
||||
# 생명주기 훅 슈가 — `OnCreated` / `OnRendered` / `OnDestroyed`
|
||||
|
||||
**상태**: research — 사용자 제안(2026-08-14 세션)으로 신설, 착수 전
|
||||
백로그. 이미 확정된 `PreRef`/`Ref`(`base/ref-plan.md`)와
|
||||
`Effect`(`base/effect-plan.md`) 프리미티브 위에 얹는 순수 슈가 후보 —
|
||||
> **[2026-08-14 아홉 번째 세션] `research/` → `base/` 승격.** 마지막 열린
|
||||
> 항목이던 `OnRendered`의 채택 여부/메커니즘을 사용자가 확정 — **채택**,
|
||||
> 메커니즘은 아래 ② 절의 `PostRef`(착수 선택지 (a)). `PostRef` 자체는
|
||||
> `base/ref-plan.md`의 "`PostRef`" 절로 편입되어 **base 프리미티브로
|
||||
> 확정**됐고, 이 문서는 그 위에 얹히는 훅 슈가 셋을 다룸.
|
||||
|
||||
**상태**: base — 확정. **[2026-08-14 아홉 번째 세션 기준]** 설계에 열린
|
||||
질문 없음(이름 재검토 여지 하나만
|
||||
`question.md` 용어 정리 대기열에 남음, 아래 "이름 컨벤션" 절).
|
||||
`Ref`/`PreRef`/`PostRef`(`base/ref-plan.md`)와
|
||||
`Effect`(`base/effect-plan.md`) 프리미티브 위에 얹는 **순수 슈가** —
|
||||
`base/fallback-plan.md`의 `Fallback`/`Traceback`이
|
||||
`additional-primitives-plan.md`의 기존 결론 위에 얹혔던 것과 같은 관계,
|
||||
이 문서도 그 프리미티브들의 확정 사항을 하나도 안 뒤집음. 우선순위는 그
|
||||
형제 백로그들(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)과 동급 —
|
||||
"quad 개발 상당 부분 끝난 뒤"로 볼 것.
|
||||
이 문서도 그 프리미티브들의 확정 사항을 하나도 안 뒤집음.
|
||||
|
||||
**구현 우선순위는 여전히 맨 뒤** — 설계가 확정됐다는 것과 지금 만든다는
|
||||
건 다름. 형제 백로그들(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)과
|
||||
동급으로 "quad 개발 상당 부분 끝난 뒤". 단 **`PostRef` 자신은 슈가가
|
||||
아니라 디스패치 코어의 일부**라 `ROADMAP.md` M8(Ref)에서 `PreRef`와 같이
|
||||
구현됨 — 이 문서의 슈가 셋만 뒤로 미뤄지는 것.
|
||||
|
||||
## 동기 (사용자 원 메모)
|
||||
|
||||
|
|
@ -21,15 +33,19 @@ React/Vue류 프레임워크의 `OnCreated`/`OnRendered`/`OnDisposed` 생명주
|
|||
`OnCreated`/`OnDestroyed` 둘**로 확정되고(`OnDisposed`라는 최초 가칭
|
||||
대신 `OnDestroyed`를 사용자가 선호 — 아래 "이름 컨벤션" 절 참고), 두
|
||||
훅 모두 **여러 번 나란히 등록 가능**하다는 게 이 제안의 특징으로
|
||||
명시됐음. `OnRendered`는 사용자가 **지금은 의도적으로 구현 안 하기로
|
||||
확정**(디스패치 코어에 실제 post-pass가 필요해 공짜가 아니라서) —
|
||||
다만 나중에 만들 때 `PreRef`의 거울상인 `PostRef`로 구현하면 될
|
||||
것 같다는 구체 스케치를 남겼고(아래 ② 절), 이 문서는 그 스케치를
|
||||
백로그 후보로 보존하는 용도.
|
||||
명시됐음. `OnRendered`는 처음엔 사용자가 **의도적으로 보류**했었으나
|
||||
(디스패치 코어에 실제 post-pass가 필요해 "공짜"가 아니라서), 그때 같이
|
||||
남겨둔 `PostRef` 스케치가 **"구현 난이도가 아주 낮고 Pre-Post 둘을 지원
|
||||
안 할 이유가 없다"**는 판단으로 2026-08-14 아홉 번째 세션에 **채택**됨
|
||||
— 아래 ② 절이 그 확정 내용(원래 "착수 시점에 판단할 선택지"였던
|
||||
(a)/(b)/(c) 중 **(a)** 확정).
|
||||
|
||||
## 핵심 논지 — 이 셋은 사실 새로운 타입/개념이 아니다
|
||||
|
||||
`OnCreated`/`OnDestroyed`가 "정말 공짜"인 이유는 단 하나로 귀결됨:
|
||||
`OnCreated`/`OnRendered`/`OnDestroyed`가 "정말 공짜"인 이유는 단 하나로
|
||||
귀결됨(`OnRendered`는 자기가 반환하는 `PostRef`라는 프리미티브가 base에
|
||||
생긴 뒤부터 — 그 프리미티브 자체는 공짜가 아니었고, 그래서 별도로 확정을
|
||||
받아야 했음):
|
||||
**이것들은 Dispatch/Brand/타입 시스템이 알아야 하는 새 개념이 아니라,
|
||||
호출되는 즉시 평가되어 이미 존재하는 프리미티브의 인스턴스로 사라지는
|
||||
plain 함수일 뿐**이기 때문.
|
||||
|
|
@ -39,15 +55,20 @@ local function OnCreated(fn: (inst: Instance) -> ()): PreRef<Instance>
|
|||
return PreRef():Callback(fn)
|
||||
end
|
||||
|
||||
local function OnRendered(fn: (inst: Instance) -> ()): PostRef<Instance>
|
||||
return PostRef():Callback(fn) -- [2026-08-14 아홉 번째 세션 확정]
|
||||
end
|
||||
|
||||
local function OnDestroyed(fn: () -> ()): EffectHandle
|
||||
return Effect(function() return fn end)
|
||||
end
|
||||
```
|
||||
|
||||
호출 즉시 `PreRef():Callback(fn)`/`Effect(...)`가 실행되고, children
|
||||
배열에 실제로 놓이는 건 **그 결과인 `PreRef`/`EffectHandle` 인스턴스
|
||||
자체**임 — `OnCreated`라는 이름이나 개념은 이 시점 이후 어디에도
|
||||
안 남음. `Dispatch`는 이미 아는 `(v=PreRef)`/`(v=EffectHandle)` 매치
|
||||
호출 즉시 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(...)`가
|
||||
실행되고, children 배열에 실제로 놓이는 건 **그 결과인 `PreRef`/`PostRef`/
|
||||
`EffectHandle` 인스턴스 자체**임 — `OnCreated`라는 이름이나 개념은 이
|
||||
시점 이후 어디에도
|
||||
안 남음. `Dispatch`는 이미 아는 `(v=PreRef)`/`(v=PostRef)`/`(v=EffectHandle)` 매치
|
||||
핸들러로 정확히 똑같이 처리하고, 새 브랜드 태그조차 필요 없음(이미
|
||||
존재하는 `Brand`로 그대로 식별됨).
|
||||
|
||||
|
|
@ -63,7 +84,7 @@ end
|
|||
"이 값이 언제/어떻게 디스패치되는가"의 문제인데, 팩토리 접근은 애초에
|
||||
디스패치될 "값"을 안 만들고 곧장 "결과 객체"를 만들어버림.
|
||||
|
||||
## ① 이미 거의 확정적인 슈가 둘
|
||||
## ① 확정된 슈가 셋
|
||||
|
||||
### `OnCreated(fn)`
|
||||
|
||||
|
|
@ -74,7 +95,8 @@ end
|
|||
불림. 새 Dispatch 메커니즘 불필요, `PreRef` 그대로 재사용.
|
||||
|
||||
**v1과의 관계 — 이름이 같아 보여도 메커니즘은 다름.** `base/ref-plan.md`
|
||||
510~513행에 이미 이렇게 확정돼 있음:
|
||||
"`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절에 이미 이렇게 확정돼
|
||||
있음:
|
||||
|
||||
> quad v1의 `OnCreated` 특수 DI 키는 이식하지 않는다.
|
||||
> `Ref():Callback(function(inst) end)`를 children 배열에 넣는 것만으로
|
||||
|
|
@ -89,6 +111,21 @@ end
|
|||
모순이 아니라 그 결론의 자연스러운 재포장임. 이름이 v1과 같아 헷갈릴
|
||||
수 있다는 점만 "이름 컨벤션" 절에서 별도로 짚음.
|
||||
|
||||
### `OnRendered(fn)` (2026-08-14 아홉 번째 세션 확정)
|
||||
|
||||
`PostRef():Callback(fn)`를 반환하는 순수 팩토리 — `OnCreated`와 완전히
|
||||
같은 패턴이고, 반환하는 프리미티브만 거울상. `PostRef`의 계약을 그대로
|
||||
물려받으므로(`base/ref-plan.md`의 "`PostRef`" 절):
|
||||
|
||||
- **불리는 시점**: 이 인스턴스의 children(과 그 서브트리 전체)과
|
||||
프로퍼티/이벤트가 **전부 세팅된 뒤**.
|
||||
- **⚠️ 불리지 *않는* 시점**: 이 인스턴스가 **부모에 붙은 뒤가 아님**.
|
||||
`PostRef`는 자기 아래(서브트리)의 완성만 보장하고 자기 위(조상 체인)는
|
||||
아직 없을 수 있음 — React `componentDidMount`가 DOM 삽입 **후**인 것과
|
||||
다르므로, 이름만 보고 "화면에 올라간 뒤"로 기대하지 않도록 **사용자
|
||||
문서에 반드시 명시**할 것(아래 "이름 컨벤션" 절도 참고).
|
||||
- 복수 `OnRendered` 간 상대 순서는 **보장 안 함**(`PostRef` 계약).
|
||||
|
||||
### `OnDestroyed(fn)`
|
||||
|
||||
`Effect(function() return fn end)`를 반환하는 팩토리. `Effect(fn, state?)`가
|
||||
|
|
@ -108,12 +145,20 @@ Dispatch/Effect 메커니즘 불필요, 기존 계약 재사용만으로 정확
|
|||
Frame {
|
||||
OnCreated(fn1),
|
||||
OnCreated(fn2),
|
||||
OnRendered(fn3),
|
||||
OnDestroyed(cleanupA),
|
||||
OnDestroyed(cleanupB),
|
||||
}
|
||||
```
|
||||
|
||||
`OnCreated(fn)`/`OnDestroyed(fn)` 호출마다 `PreRef()`/`Effect(...)`
|
||||
**단, 같은 계열끼리의 상대 순서는 보장 안 함** — `fn1`이 `fn2`보다 먼저
|
||||
불린다고 기대하지 말 것(`PreRef`/`PostRef` 둘 다 2026-08-14 아홉 번째
|
||||
세션에 계열 안 순서를 명시적 미보장으로 확정, `base/ref-plan.md`). 순서가
|
||||
정말 필요하면 훅을 여럿 등록하는 게 아니라 **하나의 훅 안에서 순서대로
|
||||
부를 것** — 그게 의도가 코드에 드러나는 유일한 방법.
|
||||
|
||||
`OnCreated(fn)`/`OnRendered(fn)`/`OnDestroyed(fn)` 호출마다
|
||||
`PreRef()`/`PostRef()`/`Effect(...)`
|
||||
**생성자가 매번 새로 불려 독립된 인스턴스**를 만들어냄 — children
|
||||
배열의 서로 다른 숫자 슬롯에 놓이므로, 같은 인스턴스에 여러 개를
|
||||
나란히 등록하는 게 자연히 지원됨. 이건 `Ref():Callback(fn)` 단일
|
||||
|
|
@ -130,12 +175,18 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
`OnCreated(fn2)`는 각각 `PreRef()`를 독립적으로 호출해 서로 다른 객체를
|
||||
만드므로, 이 가드가 막으려는 재사용 시나리오 자체가 발생하지 않음.
|
||||
|
||||
## ② `OnRendered`(+`PostRef`) — 의도적으로 지금 구현 안 함, 백로그 후보만
|
||||
## ② `OnRendered`(+`PostRef`) — 채택 확정 (2026-08-14 아홉 번째 세션)
|
||||
|
||||
**[결정, 2026-08-14 세션]** `OnCreated`/`OnDestroyed`와 달리 지금 착수
|
||||
안 함 — 아래 이유로 새 디스패치 단계가 실제로 필요해서, 착수 여부
|
||||
자체를 지금 정하지 않고 **백로그 후보로만 남겨둠**. 아래는 나중에
|
||||
꺼내볼 때 바로 쓸 수 있도록 정리해두는 조사 결과.
|
||||
> **[역전, 2026-08-14 아홉 번째 세션]** 이 절은 원래 "의도적으로 지금
|
||||
> 구현 안 함, 백로그 후보만"이었음(같은 날 여섯 번째 세션 결정) —
|
||||
> `OnCreated`/`OnDestroyed`와 달리 디스패치 코어에 새 단계가 필요해
|
||||
> "공짜가 아니다"라는 게 근거였고, 그 판단 자체는 지금도 맞음. 뒤집힌
|
||||
> 건 **그 비용을 지불할지**로, 사용자 판단은 **"Pre-Post 둘을 지원 안 할
|
||||
> 이유가 없고, 구현 난이도가 아주 낮아서 괜찮다"** — 아래 스케치가 이미
|
||||
> "새 전체 순회 없음, pre-pass에 분기 하나 + 짧은 목록 소비"까지
|
||||
> 줄여놨던 게 결정적. **정본은 이제 `base/ref-plan.md`의 "`PostRef`"
|
||||
> 절**(프리미티브로서의 계약·보장 범위·Handler)이고, 아래는 그 결론에
|
||||
> 이르게 된 조사/논거를 그대로 보존한 것.
|
||||
|
||||
`base/dispatch-core-plan.md`의 "확정된 디스패치 모델" 절이 계약하는
|
||||
두 패스(배열 파트 먼저, 해시 파트 나중) 기준으로 현재 base가 제공하는
|
||||
|
|
@ -145,13 +196,13 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
|---|---|
|
||||
| `PreRef` | 두 패스보다도 **먼저**(호이스팅 pre-pass) |
|
||||
| 일반 `Ref`/`Effect`(children 배열 위치) | **배열 파트** 처리 시점(아직 해시 파트 전) |
|
||||
| (없음) | 해시 파트(프로퍼티/이벤트)까지 **전부 끝난 뒤** |
|
||||
| `PostRef` **[2026-08-14 아홉 번째 세션 신설]** | 해시 파트(프로퍼티/이벤트)까지 **전부 끝난 뒤** |
|
||||
|
||||
즉 "이 인스턴스의 프로퍼티/이벤트까지 전부 세팅된 뒤"를 보장하는 훅이
|
||||
**현재 base 설계엔 없음**. 이건 기존 프리미티브 재사용만으로는 안 되고
|
||||
**당시 base 설계엔 없었음**. 이건 기존 프리미티브 재사용만으로는 안 되고
|
||||
디스패치 코어에 실제로 새 단계가 필요하다는 뜻이라, `OnCreated`/
|
||||
`OnDestroyed`와 달리 **진짜로 공짜가 아님** — 그래서 지금 채택하지
|
||||
않기로 함.
|
||||
`OnDestroyed`와 달리 **진짜로 공짜가 아님** — 그래서 처음엔 채택을
|
||||
보류했고, 아래 스케치로 비용이 충분히 작다는 게 드러난 뒤 채택됨.
|
||||
|
||||
**`PostRef` 스케치(사용자 제안, 2026-08-14, 두 번째 세션에 `ProcessedPreRef`
|
||||
선례 반영해 갱신, 같은 세션 후속 제안으로 다시 갱신 — "후행 스캔" 초안
|
||||
|
|
@ -181,9 +232,11 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
똑같이 필요(pre-pass가 놓쳤을 때만 매치되는 버그 케이스 전용, `error`).
|
||||
- **두 패스가 끝난 뒤, `Dispatch.drive`가 `postRefList`를 그 순서 그대로
|
||||
순회하며 각 `PostRef`를 fire** — 별도 후행 전체 재순회가 필요 없음,
|
||||
pre-pass가 이미 만들어둔 목록을 그대로 소비하면 끝. 복수 `PostRef`
|
||||
간 순서는 복수 `PreRef`와 같은 원칙(배열 index 순서 그대로)이 자연히
|
||||
적용됨.
|
||||
pre-pass가 이미 만들어둔 목록을 그대로 소비하면 끝. **[정정, 2026-08-14
|
||||
아홉 번째 세션]** 복수 `PostRef` 간 순서가 복수 `PreRef`와 같은
|
||||
원칙이라는 건 그대로지만, 그 원칙 자체가 "배열 index 순서 **보장**"에서
|
||||
**"보장 안 함"**으로 뒤집혔음 — 구현상 push 순서로 돌 뿐, 계약이
|
||||
아님(`archive/preref-order-guaranteed-reversed.md`).
|
||||
- 결과적으로 `PreRef`와 `PostRef`는 **소진 메커니즘이 완전히 대칭**
|
||||
(둘 다 pre-pass에서 즉시 `Processed*` 센티널로 소진, 둘 다 전담
|
||||
`Processed*Handler`가 Length/Offset을 등록) — 유일한 차이는 "실제
|
||||
|
|
@ -195,23 +248,24 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
초안보다 훨씬 저렴. `OnRendered(fn)`은 `PostRef():Callback(fn)`을
|
||||
반환하는 팩토리로, 위 `OnCreated`와 완전히 같은 패턴이 됨.
|
||||
|
||||
**스코프도 여전히 불명확함**: "렌더 완료"가 (a) 이 인스턴스 자신의
|
||||
프로퍼티/이벤트 세팅만 끝나면 되는지, (b) 이 인스턴스의 **자식들까지
|
||||
전부 마운트를 끝내야** 하는지 — React류 `on*Rendered` 이름들은 보통
|
||||
(b)(서브트리 전체 완료)를 뜻하는 경우가 많아, 이름만 보고 (a)로 기대하는
|
||||
사람과 실제 구현이 (b)라면(또는 반대라면) 기대치가 어긋날 위험이 있음.
|
||||
`PostRef`의 `postRefList` 소비(위 스케치)는 자연스럽게 (a)만 줌 — (b)를
|
||||
원하면 자식 서브트리 전체의 마운트 완료를 기다리는 별도 신호가 있어야
|
||||
해서 훨씬 큰 작업.
|
||||
**스코프 — 해소됨(2026-08-14 아홉 번째 세션).** 원래 이 자리엔 "렌더
|
||||
완료"가 (a) 이 인스턴스 자신의 프로퍼티/이벤트 세팅만인지 (b) 자식
|
||||
서브트리 전체 완료까지인지가 **불명확**하다고 적혀 있었고, "(a) 메커니즘은
|
||||
(b)를 못 준다"고 판단했었음 — **그 판단이 틀렸다는 게 사용자 지적으로
|
||||
드러남**: 배열 파트 루프가 각 자식의 마운트를 **동기적으로 끝내고**
|
||||
(`Slot`은 실 확정 시 요소를 그 자리에서 주입, `State<Frame>`도 최초 값을
|
||||
그 자리에서 처리) 넘어가므로, 두 패스가 끝난 시점엔 **정적으로 선언된
|
||||
서브트리가 이미 전부 완성돼 있음**. 즉 (a) 메커니즘이 사실상 (b) 스코프를
|
||||
공짜로 줌.
|
||||
|
||||
**착수 시점에 판단할 선택지(지금은 고르지 않음)**:
|
||||
- (a) 위 `PostRef` 스케치대로 두 패스 뒤 `postRefList`를 소비한다 —
|
||||
(a) 스코프(자기 자신 세팅 완료)의 정확한 보장.
|
||||
- (b) 새 메커니즘 없이 일반 `Ref`로 근사한다 — Store를 통해 늦게
|
||||
도착하는 값으로 "대충 렌더 이후"를 흉내내되, "완전한 보장은 없음"을
|
||||
문서에 명시하는 선에서 타협.
|
||||
- (c) 계속 스코프 아웃 — `OnCreated`/`OnDestroyed`만 쓰고 `OnRendered`는
|
||||
필요해질 때까지 안 만듦.
|
||||
**단, 진짜 경계는 (a)/(b)가 아니라 "자기 아래 vs 자기 위"였음** —
|
||||
`PostRef`는 자기 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙는
|
||||
것보다는 여전히 먼저** 불림. 이 캐비엇의 정본 서술은 `base/ref-plan.md`의
|
||||
"`PostRef`" 절 "보장 범위" 항목.
|
||||
|
||||
**착수 시점 선택지 — (a) 확정.** 원래 (a)/(b)/(c) 셋을 열어뒀었고
|
||||
((b)는 일반 `Ref`로 근사, (c)는 계속 스코프 아웃), 사용자가 **(a)**
|
||||
(위 `PostRef` 스케치대로 두 패스 뒤 `postRefList` 소비)를 선택함.
|
||||
|
||||
## 이름 컨벤션
|
||||
|
||||
|
|
@ -219,16 +273,18 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
`OnChange(name)`(`GetPropertyChangedSignal` 바인딩용 DI 키). 단
|
||||
**메커니즘은 다름**: `OnChange`는 이름을 인자로 받아 캐시된 키 객체를
|
||||
반환하는 **해시 파트 DI 키 팩토리**(`base/onchange-plan.md` "확정"
|
||||
절)인 반면, 이 문서의 `OnCreated`/`OnDestroyed`는 **배열 파트에
|
||||
놓이는 값(`PreRef`/`EffectHandle`)을 만드는 팩토리**라 이름 패턴만
|
||||
절)인 반면, 이 문서의 `OnCreated`/`OnRendered`/`OnDestroyed`는 **배열
|
||||
파트에 놓이는 값(`PreRef`/`PostRef`/`EffectHandle`)을 만드는 팩토리**라
|
||||
이름 패턴만
|
||||
같고 소속 카테고리가 다름 — `OnChange` 쪽 "다른 특수 DI 키와의 대조"
|
||||
표에 이 둘을 끼워 넣을 필요는 없어 보임(별도 표로 다루는 게 맞음).
|
||||
- `OnCreated`/`OnDestroyed`는 거의 확정적인 후보 — 다만 v1이 이미
|
||||
- `OnCreated`/`OnDestroyed` **이름 확정** — 다만 v1이 이미
|
||||
`OnCreated`라는 이름을 다른 메커니즘(특수 DI 키)으로 썼던 전례가
|
||||
있어 위 "①" 절의 대조 설명 없이 이름만 보면 헷갈릴 수 있음, 문서화
|
||||
시 명시할 것.
|
||||
- `OnDestroyed`는 최초 가칭이던 `OnDisposed`보다 사용자가 선호 —
|
||||
**최종 이름 결정은 여전히 열려있음.** `OnDisposed`가 제안된 이유는
|
||||
**이 이름으로 확정하되, `dispose()` 범위(0-B)가 풀리면 재검토 여지만
|
||||
남겨둠**(아래 "열린 질문" 절). `OnDisposed`가 제안된 이유는
|
||||
미래 `dispose()` 함수(`question.md`
|
||||
"0-B. `dispose(any)` — 시그니처/범위")와 이름을 맞추자는 발상이었는데,
|
||||
대조해보면 트리거 자체가 다름:
|
||||
|
|
@ -251,45 +307,50 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
|||
이름을 맞추는 재검토가 자연스러워질 수 있음.
|
||||
- 이름 자체는 런타임에 아무 의미가 없는 순수 네이밍(값은 그냥
|
||||
`PreRef`/`EffectHandle`)이라 바꾸는 비용은 0에 가까움 — 그래서
|
||||
지금은 위험 부담 낮은 `OnDestroyed`를 잠정 1순위로 두고,
|
||||
`dispose()` 범위가 확정되면 재검토하는 게 결론. 최종 결정은
|
||||
여전히 사용자 몫.
|
||||
- `OnRendered`/`PostRef`는 위 "②" 절의 스코프가 아직 안 정해져서 이름도
|
||||
가결정 — "렌더"라는 단어가 quad엔 없는 개념(React식 재렌더 루프가
|
||||
없음, `base/architecture.md`)이라 `OnRendered`라는 이름 자체가 오해를
|
||||
부를 수 있다는 점도 고려 대상. `PostRef`는 `PreRef`와의 대칭성이
|
||||
이름에서 바로 읽혀서 유력한 후보.
|
||||
위험 부담이 낮은 `OnDestroyed`로 확정하고, `dispose()` 범위가
|
||||
확정되면 재검토하는 게 결론.
|
||||
- **`OnRendered`/`PostRef` — 이름 확정(2026-08-14 아홉 번째 세션).**
|
||||
`PostRef`는 `PreRef`와의 대칭성이 이름에서 바로 읽혀 이견 없음.
|
||||
`OnRendered`는 채택되며 이름도 그대로 확정 — 다만 **"렌더"가 quad엔
|
||||
없는 개념**(React식 재렌더 루프가 없음, `base/architecture.md`)이라는
|
||||
원래 우려는 유효하고, 여기에 **"이 인스턴스가 부모에 붙기 전에
|
||||
불린다"**는 캐비엇까지 겹침. 즉 이 이름은 **"React에서 오는 사람이
|
||||
기대하는 것과 미묘하게 다른 시점"**을 가리키므로, 문서화 시 위 ①
|
||||
절의 ⚠️ 항목을 반드시 같이 노출할 것(이름을 바꾸는 대신 문서로
|
||||
대응하기로 한 것 — 이름의 친숙함이 주는 이득이 더 크다는 판단).
|
||||
|
||||
## 패키지 배치
|
||||
|
||||
`Ref`/`PreRef`/`Effect`가 전부 quad-base 프리미티브이므로,
|
||||
`OnCreated`/`OnDestroyed`는 그 위의 순수 함수일 뿐이라 **quad-base가
|
||||
자연스러워 보임** — `Operator`/`Fallback`과 같은 결(엔진 지식이 전혀
|
||||
필요 없는 순수 조합). `OnRendered`/`PostRef`를 실제로 만들게 되면
|
||||
post-pass 자체는 `Dispatch.drive`(quad-base) 소유가 맞겠지만, 이건 ②가
|
||||
착수될 때에나 확정할 문제. 최종 판단은 열어둠.
|
||||
**quad-base 확정(2026-08-14 아홉 번째 세션).** `Ref`/`PreRef`/`PostRef`/
|
||||
`Effect`가 전부 quad-base 프리미티브이므로 이 훅 셋은 그 위의 순수
|
||||
함수일 뿐 — `Operator`/`Fallback`과 같은 결(엔진 지식이 전혀 필요 없는
|
||||
순수 조합). `PostRef`의 pre-pass 수집/`postRefList` 소비도 `Dispatch.drive`
|
||||
(quad-base) 소유라 층위가 일관됨.
|
||||
|
||||
## 우선순위
|
||||
|
||||
**형제 백로그 항목들과 동급, 맨 뒤** — `quad-mock`/`quad-debug`/
|
||||
`Operator`/`Fallback`과 같이 "quad 개발 상당 부분 끝난 뒤"로 볼 것.
|
||||
`OnCreated`/`OnDestroyed`는 착수 시점에 위 코드 스케치를 그대로 옮기면
|
||||
될 만큼 단순하지만, 순수 슈가라 없어도 `Ref():Callback(fn)`/
|
||||
`Effect(fn)`를 직접 쓰면 되므로 기능 격차는 없음. `OnRendered`/`PostRef`는
|
||||
그 형제들보다도 더 뒤 — 채택 여부 자체가 아직 결정 안 됨(위 ② 절),
|
||||
백로그 후보로만 존재.
|
||||
**두 층위를 구분할 것**:
|
||||
- **`PostRef` 프리미티브 자신** — 디스패치 코어의 일부라 `ROADMAP.md`
|
||||
M8(Ref)에서 `PreRef`와 **같이** 구현됨. 뒤로 미루는 대상이 아님.
|
||||
- **이 문서의 훅 슈가 셋(`OnCreated`/`OnRendered`/`OnDestroyed`)** —
|
||||
형제 백로그 항목들과 동급, 맨 뒤(`quad-mock`/`quad-debug`/`Operator`/
|
||||
`Fallback`과 같이 "quad 개발 상당 부분 끝난 뒤"). 착수 시점에 위 코드
|
||||
스케치를 그대로 옮기면 될 만큼 단순하고, 순수 슈가라 없어도
|
||||
`PreRef()/PostRef():Callback(fn)`·`Effect(fn)`를 직접 쓰면 되므로
|
||||
기능 격차가 없음.
|
||||
|
||||
## 열린 질문
|
||||
|
||||
**[2026-08-14 세션] `OnRendered` 채택 여부는 이미 답이 나옴 — "지금은
|
||||
안 함", 그래서 `question.md`엔 안 올림(사용자가 답할 활성 질문이 아니라
|
||||
그냥 보류된 백로그 후보).** 착수하기로 결정되는 시점에 다시 열어볼
|
||||
것들만 남음:
|
||||
- `PostRef`의 정확한 메커니즘/스코프(위 ②의 (a)/(b)/(c) 중 선택,
|
||||
선택 시 (a)/(b) 하나 고르면 스코프도 자연히 정해짐).
|
||||
- 이름 최종 확정 — `OnDestroyed`가 잠정 1순위 후보(위 "이름 컨벤션"
|
||||
절의 `OnDisposed` 대조 참고, `dispose()`의 대상 범위(`question.md`
|
||||
0-B)가 확정되면 재검토 여지 있음). `OnRendered`/`PostRef`는 착수
|
||||
결정 전엔 가결정.
|
||||
- 패키지 배치 최종 확인(quad-base로 거의 확정적이나 착수 시점 재확인).
|
||||
**[2026-08-14 아홉 번째 세션] 설계상 열린 질문 없음** — 채택 여부/
|
||||
메커니즘/스코프/패키지 배치가 전부 확정됨(위 각 절). 하나만 성격이
|
||||
다른 항목으로 남음:
|
||||
|
||||
- **`OnDestroyed` 이름 재검토 여지** — 지금 이름은 확정이지만, 미래
|
||||
`dispose()`의 대상 범위(`question.md` "0-B. `dispose(any)` —
|
||||
시그니처/범위")가 "quad가 만드는 모든 것의 유일한 파괴 경로"로 풀리면
|
||||
`OnDisposed`와 맞추는 재검토가 자연스러워질 수 있음(위 "이름 컨벤션"
|
||||
절의 대조 참고). `question.md`의 **용어 정리 대기열**에 3순위로 올려둠
|
||||
— `Slot`/`Brand`처럼 "base에 확정돼 있지만 이름만 재검토 대상"인
|
||||
기존 항목들과 같은 취급이고, 이름은 런타임에 아무 의미가 없어 바꾸는
|
||||
비용이 0에 가까움.
|
||||
- 그 외 확정된 결정 없음 — 착수 시점에 위 항목들을 순서대로 확인.
|
||||
|
|
@ -237,13 +237,13 @@ Roblox API에 전혀 의존 안 하는 순수 Lua 테이블 조작이라, "base
|
|||
**Modifier의 체이닝 엔진 자체는 quad-base에 완결된 구현으로 그대로
|
||||
존재해도 됨** — 주입할 엔진별 구현이 애초에 없음.
|
||||
|
||||
**Modifier 필드에 핸들러 계층 값(Ref/PreRef/Observer/Effect/Slot/
|
||||
**Modifier 필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/
|
||||
Modifier)이 들어오면 즉시 error — UB 아님(2026-08-09 세션, 정정).**
|
||||
이전 버전("권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는
|
||||
UB로 남겨둠")은 폐기. 재검토 근거(사용자): Modifier는 애초에 자식/Ref
|
||||
같은 걸 다루는 목적이 아니고, 이런 값이 실제로 쓸모 있는 use case가
|
||||
없다고 확인된 이상 조용한 UB보다 그 자리에서 막는 쪽이 낫다 — 판별
|
||||
비용도 이미 있는 `Brand` 기반 predicate(`isRef`/`isPreRef`/
|
||||
비용도 이미 있는 `Brand` 기반 predicate(`isRef`/`isPreRef`/`isPostRef`/
|
||||
`isObserver`/`isEffect`/`isSlot`/`isModifier`, `bind-system-plan.md`의
|
||||
`Brand` 절)를 그대로 재사용하면 되므로 거의 공짜.
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ UB로 남겨둠")은 폐기. 재검토 근거(사용자): Modifier는 애초에
|
|||
못 잡음.** `isRef(v)` 등은 setter가 확정하는 바로 그 값(State 자체
|
||||
또는 plain 값)만 보므로, 값이 State/Source면 그 껍데기가 `isState`를
|
||||
통과해 검사를 그냥 지나가고, 그 State가 나중에 `:Get()`됐을 때 실제로
|
||||
내놓는 내용물(예: 그 State가 Ref/PreRef/Observer/Effect/Slot을 값으로
|
||||
내놓는 내용물(예: 그 State가 Ref/PreRef/PostRef/Observer/Effect/Slot을 값으로
|
||||
들고 있는 경우)까지는 검사하지 않음 — 검사 시점엔 아직 실체화 안 된
|
||||
값이라 정적으로 알 수 없고, 값이 바뀔 때마다 매번 `:Get()`해서
|
||||
검사하는 건 관측 시점을 앞당기는 부작용까지 생기는 오버엔지니어링.
|
||||
|
|
@ -346,7 +346,7 @@ dispatch 밖에서만 처리되는 유일한 존재")과 정면으로 충돌함
|
|||
|
||||
**[정정, 2026-08-09 세션] "UB, 가능하면 타입 차단"에서 "명시적
|
||||
`error`로 확정"으로 전환** — 위 "핸들러 계층 값이 필드로 들어오면
|
||||
즉시 error" 절(Ref/PreRef/Observer/Effect/Slot/Modifier가 Modifier
|
||||
즉시 error" 절(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier가 Modifier
|
||||
*필드*로 들어오는 걸 막은 것)과 같은 방향으로 통일: `isModifier`
|
||||
predicate(`Brand` 절)를 State/Source 쪽에도 적용해 **런타임에 직접
|
||||
막는다.** 타입 차단(`State<Modifier>` 같은 조합을 타입 정의 단계에서
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
# Ref / PreRef — 지연 없는 확정 값 박스
|
||||
# Ref / PreRef / PostRef — 지연 없는 확정 값 박스
|
||||
|
||||
> **[2026-08-14 아홉 번째 세션] `PostRef` 확정·이 문서에 편입.**
|
||||
> `base/lifecycle-hooks-plan.md`(당시 `research/`)가 백로그 후보로만 들고 있던 스케치를
|
||||
> 사용자가 확정("Pre-Post 둘을 지원 안 할 이유가 없고 구현 난이도가 아주
|
||||
> 낮음") — 아래 "`PostRef`" 절 신설, 그 문서도 `base/lifecycle-hooks-plan.md`로
|
||||
> 같이 승격됨. **같은 세션에 `PreRef`/`PostRef` 각 계열 안의 fire 순서를
|
||||
> "배열 index 순서 그대로 보장"에서 "보장하지 않음"으로 역전** — 역전 원문과
|
||||
> 근거는 `archive/preref-order-guaranteed-reversed.md`.
|
||||
|
||||
> **[2026-08-13 아홉 번째 세션] `bind-system-plan.md`에서 분리됨.** 그
|
||||
> 문서가 2989줄까지 불어나 사람이 검토하기 어렵고 한 곳의 실수가 미치는
|
||||
|
|
@ -370,17 +378,27 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
|||
패스로 처리하면 됨 — 이 pre-pass는 오직 `PreRef` 타입만 골라내므로
|
||||
범위가 좁고, "확정된 디스패치 모델" 절의 두 패스 계약과 별개로 그
|
||||
앞에 얹히는 것.
|
||||
- **복수 `PreRef` 간 순서(2026-08-07 아홉 번째 세션, 사용자 확인) —
|
||||
새 규칙 불필요, 배열 index 순서 그대로.** 같은 인스턴스에 `PreRef`가
|
||||
여럿 있으면, 이 pre-pass는 위 "props 순회 순서" 절이 이미 확정해둔
|
||||
"배열 파트는 index 순서대로" 계약을 그대로 재사용해 리터럴 순서대로
|
||||
fire하면 됨 — 서로 다른 우선순위/순서 개념을 별도로 만들 필요 없음
|
||||
(호이스팅은 "PreRef 전체 대 나머지"에만 적용되는 규칙이지, "PreRef끼리"
|
||||
에는 적용될 게 없음 — PreRef끼리는 그냥 평범한 배열 순회).
|
||||
- **복수 `PreRef` 간 순서 — 보장하지 않음(계약)** ⚠️ **[역전, 2026-08-14
|
||||
아홉 번째 세션]** 2026-08-07 아홉 번째 세션엔 "배열 index 순서 그대로"를
|
||||
**보장**으로 명시했었으나, 사용자 판단으로 **의도적 미보장**으로 뒤집음
|
||||
— 역전 원문·근거는 `archive/preref-order-guaranteed-reversed.md`.
|
||||
- **구현은 그대로**(pre-pass가 배열을 index 순서로 훑으므로 사실상
|
||||
리터럴 순서로 fire됨) — 바뀐 건 오직 **계약**임: "같은 계열 안의
|
||||
등록 순서에 의존하는 코드가 생겨선 안 된다, 그건 구조부터 잘못된
|
||||
것"이라는 게 사용자가 든 이유(순서에 의존해야 할 정도면 애초에 두
|
||||
훅으로 나눌 게 아니라 하나의 훅 안에서 순서대로 부르면 됨). 보장을
|
||||
안 하면 **나중에 pre-pass 구현을 바꿀 자유**도 남고, 그 순서에
|
||||
기대는 사용자 코드가 생기지 않아 **버그가 오히려 덜 생김**.
|
||||
- 호이스팅 규칙 자체("`PreRef` 전체가 나머지 전부보다 먼저")는 그대로
|
||||
**보장**임 — 미보장으로 바뀐 건 "`PreRef`끼리의 상대 순서" 하나뿐.
|
||||
아래 `PostRef`도 완전히 같은 규칙(계열 안 순서 미보장).
|
||||
- **호이스팅의 실제 구현 = "물리적 재배치"가 아니라 "완전히 별도의
|
||||
선행 스캔"(2026-08-07 아홉 번째 세션 후속, 사용자 질문에 답변).**
|
||||
`Dispatch.drive(inst, flattened)`는 같은 `flattened` 배열을 **두 번
|
||||
순회**한다 — (1) pre-pass: 배열 파트 전체를 index 순서대로 훑으며
|
||||
순회**한다(**[2026-08-14 아홉 번째 세션]** 그 뒤에 `postRefList` 소비가
|
||||
하나 더 붙지만, 그건 배열 재순회가 아니라 pre-pass가 만들어둔 짧은
|
||||
목록 하나를 도는 것 — 아래 "`PostRef`" 절) — (1) pre-pass: 배열 파트
|
||||
전체를 index 순서대로 훑으며
|
||||
`isPreRef(v)`인 슬롯을 찾아 그 자리에서 fire하고 즉시 **`flattened[i]
|
||||
= ProcessedPreRef`**로 소진(`nil`이 아님, 2026-08-07 열 번째 세션 정정: `nil`로
|
||||
지우면 그 순간 테이블이 "구멍 있는" 상태가 되어 이어지는 (2)의 순회
|
||||
|
|
@ -572,5 +590,139 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
|||
"고치지" 않는다** — 두 패스 순서를 뒤집거나 재배치하는 시도는
|
||||
오버엔지니어링으로 판단해 안 함(이걸 원하면 애초에 PreRef를 쓰면 됨).
|
||||
이 결정과 이유는 나중에 `quadnomicon` 콘텐츠로 문서화 예정
|
||||
(`research/documentation-content-map.md` 후보로 메모).
|
||||
(`research/documentation-content-map.md` 후보로 메모). **[보강,
|
||||
2026-08-14 아홉 번째 세션]** "두 패스가 **전부 끝난 뒤**"라는 타이밍은
|
||||
이제 아래 `PostRef`가 제공함 — 그건 두 패스의 순서를 건드리는 게 아니라
|
||||
그 뒤에 얹히는 것이라 이 결정과 상충하지 않음.
|
||||
|
||||
### `PostRef` — 두 패스가 전부 끝난 뒤 fire, `PreRef`의 거울상 (2026-08-14 아홉 번째 세션 확정)
|
||||
|
||||
**확정.** `research/lifecycle-hooks-plan.md`(현
|
||||
`base/lifecycle-hooks-plan.md`) ② 절이 "지금은 구현 안 함, 백로그 후보"로
|
||||
남겨뒀던 스케치를 사용자가 착수 선택지 **(a)**(pre-pass 공동 수집 +
|
||||
두 패스 뒤 `postRefList` 소비)로 확정 — 근거는 "Pre/Post 둘을 지원 안 할
|
||||
이유가 없고, 구현 난이도가 아주 낮음". 이걸로 `OnRendered`도 같이 채택됨
|
||||
(`base/lifecycle-hooks-plan.md`).
|
||||
|
||||
**정의**: `PostRef`는 `PreRef`와 마찬가지로 **`Ref` 런타임을 그대로 재사용하고
|
||||
브랜드 태그만 다른 nominal 타입**(`.Value`/`:Set`/`:Callback`/`:Wait` 동일,
|
||||
`base/brand-plan.md`). 다른 점은 **fire 시점 하나뿐** — `PreRef`가 "이
|
||||
인스턴스에 뭐가 됐든 일어나기 **전**"이라면, `PostRef`는 "이 인스턴스의
|
||||
배열 파트(children/Ref)와 해시 파트(프로퍼티/이벤트)가 **전부 끝난 뒤**".
|
||||
|
||||
**세 Ref의 타이밍 대조(이게 전부임)**:
|
||||
|
||||
| 타입 | fire 시점 | 계열 안 상대 순서 |
|
||||
|---|---|---|
|
||||
| `PreRef` | 두 패스보다도 **먼저**(호이스팅 pre-pass) | **보장 안 함** |
|
||||
| 일반 `Ref` | **정해진 시점 없음** — 그 값이 dispatch에 도착한 순간(배열 위치/Store 도착 시점에 따라 달라짐) | (해당 없음) |
|
||||
| `PostRef` | 두 패스가 **전부 끝난 뒤** | **보장 안 함** |
|
||||
|
||||
- 일반 `Ref`의 "언제 채워지는지 모른다"는 건 결함이 아니라 원래 계약임 —
|
||||
그래서 "이미 채워졌는지 먼저 확인, 없으면 `:Wait()`/`:Callback()`"이
|
||||
Ref 전체의 관용구로 이미 확정돼 있음(위 "Ref 일반화" 절). `PreRef`/`PostRef`는
|
||||
그 관용구를 안 써도 되도록 **시점을 계약으로 고정한** 두 특수 케이스.
|
||||
- **계열 안 순서를 둘 다 미보장으로 두는 이유**는 위 `PreRef` 절의 역전
|
||||
항목과 같음 — 같은 계열 훅들끼리 등록 순서에 의존하는 코드가 생기면
|
||||
그건 구조부터 잘못된 것이라, 보장을 안 하는 쪽이 버그를 덜 만듦.
|
||||
|
||||
**보장 범위 — 무엇이 끝나 있고 무엇이 안 끝나 있는가(중요)**
|
||||
|
||||
`PostRef`가 fire될 때 **끝나 있는 것**:
|
||||
- **이 인스턴스의 모든 자식**, 그리고 그 자식들의 **서브트리 전체**. 배열
|
||||
파트는 "각 자식이 자기 서브트리까지 전부 동기적으로 마운트를 끝내야 다음
|
||||
형제로 넘어간다"가 이미 계약이고(위 "`phase` 옵션 폐기" 절), `Slot`도
|
||||
마운트 시점에 자기 초기 요소를 그 자리에서 주입하며(`base/slot-plan.md`),
|
||||
`State<Frame>`류 store-bind도 최초 값을 그 자리에서 동기적으로 처리함
|
||||
(`base/dispatch-core-plan.md`) — 즉 배열 파트가 끝난 시점엔 **정적으로
|
||||
선언된 트리가 전부 완성**돼 있음. 사용자 표현 그대로 "중간 for문에서
|
||||
모든 Slot/`State<Frame>`/`Frame` 요소의 마운트가 처리되므로, 바로 뒤에서
|
||||
실행하면 모든 트리가 완성된 이후".
|
||||
- **이 인스턴스의 모든 프로퍼티/이벤트**(해시 파트) — `PreRef`가 존재하는
|
||||
이유였던 "이벤트가 setup 도중 동기 발화" 문제의 반대편 끝.
|
||||
|
||||
**끝나 있지 **않은** 것**(문서화 필수, 이름만 보고 오해하기 쉬움):
|
||||
- **이 인스턴스 자신이 부모에 붙는 것(`.Parent` 대입)** — 부모가 이
|
||||
인스턴스를 자기 배열 파트에서 처리하는 건 이 `drive`가 **끝난 뒤**임
|
||||
(`Frame { Frame {...} }`처럼 리터럴로 중첩하면 안쪽 `Frame` 호출이 먼저
|
||||
완결되어야 바깥 `Frame`의 props 테이블이 완성됨). 즉 `PostRef`는
|
||||
**자기 아래(서브트리)의 완성만 보장하고, 자기 위(조상 체인)는 아직
|
||||
없을 수 있음** — "화면에 올라간 시점"이 아님. `OnRendered`라는 이름이
|
||||
React `componentDidMount`(DOM 삽입 후)처럼 읽힐 수 있으므로 이 차이를
|
||||
`base/lifecycle-hooks-plan.md`와 사용자 문서에 명시할 것.
|
||||
- **나중에 동적으로 도착하는 것들** — Store를 통해 뒤늦게 바뀌는 값,
|
||||
`Slot:Add`/`:List`로 나중에 추가되는 요소는 정의상 이 `drive` 밖의
|
||||
사건이라 당연히 포함 안 됨.
|
||||
|
||||
**메커니즘 — pre-pass 한 스윕으로 `PreRef`/`PostRef` 둘 다 처리**
|
||||
|
||||
새 전체 순회를 추가하지 않는 게 핵심(사용자 제안). `Dispatch.drive`의
|
||||
기존 pre-pass가 이미 배열 파트를 index 순서로 한 번 훑고 있으므로:
|
||||
|
||||
1. **pre-pass(기존 루프에 분기 하나 추가)**:
|
||||
- `isPreRef(v)`면 지금까지처럼 **그 자리에서 즉시 fire**하고
|
||||
`flattened[i] = ProcessedPreRef`로 소진.
|
||||
- `isPostRef(v)`면 **아직 fire하지 않고**, 이 `Dispatch.drive` 호출
|
||||
하나에만 로컬인 평범한 배열 `postRefList`에 그 인스턴스를 push한 뒤
|
||||
**즉시** `flattened[i] = ProcessedPostRef`로 소진. 1회용 재사용 가드
|
||||
(`_fired`)도 **이 시점에** 세팅 — "슬롯이 소진되는 시점"과 "재사용이
|
||||
막히는 시점"을 `PreRef`와 동일하게 맞춤(실제 콜백 fire와 시점이
|
||||
갈리는 건 아래 3번뿐).
|
||||
- `postRefList`는 **`Relate` 같은 별도 저장소가 아님** — 이 함수
|
||||
콜스택 안에서만 살면 되므로 그냥 로컬 테이블.
|
||||
2. **정상 두 패스**(배열 → 해시)가 평소대로 돎. `ProcessedPostRef`로
|
||||
소진된 슬롯은 `ProcessedPreRef`와 **완전히 대칭적으로** 정상
|
||||
`Dispatch.process` 경로를 타고 아래 전담 Handler에 매치됨.
|
||||
3. **두 패스가 끝난 뒤 `Dispatch.drive`가 `postRefList`를 순회하며 각
|
||||
`PostRef`를 fire.** 추가 비용은 전체 배열 재순회가 아니라 **실제
|
||||
`PostRef` 개수만큼의 순회**뿐. (순회 자체는 자연히 push 순서지만, 위
|
||||
표대로 **그 순서는 계약이 아님** — 의존 금지.)
|
||||
|
||||
**`ProcessedPostRefHandler` — `ProcessedPreRefHandler`의 거울상, 새 규칙 없음**
|
||||
|
||||
```lua
|
||||
ProcessedPostRefHandler.priority = <매우 높음, ProcessedPreRefHandler와 동급>
|
||||
ProcessedPostRefHandler.isHandlable(inst, k, v) = (v == ProcessedPostRef)
|
||||
function ProcessedPostRefHandler.process(inst, i, v)
|
||||
Dispatch.setLength(inst, i, 0)
|
||||
Dispatch.setOffsetSource(inst, i, None)
|
||||
return function() end -- no-op retract, PreRef와 같은 이유(되돌릴 상태가 없음)
|
||||
end
|
||||
```
|
||||
|
||||
`ProcessedPreRefHandler`(위 절)와 한 글자 차이 — "이 위치를 처음 매치한
|
||||
Handler가 `setLength`/`setOffsetSource` 등록 책임을 진다"는 `base/
|
||||
dispatch-core-plan.md` "Length/Offset" 절의 계약을 특수 취급 없이 그대로
|
||||
만족시킴. `ProcessedPostRef`도 `None`이 아니라 **전용 센티널**(단일 `{}`)인
|
||||
이유는 `ProcessedPreRef`와 동일 — "원래부터 빈 자리(`None`)"와 구별돼야
|
||||
등록 책임 소재가 분명해지고, 배열에 구멍이 안 생김.
|
||||
|
||||
**동적 경로 가드 Handler도 거울상으로 하나 더**
|
||||
|
||||
`PreRef`와 똑같이, `PostRef`도 **children 배열의 리터럴 아이템으로만** 놓을
|
||||
수 있음 — Modifier 필드/Source/Store 값으로는 **타입으로 차단**(이유도
|
||||
동일: flatten되면 해시 파트로 존재하게 돼 "배열 파트" 전제를 벗어나고,
|
||||
Store 경로로 뒤늦게 도착한 값은 "이 인스턴스의 construction 훅"이라는
|
||||
정의 자체를 만족시킬 수 없음). 타입은 런타임에 지워지므로 정상 우선순위
|
||||
레지스트리에 `{ isHandlable = isPostRef(v), process = error("PostRef는
|
||||
children 배열 리터럴에만 놓을 수 있음") }` Handler를 등록 — pre-pass가
|
||||
이미 소진시키므로 이게 매치되면 곧 타입 차단을 우회한 버그라는 뜻.
|
||||
|
||||
**1회용, 재사용은 즉시 error** — `PreRef`와 같은 `_fired` 플래그를 그대로
|
||||
재사용(위 "PreRef는 '취소'라는 개념이 없다" 절과 같은 근거: 이미 fire된
|
||||
객체를 다시 놓으면 stale `.Value`로 콜백이 조용히 잘못 호출됨). "취소
|
||||
개념이 없다"도 동일 — 체인엔 올라가지만 그 자리 retract가 하드코딩된
|
||||
no-op이라 되돌릴 상태가 없음.
|
||||
|
||||
**타입/판별**: `isPostRef`는 `isPreRef`와 같은 층위의 가장 구체적인 항등
|
||||
체크이고, `isRef`가 그 위에 얹히는 상위 개념 — `Dispatch/Leaf.luau`의
|
||||
일반 Ref 매치는 이제 `isRef(v) and not isPreRef(v) and not isPostRef(v)`.
|
||||
상세는 `base/brand-plan.md`.
|
||||
|
||||
**대표 유스케이스(사용자 제시)** — `ChildAdded` 같은 이벤트에서 **나중에
|
||||
들어오는 것만** 처리하고 싶을 때, `PostRef`의 콜백이 `mounted = true`
|
||||
같은 boolean 플래그를 세워두고 핸들러가 그 플래그를 먼저 보게 하는 패턴.
|
||||
초기 construction 중에 발생한 이벤트와 그 이후 동적으로 들어온 것을
|
||||
사용자 코드가 스스로 구분할 수 있게 해주는, `PreRef`만으론 표현이 안 되던
|
||||
자리임.
|
||||
|
||||
|
|
|
|||
|
|
@ -78,9 +78,9 @@ InstanceChild.luau`. Slot은 "뮤터블 배열"을 다루고 이 핸들러는 "
|
|||
허용해야 할 이유가 없어짐. `element == nil`뿐 아니라 `element == None`도
|
||||
`Add`(및 내부 `raw*`)에서 즉시 `error` — "Slot 안엔 실제로 마운트
|
||||
가능한 값만 들어간다"는 단일 규칙으로 단순화.
|
||||
- **핸들러 계층 값(Ref/PreRef/Observer/Effect/Modifier) 금지, 즉시
|
||||
- **핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Modifier) 금지, 즉시
|
||||
`error`** — `Modifier` 필드가 이 값들을 담으면 즉시 `error`로 확정했던
|
||||
것(`modifier-plan.md` 7번)과 같은 판별 메커니즘(`isRef`/`isPreRef`/
|
||||
것(`modifier-plan.md` 7번)과 같은 판별 메커니즘(`isRef`/`isPreRef`/`isPostRef`/
|
||||
`isObserver`/`isEffect`/`isModifier` Brand predicate)을 그대로 재사용.
|
||||
근거: `Dispatch/Leaf.luau`가 처리하는 "children 배열에 `Ref`/`Observer`/
|
||||
`PreRef`가 직접 놓이는" 케이스는 **그 컴포넌트가 지금 만들고 있는
|
||||
|
|
@ -611,7 +611,7 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. `
|
|||
- `Add`: element가 이미 어딘가(같은 Slot이든 다른 Slot이든) 마운트돼
|
||||
있으면 에러 — "라이브러리 차원에서 다중 마운팅 절대 금지" 원칙을
|
||||
CRUD 경로에도 동일 적용. `element`가 `nil`/`None`이거나 핸들러 계층
|
||||
값(Ref/PreRef/Observer/Effect/Modifier)이면 에러 — 위 "요소 타입 제약" 절.
|
||||
값(Ref/PreRef/PostRef/Observer/Effect/Modifier)이면 에러 — 위 "요소 타입 제약" 절.
|
||||
`index`가 범위 밖(1..현재 개수+1, 즉 끝에 추가하는 위치까지 포함)이면
|
||||
에러 — **clamp 안 함**(2026-08-10 세션 확정): index가 조용히 다른
|
||||
자리로 보정되면 "의도한 위치가 아닌데 그대로 성공한" 조용한 버그가
|
||||
|
|
@ -1382,7 +1382,7 @@ result = SomeComponent(props)`가 `Instance`를 리턴하든 `Slot`(멀티루트
|
|||
### 요소 타입 — `Slot` 허용
|
||||
|
||||
위 "요소 타입 제약" 절 갱신대로 `isMountable`이 `isSlot(v)`을 더 이상
|
||||
배제하지 않음 — 나머지(Ref/PreRef/Observer/Effect/Modifier 금지,
|
||||
배제하지 않음 — 나머지(Ref/PreRef/PostRef/Observer/Effect/Modifier 금지,
|
||||
nil/None 금지)는 그대로.
|
||||
|
||||
### 재귀 메커니즘 — 새 프리미티브 없이 `Dispatch.setLength`/`setOffsetSource`를 Slot 자신 키로 재사용
|
||||
|
|
|
|||
|
|
@ -254,6 +254,10 @@ RFC가 순수 내부 변경이고 우리 선언이 이미 그 대상 모양이
|
|||
유지해야 함**(`State<T>`가 `Source`를 참조하면 안 됨 — 두 제네릭
|
||||
별칭의 상호 재귀는 솔버가 취약한 패턴).
|
||||
- **`PreRef<T>`가 `Ref<T>` 자리에 대입 가능** — `luau-test/13` A섹션.
|
||||
**[2026-08-14 아홉 번째 세션] `PostRef<T>`도 완전히 같은 관계**(같은
|
||||
`Ref` 런타임 재사용, 브랜드 태그만 다름 — `base/ref-plan.md`의
|
||||
"`PostRef`" 절) — `13`은 지금 `rewrite-required/`라 재작성할 때
|
||||
`PostRef`도 같이 커버할 것.
|
||||
- **Modifier의 제네릭 `__index` + `table.clone` 체이닝** — `luau-test/done/17`.
|
||||
- **콜백 파라미터/본문의 타입 체크**(1번의 쪼개기 적용 시) — 진짜
|
||||
살아있음.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
| | 공유 자원 | 방어 | 상태 |
|
||||
|---|---|---|---|
|
||||
| `Slot` | element | `claimOwner`/`claimOwnerAt` → 즉시 error(`Slot{a,a}`/`Frame{slot,slot}`) | 막힘 |
|
||||
| `PreRef` | 자기 자신 | `_fired` → 재사용 시 error | 막힘 |
|
||||
| `PreRef`/`PostRef` | 자기 자신 | `_fired` → 재사용 시 error | 막힘 (`PostRef`는 2026-08-14 아홉 번째 세션 신설, 같은 가드 그대로) |
|
||||
| `Tag` | 태그 이름 | 위치별 참조 카운트 — 겹침이 **의도된 동작**(합집합) | 설계상 정상 |
|
||||
| `Attribute` | 이름 | 그룹 전용 키 + 이름 claim → 즉시 error | 막힘(0-Z 해소, 14차 세션) |
|
||||
| **`Ref`** | 자기 자신 | **없음** | **이 항목** |
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
쓰기 시작했음).
|
||||
- **`Brand`(3순위, 사소함, 2026-08-07 여덟 번째 세션 추가)**: 런타임
|
||||
nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`, `isState`를
|
||||
10종 branded 타입 전부로 일반화) — `brand-plan.md`의 `Brand`
|
||||
branded 타입 전부로 일반화) — `brand-plan.md`의 `Brand`
|
||||
절에서 동작/구현 방식은 확정, "OOP 인스턴스의 클래스명을 얻는 느낌"을
|
||||
전달할 더 나은 이름이 있는지가 열린 질문(사용자가 직접 제기) — `Tag`는
|
||||
이미 quad-roblox의 `CollectionService` 래퍼로 쓰여서 이름 충돌, 후보로
|
||||
|
|
@ -167,6 +167,16 @@
|
|||
이미 없앴으니 급하지 않지만, 최종 이름은 여전히 이 목록의 다른
|
||||
가칭들과 함께 검토 대상. `base/attribute-plan.md` "그룹 `Attribute(...)`"
|
||||
절 참고.
|
||||
- **`OnDestroyed`(3순위, 사소함, 2026-08-14 아홉 번째 세션 추가)**:
|
||||
`base/lifecycle-hooks-plan.md`가 이 이름으로 **확정**하되, 위 0-B
|
||||
(`dispose(any)` — 시그니처/범위)가 "quad가 만드는 모든 것의 유일한 파괴
|
||||
경로"로 풀리면 `OnDisposed`와 맞추는 재검토 여지를 남겨둠. 지금
|
||||
`OnDestroyed`인 이유는 실제 트리거가 `dispose()` 호출이 아니라 엔진
|
||||
`Destroying` 신호라서(그 문서 "이름 컨벤션" 절). 이름은 런타임에 아무
|
||||
의미가 없는 순수 네이밍이라 바꾸는 비용이 0에 가까움 — **0-B가 풀리기
|
||||
전엔 이 항목을 열지 말 것**(형제 `OnCreated`/`OnRendered`는 재검토
|
||||
대상 아님, 다만 `OnRendered`엔 "부모에 붙기 전에 불린다"는 캐비엇이
|
||||
있어 이름이 아니라 *문서화*로 대응하기로 확정됨).
|
||||
- **참고 — 이미 지나간 사례**: `register`(v1) → `State`(v2) 리네임은
|
||||
"모호함"은 풀었지만 "다른 뜻으로 이미 쓰이는 단어"라는 새 문제를 만든
|
||||
셈 — 이번 정리에서 같은 패턴을 조심할 것.
|
||||
|
|
|
|||
134
.claude/session/2026-08-14-09-postref-confirmed.md
Normal file
134
.claude/session/2026-08-14-09-postref-confirmed.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# 2026-08-14 아홉 번째 세션 — `PostRef` 확정, `OnRendered` 채택, `PreRef`/`PostRef` 계열 안 순서 미보장으로 역전, `lifecycle-hooks-plan.md` base 승격
|
||||
|
||||
## 사용자 지시 (원문 그대로)
|
||||
|
||||
> PostRef 확정. PreRef 랑 PostRef(lifecycle-hooks-plan.md 에서 스케치됨)
|
||||
> 을 이제 완전 ref 에 반영 가능할듯. PostRef 는 child 전부 마운트 된 후에
|
||||
> 처리됨 - 중간 for문에서 모든 Slot, State<Frame>, Frame 등 요소의 마운트가
|
||||
> 처리될것이므로, Slot 의 실 확정이 바로 요소를 주입해주므로, 바로 뒤에서
|
||||
> 실행하면 모든 트리가 완성된 이후가 됨. '착수 시점에 판단할 선택지' 는
|
||||
> (a) 택해도 될듯 하고, Pre-Post 둘을 지원 안 할 이유가 없고, 구현 난이도가
|
||||
> 아주 낮아서 괜찮다는게 내 결론. ChildAdded 를 나중에 들어오는것만 처리하고
|
||||
> 싶어서, boolean 으로 flag 지정해둔다던가 뭐, 아에 쓸모 없을 만한건 아니라
|
||||
> 봄. Ref 는 그냥 설정해주고, 언제 설정할진 모름. PreRef 는 프로퍼티/자식
|
||||
> 들어오기 전에 설정됨. 각 PreRef 간의 순서는 보장 안함. PostRef 는
|
||||
> 프로퍼티/자식 들어온 후 설정됨, PostRef간 순서는 보장 안함(실제로 하더라도,
|
||||
> 안 한다고 두는게 버그를 차라리 덜 만든다고 생각함. 같은 계열 내 Ref 등록
|
||||
> 순서에 의존하는 무언가가 생겨선 안된다는 생각, 그건 구조부터 잘못된거니까)
|
||||
> OnRendered 도 채택. ref 문서 업데이트와 lifecycle-hooks-plan.md 의 승격을
|
||||
> 시키고, 핸드오버/세션기록 준비하고 커밋해줘, 뭔가 걸리는게 있으면 말 해
|
||||
|
||||
세션 도중 사용자 추가 메시지 둘: (1) 다른 에이전트가 `session/`에 파일을
|
||||
export 중이니 커밋 시 예외하라 → (2) 곧이어 "이제 너 말고 다른 에이전트
|
||||
없어, 무엇을 바꾸든 상관 없어"로 해제.
|
||||
|
||||
## 확정된 것
|
||||
|
||||
### 1. `PostRef` — base 프리미티브로 확정
|
||||
|
||||
여섯 번째 세션이 백로그 후보로만 남겨뒀던 스케치(그 뒤 네 번째 세션이
|
||||
`ProcessedPreRef` 선례를 반영해 갱신)를 그대로 채택. 선택지 (a)/(b)/(c)
|
||||
중 **(a)** — pre-pass 공동 수집 + 두 패스 뒤 `postRefList` 소비.
|
||||
|
||||
메커니즘(정본은 `base/ref-plan.md`의 "`PostRef`" 절):
|
||||
|
||||
1. 기존 `PreRef` pre-pass **한 스윕**에 분기 하나 추가 — `isPostRef(v)`면
|
||||
fire하지 않고 로컬 배열 `postRefList`에 push, 즉시
|
||||
`flattened[i] = ProcessedPostRef`로 소진(+`_fired` 세팅).
|
||||
2. 정상 두 패스가 `ProcessedPostRefHandler`로 그 자리를 매치해
|
||||
`setLength(0)`/`setOffsetSource(None)` 등록(= `ProcessedPreRefHandler`의
|
||||
완전한 거울상, 코드 한 글자 차이).
|
||||
3. 두 패스가 끝난 뒤 `Dispatch.drive`가 `postRefList`를 순회하며 fire.
|
||||
|
||||
새 전체 순회 없음 — 추가 비용은 실제 `PostRef` 개수만큼의 짧은 루프뿐.
|
||||
동적 경로 가드 Handler, 1회용 `_fired`, Modifier/Store 타입 차단, `isRef`
|
||||
포함 관계까지 전부 `PreRef`의 거울상으로 그대로 복제됨.
|
||||
|
||||
### 2. 스코프 — 원래 문서의 (a)/(b) 구분이 애초에 잘못된 축이었음
|
||||
|
||||
원 문서는 "(a) 자기 프로퍼티/이벤트만 vs (b) 자식 서브트리까지"를 열어두고
|
||||
"(a) 메커니즘은 (b)를 못 준다"고 적어놨었는데, **사용자 지적으로 그게
|
||||
틀렸음이 드러남** — 배열 파트 루프가 각 자식의 마운트를 동기적으로 끝내고
|
||||
넘어가고(`Slot`은 실 확정 시 요소를 그 자리에서 주입, `State<Frame>`도
|
||||
최초 값을 그 자리에서 처리) 해시 파트는 그 뒤이므로, (a) 메커니즘이 사실상
|
||||
(b) 스코프를 공짜로 줌.
|
||||
|
||||
**대신 진짜 경계는 "자기 아래 vs 자기 위"였음** — Claude가 추가로 짚은
|
||||
캐비엇: `PostRef`는 자기 서브트리 완성은 보장하지만 **이 인스턴스가
|
||||
부모에 붙는 것(`.Parent` 대입)보다는 여전히 먼저** 불림. `Frame{ Frame{...} }`
|
||||
처럼 리터럴로 중첩하면 안쪽 `Frame` 호출이 먼저 완결되어야 바깥 `Frame`의
|
||||
props 테이블이 완성되기 때문. React `componentDidMount`(DOM 삽입 **후**)와
|
||||
다르므로 `OnRendered` 이름과 함께 반드시 문서화하기로 함.
|
||||
|
||||
### 3. `PreRef`/`PostRef` 계열 안 fire 순서 — 미보장으로 역전
|
||||
|
||||
2026-08-07 아홉 번째 세션이 "복수 `PreRef` 간 순서 = 배열 index 순서
|
||||
**그대로 보장**"으로 확정해뒀던 걸 뒤집음. **구현이 아니라 계약만** 바뀜
|
||||
(pre-pass는 여전히 index 순서로 훑음). 사용자 근거: "같은 계열 내 Ref 등록
|
||||
순서에 의존하는 무언가가 생겨선 안된다는 생각, 그건 구조부터 잘못된거니까."
|
||||
|
||||
역전 원문·근거·영향 범위는 `archive/preref-order-guaranteed-reversed.md`.
|
||||
보장이 그대로 유지되는 건 호이스팅(`PreRef` 전체가 나머지보다 먼저)과
|
||||
`PostRef`("두 패스가 전부 끝난 뒤") 쪽.
|
||||
|
||||
### 4. `OnRendered` 채택 + `lifecycle-hooks-plan.md` base 승격
|
||||
|
||||
`OnRendered(fn) = PostRef():Callback(fn)`. 이걸로 그 문서의 마지막 열린
|
||||
항목(채택 여부/메커니즘/스코프)이 전부 닫혀 `research/` → `base/` 승격
|
||||
(`git mv`). 패키지는 quad-base 확정. 남은 건 `OnDestroyed` 이름 재검토
|
||||
여지 하나뿐이라 `question.md` 용어 정리 대기열에 3순위로 올림 —
|
||||
`Slot`/`Brand`처럼 "base 확정, 이름만 재검토 대상"인 기존 항목들과 같은
|
||||
취급.
|
||||
|
||||
**우선순위는 두 층위로 갈림**: `PostRef` 프리미티브는 디스패치 코어라
|
||||
ROADMAP M8에서 `PreRef`와 같이 구현되고, 훅 슈가 셋만 형제 백로그
|
||||
(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)와 동급으로 맨 뒤.
|
||||
|
||||
### 5. 대표 유스케이스 (사용자 제시)
|
||||
|
||||
`ChildAdded` 같은 이벤트에서 **나중에 들어오는 것만** 처리하고 싶을 때,
|
||||
`PostRef` 콜백이 `mounted = true` 플래그를 세우고 핸들러가 그걸 먼저 보는
|
||||
패턴. 초기 construction 중 발생한 이벤트와 그 이후 동적으로 들어온 것을
|
||||
사용자 코드가 스스로 구분할 수 있게 해줌 — `PreRef`만으론 표현이 안 되던
|
||||
자리.
|
||||
|
||||
## 반영한 파일
|
||||
|
||||
- `base/ref-plan.md` — 제목 `Ref / PreRef / PostRef`, 승격/역전 배너,
|
||||
"`PostRef`" 절 신설(타이밍 대조표/보장 범위/메커니즘/Handler/가드/
|
||||
유스케이스), `PreRef` 순서 bullet 역전, pre-pass 서술에 `postRefList` 반영.
|
||||
- `base/lifecycle-hooks-plan.md` — `research/`에서 `git mv`, 상태를 base
|
||||
확정으로, `OnRendered` 절 신설, ② 절을 "보류"에서 "채택 확정"으로
|
||||
(역전 배너 + 근거 보존), 스코프/선택지/이름/패키지/우선순위/열린 질문 절
|
||||
전면 갱신.
|
||||
- `base/dispatch-core-plan.md` — `Dispatch.drive` 정의에 pre-pass/후행
|
||||
`postRefList` 소비 명시, `None` 센티널 절과 Length/Offset 절에
|
||||
`ProcessedPostRef` 대칭 반영.
|
||||
- `base/brand-plan.md` — `PostRefTag`/`isPostRef` 추가, `isRef` 포함
|
||||
관계를 셋으로 확장, Leaf 핸들러 predicate 갱신.
|
||||
- `base/architecture.md` — 소스 트리에 `PostRef.luau`/`LifecycleHooks.luau`,
|
||||
`Leaf.luau` 주석, 생성자/유틸 목록.
|
||||
- `base/slot-plan.md`/`base/modifier-plan.md` — 핸들러 계층 값 금지
|
||||
목록에 `PostRef` 추가(4곳+4곳).
|
||||
- `base/typing-limits.md` — `PostRef<T>`도 `PreRef<T>`와 같은 서브타입
|
||||
관계, 스파이크 `13` 재작성 시 같이 커버할 것.
|
||||
- `archive/preref-order-guaranteed-reversed.md` — 신설.
|
||||
- `.claude/README.md` — lifecycle-hooks 행을 research→base 표로 이동·전면
|
||||
갱신, `ref-plan.md`/`brand-plan.md` 행 갱신, archive 새 행.
|
||||
- `ROADMAP.md` — M8 체크리스트에 `PostRef.luau`/`postRefList` 소비/
|
||||
`ProcessedPostRefHandler`/동적 경로 가드 추가, pre-pass 항목의 순서
|
||||
보장 문구 역전 반영, Brand/Leaf 항목 갱신, 백로그에 훅 슈가 셋 추가.
|
||||
- `question.md` — 용어 대기열에 `OnDestroyed` 항목, `Brand` 항목의
|
||||
"10종" 표현 정리.
|
||||
- `CLAUDE.md` — 백로그 항목 갱신 + 이 세션 요약.
|
||||
|
||||
`python3 .claude/tools/doc-check.py` ERROR 0 유지 확인.
|
||||
|
||||
## Claude가 걸린다고 보고한 것
|
||||
|
||||
1. **`OnRendered`가 "부모에 붙기 전"에 불린다는 캐비엇** — 위 2번.
|
||||
이름이 React 관용어라 오해를 부를 수 있어 문서화로 대응하기로 함
|
||||
(사용자가 채택을 지시했으므로 이름 자체는 유지).
|
||||
2. **순서 미보장은 계약 변경이지 구현 변경이 아님** — 실제로는 여전히
|
||||
index 순서로 fire될 것이므로, 사용자 코드가 우연히 그 순서에 기대도
|
||||
당장은 안 깨짐(그래서 문서에 명시적으로 "의존 금지"로 적어둠).
|
||||
38
CLAUDE.md
38
CLAUDE.md
|
|
@ -246,15 +246,19 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션
|
|||
`Fallback`과 `xpcall`+`debug.traceback` 기반 `Traceback`으로 분리,
|
||||
`err: any` 확정, 패키지·이름 전부 확정 — **설계만 끝났을 뿐 구현
|
||||
우선순위는 그대로 맨 뒤**), 생명주기 훅
|
||||
`OnCreated`/`OnDestroyed`(**[2026-08-14 신설]** `PreRef`/`Effect`를
|
||||
반환하는 순수 팩토리 함수 슈가, `research/lifecycle-hooks-plan.md` —
|
||||
`OnRendered`는 base에 없는 post-pass가 필요해 공짜가 아니라 지금은
|
||||
의도적으로 구현 안 함, 거울상 `PostRef` 스케치만 백로그 후보) — 전부
|
||||
`OnCreated`/`OnRendered`/`OnDestroyed`(**[2026-08-14 아홉 번째 세션,
|
||||
`research/`에서 `base/lifecycle-hooks-plan.md`로 승격]** 각각
|
||||
`PreRef`/`PostRef`/`Effect`를 반환하는 순수 팩토리 함수 슈가 —
|
||||
`OnRendered`도 **채택 확정**, 그게 얹히는 `PostRef` 프리미티브 자체는
|
||||
슈가가 아니라 디스패치 코어라 **ROADMAP M8에서 `PreRef`와 같이 구현됨**
|
||||
(백로그가 아님, `base/ref-plan.md`의 "`PostRef`" 절). 훅 슈가 셋만
|
||||
후순위) — 전부
|
||||
"quad 개발 상당 부분 끝난 뒤"로 사용자가 못박은 후순위. 상세는
|
||||
`.claude/README.md`의 `base/` 표(`fallback-plan.md`)와 `research/` 표
|
||||
`.claude/README.md`의 `base/` 표(`fallback-plan.md`/
|
||||
`lifecycle-hooks-plan.md`)와 `research/` 표
|
||||
(`debug-tooling-plan.md`/`documentation-plan.md`/
|
||||
`documentation-content-map.md`/`framework-comparison-findings.md`/
|
||||
`operator-sugar-plan.md`/`lifecycle-hooks-plan.md`).
|
||||
`operator-sugar-plan.md`).
|
||||
**[2026-08-14 추가, 성격이 다름]** 시간 기반 전파 게이트
|
||||
`Debounce`/`Throttle`(`research/debounce-throttle-plan.md`)도 백로그이긴
|
||||
하나 위 항목들과 달리 **사용자가 직접 요청한 실제 기능 갭**이고 순수
|
||||
|
|
@ -1309,3 +1313,25 @@ pull-recompute+캐시가 막고 중복 **통지**는 안 접음(접으려면 `Bl
|
|||
쌓였다가 통째로 철회됨. **확정 문서의 한 문장을 근거로 새 설계를 세울 땐,
|
||||
그 문장이 *같은 문서의 다른 확정 문장*과 모순되지 않는지까지 확인할 것** —
|
||||
`doc-check.py`는 참조 존재는 봐도 서술 간 모순은 못 봄.
|
||||
|
||||
**2026-08-14 아홉 번째 세션 — `PostRef` 확정·`OnRendered` 채택, 계열 안
|
||||
fire 순서 미보장으로 역전, `lifecycle-hooks-plan.md` base 승격**
|
||||
(`session/2026-08-14-09-postref-confirmed.md`)
|
||||
사용자가 백로그 후보로만 남아 있던 `PostRef`를 확정(선택지 (a) — pre-pass
|
||||
공동 수집 + 두 패스 뒤 `postRefList` 소비, "Pre-Post 둘을 지원 안 할 이유가
|
||||
없고 구현 난이도가 아주 낮음"). `PreRef`의 거울상이라 소진 센티널
|
||||
(`ProcessedPostRef`)·전담 Handler·동적 경로 가드·`_fired`·타입 차단이 전부
|
||||
복제 — `base/ref-plan.md`에 "`PostRef`" 절로 편입. **원 문서가 열어뒀던
|
||||
(a)/(b) 스코프 구분은 애초에 잘못된 축이었음이 드러남**: 배열 파트 루프가
|
||||
각 자식 마운트를 동기적으로 끝내므로 (a) 메커니즘이 (b)(서브트리 완성)를
|
||||
공짜로 줌 — 진짜 경계는 **"자기 아래 vs 자기 위"**로, `PostRef`는 서브트리
|
||||
완성은 보장하되 **이 인스턴스가 부모에 붙기 전**에 불림(React
|
||||
`componentDidMount`와 다름, `OnRendered` 이름 때문에 문서화 필수 캐비엇).
|
||||
같은 세션에 **`PreRef`/`PostRef` 계열 안 fire 순서를 "배열 index 순서 보장"
|
||||
에서 미보장으로 역전**(2026-08-07 아홉 번째 세션 결정을 뒤집음, 구현이
|
||||
아니라 계약만 — "같은 계열 내 등록 순서에 의존하는 코드가 생겨선 안 됨",
|
||||
`archive/preref-order-guaranteed-reversed.md`). `OnRendered` 채택으로
|
||||
`lifecycle-hooks-plan.md`의 마지막 열린 항목이 닫혀 `base/`로 승격, 남은
|
||||
건 `OnDestroyed` 이름 재검토 여지 하나(0-B 확정 시, `question.md` 용어
|
||||
대기열 3순위). ROADMAP M8/백로그·README·brand/architecture/slot/modifier/
|
||||
typing-limits 전파 완료, `doc-check.py` ERROR 0.
|
||||
|
|
|
|||
66
ROADMAP.md
66
ROADMAP.md
|
|
@ -128,7 +128,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
재정정됨 — `isPreRef`가 가장 구체적인 항등, `isRef`는 그 위에 얹혀
|
||||
`isPreRef`도 `true`로 통과시킴(PreRef가 Ref 런타임을 재사용하는
|
||||
것과 정합). `(v=Ref)` children leaf 매치 핸들러는 이제
|
||||
`isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 함. `isModifier`는
|
||||
`isRef(v) and not isPreRef(v) and not isPostRef(v)`로 명시적으로
|
||||
좁혀야 함(**[2026-08-14 아홉 번째 세션]** `PostRef` 확정으로 제외
|
||||
항 하나 추가, `isPostRef`도 `isRef` 아래 형제로 신설). `isModifier`는
|
||||
여전히 단순 항등, 상위 개념 없음. **[정정, 2026-08-11 아홉 번째
|
||||
세션]** `isAttribute` 하나였던 게 `isAttributeKey`(단일 키 DI 키
|
||||
predicate, 해시파트 `k`를 판별)와 `isAttribute`(그룹 값 predicate,
|
||||
|
|
@ -196,7 +198,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
미주입 상태도 이 경로로 자동 커버, `pre-implementation-audit.md`
|
||||
1-3/1-4), 핸들러 등록/정렬 시점 동률 감지 print 경고 +
|
||||
`Dispatch.listHandlers()` 디버그 유틸
|
||||
- [ ] `Dispatch/Leaf.luau` — `(i:number, v=Ref/Observer/PreRef)` children-array
|
||||
- [ ] `Dispatch/Leaf.luau` — `(i:number, v=Ref/Observer/PreRef/PostRef)` children-array
|
||||
leaf 매칭 Handler, `StoreBind.luau`와 같은 층위(범용/엔진무관) —
|
||||
quad-base 소속으로 확정(2026-08-08 두 번째 세션, `base/
|
||||
dispatch-core-plan.md` "Dispatch는 프리미티브가 아니다" 절)
|
||||
|
|
@ -573,34 +575,50 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
## M8 — Ref
|
||||
|
||||
- [ ] `Ref.luau`(`.Value` 읽기 전용 필드 + `:Set(value)`/`:Callback(fn)`/
|
||||
`:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`(별도 파일, Ref
|
||||
`:Wait(thread?)`, 전부 self 반환) + `PreRef.luau`/`PostRef.luau`(별도 파일, Ref
|
||||
런타임 재사용 + children 배열 전용, Modifier/Store 타입 차단,
|
||||
위치 무관 호이스팅 pre-pass — `base/ref-plan.md` "`phase`
|
||||
옵션 폐기 → 위치로 표현, `PreRef` 신설" 절 + "API 모양" 절)
|
||||
- [ ] `(v=Ref)` 매치 핸들러 — children 배열의 숫자 슬롯에 놓인
|
||||
`Ref(default)` 인스턴스를 인식해 바인드(별도 `CreatedRef` 래퍼
|
||||
없음 — 이름 자체가 폐기됨, 아래 참고)
|
||||
- [ ] `PreRef` pre-pass — 새 `Dispatch.*` 함수 없이 `Dispatch.drive(inst,
|
||||
flattened)` 자신이 두 패스(배열→해시) 루프 전에 배열 파트를 훑어
|
||||
`PreRef` 항목만 fire(Dispatch.process/getHandler 우회하는 raw 루프,
|
||||
`flatten` 함수에는 얹지 않음 — 재바인드 시 flatten 재호출 가능성과
|
||||
충돌하므로 기각). 복수 `PreRef`는 배열 index 순서 그대로(별도 규칙
|
||||
없음). fire된 슬롯은 그 자리에서 소진(**[정정, 2026-08-14 두 번째
|
||||
세션] `None`이 아니라 전용 센티널 `ProcessedPreRef` 처리** — 아래
|
||||
`ProcessedPreRefHandler` 항목이 그 자리를 정상 두 패스로 마저 처리)
|
||||
— `base/ref-plan.md` "PreRef" 절
|
||||
- [ ] `PreRef`/`PostRef` pre-pass — 새 `Dispatch.*` 함수 없이
|
||||
`Dispatch.drive(inst, flattened)` 자신이 두 패스(배열→해시) 루프
|
||||
전에 배열 파트를 **한 번** 훑어, `PreRef`는 그 자리에서 fire하고
|
||||
`PostRef`는 로컬 `postRefList`에 push만 함(Dispatch.process/getHandler
|
||||
우회하는 raw 루프, `flatten` 함수에는 얹지 않음 — 재바인드 시 flatten
|
||||
재호출 가능성과 충돌하므로 기각). **복수 `PreRef`/`PostRef`의 계열 안
|
||||
상대 순서는 보장 안 함**(**[역전, 2026-08-14 아홉 번째 세션]** 예전엔
|
||||
"배열 index 순서 그대로"를 보장으로 명시했었음 —
|
||||
`archive/preref-order-guaranteed-reversed.md`, 구현은 그대로고 계약만
|
||||
좁힌 것). fire/수집된 슬롯은 그 자리에서 소진(**[정정, 2026-08-14 두
|
||||
번째 세션] `None`이 아니라 전용 센티널 `ProcessedPreRef`/
|
||||
`ProcessedPostRef` 처리** — 아래 `Processed*Handler` 항목이 그 자리를
|
||||
정상 두 패스로 마저 처리)
|
||||
— `base/ref-plan.md` "PreRef" 절 / "`PostRef`" 절
|
||||
- [ ] **[2026-08-14 아홉 번째 세션 신설]** `PostRef.luau` + 두 패스 뒤
|
||||
`postRefList` 소비 루프 — `PreRef.luau`와 같은 방식(`Ref` 런타임
|
||||
재사용 + 브랜드 태그만 다름, children 배열 리터럴 전용, Modifier/Store
|
||||
타입 차단, `_fired` 1회용 가드). `Dispatch.drive`가 해시 파트까지
|
||||
끝낸 뒤 `postRefList`를 순회하며 각 `PostRef`를 fire — 배열 재순회가
|
||||
아니라 실제 개수만큼의 짧은 루프. **보장 범위 주의**: 자기 서브트리
|
||||
완성은 보장하되 **이 인스턴스가 부모에 붙는 것보다는 먼저**임
|
||||
— `base/ref-plan.md` "`PostRef`" 절
|
||||
- [ ] `PostRef` 동적 경로 가드 Handler — `PreRef`의 것과 완전한 거울상
|
||||
(`{isHandlable = v is PostRef, process = error(...)}`), 같은 절 참고
|
||||
- [ ] `PreRef` 동적 경로 가드 Handler — `{isHandlable = v is PreRef,
|
||||
process = error(...)}` 형태로 정상 우선순위 레지스트리에 등록,
|
||||
`NoneHandler`와 같은 "한 값 종류 전담" 패턴. 리터럴 배열 경로는
|
||||
pre-pass가 이미 소진시키므로 이 Handler가 매치되면 곧 타입 차단을
|
||||
우회한 버그라는 뜻 — 같은 절 참고
|
||||
- [ ] **[2026-08-14 두 번째 세션 신설]** `ProcessedPreRefHandler` —
|
||||
`{isHandlable = v == ProcessedPreRef, process = setLength(0)+
|
||||
setOffsetSource(None)+no-op retract}` 형태로 정상 우선순위
|
||||
레지스트리에 등록, `NoneHandler`와 같은 "한 값 종류 전담" 패턴.
|
||||
PreRef pre-pass가 소진시킨 자리가 Length/Offset에 "0 기여"를 등록할
|
||||
책임을 지는 자리 — `base/ref-plan.md` "PreRef" 절, `base/
|
||||
dispatch-core-plan.md` "Length/Offset" 절
|
||||
- [ ] **[2026-08-14 두 번째 세션 신설]** `ProcessedPreRefHandler` +
|
||||
**[아홉 번째 세션] `ProcessedPostRefHandler`**(완전한 거울상, 코드
|
||||
한 글자 차이) — `{isHandlable = v == Processed*Ref, process =
|
||||
setLength(0)+setOffsetSource(None)+no-op retract}` 형태로 정상
|
||||
우선순위 레지스트리에 등록, `NoneHandler`와 같은 "한 값 종류 전담"
|
||||
패턴. pre-pass가 소진시킨 자리가 Length/Offset에 "0 기여"를 등록할
|
||||
책임을 지는 자리 — `base/ref-plan.md` "PreRef" 절 / "`PostRef`" 절,
|
||||
`base/dispatch-core-plan.md` "Length/Offset" 절
|
||||
- [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면
|
||||
`coroutine.resume(v, self)`+`nil`로 소진(2026-08-09 열한 번째
|
||||
세션 최종 정정 — 순서 안 중요 + 슬롯 재사용 위해 `None`이 아닌
|
||||
|
|
@ -767,3 +785,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
주의**)가 `bindLifetime`/`canExecute`와 같은 base 범용 유틸 그룹에
|
||||
추가될 예정이라는 것만 M1 설계 시 인지. `os.clock()`은 Luau 표준
|
||||
라이브러리라 주입 대상 아님(단 절대 시각이 아니라 diff 전용)
|
||||
- [ ] **[2026-08-14 아홉 번째 세션 신설]** 생명주기 훅 슈가
|
||||
`OnCreated`/`OnRendered`/`OnDestroyed`(`base/lifecycle-hooks-plan.md`)
|
||||
— 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/
|
||||
`Effect(function() return fn end)`를 반환하는 순수 팩토리 함수
|
||||
3개라, 착수 시점에 그 문서의 코드 스케치를 그대로 옮기면 끝(새 타입/
|
||||
Dispatch 개념 없음, 패키지는 quad-base 확정). **설계는 확정됐지만
|
||||
구현은 형제 백로그(`quad-mock`/`quad-debug`/`Operator`/`Fallback`)와
|
||||
동급으로 맨 뒤** — 없어도 프리미티브를 직접 쓰면 되므로 기능 격차
|
||||
없음. 단 이들이 얹히는 `PostRef` 자신은 슈가가 아니라 디스패치
|
||||
코어의 일부라 **M8에서 `PreRef`와 같이 구현됨**(위 M8 참고).
|
||||
|
|
|
|||
Loading…
Reference in a new issue