quad/.claude/archive/ref-phase-option-reversed.md
qwreey 6b73cb4208
refactor(base): bind-system-plan.md 1단계 분할 — Ref/PreRef·이벤트·Brand를 별도 문서로
사용자 지적: 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
2026-08-13 18:06:24 +09:00

5.8 KiB

[역전됨] CreatedRefphase 옵션 + "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 옵션 하나로 고르는 평범한 참가자" 모델이 두 가지를 보장하지 못한다는 게 드러남:

  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 이벤트의 동기 발화)를 만나 타입 분리로 귀결된 사례"로 쓰기 좋음.