사용자 지적: 2989줄이라 검토가 불가능하고 한 곳의 실수가 미치는 범위가 너무 큼. 특히 "이벤트 연결은 다른 base 문서가 되어야 할 듯"이라고 직접 지목. **1단계 (완료, 2989 → 2263줄)** — 인바운드 참조가 적고 자기 완결적인 것부터: - base/ref-plan.md (509줄) — Ref/PreRef 전체 - base/event-plan.md (103줄) — 이벤트 self 미전달, false로 disconnect - base/brand-plan.md (137줄) — Brand 런타임 nominal 판별 내용은 **순수 이동, 결정은 하나도 안 바뀜**. 원위치엔 포인터 스텁을 남기고, 이 절들을 지목하던 인바운드 참조 20곳을 새 파일로 갱신(잔존 0 확인). README 색인에 3개 행 추가. **2단계는 의도적으로 미룸** — 남은 디스패치 코어(~1000줄)/반응형 코어 (~950줄)는 0-Z 확정 시 어차피 전면 재작성 대상이라, 재작성하는 그 패스에서 파일을 가르는 게 총 변경량과 실수 위험이 모두 작음(인바운드 ~37곳을 한 번만 고침). dispatch-redispatch-diff-plan.md 6절에 그 지시를 명시했고, bind-system-plan.md 헤더에도 분할 현황표를 남김. 부수: 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 쪼개면 양쪽이 반토막 나므로 원위치 유지(event-plan.md 헤더에 명시). luau-test/README의 "에이전트가 직접 실행 못 함" stale 서술도 정정. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6hzeUi5QdLPEk69B6cXFa
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
원문, ref-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 이벤트의 동기 발화)를 만나 타입
분리로 귀결된 사례"로 쓰기 좋음.