base 밖으로 늘 읽을 필요 없는 참고자료(quad-v1-architecture, comparison-fusion-vide)를 새 reference/ 폴더로 분리하고, ui-shorthand-plan을 base로 승격(RoundSize 드롭+ UICorner/UIPadding/UIScale 리네임), additional-primitives-plan을 Blocker/Effect(base 승격)·Batch/Context(archive 기각)·키 기반 컬렉션 재조정(research 잔류)으로 4분할했다. component-composition-plan의 중복 역전 서사는 기존 archive 포인터로 압축하고, archive 제목 컨벤션을 [역전됨]/[기각됨]로 분화했다. tween-plan에는 retract/canExecute 구분 메모와 트윈 옵션 값 모양 논의를 추가했다. Effect가 Observer 변형인지는 임의로 결론내지 않고 question.md에 열린 질문으로 남겼다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
38 KiB
quad-debug — 런타임 디버깅/역추적 도구 계획
상태: research — 착수 전, 사용자와 계속 논의 필요. 사용자가 "quad 개발이
어느 정도 끝날 때까지는 실제 구현에 못 들어갈 것"이라고 스스로 판단한 후순위
항목이지만, base 설계(디스패치 엔진/Source/DI 생성자) 시점에 훅 확장
지점만 미리 고려해두면 나중에 훨씬 싸게 먹힌다는 문제의식으로 지금 미리
정리해둠. ROADMAP.md 백로그 항목("범용 렌더 디버깅 도구로서의 quad-mock")과
목적이 다름 — 아래 "quad-mock 백로그와의 관계" 절 참고.
2026-08-06 세션 결론(핸드오버 요약): 설계를 막던 유일한 기술적 불확실성
(플러그인이 Play 중인 게임과 실시간으로 통신 가능한가)이 사용자의 Studio
실측으로 해소됨 — BindableEvent/BindableFunction 둘 다 Plugin↔Play
클라이언트 경계를 넘는다(아래 "데이터 채널" 절). 이후 그 위에서 채널 위치
(quad 모듈 내부+CollectionService 태그), 페이로드 제약(순수 직렬화 값만),
UUID 기반 on-demand compute, Element Inspector, flash 범위 축소까지
설계가 한 라운드 더 수렴함(아래 "핵심 설계 방향" 7/8번, React DevTools
절 4번). 남은 건 세부 API 이름과 구현 착수뿐 — 남은 열린 질문은 전부
후순위/백로그 표시된 것들, 다음 세션에서 뭔가 막혀있지 않음.
배경 — 팀원 피드백 원문 요지
Roblox 안의 대부분 렌더러는 "뭐가 어디서 어떻게 렌더링됐는지" 알기 힘들다. react-lua는 방법이 있긴 하지만 쓰기 어렵고, Studio 안에서 플러그인처럼 바로 볼 수 있으면 좋겠다는 요청. 구체적으로 원하는 것:
- 실물 Frame → 생성한 코드 위치 역추적 (제일 핵심 요청)
- Explorer에서 선택한 인스턴스로부터 코드 위치를 보여주는 플러그인, 또는 플러그인 자체 트리뷰
- 어떤 프로퍼티가 어떤 파이프(Store/State 체인)에 연결됐는지, 파이프라인이 어떻게 생겼는지 UI로 확인
- 웹 devtools처럼 변경된 부분을 반짝이게(flash) 보여주기
- 생성된 Source/Store 목록을 스크립트별로 보여주기
스토리북(ui-labs, architecture.md 9번 항목으로 이미 대체 확정된 것)과는
다른 문제라는 점을 사용자가 명시적으로 구분함 — 스토리북은 컴포넌트 단위
격리 테스트, 이건 인게임 전체를 실행한 상태에서 발생하는 실제 버그를
찾는 용도. 실사용 인게임 버그는 이 방식이 훨씬 찾기 쉽다는 게 사용자 판단.
스코프 확정 (사용자 확인)
1차 설계는 클라이언트 UI 한정. quad는 거의 항상 LocalScript/클라이언트 UI를 다루므로, 1차 통신 채널은 "플러그인 ↔ 같은 머신의 로컬 클라이언트" BindableEvent 채널로 한정. 서버에서 생성되는 인스턴스(팀 테스트/멀티플레이 시나리오, RemoteEvent 필요)까지 다루는 건 실제 필요성이 확인되면 그때 확장 — 지금 설계를 막지 않음.
리서치 결과 요약
서브에이전트로 .claude/initreq/ 전체(quad v1, fusion, vide, rbvm, tbox,
quad2-try, artworks)를 조사, 일반 지식으로 Roblox 엔진 제약도 확인:
- 참고할 기존 구현체가 없음 — react-lua/roact devtools 소스 자체가 이
레포에 없음(react-lua가 애초에 이 프로젝트의 참고 레포 목록에 없음).
quad2-try/out/quad-debug/라는 빈 디렉토리가 이미 예약되어 있었으나 파일 0개 — 이전 시도도 손댄 적 없는 영역. v1에도 재사용할 인프라 없음 (customWarn.lua정도,debug.traceback출력만 하는 3줄). - 에러 발생 시점 스냅샷 방식은 있지만 상시 레지스트리는 없음 — Vide
(
src/graph.luau)와 Fusion(src/Logging/parseError.luau)은 둘 다xpcall+debug.traceback/debug.info로 에러 나는 순간에만 스택을 찍음. Instance→생성 위치를 항상 기록해두는 상시 레지스트리를 유지하는 선례는 없음 — quad-debug가 여기까지 해낸다면 차별점. - Roblox Luau의
debug라이브러리는 제한적 — 표준 Lua/LuaJIT에 있는debug.sethook(라인/콜 단위 훅),debug.getlocal/setlocal/getupvalue/setupvalue가 Roblox엔 없음(보안/성능 이유로 제거).debug.info/debug.traceback/debug.profilebegin류만 노출. 즉 엔진이 공짜로 주는 동적 트레이싱 방법은 없고, quad 코드 안에 직접 계측을 심는 것 외엔 방법이 없음 — 사용자가 우려한 그대로 확인됨. - "no-op 기본값 → 나중에 실제 구현으로 교체" 패턴의 실사용 선례 발견 —
Fusion
src/External.luau가 정확히 이 모양: 모듈 상단 upvaluecurrentProvider: ExternalProvider? = nil을 두고External.setExternalProvider(newProvider)(31행)로 통째로 교체, 소비 함수(logWarn등)는 매 호출 시if currentProvider then ... end로만 분기(101행).RobloxExternal.luau:51의RobloxExternal.logWarn = warn도 같은 기법의 정적 버전. quad가 이미 쓰기로 확정한 패턴(base는 인터페이스, 구현은 팩토리가 나중에 주입 —RobloxFactory등)과 정확히 같은 모양 — 새로 발명할 필요 없이 기존 설계 원칙을 트레이싱에도 그대로 적용하면 됨. - 프로덕션에서 완전 제거하고 싶다면(당장 필요한 결정 아님) darklua의
전역 값 주입(
inject_global_value) + dead-branch 제거, 또는 Rojoproject.json을 릴리즈용으로 따로 둬 디버그 전용 파일 자체를 트리에서 빼는 방법이 있음 — 일반 지식 수준으로만 확인, 실제 필요해지면 재조사. - 플러그인과 실행 중인 게임은 별도 Luau VM(별도 보안 컨텍스트) —
_G/shared가 공유되지 않음.require()는 공식 문서(creator-docsscripting/module.md)로 "클라이언트-서버 경계를 넘어 같은 ModuleScript를 require하면 각 쪽이 독립적인 참조를 받는다"가 확인됨 — 플러그인 경계에도 같은 메커니즘(별도 require 캐시)이 적용될 가능성이 높으나 공식 문서가 플러그인 케이스를 명시하진 않음. 따라서 플러그인이 게임 내부의 Store/Source 레지스트리에 직접 접근하는 경로는 없다고 봐야 함.- 정정 이력(2026-08-06): 이전 초안이
BindableEvent브릿지를 "차별점"으로 단정했던 건 근거 부족이었음(공식 문서는 플러그인 언급 없음, DevForum엔 실패 사례도 있었음) — 이후 사용자가 Studio에서 직접 실측해 BindableEvent Fire/Connect가 Plugin↔Play 중 클라이언트 경계를 실제로 넘는다는 걸 확인(아래 "데이터 채널" 절 5번 참고). 핵심 원인 추정("react-lua devtools가 쓰기 어렵다")은 여전히 "게임 쪽에 브릿지를 미리 심어야 하는 설정 부담" 쪽으로 유지 가능해짐 — 채널 자체는 되는 게 확인됐으므로.
- 정정 이력(2026-08-06): 이전 초안이
핵심 설계 방향
1. "존재하는 State 목록"이 아니라 "무엇이 무엇에 연결됐는가" — 사용자 확정
bind-system-plan.md에 이미 있듯 State는 store.key로 접근할 때마다
매번 새로 만들어지는 ephemeral 캐시 핸들이라 "지금 존재하는 State 목록"이라는
개념 자체가 성립하지 않음. 사용자가 이 논의 중 직접 정정: 값 목록을
보여주는 대신, Frame을 선택했을 때 "어디에 어떻게 훅이 연결돼 있는지", "이
Compute 함수가 어디서 생성됐는지"를 보여주는 연결 그래프 중심으로 UX를
잡는 게 맞음. 이건 quad 온톨로지와도 자연히 맞아떨어짐 — 열거해야 할 진짜
실체는 State가 아니라 Source(Store가 소유하는 유일한 진짜 값 지점)와
디스패치 이력(무엇이 언제 어떤 값으로 process됐는가) 둘뿐. 파이프라인
그래프는 이 디스패치 이력을 재구성해서 보여주는 것.
2. 계측 지점 3곳 — no-op 훅 upvalue, Fusion External.luau 패턴 재사용
사용자가 요청한 "빈 함수 만들어두고 나중에 트레이스 뽑는 동적 계측"을
그대로 적용. if DEBUG then 분기를 코드 전체에 뿌리지 않고, 아래 세 지점에
모듈 upvalue 형태의 no-op 기본 훅만 심어두면 됨(위 리서치 4번 패턴):
Dispatch/init.luau의process/retract스캔 루프 — 어차피 매 호출마다 우선순위 스캔이 도는 지점이라, 여기에 훅 호출 1개(no-op이면 사실상 함수 호출 오버헤드뿐, 무시 가능 수준) 추가.(inst, k, v, handler, timestamp)를 훅에 넘기면 "무엇이 무엇을 바꿨는가" 이력의 원천이 됨.Source.luau생성자 — Source 인스턴스를 weak-keyed 전역 레지스트리에 등록하는 훅(기본 no-op). 켜졌을 때만 이 레지스트리가 존재하므로 GC-native 원칙(lifecycle-pattern.md)과 충돌 없음 — 꺼져 있으면 레지스트리 자체가 안 만들어짐.- quad-roblox
DI/init.luau의 제네릭 생성자(new(className)) — 인스턴스 생성 순간debug.info(2, "sl")로 caller의 script+line을 얻어 기록하는 훅(기본 no-op). Instance 생성은 프로퍼티 변경보다 훨씬 드물게 일어나므로 (렌더 타임 1회), 여기서만 비교적 비싼debug.info호출을 해도 부담 적음.
이 세 곳 모두 "인터페이스는 base가 정의, 실 구현은 quad-debug가 나중에
주입"하는 기존 확정 원칙(bind-system-plan.md "base 유틸은 인터페이스,
실제 구현은 백엔드 팩토리가 주입")과 완전히 같은 모양 — quad-debug를 위해
새 아키텍처 패턴을 만드는 게 아니라 기존 패턴을 재사용하는 것뿐.
3. 표준 디버그 인터페이스 — 핸들러가 선택적으로 구현하는 5번째 훅 (사용자 제안)
사용자 제안: "트윈이 뭐 땜에 일어나냐, quad 땜인지 아님 Ref로 밖에 나가진
instance로 직접 트윈되어버리는지" 같은 걸 구분하고 싶음. 이건 디스패치
이력만으론 부족함 — quad가 만든 Tween과 사용자 코드가 Ref로 얻은 raw
Instance에 직접 TweenService:Create()를 건 것을 구분하려면 핸들러 자신만
아는 맥락이 필요.
제안: isHandlable/priority/process/retract 4종 계약에 선택적
5번째 훅을 추가 — describe(inst, k, v): DebugInfo?(가칭, 기본 미구현
= no-op과 동일 효과). quad-debug가 트레이스 이벤트를 기록할 때 해당 키를
처리한 핸들러에게 describe가 있으면 호출해서 사람이 읽을 수 있는 부가
정보(예: Tween 핸들러라면 "store-bind 유발" vs "직접 세팅"인지, 어떤 store
key에서 왔는지)를 이벤트에 덧붙임. bind-system-plan.md가 이미 "4종 계약은
지금 확정이지만 실제 구현하며 부족한 지점이 보이면 그때 hook 추가(점진적
확장)"라고 열어둔 것과 정확히 맞아떨어지는 케이스 — 새 원칙이 아니라 이미
예견된 확장.
4. 외부 변경 감지 — 보조 신호일 뿐, 핵심 채널로 쓸 수 없음(사용자 정정)
위 3번의 한계: quad가 전혀 모르는 코드 경로(Ref로 얻은 raw Instance에 대한
직접 조작)는 애초에 process()를 거치지 않으므로 quad-debug의 계측
지점으로는 절대 안 잡힘. 처음 검토했던 방법: quad가 관리하는 인스턴스에
대해 inst:GetPropertyChangedSignal(prop)(Roblox 엔진 자체가 모든
인스턴스에 제공하는 범용 시그널)을 구독해두고, 변경 시점을 직전 quad
디스패치 이력과 타임스탬프로 대조해 "일치하는 트레이스가 없으면 외부
변경"으로 표시하는 아이디어.
사용자 정정(2026-08-06): 이건 quad-debug의 핵심 가치와 맞지 않음 —
"이 프로퍼티가 바뀌었다"는 사실 자체는 PropertyChangedSignal로 누구나
알 수 있고, quad-debug가 진짜 필요한 이유는 그 변경이 quad의 어떤
Store/파이프라인/handler에서 왔는지, 코드 몇 번째 줄에서 유발됐는지를
보여주는 것 — 순수 관찰(passive observation)만으로는 "무엇이 바뀌었다"만
알 수 있을 뿐 "왜/어디서"는 증발함. 결론: PropertyChangedSignal 기반
교차검증은 (실제로 크로스 컨텍스트에서 작동한다는 전제하에도) 기껏해야
"quad가 설명 못 하는 변경이 있었다"는 보조 신호 정도이지, 핵심 트레이스
채널의 대체재가 될 수 없음. 핵심 채널(어디서/왜)은 여전히 능동적 계측
(위 2번, 3번의 훅)에서 나와야 함 — 그 계측 데이터를 플러그인까지 실제로
전달할 수 있는지는 실측으로 확인됨(아래 "데이터 채널" 절), 이 항목
자체는 채택하더라도 어디까지나 보조 기능으로만 남음(백로그, 아래 "열린
질문" 참고).
5. 데이터 채널 — Attribute(스냅샷) + BindableEvent(스트림), BindableEvent 크로스 컨텍스트 확인 완료
상태(2026-08-06): 사용자가 Studio에서 실측 검증 완료 — BindableEvent가
Plugin↔Play 중인 클라이언트(LocalScript) 경계를 실제로 넘는다.
plugin-ignoreme.luau(Explorer에서 선택한 BindableEvent에 Connect)와
game-ignoreme.luau(2초마다 반복 Fire)로 테스트한 결과, Fire와 플러그인
수신이 거의 동일한 타임스탬프로 반복 확인됨(14:10:42.243 Fire /
14:10:42.243 Plugin 수신 등, 여러 사이클에 걸쳐 안정적).
사용자가 정리한 이유: Play 모드에 들어가도 플러그인이 다루는 game은
edit 모드와 별도로 복제된 DataModel이 아니라 같은 DataModel이고(Play
진입 시 "복사된 컨텍스트"라 부른 건 script identity/보안 컨텍스트가
다르다는 뜻이지 Instance 자체가 별도 메모리에 복제된다는 뜻이 아니었음),
Roblox Luau 샌드박스가 분리하는 건 스레드/스크립트 컨텍스트뿐이고
그 밑의 C++ 구현 userdata(Instance 자체)는 어느 컨텍스트에서 접근하든
같은 참조를 가리킴 — 그래서 Instance 기반 메커니즘(시그널 Connect/Fire
포함)이 자연스럽게 경계를 넘어 작동함. require()가 컨텍스트별로 독립
모듈 인스턴스를 주는 것(이전 확인 사항)과는 완전히 다른 층위 — 그건
Lua 모듈 캐시가 컨텍스트별로 분리된다는 것이지 Instance 자체가
분리된다는 뜻이 아니었음, 이번 실측으로 그 구분이 명확해짐.
BindableFunction(요청-응답)도 확인 완료: Invoke→OnInvoke 실행→
리턴값 수신까지 왕복이 여러 사이클에 걸쳐 안정적으로 동작(ok=true, "plugin-received"). 이걸로 위 "React DevTools에서 가져올 아이디어" 3번의
"기본은 얇은 스트림(BindableEvent), 상세는 on-demand 요청-응답
(BindableFunction)" 구조가 양쪽 다 실측 검증됨.
설계 제약으로 반영할 관측(사용자 지적): 같은 스크립트 컨텍스트 안에서
BindableEvent/Function은 원래 인자를 직렬화 없이 레퍼런스 그대로
넘기는 게 문서화된 특징(RemoteEvent와 달리 함수/메타테이블도 그대로
통과 가능)인데, 플러그인 경계를 넘을 때는 RemoteEvent와 비슷하게
내부적으로 마샬링(직렬화/역직렬화)되는 것으로 보임 — 실측으로 함수
자체를 못 넘겨본 건 아니지만(테스트는 단순 값만 사용), Instance/Plugin
간 별도 Luau VM 경계라는 점을 고려하면 합리적인 추정. 결론:
trace 이벤트 페이로드는 처음부터 함수/클로저 없이 순수 직렬화 가능한
값(숫자/문자열/불리언/plain 테이블/Instance 참조)만 담는다는 원칙으로
설계 — 애초에 "State를 그대로 넘기고 플러그인이 나중에 .Get()한다"류의
설계는 안 되고(State는 클로저를 담은 객체라 직렬화 불가능할 가능성이
높음), 넘길 값은 항상 quad-debug가 미리 원시 값으로 변환해서 보내야 함.
남은 미확인 범위: 이번 테스트는 (a) 같은 로컬 머신의 Play/Play Solo 클라이언트 컨텍스트, (b) 원시 값 인자만 확인함. 서버 컨텍스트나 복잡한 중첩 테이블/Instance 배열 전달까지는 실제 구현 단계에서 재확인 권장 — 단, 1차 스코프(클라이언트 한정, 원시 값 위주 trace 이벤트)에서는 이번 검증만으로 채널 자체의 실현 가능성은 확정됐다고 봐도 됨.
- Attribute: 인스턴스 생성 시점 1회성 정보(생성 위치, "quad가 관리하는
인스턴스인가" 마커)에 적합 — DataModel 자체의 일부라 플러그인과 게임이
별도 Luau VM이어도 문제없이 공유됨(Selection 서비스로 바로 읽힘). 단점:
문자열 크기 제약, 그리고 배포된 게임에 실수로 남으면 유저가 F9 콘솔이나
Explorer로 내부 코드 경로를 볼 수 있는 정보 노출 위험 —
RunService:IsStudio()가드가 필수(quad-debug require 자체가 옵트인이라는 1차 방어선 + IsStudio가 2차 방어선, 이중 게이팅). - Value 오브젝트(StringValue/ObjectValue 등)는 기각 — 사용자 확정:
Attribute의 대안으로 자식 Instance로 값을 담는 Value 오브젝트도 검토했으나,
:GetChildren()을 호출하면 그대로 드러나 트리를 오염시킴(quad가 실제로 마운트한 자식과 섞여버려base/slot-plan.md의 자식 재조정 로직이나 사용자 코드의 children 순회를 방해할 위험) — Attribute는 자식이 아니라 메타데이터라 이 문제 자체가 없음. 스냅샷성 데이터는 Attribute로 확정, Value 오브젝트는 후보에서 제외. - BindableEvent+BindableFunction(크로스 컨텍스트 확인 완료, 위치는
재검토): "지금 이 순간 일어난 일" 스트림은 BindableEvent, 특정
Instance 선택 시 상세 정보 요청-응답은 BindableFunction — 둘 다 실측
확인됨(위 참고).
ReplicatedStorage자동 생성 방식은 기각 — 사용자 정정: 처음 구상은 quad-debug-roblox가 초기화 시ReplicatedStorage밑에 잘 알려진 이름으로 Bindable을 만들어 노출하는 것이었으나, 이건 개발자가 의도하지 않은 Instance를 게임 트리에 주입하는, 부작용이 큰 행위라 기각. 대신 Bindable을 quad 모듈 자신의 Instance 트리 안(quad가 이미 설치돼 있는 위치 그대로, 새 위치를 따로 안 만듦)에 두고,CollectionService태그로 노출 — 플러그인은 quad가 어디 설치됐는지 몰라도CollectionService:GetTagged(tag)로 바로 찾음(GetDescendants()로 전체 트리를 훑어 필터링할 필요 없음 — 사용자가 "roblox query descendants" 관련해서 짚어준 더 저렴한 방법). 태그를 모듈 자신에 달지 Bindable 각각에 달지는 취향 차이 — 사용자 확정("큰 차이는 없는 엔지니어링 선택").
- 공통 원칙(사용자 확정): 어떤 채널이든 debug를 안 켰을 땐 CPU/메모리 영향이 사실상 없어야 함(패시브하게 가볍게만 들고 있거나, 아예 아무것도 안 함) — 무거운 트레이스 함수 실행은 debug를 켠 다음부터만. 이건 위 "계측 지점 3곳"의 no-op 훅 upvalue 패턴과 정확히 같은 원칙의 재확인.
6. "관측해야 실체화된다" 원칙 — quad-debug 자신이 위반하면 안 됨
bind-system-plan.md의 전역 원칙: 어떤 파생값도 :Get()으로 직접
읽히기 전까지 계산되지 않음. quad-debug UI가 트리뷰를 그리면서 모든 State를
자동으로 펼쳐 값을 미리 읽어버리면, 원래 필요 없었을 계산을 디버그 도구가
유발하는 부작용이 생김 — 디버그 도구 자체도 lazy해야 함: 사용자가 UI에서
노드를 명시적으로 펼칠 때만 그 시점의 값을 읽고, 자동 폴링/자동 전개는
지양. 이건 UI 설계 시 지켜야 할 제약으로 문서화만 해두고 지금 확정할 필요는
없음.
7. UUID 기반 on-demand compute — "관측"을 플러그인 클릭으로 명시화
debug 모드가 켜지면 quad의 내부 객체(Source/State/handler 등)에 uuid를
부여해 trace 이벤트와 함께 플러그인에 넘김(위 "데이터 채널" 절의 페이로드
제약 — uuid 자체는 순수 문자열이라 문제없음). 플러그인 UI에서 특정
State 노드를 클릭하면 그 uuid로 BindableFunction을 통해 "지금 이 값을
계산해서 보여줘" 요청을 보내고, quad-debug-roblox가 해당 uuid에
대응하는 실제 State를 찾아 .Get()을 호출해 원시 값으로 변환해 돌려줌 —
사용자 제안, 위 "6. 관측해야 실체화된다" 원칙과 정확히 맞아떨어짐
(플러그인 클릭이 곧 명시적 관측 행위).
안전 문서화 경고 필요(사용자 지적): 이 compute 호출은 여전히 부작용을
일으킬 수 있음 — purity-and-effects-plan.md가 이미 Store는 부작용
허용이 기본이라고 확정해뒀고, Compute 함수는 원래 "State가 최신 상태를
요구받는 시점"에만 실행되는 게 전제인데, 플러그인이 임의의 시점(사용자가
UI를 클릭한 순간)에 강제로 그 계산을 트리거하는 건 이 전제를 벗어남 —
당장 문제를 일으키진 않더라도, quad-debug 문서에는 "State를 눌러보는
행위 자체가 그 계산과 딸린 부작용을 실행시킨다"는 걸 명확히 경고해야 함.
비직렬화 값의 표시: 함수/클로저처럼 순수 직렬화 불가능한 값은
print류 다른 디버깅 도구들이 흔히 하듯 "function" 같은 플레이스홀더
라벨로 표시(루아 사이드에서 포인터 주소를 얻는 표준적 방법은 없어 보임,
필요하면 재조사). 다만 사용자가 직접 만든 복잡한 값(예: 커스텀 Tween류
객체)이 그냥 raw 테이블로만 보이면 알아보기 힘드므로, 개발자가 자기
타입에 대해 "디버거에 어떻게 보여줄지"를 지정할 수 있는 선택적 직렬화
인터페이스를 제공하는 것도 검토할 만함(사용자 제안) — 단, 사용자가
강조한 대로 오버엔지니어링 경계 주의 — 디버깅 도구는 한정된 규모에서
도움이 되는 게 목적이지 모든 걸 다 예쁘게 보여주는 게 목적이 아님,
구현 비용/이점/타당성을 따져서 결정.
8. Element Inspector — 마우스로 UI 요소 피킹 (사용자의 실제 pain point)
사용자가 직접 겪은 문제: Roblox가 최근 Play 중 라이브 UI 편집 도구를 꺼버려서, 실제 화면에 보이는 UI 요소의 위치를 찾으려면 Explorer를 계속 펼치고 접으며 찾거나 검색해야 하는데, quad로 만든 요소는 보통 이름을 잘 안 지정해서 특히 힘듦. 웹 devtools의 "inspect element"처럼 화면을 클릭해서 바로 그 자리의 (quad가 관리하는) UI 요소를 선택하는 도구가 필요 — 최상위에 클릭을 가로채는 투명 레이어를 하나 띄우고 마우스 위치를 추적, 그 좌표에 있는 요소를 히트테스트해서 quad 요소로 필터링해 사용자에게 보여주는 방식. Explorer 기반 트리뷰(위 "핵심 설계 방향" 1번)를 보완하는 별도 진입점 — "무엇을 선택할지도 모르는 상태에서 화면만 보고 찾아 들어가야 하는" 초기 탐색 단계의 마찰을 없애는 게 목적. 사용자가 이번 논의에서 원래 요청("Explorer에서 코드 위치를 알려주는 플러그인") 보다 실제로는 더 크게 느낀 pain point로 언급.
9. Explorer ↔ 플러그인 트리 동기화, UI 아키텍처 확인 (2026-08-06)
질문: 플러그인 자체 트리에는 없는 내부 구현 디테일(예: 특수 핸들러가 자동 생성해 붙인 자식 Instance)을, 사용자가 Roblox 기본 Explorer에서 직접 선택하면 어떻게 처리할까?
사용자 확정 — 두 경우로 분기:
- 플러그인 트리에 대응 노드가 없는 내부 전용 자동 생성물(quad가 bind/track하지 않고 그냥 만들어 붙여만 둔 것)이면, 플러그인 트리가 실제로 알고 있는 가장 가까운 부모를 대신 선택/하이라이트.
- 플러그인 트리에 대응 노드가 있는 경우(예: 사용자가 어떤 Instance를
직접 컴포넌트화해서 quad로 bind한 경우 — 이건 UB가 아니라 충분히 유효한
사용법,
base/component-composition-plan.md의 "컴포넌트 = 플레인 함수" 원칙과도 맞음) 트리에 있는 그 실제 노드를 정확히 선택.
네이밍 컨벤션(사용자 제안): 내부 자동 생성 helper Instance는 _나
QUAD_ 같은 접두어를 붙여서 Explorer에서 직접 봤을 때 헷갈리지 않게 함 —
이름 바꾸는 건 비용이 크지 않음. v1이 이미 _quad_round/_quad_padding/
_quad_scale 네이밍(base/ui-shorthand-plan.md 참고)으로 정확히
이 관습을 썼던 전례 — quad-v2에서 내부 자동 생성물이 생기면 그대로
재사용. research/documentation-plan.md의 "UI 네이밍 컨벤션 문서"
백로그에도 이 구체적 규칙을 추가해둠.
플러그인 UI 아키텍처 확인(사용자 질문에 대한 답 — 맞음): 세 개의 구분된 상호작용면으로 구성됨 —
- 자기 트리 뷰 — React DevTools 컴포넌트 트리처럼, 플러그인 자체
DockWidgetPluginGui안에 quad가 관리하는 계층을 보여줌(위 "핵심 설계 방향" 1번, "무엇이 무엇에 연결됐는가" 그래프). - 리프 클릭 → 상세/상태 패널 — 노드(State 등)를 누르면 그 상세를 on-demand로 보여줌(위 7번, UUID 기반 compute-on-click).
- 실제 Instance 선택과의 연동 — Roblox 기본 Explorer에서 직접
선택하거나(
Selection서비스로 감지, 위 이 절의 동기화 규칙), 또는 Element Inspector(위 8번)로 화면을 클릭해서 선택하면, 그 실제 Instance에 대응하는 노드가 1번의 자기 트리 뷰에서 하이라이트/선택됨.
Explorer(Studio 기본 창)와 플러그인의 트리 뷰(DockWidgetPluginGui)는
서로 다른 별도 창 — 하나로 합쳐진 UI가 아니라 나란히 떠 있는 도킹
위젯 두 개고, 3번이 그 둘을 이어주는 동기화 레이어.
React DevTools에서 가져올 아이디어 (2026-08-06 조사)
서브에이전트로 React DevTools 오픈소스(facebook/react 내
react-devtools-shared 등)를 조사. 그대로 베낄 순 없지만(브라우저
익스텐션 ↔ 웹페이지 구조는 Roblox와 다름) 4가지 발상 중 2개는 상당히
바로 적용 가능:
1. 전역 훅 주입(__REACT_DEVTOOLS_GLOBAL_HOOK__) — 참고는 되지만 그대로는 못 씀
익스텐션이 React 로드 전에 window.__REACT_DEVTOOLS_GLOBAL_HOOK__을
먼저 심어두고, React 렌더러가 부팅하며 그걸 찾아 hook.inject(...)로
스스로 등록하는 "로드 순서 무관 레지스트리" 패턴 — Fusion External.luau의
"이미 로드된 모듈의 업밸류를 나중에 스왑"과는 다른 축(React 쪽은 "누가
먼저 로드되든 상관없게", Fusion 쪽은 "함수 포인터 교체"). quad-debug에도
개념은 유효하나, Roblox는 플러그인/게임이 애초에 별도 프로세스(VM)라
"전역"이 그 경계를 못 넘는다는 근본 제약이 있어 그대로 못 씀 — 이미 알고
있는 문제(위 "데이터 채널" 절)와 동일선상.
2. 소스 위치 캡처는 런타임 스택 트레이스가 아니라 컴파일타임 주입 — 유력한 대안 후보
확인 결과 React DevTools의 "이 컴포넌트가 어디서 정의됐나"는 런타임
스택 트레이스에 전혀 의존하지 않음 — @babel/plugin-transform-react-jsx-source가
빌드 타임에 모든 JSX 생성 호출에 __source: {fileName, lineNumber, columnNumber}를 리터럴로 박아 넣는 컴파일타임 트랜스폼. 런타임엔 이미
값으로 존재.
quad-debug 적용 후보: 위 "계측 지점 3곳"에서 제안한
debug.info(2, "sl") 런타임 캡처(DI 제네릭 생성자, 호출 시점 caller
위치)의 대안/보완으로, darklua 같은 빌드타임 Luau 변환기로 quad
생성자 호출부를 순회하며 파일/라인 리터럴을 인자로 미리 주입하는 방식을
검토할 만함. debug.info가 "호출자(caller)의 정확한 라인"을 항상
안정적으로 못 주는 경우(꼬리 호출 최적화, 인라인화 등)에 특히 유용 —
런타임 계측보다 신뢰도가 높을 가능성. 단, darklua를 빌드 파이프라인에
편입해야 한다는 전제가 새로 생기므로(지금 프로젝트는 아직 별도
빌드/번들 단계가 없음, 순수 Rojo 싱크) 실제 채택은 quad-debug 착수
시점에 비용 대비 검토.
3. 얇은 operation diff + on-demand 상세조회 — 데이터 채널 설계에 바로 적용 가능
content script(페이지) ↔ devtools panel은 별도 프로세스라 매 커밋마다
전체 트리를 보내지 않음 — 압축된 "operation" 배열(add/remove/reorder
같은 짧은 코드 시퀀스)만 기본으로 보내고, props/state 같은 무거운 데이터는
사용자가 실제로 그 노드를 선택했을 때만 별도 요청-응답(inspectElement)으로
가져옴. "기본은 얇은 델타, 상세는 온디맨드"라는 원칙.
quad-debug 적용: BindableEvent 크로스 컨텍스트 검증 결과와 무관하게
(되든 안 되든, 채널이 무엇이든) 이 원칙 자체는 그대로 채택할 만함 — 매
process/retract 호출마다 전체 상태를 흘려보내지 않고 "무슨 일이
있었다"는 최소 메타데이터(대상 id, key, handler id, timestamp)만 기본
스트림으로 보내고, 플러그인이 실제로 그 Instance를 선택했을 때만 상세
정보(생성 스택, props 스냅샷)를 별도로 가져오는 구조. 이러면 채널
대역폭/오버헤드 문제(위 "공통 원칙 — debug 꺼졌을 때 영향 없어야 함"과
직결)가 크게 완화됨.
4. flash-on-update 오버레이 — 전체 상시 적용은 기각, 범위를 좁혀 채택
원안: getBoundingClientRect()류(Roblox면 AbsolutePosition/
AbsoluteSize, 3D면 바운딩 박스)를 읽어 오버레이 박스를 그리고 매
커밋마다 갱신 — Studio 플러그인 오버레이(Highlight/SelectionBox/
BoxHandleAdornment)로 이식 가능한 아이디어 자체는 유효.
사용자 정정(2026-08-06): 이걸 quad가 관리하는 모든 Instance의 모든 프로퍼티 변경에 상시 적용하면 안 됨 — "정말 많은 것들이 다 반짝일 것"(노이즈)이고 추적 비용도 큼. 범위를 좁혀서 채택:
- Instance 마운트/언마운트(생성/파괴)는 상시 flash 가능 — 사용자에게 "뭔가 새로 생겼다/사라졌다"는 notice로 유용하고 빈도도 낮아 비용 문제 없음.
- 개별 프로퍼티 변경 flash는 플러그인에서 현재 열어본(inspect 중인) Instance 한정 — 위 "핵심 설계 방향" 7/8번의 on-demand 상세조회 패널을 연 상태에서만 그 Instance의 값 변경을 반짝이게 표시, 나머지는 안 함. "얇은 스트림 + on-demand 상세"라는 이미 확정된 프로토콜 모양 (아래 3번)과도 자연히 맞아떨어짐.
- 백로그(낮은 우선순위, 사용자 확정): 선택된 Instance에서 quad가 건드리지 않은 프로퍼티 중 기본값이 아닌 것까지 같이 알려주는 기능 — "쉽다면 있으면 좋겠지만 엄청 중요하진 않다"는 평가, 초기 설계 시 가능성 정도만 열어두고 실제 채택은 나중에.
quad-mock 백로그와의 관계
architecture.md의 기존 백로그("범용 렌더 디버깅 도구로서의 quad-mock,
Tween mock 등 동적 동작 포함")와 목적이 다름:
| quad-mock 확장판 (기존 백로그) | quad-debug (이 문서) | |
|---|---|---|
| 실행 환경 | Studio 불필요, 순수 luau CLI, CI |
Studio Play 세션, 실제 엔진 |
| 시점 | 오프라인 스냅샷/리플레이 | 실시간 라이브 관찰 |
| 목적 | 렌더 결과 회귀 검증 | 실사용 중 버그 위치 역추적 |
공유 가능한 기반: 둘 다 "quad 내부 이벤트(process/retract 호출, Source 변경)를 관찰 가능한 스트림으로 노출하는 계측 레이어"가 필요하다는 점은 같음 — 위 "계측 지점 3곳"에서 정의하는 trace 이벤트 스키마를 하나로 설계해두면, quad-mock(오프라인 검증)과 quad-debug(실시간 스트리밍)가 같은 이벤트 포맷을 재사용할 수 있음. 지금 당장 통합할 필요는 없고, quad-mock을 실제로 확장하게 될 때 이 문서를 먼저 참고하라는 정도로만 기록.
패키지 구조 제안 (가칭, 확정 아님)
기존 quad-base/quad-roblox 경계 원칙을 그대로 따름 — base는 인터페이스만,
실 구현은 백엔드/애드온이 주입:
quad-debug— 엔진 무관 core. trace 이벤트 스키마 정의, 위 5개 훅 지점의 no-op 기본 구현, 이벤트 버퍼/필터링 같은 순수 로직.quad-base자체에 넣지 않고 별도 패키지로 두는 이유:quad-base는 프로덕션 코드가 항상 의존하는 코어라 디버그 전용 코드를 섞고 싶지 않음(위 리서치 5번, 나중에 완전 제거하고 싶을 때도 별도 패키지면 그냥 require 자체를 안 하면 끝).quad-debug-roblox— 게임(클라이언트) 쪽에서 require하는 provider. quad-roblox의 Dispatch/DI에 실제 훅을 꽂고, BindableEvent/Function을 quad 모듈 자신의 Instance 트리 안에 만들어 CollectionService 태그로 노출(위 "데이터 채널" 절 —ReplicatedStorage등 게임 트리에 별도 주입 안 함),IsStudio가드 포함.quad-debug-roblox-plugin— Studio 플러그인.DockWidgetPluginGuiUI,Selection서비스로 Explorer 선택 감지, 브릿지 BindableEvent 구독, 연결 그래프/트리뷰/flash 렌더링.
지금 로드맵에 반영할 것 (최소한만)
사용자도 동의한 대로 지금 구현할 단계는 아님 — 다만 아래는 관련 마일스톤
설계 시 "고려는 해두되 지금 확정/구현하지는 않는" 참고용 메모로 남김
(ROADMAP.md의 M2/M3/M5 근처에 훅 확장 지점 존재 가능성만 인지해두는 정도):
- M2(디스패치 엔진) 구현 시
process/retract스캔 루프에 나중에 훅 하나를 끼워 넣기 쉬운 모양으로 짜여 있는지 정도만 유의(지금 훅 자체를 만들 필요는 없음). - M3(Source) 구현 시 마찬가지로 나중에 weak-registry 등록 훅을 끼우기 쉬운 생성자 모양인지만 유의.
- M5(quad-roblox DI 제네릭 생성자) 구현 시 caller 정보를 나중에 끼워넣기
쉬운 단일 진입점(생성자 함수 하나)인지만 유의 — 이건 이미
bind-system-plan.md가 "제네릭 생성자 함수 하나로 통일"이라 확정해둔 것과 자연히 맞아떨어짐, 별도 조치 불필요할 가능성이 큼.
중요: 위는 "이런 게 나중에 필요할 수 있으니 지금 설계를 크게 바꾸라"는 게 아니라, 이미 확정된 설계(단일 디스패치 진입점, 단일 생성자 진입점)가 우연히도 계측 친화적이라는 걸 확인해두는 것에 가까움 — M0~M11 순서/범위 자체를 바꿀 이유는 없음.
열린 질문 (.claude/question.md에도 취합)
기술적 실현 가능성은 전부 해소됨(위 핸드오버 요약) — 아래는 우선순위별로만 분류, 다음 세션 진행을 막는 항목 없음.
해소됨 (2026-08-06 후속 세션)
- "이벤트 함수들이 실제 instance를 읽을 수 있게 self를 건네받는 게 quad의
관습"이라는 언급 — v1
event.lua의func(self or this, ...)관습이 실존함은 확인됐으나(v1 튜토리얼에도 문서화), quad 재설계에서는 채택하지 않기로 확정. Ref가 이미 인스턴스 접근 용도를 커버하고, thin wrapper를 준다면 Modifier 정적 flatten과 경쟁하는 두 번째 쓰기 경로가 생겨 오버엔지니어링/성능/디버깅 추적성 문제가 생긴다는 게 이유 — 클로저 래핑 비용도 근거로 추가됨. 상세 근거와 결정문은base/bind-system-plan.md의 "이벤트 핸들러는 self(Instance)를 받지 않는다" 절 참고. quad-debug 입장에서는 self/thin wrapper 경로가 아예 없어지므로 오히려 계측 대상이 단순해짐(추적 안 되는 경로 자체가 존재하지 않게 됨).
세부 API 이름 (후순위, 구현 착수 시점에 자연히 정리)
describe(가칭) 5번째 핸들러 훅의 정확한 시그니처/이름.- Attribute 이름 네임스페이싱(
__quadSource류)과 노출 정보 범위(스크립트 전체 경로를 노출해도 되는지, 파일명만 남길지 등 보안/정보노출 고려).
백로그(채택 여부만 남음, 핵심 설계와 무관)
- 외부 변경 감지(핵심 설계 방향 4번)를 보조 신호로라도 실제로 켤지 — 타이밍 매칭 정확도는 프로토타입 단계에서 검증 필요.
- 서버 확장(RemoteEvent) 여부 — 필요성이 실제로 드러나면 그때.
- quad-mock과 trace 이벤트 스키마를 실제로 공유할지, 아니면 별도로 갈지 — quad-mock 확장 착수 시점에 재검토.
- 인스턴스 변경을 추적하는 전용 새 Source 메커니즘(다크패턴 방지용으로
사용자가 제안했다가 스스로 낮게 평가) — quad가 이미 생성 객체에
GetPropertyChangedSignal역바인딩 옵션을 제공해서 3줄로 되는 것과 큰 차별점이 없어 보임, 타당성 조사만 백로그로.
범위 밖 — 별도 문서로 분리됨
- 이번 논의에서 파생된 문서화 숙제(UI 네이밍 컨벤션, Store 부작용을 게임
시스템에서 깔끔하게 쓰는 패턴)는 quad-debug 범위가 아니라
research/documentation-plan.md로 분리해 뼈대만 기록함.