v1-compat-plan.md: quad-roblox-v1-compat 기술 계획 — 두 임베딩 방향 + Slot 미결 항목

사용자 확정 사항 반영: 브리지는 v2→v1 단방향만, 패키지명 quad-roblox-v1-compat.
v1 mount.lua/class.lua(Update 재렌더 시 __child 재부모, Clone 트리거 조건,
cascading destroy 의존)와 v2 slot-plan.md(단일 마운트 소유권, retract=폐기,
foreign Instance 처리 미명시)를 대조 조사해 두 임베딩 방향(v2 트리에 v1 리프
박기 / v1 트리 요소를 v2로 점진 교체)에 대한 구체적 안전 규칙을 도출.
Slot이 quad 밖에서 만들어진 Instance를 어떻게 다루는지만 Slot 코어 구현
시점까지 결정 불가로 남겨 question.md에 교차 참조 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-06 20:39:32 +09:00
parent 24c82a299e
commit 3d9e48f5f1
Signed by: qwreey
GPG key ID: D28DB79297A214BD
3 changed files with 120 additions and 28 deletions

View file

@ -45,7 +45,7 @@
| `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 | | `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 |
| `documentation-plan.md` | UI 네이밍 컨벤션 문서 + Store 부작용을 게임 시스템에서 깔끔하게 쓰는 패턴 문서 — 뼈대만, `debug-tooling-plan.md` 논의에서 파생 | 하 — 착수 시점 미정, 뼈대만 기록해둔 상태 | | `documentation-plan.md` | UI 네이밍 컨벤션 문서 + Store 부작용을 게임 시스템에서 깔끔하게 쓰는 패턴 문서 — 뼈대만, `debug-tooling-plan.md` 논의에서 파생 | 하 — 착수 시점 미정, 뼈대만 기록해둔 상태 |
| `ui-shorthand-plan.md` | UICorner/UIPadding/UIScale 인라인 편의 키(v1 `Corner`/`PaddingAll`/`Scale` 선례) — 여전히 필요한 기능으로 재확정(RoundSize만 네이티브 UICorner로 대체돼 불필요), 메커니즘(Handler)·패키지 배치(quad-roblox 코어) 확정 | 하 — 결론 남, M10 전후 구현하면 됨 | | `ui-shorthand-plan.md` | UICorner/UIPadding/UIScale 인라인 편의 키(v1 `Corner`/`PaddingAll`/`Scale` 선례) — 여전히 필요한 기능으로 재확정(RoundSize만 네이티브 UICorner로 대체돼 불필요), 메커니즘(Handler)·패키지 배치(quad-roblox 코어) 확정 | 하 — 결론 남, M10 전후 구현하면 됨 |
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 타당성 검토 — v1 런타임 재구현(문법 흉내)은 컴포넌트 정체성 모델 충돌로 얇게 안 됨, 대신 **v1을 그대로 병행 실행 + 경계에서만 `state:Observer()`+v1 프로퍼티 재대입으로 값 리졸브해 넘기는 브리지**가 유력 방향으로 수렴(사용자 제안). quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — 브리지 세부 범위(양방향 필요 여부, 패키지 위치) 사용자 결정 대기 | | `v1-compat-plan.md` | v1 하위호환(compat) 레이어 `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
## 참고 ## 참고

View file

@ -63,7 +63,11 @@
- **여러 Slot이 형제로 섞일 때 순서 보장**`base/slot-plan.md`의 "여러 - **여러 Slot이 형제로 섞일 때 순서 보장**`base/slot-plan.md`의 "여러
Slot이 섞일 때 순서 보장" 절 참고. Roblox 단일 백엔드로는 급하지 않음 Slot이 섞일 때 순서 보장" 절 참고. Roblox 단일 백엔드로는 급하지 않음
(LayoutOrder/ZIndex로 대부분 해결), 다른 백엔드(웹 DOM 등) 재사용성과 (LayoutOrder/ZIndex로 대부분 해결), 다른 백엔드(웹 DOM 등) 재사용성과
직결되는 문제라 Slot 코어 로직 구현 시점에 재검토. 직결되는 문제라 Slot 코어 로직 구현 시점에 재검토. **같은 구현 시점에
같이 확인할 것(2026-08-06 추가)**: Slot이 quad 밖(v1 compat 등)에서
만들어진 임의 Instance를 동적 배열 원소로 받을 수 있는지, retract 시
foreign Instance를 어떻게 다루는지 — `research/v1-compat-plan.md` 7-3
참고.
- **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고. - **`quad-debug` 세부 API 이름** — `research/debug-tooling-plan.md` 참고.
채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를 채널 실현 가능성(BindableEvent/Function이 플러그인↔Play 중 게임 경계를
넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은 넘는지)까지 사용자가 Studio에서 직접 실측 검증 완료 — 기술적 불확실성은
@ -86,14 +90,15 @@
ui-shorthand-plan.md`. 기능 필요 여부(여전히 필요로 재확정)·메커니즘 ui-shorthand-plan.md`. 기능 필요 여부(여전히 필요로 재확정)·메커니즘
(Handler)·패키지 배치(quad-roblox 코어)는 확정, 남은 건 이름 재검토 (Handler)·패키지 배치(quad-roblox 코어)는 확정, 남은 건 이름 재검토
(용어 정리 합류)와 `RoundSize`(이미지 라운드) 대체 방식뿐. (용어 정리 합류)와 `RoundSize`(이미지 라운드) 대체 방식뿐.
- **v1 하위호환(compat) 레이어 방향**`research/v1-compat-plan.md`(신규, - **v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat`**
2026-08-06, 같은 날 후속 논의로 수렴). v1 런타임을 v2 위에 재구현하는 `research/v1-compat-plan.md`(신규, 2026-08-06, 두 차례 후속 논의로 수렴).
건(문법 흉내) 컴포넌트 정체성 모델 충돌로 얇게 안 되지만, **v1을 그대로 방향 확정: v1을 그대로 병행 실행 + 경계에서만 `state:Observer()`(lazy
두고 v2와 병행 실행 + 경계에서만 `state:Observer()`(lazy 포기, 항상 포기)로 값을 리졸브해 v1 프로퍼티에 재대입하는 브리지, v2→v1 단방향만
관측)로 값을 리졸브해 v1 프로퍼티에 재대입하는 브리지**는 유력 방향으로 (양방향 불필요로 확정), 패키지명 `quad-roblox-v1-compat`으로 확정(소스
수렴(사용자 제안, 기존 base 원시들만으로 조립 가능함을 확인). 남은 결정: 트리에 세 번째 패키지로 추가될 예정). v2-in-v1/v1-in-v2 두 임베딩 방향
이 브리지가 양방향(v1→v2도) 필요한지, 브리지 글루 코드를 별도 패키지로 모두 기술적 근거와 안전 규칙까지 정리됐으나(문서 7번), **Slot이 foreign
뺄지. Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 결정 불가로 남음**
(위 "여러 Slot이 형제로 섞일 때 순서 보장" 항목과 같은 시점에 확인).
## 참고: 지금까지 확정된 것 (요약) ## 참고: 지금까지 확정된 것 (요약)

View file

@ -153,30 +153,117 @@ v2 문법으로 흉내내는 작업 자체를 없앰** — v1 코드는 그냥 v
- **"v1 코드를 완전히 무수정으로 v2 런타임 위에서 돌리는 것"(3-2, OOP - **"v1 코드를 완전히 무수정으로 v2 런타임 위에서 돌리는 것"(3-2, OOP
mutate 재구현)은 여전히 비권장** — 1순위 방향이 그 문제 자체를 안 만들기 mutate 재구현)은 여전히 비권장** — 1순위 방향이 그 문제 자체를 안 만들기
때문에 불필요. 때문에 불필요.
- 확정된 소스 트리(`base/architecture.md` "구현 착수" 절)는 `quad-base`/
`quad-roblox` 두 패키지뿐 — 경계 브리지 글루 코드를 어디 둘지(별도
`quad-compat` 패키지 신설 vs 필요한 프로젝트마다 로컬 유틸)는 아래 열린
질문.
## 6. 열린 질문 (사용자 판단 필요) ## 6. 확정된 것 (2026-08-06 후속 라운드)
- **양방향이 실제로 필요한가, 한쪽 방향(v2→v1, 데이터 새로 짜고 v1 UI는 - **방향: v2→v1 단방향만.** 4번 3항목의 v1→v2(시그널 구독 → `Source:Set()`)
유지)만으로 충분한가** — 사용자 예시는 v2→v1 한 방향. 반대 방향까지 방향은 사용자가 "필요성 모르겠다"고 확정 — 설계 범위에서 제외. 굳이
필요하면 4번의 3번 항목(v1 시그널 구독 → v2 Source 갱신)을 실제로 대칭성 때문에 만들 필요 없음.
설계해야 함. - **패키지명: `quad-roblox-v1-compat`.** `quad-compat`처럼 엔진 무관을
- 브리지 글루 코드를 별도 패키지(`quad-compat`, 소스 트리에 아직 없음)로 가장하는 이름 대신, v1 자체가 애초에 Roblox 전용이라(quad가 엔진 무관화에
뺄지, 아니면 정식 패키지 없이 "필요할 때 짜는 유틸 패턴" 정도로 문서화만 실패한 전례가 있다는 사용자 확인) 이 브리지도 처음부터 `quad-roblox`
해둘지. 계열의 Roblox 전용 패키지로 이름 붙임 — `quad-base`/`quad-roblox` 확정
트리에 세 번째로 추가되는 패키지.
- **번역 경계 원칙 확정**: v1의 원시 타입(Linker, v1 store의
`registerClass` 객체, `Class.Extend().New()`가 만드는 `this` OOP
인스턴스)이 v2 코드 쪽으로 그대로 흘러들어가지 않고, v2의 원시 타입
(Source/State/Store/Modifier/Ref)도 v1 코드 쪽으로 흘러들어가지 않는다
`quad-roblox-v1-compat`의 공개 표면은 오직 (a) 리졸브된 평범한 값과
(b) Roblox Instance만 주고받는다. 두 런타임의 내부 핸들 타입이 서로의
영역을 침범하지 않는 게 핵심 — 아래 7번의 구체적 규칙들이 전부 이 원칙의
적용.
## 7. 기술 계획 — 두 임베딩 방향 + Slot 조사 결과 (2026-08-06 후속)
v1/v2를 병행 사용할 때 실제로 쓰이는 모양은 두 가지다: (A) 신규로 짜는
v2 트리 안에 과거 v1 컴포넌트를 리프로 박아넣는 것, (B) 기존 v1 앱 안의
요소를 하나씩 v2로 교체하는 것. 둘 다 지원 가능한지 v1 `mount.lua`/
`class.lua`와 v2 `base/slot-plan.md`를 대조 조사했다.
### 7-1. (A) v2 트리 안에 v1 컴포넌트를 리프로 박기
제안: `quad-roblox-v1-compat``EmbedV1(v1ClassOrFactory, propsBuilder)`
어댑터 — v1 컴포넌트를 생성하고 루트 Instance를 v2 Slot/`InstanceChild`가
받을 수 있는 leaf 값으로 반환. 내부에 흘려줄 v2 State는 4번에서 확정한
`state:Observer()` 브리지로 v1 인스턴스 프로퍼티에 재대입.
- **근거**: v1의 `mount()`(`mount.lua:49-87`)는 부모-자식 관계에 소유권
검사가 전혀 없음(누가 만든 Instance든 그냥 Parent 세팅 + `__child`
등록) — v2가 v1이 만든 루트 Instance를 자기 Slot에 끼우는 것 자체는
막힘 없음.
- **위험 + 제안 규칙**: v1의 `mountClass:Unmount()`(`mount.lua:21-46`)는
`this`가 Instance면 무조건 `this:Destroy()`를 직접 호출함. 반대로 v2
Slot의 retract(교체) "폐기" 시맨틱이 quad가 안 만든(v1이 만든) foreign
Instance에 대해 뭘 하는지는 `slot-plan.md`에 명시가 없음(7-2 참고).
**→ v2 Slot이 `EmbedV1` 결과물을 폐기할 때 절대 직접 `:Destroy()`
부르지 말고, 반드시 `EmbedV1`이 반환한 핸들의 v1 쪽 정식 `Unmount()`
거치게 한다** — 이게 6번 "번역 경계 원칙"의 구체적 적용 하나.
### 7-2. (B) v1 트리 안 요소를 하나씩 v2로 교체
제안: `quad-roblox-v1-compat``EmbedV2(v2Component, props)`류 반대쪽
어댑터 — v2 컴포넌트를 렌더한 루트 Instance를 v1 prop 테이블의 숫자 키
자식으로 그냥 꽂을 수 있는 값으로 반환.
- **위험 1 — 재렌더 시 파괴**: v1의 `Update()`(`class.lua:452-491`)는
루트 Instance를 파괴 후 재생성하되, `__child`에 정식 등록된(=`mount()`/
`mountfunc` 경로를 거친) 자식만 새 루트로 재부모 지정하고, 그 외(직접
`.Parent=` 대입 등)는 옛 루트와 함께 파괴됨. **`EmbedV2` 결과물은
반드시 v1의 정식 children 경로(prop 테이블의 숫자 키)로만 붙여야 함,
`.Parent=` 직접 대입 금지.**
- **위험 2 — Clone 함정**: `ProcessQuadProperty`(`class.lua:209-212`)는
같은 prop 테이블이 여러 인스턴스 생성 호출에 걸쳐 재사용되면(첫 번째
인자, `iprop==1`이 아닌 경우) 그 안의 자식 Instance를 통째로 `Clone()`
— v2 루트가 Clone되면 원본과 반응형 그래프 연결이 끊긴 죽은 복제본이
생김. **`EmbedV2` 결과물은 절대 공유/캐시된 prop 테이블(`Import`의
defaultProperties, 재사용 style 테이블 등)에 넣지 말고, 매번 새로 만드는
최초(iprop==1) prop 테이블에만 넣도록 문서화** — 가능하면 구현 시점에
Clone 감지 가드(예: 복제 발생 시 error) 추가 검토.
- **거저 얻는 이득 — 파괴 방향은 이미 맞물림**: v1은 자기가 파괴될 때
children을 순회하며 개별 Destroy하지 않고 Roblox 엔진의 cascading
destroy에 의존함(`class.lua:494-508`에 순회 로직 없음, 확인 완료). v2의
라이프사이클은 이미 `Destroying` 훅 기반 GC-native 패턴
(`base/lifecycle-pattern.md`)이라 "누가 파괴를 트리거했든 Destroying만
감지하면 됨" — v1이 자기 루트를 Destroy()해서 안에 박힌 v2 서브트리가
cascading으로 같이 파괴돼도 v2 쪽 정리가 별도 브리지 코드 없이 자동으로
맞물림.
### 7-3. Slot — 조사했지만 완전히 못 푼 부분 (사용자가 예상한 대로)
- `base/slot-plan.md`엔 "엄격한 단일 마운트 소유권"(`isMounted` 관리,
재마운트 시 즉시 `error()`)은 확정돼 있지만, **Slot이 이미 만들어진
임의 Instance를 동적 배열 원소로 받을 수 있는지, 아니면 그건 별도
`InstanceChild`(정적 단일 삽입) 핸들러 전용인지가 문서에 명시 안 됨.**
`EmbedV1`의 반환값을 v2 쪽에서 Slot(동적 배열)에 넣을 수 있는지
`InstanceChild`(정적 단일)로만 넣을 수 있는지는 실제 Dispatch/Slot
구현 시점에 가서야 확인 가능.
- Slot의 retract "폐기"가 quad가 안 만든 Instance에 대해 정확히 뭘 하는지
(그냥 `:Destroy()`인지, 다른 처리인지)도 문서 밖 — 7-1에서 제안한
"직접 Destroy 금지, Unmount 경유" 규칙을 Dispatch 엔진의 어느 지점에
훅으로 강제할지도 Slot 실제 구현 시점 확인 필요.
- **결론: 지금 결정 불가.** M0 이후 Slot 코어 로직 구현 라운드
(`question.md`의 "여러 Slot이 형제로 섞일 때 순서 보장" 항목과 같은
시점)에서 이 두 가지를 실제 구현과 함께 재확인해야 함.
## 8. 남은 확인 사항 (추가 리서치 후보, 지금 결정 불필요)
- v1이 자기 루트 Instance의 `Destroying`(또는 유사 신호)을 듣고 Lua측
부기(`store.AddObject` 태그 레지스트리 등)를 스스로 청소하는 경로가
있는지 미확인 — 7-1의 "v2가 v1 임베딩을 Destroy 대신 Unmount 경유해서
정리하라"는 규칙이 얼마나 엄격히 지켜져야 하는지가 여기 달림(v1이
Destroying만 들어도 알아서 청소한다면 직접 Destroy해도 무방해질 수
있음).
- v1 store의 `registerClass` 체이닝 기능(`:Tween`/`:Default` 등)까지 - v1 store의 `registerClass` 체이닝 기능(`:Tween`/`:Default` 등)까지
브리지가 흡수해야 하는 케이스가 있는지 — 단순 프로퍼티 재대입만으로 브리지가 흡수해야 하는 케이스가 있는지 — 단순 프로퍼티 재대입만으로
충분한 범위인지 실사용 예시로 확인 필요. 충분한 범위인지 실사용 예시로 확인 필요.
- (3-1을 실제로 병행 채택할 경우) 이벤트 self 관습을 compat에서 되살릴 때, - (2순위 문법 설탕 어댑터를 실제 채택할 경우) 이벤트 self 관습을 compat에서
`base/bind-system-plan.md`가 명시한 반대 근거 4번(quad-debug 추적 밖 되살릴 때, `base/bind-system-plan.md`가 명시한 반대 근거 4번(quad-debug
mutate 경로)을 어떻게 처리할지 — quad-debug는 어차피 후순위라 지금 결정 추적 밖 mutate 경로)을 어떻게 처리할지 — quad-debug는 어차피 후순위라
불필요할 수도 있음. 지금 결정 불필요할 수도 있음.
## 착수 시점 ## 착수 시점
지금 당장 설계/착수 불필요 — `CLAUDE.md` "지금 할 일" 1번(구현 착수, 지금 당장 설계/착수 불필요 — `CLAUDE.md` "지금 할 일" 1번(구현 착수,
ROADMAP M0)이 최우선. 이 문서는 타당성 평가 결과만 남겨두고, 실제 설계는 ROADMAP M0)이 최우선. 7-3의 Slot 관련 두 항목은 M0 이후 Slot 구현
사용자가 방향(4번 병행+브리지의 세부 범위)을 정한 뒤 진행. 라운드에서 실제 코드와 함께 재확인해야 풀림 — 그 전까진 이 문서의 제안
(7-1/7-2 규칙들)이 최선의 추정치.