diff --git a/.claude/README.md b/.claude/README.md index 40b223e..bd74388 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -48,7 +48,7 @@ | `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** 단일 키 `[AttributeKey "Name"]`(구 `Attribute`) — `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`은 기존 이벤트 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` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State`로 받아 `: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 전달로 충분하다는 판단 | diff --git a/.claude/archive/preref-order-guaranteed-reversed.md b/.claude/archive/preref-order-guaranteed-reversed.md deleted file mode 100644 index ce1b7cf..0000000 --- a/.claude/archive/preref-order-guaranteed-reversed.md +++ /dev/null @@ -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 순서 그대로(별도 규칙 - 없음)" 체크리스트 문구 정정. -- 코드 영향 없음(구현이 안 바뀌므로) — 바뀐 건 문서화 대상 계약뿐. diff --git a/.claude/archive/preref-order-unguaranteed-withdrawn.md b/.claude/archive/preref-order-unguaranteed-withdrawn.md new file mode 100644 index 0000000..bbff437 --- /dev/null +++ b/.claude/archive/preref-order-unguaranteed-withdrawn.md @@ -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` +양쪽에 기재). diff --git a/.claude/base/lifecycle-hooks-plan.md b/.claude/base/lifecycle-hooks-plan.md index 2331347..54a9b3e 100644 --- a/.claude/base/lifecycle-hooks-plan.md +++ b/.claude/base/lifecycle-hooks-plan.md @@ -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을 등록) — 유일한 차이는 "실제 diff --git a/.claude/base/ref-plan.md b/.claude/base/ref-plan.md index f360a37..9f77366 100644 --- a/.claude/base/ref-plan.md +++ b/.claude/base/ref-plan.md @@ -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`의 거울상, 새 규칙 없음** diff --git a/.claude/session/2026-08-14-09-postref-confirmed.md b/.claude/session/2026-08-14-09-postref-confirmed.md index 7763dc3..994f062 100644 --- a/.claude/session/2026-08-14-09-postref-confirmed.md +++ b/.claude/session/2026-08-14-09-postref-confirmed.md @@ -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`도 `PreRef`와 같은 서브타입 관계, 스파이크 `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번의 재검토·철회로 이어졌음. diff --git a/CLAUDE.md b/CLAUDE.md index 35ca10b..3908ba5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/ diff --git a/ROADMAP.md b/ROADMAP.md index 8bd1bc3..21ab40b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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` 항목이 그 자리를 정상 두 패스로 마저 처리)