docs(base): PreRef/PostRef 계열 안 fire 순서 미보장안 철회 — 배열 index 순서 보장 유지

직전 커밋(bcd02f1)에서 미보장으로 뒤집었던 것을 같은 세션에 철회.
반례는 `FastQuery(...) -> PreRef`류 조합 — 앞자리 항목이 뒤 항목의
전제를 만들어주는 정당한 합성이고, children 마운트 선후가 배열 위치로
정해지는 것과 같은 성격. 보장 비용 0(pre-pass가 어차피 index 순서)이고
배열 파트 index 순서는 이미 백엔드 이식성 때문에 명시적 계약이라 새로
내주는 자유도 없음. 보장 + 스타일 권고를 같이 기재.

- archive/preref-order-guaranteed-reversed.md → preref-order-unguaranteed-withdrawn.md
  (제안·철회 양쪽 논거 보존)
- OnRendered의 "부모에 붙기 전에 불림" 캐비엇은 사용자 지시대로 이름 유지 +
  명시만(ref-plan "보장 범위" 항목, lifecycle-hooks-plan ⚠️ 항목)
- ref-plan/lifecycle-hooks-plan/ROADMAP/README/CLAUDE.md/세션 로그 동기화

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-14 05:54:05 +09:00
parent bcd02f1cec
commit f9f0babc87
Signed by: qwreey
GPG key ID: D28DB79297A214BD
8 changed files with 193 additions and 126 deletions

View file

@ -48,7 +48,7 @@
| `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`/`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`) |
| `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까지 완전 대칭). 보장 범위는 "자기 서브트리 완성"이고 **자기가 부모에 붙는 것보다는 여전히 먼저**임에 주의. 계열 안 fire 순서는 **배열 index 순서 보장 유지**(같은 세션에 미보장으로 뒤집었다 철회 — `archive/preref-order-unguaranteed-withdrawn.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`를 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`는 사용자가 직접 처리(에이전트 범위 제외) |
@ -83,7 +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`도 같은 규칙 |
| `preref-order-unguaranteed-withdrawn.md` | **[철회됨, 2026-08-14 아홉 번째 세션 신설]** 복수 `PreRef`/`PostRef` 간 fire 순서를 "배열 index 순서 보장"에서 **미보장으로 바꾸려던 안** — 같은 세션에 제안·철회, 현재 계약은 **보장**(2026-08-07 결정 그대로). 반례는 `FastQuery(...) -> PreRef`류 조합(앞자리 항목이 뒤 항목의 전제를 만들어주는 정당한 합성), 보장 비용 0 + 배열 파트 index 순서 계약의 자동 귀결이라 새로 내주는 자유도 없음. 양쪽 논거 보존 |
| `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 전달로 충분하다는 판단 |

View file

@ -1,63 +0,0 @@
# [역전됨] 복수 `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 순서 그대로(별도 규칙
없음)" 체크리스트 문구 정정.
- 코드 영향 없음(구현이 안 바뀌므로) — 바뀐 건 문서화 대상 계약뿐.

View file

@ -0,0 +1,75 @@
# [철회됨] 복수 `PreRef`/`PostRef` 간 fire 순서를 "미보장"으로 두려던 안
**제안·철회 모두 2026-08-14 아홉 번째 세션**(원 결정: 2026-08-07 아홉 번째
세션). **현재 유효한 계약은 "배열 index 순서 그대로 보장"** — 즉
2026-08-07 결정이 그대로 살아 있음, 정본은 `base/ref-plan.md`
"`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절(복수 `PreRef` 간 순서
항목)과 "`PostRef`" 절. 이 파일은 **한 세션 안에서 뒤집었다 되돌린 왕복의
기록**이고, 남겨두는 이유는 양쪽 논거가 나중에 비슷한 판단(“보장할까
말까”)에서 다시 쓸 만해서임.
## 원래 계약 (2026-08-07 아홉 번째 세션, 지금도 유효)
> 복수 `PreRef` 간 순서 — 새 규칙 불필요, **배열 index 순서 그대로**.
> 이 pre-pass는 "배열 파트는 index 순서대로" 계약을 그대로 재사용해
> 리터럴 순서대로 fire하면 됨.
## 미보장으로 바꾸자던 안 (제안 → 같은 세션에 철회)
사용자 원 메모:
> 각 PreRef 간의 순서는 보장 안함. PostRef간 순서는 보장 안함(실제로
> 하더라도, 안 한다고 두는게 버그를 차라리 덜 만든다고 생각함. 같은 계열
> 내 Ref 등록 순서에 의존하는 무언가가 생겨선 안된다는 생각, 그건 구조부터
> 잘못된거니까)
논지: (1) 순서에 의존하는 훅 코드는 애초에 설계가 잘못된 것이니 보장을
제공하면 그 구조를 권장하게 됨, (2) 보장을 안 하면 나중에 pre-pass 구현을
바꿀 자유가 남음, (3) 그 순서에 기대는 사용자 코드가 안 생겨 버그가 덜 남.
**구현은 어차피 안 바뀌고 계약만 좁히는 변경**이었음(pre-pass는 미보장
상태에서도 index 순서로 훑음).
## 왜 철회했나
**결정적 반례 — `FastQuery(...) -> PreRef`류 조합(사용자 제시).**
어떤 팩토리가 `PreRef`를 반환하면서 뭔가를 미리 해결해두면, **배열에서
그 뒤에 오는 `OnCreated`가 "그게 이미 끝났다"를 전제로** 동작할 수 있음:
```lua
Frame {
FastQuery(...), -- PreRef를 반환하는 팩토리
OnCreated(function(inst) -- 위가 끝난 뒤에 불림
...
end),
}
```
이건 "잘못된 구조"가 아니라, **이 배열이 원래 제공하는 정당한 합성
방식**임 — children의 마운트 선후가 배열 위치로 정해지는 것(`base/ref-plan.md`
"`phase` 옵션 폐기 → 위치로 표현, `PreRef` 신설" 절이 `phase` 옵션을
없앨 수 있었던 바로 그 이유)과 정확히 같은 성격이고, 서로 다른 저자가
쓴 항목(팩토리 제공자 vs 호출자)이 한 배열에 섞이는 유일한 자리이기도 함.
곁들여 확인된 것들:
1. **보장 비용이 0** — pre-pass가 어차피 index 순서로 훑고, `PostRef`
`postRefList`도 그 스윕이 만든 목록이라 push 순서가 곧 배열 순서.
2. **미래 구현 자유를 내주는 것도 없음**`base/dispatch-core-plan.md`
이미 "배열 파트는 index 순서대로"를 **백엔드 이식성 때문에 명시적
계약**으로 못박아 뒀으므로, 어떤 백엔드든 index 순서를 제공해야 함.
PreRef 순서 보장은 새 약속이 아니라 그 계약의 자동 귀결.
3. **미보장인데 결정론적이면 실효가 없음** — 런타임이 항상 index 순서로
주는 이상 위반이 테스트에서 절대 안 걸림. (Go의 map 순회 무작위화처럼
디버그 모드에서 일부러 역순으로 돌려 노출시키는 방법은 있으나, 이
사소한 규범 하나를 위해 장치를 더 만들 가치는 없다고 판단.)
4. **계약 표면에 예외가 하나 느는 비용** — "배열 파트는 순서 보장, 단 그
안의 PreRef끼리는 아님"을 읽는 사람이 따로 기억해야 함.
## 남은 것
보장은 유지하되 **스타일 권고**만 문서에 남김 — 순서로 조율해야 할 만큼
얽힌 훅이면 대개 하나의 훅 안에서 순서대로 부르는 게 낫고, 위
`FastQuery`처럼 "앞의 것이 뒤의 것의 전제를 만들어주는" 명시적 합성이
아니면 순서에 기대지 말 것(`base/ref-plan.md`/`base/lifecycle-hooks-plan.md`
양쪽에 기재).

View file

@ -124,7 +124,8 @@ end
아직 없을 수 있음 — React `componentDidMount`가 DOM 삽입 **후**인 것과
다르므로, 이름만 보고 "화면에 올라간 뒤"로 기대하지 않도록 **사용자
문서에 반드시 명시**할 것(아래 "이름 컨벤션" 절도 참고).
- 복수 `OnRendered` 간 상대 순서는 **보장 안 함**(`PostRef` 계약).
- 복수 `OnRendered` 간 상대 순서는 **배열 index 순서로 보장**(`PostRef`
계약, `OnCreated`도 동일).
### `OnDestroyed(fn)`
@ -151,11 +152,26 @@ Frame {
}
```
**단, 같은 계열끼리의 상대 순서는 보장 안 함** — `fn1``fn2`보다 먼저
불린다고 기대하지 말 것(`PreRef`/`PostRef` 둘 다 2026-08-14 아홉 번째
세션에 계열 안 순서를 명시적 미보장으로 확정, `base/ref-plan.md`). 순서가
정말 필요하면 훅을 여럿 등록하는 게 아니라 **하나의 훅 안에서 순서대로
부를 것** — 그게 의도가 코드에 드러나는 유일한 방법.
**같은 계열끼리의 상대 순서는 배열 index 순서로 보장됨** — `fn1`
`fn2`보다 먼저 불림(`PreRef`/`PostRef` 공통 계약, `base/ref-plan.md`).
이게 유용한 대표 사례는 **`PreRef`를 반환하는 다른 팩토리와의 합성**:
예컨대 `FastQuery(...) -> PreRef`처럼 앞자리 항목이 뭔가를 미리
해결해두면, 그 뒤에 오는 `OnCreated(fn)`은 **그게 이미 끝났음을 전제로**
동작할 수 있음(사용자 제시 사례).
```lua
Frame {
FastQuery(...), -- PreRef를 반환하는 팩토리
OnCreated(function(inst) -- 위가 이미 끝난 뒤에 불림
...
end),
}
```
**다만 스타일 권고**: 서로 얽힌 두 훅을 순서로 조율하는 것보다 **하나의
훅 안에서 순서대로 부르는 게** 대개 의도가 더 잘 드러남 — 보장은 하되,
위처럼 "앞의 것이 뒤의 것의 전제를 만들어주는" 명시적 합성이 아니면
기대지 말 것.
`OnCreated(fn)`/`OnRendered(fn)`/`OnDestroyed(fn)` 호출마다
`PreRef()`/`PostRef()`/`Effect(...)`
@ -232,11 +248,11 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
똑같이 필요(pre-pass가 놓쳤을 때만 매치되는 버그 케이스 전용, `error`).
- **두 패스가 끝난 뒤, `Dispatch.drive``postRefList`를 그 순서 그대로
순회하며 각 `PostRef`를 fire** — 별도 후행 전체 재순회가 필요 없음,
pre-pass가 이미 만들어둔 목록을 그대로 소비하면 끝. **[정정, 2026-08-14
아홉 번째 세션]** 복수 `PostRef` 간 순서가 복수 `PreRef`와 같은
원칙이라는 건 그대로지만, 그 원칙 자체가 "배열 index 순서 **보장**"에서
**"보장 안 함"**으로 뒤집혔음 — 구현상 push 순서로 돌 뿐, 계약이
아님(`archive/preref-order-guaranteed-reversed.md`).
pre-pass가 이미 만들어둔 목록을 그대로 소비하면 끝. 복수 `PostRef`
간 순서는 복수 `PreRef`와 같은 원칙(배열 index 순서 그대로, **보장**)이
자연히 적용됨 — 2026-08-14 아홉 번째 세션에 이 보장을 잠깐 미보장으로
뒤집었다가 같은 세션에 철회했음
(`archive/preref-order-unguaranteed-withdrawn.md`).
- 결과적으로 `PreRef``PostRef`는 **소진 메커니즘이 완전히 대칭**
(둘 다 pre-pass에서 즉시 `Processed*` 센티널로 소진, 둘 다 전담
`Processed*Handler`가 Length/Offset을 등록) — 유일한 차이는 "실제

View file

@ -4,9 +4,10 @@
> `base/lifecycle-hooks-plan.md`(당시 `research/`)가 백로그 후보로만 들고 있던 스케치를
> 사용자가 확정("Pre-Post 둘을 지원 안 할 이유가 없고 구현 난이도가 아주
> 낮음") — 아래 "`PostRef`" 절 신설, 그 문서도 `base/lifecycle-hooks-plan.md`
> 같이 승격됨. **같은 세션에 `PreRef`/`PostRef` 각 계열 안의 fire 순서를
> "배열 index 순서 그대로 보장"에서 "보장하지 않음"으로 역전** — 역전 원문과
> 근거는 `archive/preref-order-guaranteed-reversed.md`.
> 같이 승격됨. **같은 계열 안의 fire 순서(복수 `PreRef`끼리, 복수
> `PostRef`끼리)는 배열 index 순서 그대로 보장** — 같은 세션에 이걸 잠깐
> "미보장"으로 뒤집었다가 곧바로 철회했음, 그 왕복의 기록은
> `archive/preref-order-unguaranteed-withdrawn.md`.
> **[2026-08-13 아홉 번째 세션] `bind-system-plan.md`에서 분리됨.** 그
> 문서가 2989줄까지 불어나 사람이 검토하기 어렵고 한 곳의 실수가 미치는
@ -378,20 +379,32 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
패스로 처리하면 됨 — 이 pre-pass는 오직 `PreRef` 타입만 골라내므로
범위가 좁고, "확정된 디스패치 모델" 절의 두 패스 계약과 별개로 그
앞에 얹히는 것.
- **복수 `PreRef` 간 순서 — 보장하지 않음(계약)** ⚠️ **[역전, 2026-08-14
아홉 번째 세션]** 2026-08-07 아홉 번째 세션엔 "배열 index 순서 그대로"를
**보장**으로 명시했었으나, 사용자 판단으로 **의도적 미보장**으로 뒤집음
— 역전 원문·근거는 `archive/preref-order-guaranteed-reversed.md`.
- **구현은 그대로**(pre-pass가 배열을 index 순서로 훑으므로 사실상
리터럴 순서로 fire됨) — 바뀐 건 오직 **계약**임: "같은 계열 안의
등록 순서에 의존하는 코드가 생겨선 안 된다, 그건 구조부터 잘못된
것"이라는 게 사용자가 든 이유(순서에 의존해야 할 정도면 애초에 두
훅으로 나눌 게 아니라 하나의 훅 안에서 순서대로 부르면 됨). 보장을
안 하면 **나중에 pre-pass 구현을 바꿀 자유**도 남고, 그 순서에
기대는 사용자 코드가 생기지 않아 **버그가 오히려 덜 생김**.
- 호이스팅 규칙 자체("`PreRef` 전체가 나머지 전부보다 먼저")는 그대로
**보장**임 — 미보장으로 바뀐 건 "`PreRef`끼리의 상대 순서" 하나뿐.
아래 `PostRef`도 완전히 같은 규칙(계열 안 순서 미보장).
- **복수 `PreRef` 간 순서(2026-08-07 아홉 번째 세션 확정, 2026-08-14
아홉 번째 세션 재확인) — 새 규칙 불필요, 배열 index 순서 그대로
보장.** 같은 인스턴스에 `PreRef`가 여럿 있으면, 이 pre-pass는 위
"props 순회 순서" 절이 이미 확정해둔 "배열 파트는 index 순서대로"
계약을 그대로 재사용해 리터럴 순서대로 fire함 — 서로 다른 우선순위/
순서 개념을 별도로 만들 필요 없음(호이스팅은 "PreRef 전체 대 나머지"에만
적용되는 규칙이지, "PreRef끼리"에는 적용될 게 없음 — PreRef끼리는
그냥 평범한 배열 순회). 아래 `PostRef`도 같은 규칙.
- **왜 보장까지 하는가(2026-08-14 아홉 번째 세션, 한 번 미보장으로
뒤집었다가 철회)** — 잠깐 "계열 안 순서에 의존하는 코드가 생겨선
안 된다"는 이유로 미보장으로 갔었으나 같은 세션에 되돌림. 결정적
반례는 사용자가 든 **`FastQuery(...) -> PreRef`류 조합**: 어떤
팩토리가 `PreRef`를 반환하며 뭔가를 미리 해결해두면, **배열에서
그 뒤에 오는 `OnCreated`가 "그게 이미 끝났다"를 전제로 동작**할 수
있음 — 위치로 선후를 표현하는 이건 잘못된 구조가 아니라 이
배열이 원래 제공하는 정당한 합성 방식임(children 마운트 순서가
위치로 정해지는 것과 같은 성격). 비용도 0 — pre-pass가 어차피
index 순서로 훑고, `base/dispatch-core-plan.md`가 이미 배열 파트
index 순서를 **백엔드 이식성 때문에 명시적 계약**으로 못박아
뒀으므로 여기서 새로 약속하는 게 하나도 없음(=미래 구현 자유를
내주는 것도 없음). 상세한 양쪽 논거는
`archive/preref-order-unguaranteed-withdrawn.md`.
- **다만 스타일 권고는 남김**: 순서에 의존해야 할 정도로 두 훅이
얽혀 있으면 대개 **하나의 훅 안에서 순서대로 부르는 게** 의도가
더 잘 드러남 — 보장은 하되, 위 `FastQuery`처럼 "앞의 것이 뒤의
것의 전제를 만들어주는" 명시적 합성이 아니면 기대지 말 것.
- **호이스팅의 실제 구현 = "물리적 재배치"가 아니라 "완전히 별도의
선행 스캔"(2026-08-07 아홉 번째 세션 후속, 사용자 질문에 답변).**
`Dispatch.drive(inst, flattened)`는 같은 `flattened` 배열을 **두 번
@ -614,17 +627,18 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
| 타입 | fire 시점 | 계열 안 상대 순서 |
|---|---|---|
| `PreRef` | 두 패스보다도 **먼저**(호이스팅 pre-pass) | **보장 안 함** |
| `PreRef` | 두 패스보다도 **먼저**(호이스팅 pre-pass) | 배열 index 순서 **보장** |
| 일반 `Ref` | **정해진 시점 없음** — 그 값이 dispatch에 도착한 순간(배열 위치/Store 도착 시점에 따라 달라짐) | (해당 없음) |
| `PostRef` | 두 패스가 **전부 끝난 뒤** | **보장 안 함** |
| `PostRef` | 두 패스가 **전부 끝난 뒤** | 배열 index 순서 **보장**(= `postRefList` push 순서) |
- 일반 `Ref`의 "언제 채워지는지 모른다"는 건 결함이 아니라 원래 계약임 —
그래서 "이미 채워졌는지 먼저 확인, 없으면 `:Wait()`/`:Callback()`"이
Ref 전체의 관용구로 이미 확정돼 있음(위 "Ref 일반화" 절). `PreRef`/`PostRef`는
그 관용구를 안 써도 되도록 **시점을 계약으로 고정한** 두 특수 케이스.
- **계열 안 순서를 둘 다 미보장으로 두는 이유**는 위 `PreRef` 절의 역전
항목과 같음 — 같은 계열 훅들끼리 등록 순서에 의존하는 코드가 생기면
그건 구조부터 잘못된 것이라, 보장을 안 하는 쪽이 버그를 덜 만듦.
- **계열 안 순서를 둘 다 보장하는 근거**는 위 `PreRef` 절의 같은 항목
참고(`FastQuery(...) -> PreRef`류 조합, 비용 0, 이미 있는 배열 파트
index 순서 계약의 귀결) — `PostRef``postRefList`도 그 index 순서
스윕이 만드는 목록이라 push 순서 = 배열 순서.
**보장 범위 — 무엇이 끝나 있고 무엇이 안 끝나 있는가(중요)**
@ -675,8 +689,8 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
`Dispatch.process` 경로를 타고 아래 전담 Handler에 매치됨.
3. **두 패스가 끝난 뒤 `Dispatch.drive``postRefList`를 순회하며 각
`PostRef`를 fire.** 추가 비용은 전체 배열 재순회가 아니라 **실제
`PostRef` 개수만큼의 순회**뿐. (순회 자체는 자연히 push 순서지만, 위
표대로 **그 순서는 계약이 아님** — 의존 금지.)
`PostRef` 개수만큼의 순회**뿐. push 순서 그대로 돌면 되고, 그게 곧
배열 index 순서라 위 표의 보장이 자동으로 성립(별도 정렬 불필요).
**`ProcessedPostRefHandler` — `ProcessedPreRefHandler`의 거울상, 새 규칙 없음**

View file

@ -1,4 +1,4 @@
# 2026-08-14 아홉 번째 세션 — `PostRef` 확정, `OnRendered` 채택, `PreRef`/`PostRef` 계열 안 순서 미보장으로 역전, `lifecycle-hooks-plan.md` base 승격
# 2026-08-14 아홉 번째 세션 — `PostRef` 확정, `OnRendered` 채택, 계열 안 순서는 미보장으로 갔다가 철회(보장 유지), `lifecycle-hooks-plan.md` base 승격
## 사용자 지시 (원문 그대로)
@ -60,16 +60,32 @@ export 중이니 커밋 시 예외하라 → (2) 곧이어 "이제 너 말고
props 테이블이 완성되기 때문. React `componentDidMount`(DOM 삽입 **후**)와
다르므로 `OnRendered` 이름과 함께 반드시 문서화하기로 함.
### 3. `PreRef`/`PostRef` 계열 안 fire 순서 — 미보장으로 역전
### 3. `PreRef`/`PostRef` 계열 안 fire 순서 — 미보장으로 갔다가 같은 세션에 철회, **보장 유지**
2026-08-07 아홉 번째 세션이 "복수 `PreRef` 간 순서 = 배열 index 순서
**그대로 보장**"으로 확정해뒀던 걸 뒤집음. **구현이 아니라 계약만** 바뀜
(pre-pass는 여전히 index 순서로 훑음). 사용자 근거: "같은 계열 내 Ref 등록
순서에 의존하는 무언가가 생겨선 안된다는 생각, 그건 구조부터 잘못된거니까."
지시 원문대로 처음엔 뒤집었음 — 2026-08-07 아홉 번째 세션이 "복수 `PreRef`
간 순서 = 배열 index 순서 **그대로 보장**"으로 확정해뒀던 걸 미보장으로
바꾸고(`구현이 아니라 계약만` — pre-pass는 여전히 index 순서로 훑음),
역전 기록까지 만들어 커밋(`bcd02f1`).
역전 원문·근거·영향 범위는 `archive/preref-order-guaranteed-reversed.md`.
보장이 그대로 유지되는 건 호이스팅(`PreRef` 전체가 나머지보다 먼저)과
`PostRef`("두 패스가 전부 끝난 뒤") 쪽.
**그 뒤 사용자가 "그냥 순서 보장된다 하면 부작용 크겠음?"이라고 되물어
재검토** — Claude 답변 요지: 기술적 부작용은 사실상 0(pre-pass가 어차피
index 순서, `dispatch-core-plan.md`가 배열 파트 index 순서를 백엔드
이식성 때문에 이미 명시적 계약으로 못박아 둬서 새로 약속하는 게 없음),
오히려 미보장 쪽이 (a) 결정론적이라 위반이 테스트에 안 걸리고 (b) 계약
표면에 예외를 하나 더 만드는 비용이 있음.
**사용자가 결정적 반례를 제시하며 보장으로 확정**:
> 솔찍히 이런게 가능해서: FastQuery(...) -> PreRef, 같은걸 만든다 쳐,
> 아래 다른 oncreated 에서 fastquery 를 완료해냈다면 할 수 있는것들을
> 수행해버릴 수도 있겠긴함. 그냥 완전 보장한다고 해버리자.
즉 앞자리 항목이 뒤 항목의 전제를 만들어주는 **정당한 합성**이 실제로
가능하고, 이건 children 마운트 선후가 배열 위치로 정해지는 것(=`phase`
옵션을 없앨 수 있었던 바로 그 이유)과 같은 성격임. 양쪽 논거는
`archive/preref-order-unguaranteed-withdrawn.md`(파일명도 "역전"에서
"철회"로 바꿔 `git mv`), 문서엔 보장 + 스타일 권고("이런 명시적 합성이
아니면 기대지 말 것")를 같이 남김.
### 4. `OnRendered` 채택 + `lifecycle-hooks-plan.md` base 승격
@ -94,9 +110,10 @@ ROADMAP M8에서 `PreRef`와 같이 구현되고, 훅 슈가 셋만 형제 백
## 반영한 파일
- `base/ref-plan.md` — 제목 `Ref / PreRef / PostRef`, 승격/역전 배너,
- `base/ref-plan.md` — 제목 `Ref / PreRef / PostRef`, 승격 배너,
"`PostRef`" 절 신설(타이밍 대조표/보장 범위/메커니즘/Handler/가드/
유스케이스), `PreRef` 순서 bullet 역전, pre-pass 서술에 `postRefList` 반영.
유스케이스), `PreRef` 순서 bullet에 "왜 보장까지 하는가"(`FastQuery`
사례) 추가, pre-pass 서술에 `postRefList` 반영.
- `base/lifecycle-hooks-plan.md``research/`에서 `git mv`, 상태를 base
확정으로, `OnRendered` 절 신설, ② 절을 "보류"에서 "채택 확정"으로
(역전 배너 + 근거 보존), 스코프/선택지/이름/패키지/우선순위/열린 질문 절
@ -112,12 +129,13 @@ ROADMAP M8에서 `PreRef`와 같이 구현되고, 훅 슈가 셋만 형제 백
목록에 `PostRef` 추가(4곳+4곳).
- `base/typing-limits.md``PostRef<T>``PreRef<T>`와 같은 서브타입
관계, 스파이크 `13` 재작성 시 같이 커버할 것.
- `archive/preref-order-guaranteed-reversed.md` — 신설.
- `archive/preref-order-unguaranteed-withdrawn.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 항목 갱신, 백로그에 훅 슈가 셋 추가.
보장 문구 갱신, Brand/Leaf 항목 갱신, 백로그에 훅 슈가 셋 추가.
- `question.md` — 용어 대기열에 `OnDestroyed` 항목, `Brand` 항목의
"10종" 표현 정리.
- `CLAUDE.md` — 백로그 항목 갱신 + 이 세션 요약.
@ -127,8 +145,11 @@ ROADMAP M8에서 `PreRef`와 같이 구현되고, 훅 슈가 셋만 형제 백
## Claude가 걸린다고 보고한 것
1. **`OnRendered`가 "부모에 붙기 전"에 불린다는 캐비엇** — 위 2번.
이름이 React 관용어라 오해를 부를 수 있어 문서화로 대응하기로 함
(사용자가 채택을 지시했으므로 이름 자체는 유지).
2. **순서 미보장은 계약 변경이지 구현 변경이 아님** — 실제로는 여전히
index 순서로 fire될 것이므로, 사용자 코드가 우연히 그 순서에 기대도
당장은 안 깨짐(그래서 문서에 명시적으로 "의존 금지"로 적어둠).
이름이 React 관용어라 오해를 부를 수 있다고 보고했고, **사용자 답변은
"OnRendered는 부모에 들어가기 전이라고 명시만 해줘"** — 이름은 그대로
두고 문서에 캐비엇을 명시하는 것으로 확정
(`base/ref-plan.md` "보장 범위" 항목 + `base/lifecycle-hooks-plan.md`
`OnRendered` 절의 ⚠️ 항목).
2. **순서 미보장은 계약 변경이지 구현 변경이 아니라 실효가 없음**
실제로는 여전히 index 순서로 fire될 것이라 위반이 절대 안 드러남.
이 지적이 위 3번의 재검토·철회로 이어졌음.

View file

@ -1327,10 +1327,13 @@ fire 순서 미보장으로 역전, `lifecycle-hooks-plan.md` base 승격**
공짜로 줌 — 진짜 경계는 **"자기 아래 vs 자기 위"**로, `PostRef`는 서브트리
완성은 보장하되 **이 인스턴스가 부모에 붙기 전**에 불림(React
`componentDidMount`와 다름, `OnRendered` 이름 때문에 문서화 필수 캐비엇).
같은 세션에 **`PreRef`/`PostRef` 계열 안 fire 순서를 "배열 index 순서 보장"
에서 미보장으로 역전**(2026-08-07 아홉 번째 세션 결정을 뒤집음, 구현이
아니라 계약만 — "같은 계열 내 등록 순서에 의존하는 코드가 생겨선 안 됨",
`archive/preref-order-guaranteed-reversed.md`). `OnRendered` 채택으로
같은 세션에 **`PreRef`/`PostRef` 계열 안 fire 순서**를 미보장으로 뒤집었다가
**곧바로 철회, "배열 index 순서 보장" 유지**(2026-08-07 결정 그대로) —
사용자가 든 반례가 `FastQuery(...) -> PreRef`류 조합(앞자리 항목이 뒤
항목의 전제를 만들어주는 정당한 합성)이었고, 보장 비용이 0인 데다 배열
파트 index 순서가 이미 백엔드 이식성 때문에 명시적 계약이라 새로 내주는
자유도 없음이 확인됨. 양쪽 논거는
`archive/preref-order-unguaranteed-withdrawn.md`. `OnRendered` 채택으로
`lifecycle-hooks-plan.md`의 마지막 열린 항목이 닫혀 `base/`로 승격, 남은
`OnDestroyed` 이름 재검토 여지 하나(0-B 확정 시, `question.md` 용어
대기열 3순위). ROADMAP M8/백로그·README·brand/architecture/slot/modifier/

View file

@ -588,10 +588,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
`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 두
상대 순서는 배열 index 순서 그대로 보장**(별도 규칙 없음 — 배열 파트
index 순서 계약의 귀결. 2026-08-14 아홉 번째 세션에 잠깐 미보장으로
뒤집었다가 같은 세션에 철회 —
`archive/preref-order-unguaranteed-withdrawn.md`, `FastQuery(...) ->
PreRef`류 조합이 반례). fire/수집된 슬롯은 그 자리에서 소진(**[정정, 2026-08-14 두
번째 세션] `None`이 아니라 전용 센티널 `ProcessedPreRef`/
`ProcessedPostRef` 처리** — 아래 `Processed*Handler` 항목이 그 자리를
정상 두 패스로 마저 처리)