- 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>
5.8 KiB
[역전됨] 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 옵션 하나로 고르는 평범한 참가자" 모델이 두 가지를 보장하지 못한다는 게 드러남:
- "평범한 참가자"라는 전제 자체가 Modifier/Store를 거치면 깨짐. Ref가 Modifier 필드로 flatten되거나 Source/Store 값으로 나중에 도착하면, "이 인스턴스에 다른 무엇보다 먼저"라는 순서 보장을 구조적으로 줄 방법이 없음(Modifier flatten은 해시 파트로 합쳐지고, Store 값은 process/retract 재귀 경로로 원래 스캔보다 나중에 도착하므로).
- "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 이벤트의 동기 발화)를 만나 타입
분리로 귀결된 사례"로 쓰기 좋음.