quad/.claude/archive/ref-phase-option-reversed.md
qwreey e4d6181fcf
decide(base): Ref/PreRef 디스패치 타이밍 확정, phase 옵션은 archive로 역전
- CreatedRef의 {phase="created"|"mounted"} 옵션 폐기, 위치 기반 순서로 대체
- base 디스패치가 배열 파트(children/Ref)를 해시 파트(프로퍼티/이벤트)보다
  먼저 처리하도록 명시적으로 두 패스 계약화
- PreRef 신설: 프로퍼티/이벤트보다도 먼저 채워져야 하는 케이스(Roblox
  ChildAdded/DescendantAdded/Changed의 동기 발화 대응) 전용, Modifier/
  Store 타입 차단 + 위치 무관 호이스팅
- Ref 콜백/대기자 실행 구현 디테일(coroutine vs function 분기) 추가
- 역전된 원 서술은 archive/ref-phase-option-reversed.md로 보존
- architecture.md/question.md/documentation-content-map.md/ROADMAP.md 동기화

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:38:06 +09:00

74 lines
5.8 KiB
Markdown

# [역전됨] `CreatedRef`의 `phase` 옵션 + "Ref는 특수 처리 없는 평범한 참가자" — 위치 기반 순서 + `PreRef` 신설로 대체됨
**역전 일시**: 2026-08-07 (세 번째 세션). **원 확정 일시**: 2026-08-04
(Ref 도입 확정 절)~2026-08-06(Ref 일반화 절)에 걸쳐 누적 확정.
**현재 유효한 설계**: `base/bind-system-plan.md`의 "확정된 디스패치
모델" 절 하단(배열/해시 두 패스 계약)과 "`phase` 옵션 폐기 →
위치로 표현, `PreRef` 신설" 절이 최종 소스. 이 파일은 더 이상 능동적으로
참고할 필요 없음(구현에 안 씀) — 왜 "옵션 하나로 phase를 고르는 설계"에서
"위치 기반 순서 + 별도 타입 분리"로 넘어갔는지가 `quadnomicon`(프레임워크
설계자용 심화 콘텐츠) 소재로 가치 있어서 사유·원문을 통째로 보존해둔 것.
## 역전된 사례 — 원래 무엇을 확정했었나
**1. Ref는 dispatch 레지스트리의 "평범한 참가자"였음** (2026-08-04
원문, `bind-system-plan.md` "Ref — 도입 확정" 절):
> **바인드 방법**: 특수 처리 없이, children을 배열 아이템으로 넣듯
> `CreatedRef` 같은 값을 숫자 키 슬롯에 넣는 방식 — child와 동일한
> 층위에서 `process(inst,k,v)` 디스패치를 그대로 타게 함. 즉 Ref도
> pluggable 핸들러 레지스트리의 평범한 참가자.
**2. "생성 직후"/"마운트 후" 두 타이밍은 옵션 값으로 골랐음** (같은 절):
> **콜백 호출 시점 확정**: 생성 직후(construction 시점)와 트리 마운트
> 후(Parent 세팅 완료) 둘 다 지원 — 옵션으로 선택(`CreatedRef(fn,
> {phase="created"|"mounted"})`류, 정확한 API 이름은 구현 단계에서 확정).
당시엔 이 두 문장이 서로 모순되지 않는다고 봤음 — "평범한 참가자"이면서
동시에 "옵션으로 두 시점 중 골라 fire"하는 게 가능하다고 전제했던 것.
## 역전된 이유
실제 사용 시나리오를 짚다가 드러남: quad-roblox 이벤트는 `self(Instance)`
안 주기로 이미 확정돼 있어서(`base/bind-system-plan.md` "이벤트 핸들러는
self를 받지 않는다"), 이벤트 안에서 인스턴스 자신을 참조하려면 Ref로
받아둔 값을 읽는 수밖에 없음. 그런데 Roblox 이벤트 중 일부(`ChildAdded`/
`DescendantAdded`/`Changed`류)는 유저 인터랙션을 기다리지 않고 **setup
도중 프로퍼티 대입/Parent 세팅 자체의 부작용으로 동기적으로 발화**할 수
있음 — 이 시점에 self-ref가 아직 안 채워져 있으면 그대로 터짐.
이 문제를 실제로 풀려고 보니 "phase 옵션 하나로 고르는 평범한 참가자"
모델이 두 가지를 보장하지 못한다는 게 드러남:
1. **"평범한 참가자"라는 전제 자체가 Modifier/Store를 거치면 깨짐.**
Ref가 Modifier 필드로 flatten되거나 Source/Store 값으로 나중에
도착하면, "이 인스턴스에 다른 무엇보다 먼저"라는 순서 보장을 구조적으로
줄 방법이 없음(Modifier flatten은 해시 파트로 합쳐지고, Store 값은
process/retract 재귀 경로로 원래 스캔보다 나중에 도착하므로).
2. **"created" phase가 실제로 뭘 보장하는지가 원래 정의돼 있지 않았음.**
"생성 직후"가 "다른 모든 프로퍼티/이벤트보다 먼저"까지 보장하는 건지,
아니면 "그냥 루프 어딘가에서, construction 이후"면 충분한 건지가
불명확했음 — 후자로 해석하면 옵션이 무의미해지고, 전자로 해석하면
`process(inst,k,v)` 우선순위 스캔만으로는 줄 수 없는 순서 보장이라
드라이버 레벨 개입이 필요해짐.
## 이전 것과 지금 것의 차이
| | phase 옵션(역전됨) | 위치 기반 + `PreRef`(현재) |
|---|---|---|
| "자식 마운트 전/후" 표현 | `{phase="created"\|"mounted"}` 옵션 값 | children 배열에서 다른 형제보다 앞/뒤에 놓는 것만으로 결정(두 패스 계약 위에서 공짜로 나옴) |
| "프로퍼티/이벤트보다 먼저" 표현 | 같은 옵션의 `"created"` 값 — 실제로 이 보장을 줄 메커니즘은 없었음 | 별도 nominal 타입 `PreRef` — Modifier/Store엔 타입으로 아예 못 들어가고, 배열 파트 스캔 전에 driver가 따로 pre-pass로 fire, 위치와도 무관하게 항상 최우선(호이스팅) |
| Ref/CreatedRef가 참가자로서 특수한지 | "특수 처리 없이, 평범한 참가자"라고 명시 | 일반 Ref/CreatedRef는 여전히 평범한 `(v=Ref)` 핸들러 매치 — 다만 그 매치가 성립하려면 base가 배열 파트/해시 파트 순서를 **명시적으로 계약화**해야 했음(우연한 Luau 테이블 동작에 기대지 않음), `PreRef`는 아예 별도 pre-pass 대상이라 진짜 특수 취급 |
| Store/Modifier 조합 가능 범위 | 논의 안 됨(암묵적으로 전부 가능하다고 전제) | 일반 Ref는 자유, `PreRef`는 타입으로 원천 차단 |
## 왜 완전히 헛수고는 아니었나
"children 배열 슬롯에 넣으면 dispatch가 채워준다"는 `CreatedRef`
핵심 아이디어 자체는 그대로 살아남음 — 바뀐 건 "그 안에서 두 타이밍을
옵션 하나로 고르게 하자"는 세부 설계뿐. 오히려 이 반전 덕분에 "왜 굳이
`PreRef`라는 별도 타입이 필요한가"(=순서 보장이 안 되는 경로가 실제로
있다는 것)와 "base 드라이버가 왜 배열/해시 순서를 명시적으로 계약화해야
하는가"(=Lua 테이블의 우연한 동작에 기대면 다른 백엔드에서 깨질 수
있다는 것) 두 가지가 훨씬 선명해짐 — `quadnomicon`에서 "옵션 하나로
퉁치려던 설계가 실제 시나리오(Roblox 이벤트의 동기 발화)를 만나 타입
분리로 귀결된 사례"로 쓰기 좋음.