quad/.claude/research/tween-plan.md
qwreey c00e2e67d6
소스 구조 확정 라운드(2026-08-04, 5차) 결과 반영
bind-system-plan/module-lifecycle-plan/slot-plan을 research/에서 base/로
승격, quad-base(인터페이스)/quad-roblox(구현) 패키지 경계와 모노레포 소스
트리를 architecture.md에 확정. Slot의 base/roblox 분리, InstanceChild
핸들러 필요성도 함께 반영.
2026-08-04 13:57:01 +09:00

5.8 KiB

Tween / 애니메이션 플러깅 (착수 전, 사용자와 상의 필요)

상태: research — 방향은 뚜렷하게 잡혀 있으나(라이브러리가 트윈을 직접 구현하지 않는다) cleanup 순서/오버라이드 시맨틱은 미확정. 원본: .claude/initreq/raw-userinput.md "트윈은 어떻게 할 것이냐" / "스토어 값은 항상 먼저 캐치한다" / "네임스페이스드 객체" 절. Fusion의 Tween/Spring이 반응 그래프 안에 있는 설계는 명시적 반면교사 — base/comparison-fusion-vide.md "Animation(Tween/Spring) integration — 경고 신호" 절 참고.

확정된 방향: 트윈을 Store/반응 그래프 밖에 둔다

렌더 라이브러리가 트윈을 직접 구현하는 건 처음부터 디자인이 잘못된 접근 — 렌더링 엔진(Roblox TweenService)의 애니메이션 관리를 전혀 안 따르게 되기 때문. 대신:

[Tween(key, tweenData...)] = storeValue

형태의 특수 bind key로 제공. 처음 실행될 때는 그냥 바인드로 필드를 쓰지만, 이후에는 store 값을 핸들해서 바뀔 때마다 트윈을 처리. 아니면 사용자가 직접 태그를 얻어 관리하게 둠(둘 다 허용 가능한 경로로 열어둘 것).

왜 이게 중요한가(Fusion 리서치에서 확인된 근거): Fusion은 Tween/Spring을 GraphObject(1급 반응 노드, timeliness="eager")로 만들어서 Computed의 입력으로 자유롭게 합성 가능하게 했지만, 그 대가로 (1) 매 프레임 틱하는 외부 클럭 소스(Stopwatch/ExternalTime)를 반응 그래프에 통합해야 했고, (2) eager 재계산 노드가 무효화/전파 로직과 경쟁하게 됐고, (3) 트윈-입력 간 별도의 교차 lifetime 체크 기계장치가 필요해졌다. quad가 트윈을 특수 bind key로 빼면 이 세 가지 복잡도를 전부 피할 수 있다 — 대신 트윈된 값이 Computed류의 추가 입력으로 자유롭게 합성되진 못한다는 걸 감수(Fusion 대비 유일한 손해).

정정: Ref 불필요 — 핸들러는 항상 대상 Instance를 직접 받는다

이전 초안의 전제가 틀렸음. Tween 핸들러도 base/bind-system-plan.md의 "확정된 디스패치 모델"을 그대로 따르는 store-bind 핸들러 중 하나 — process(inst, k, v)가 항상 대상 Instance(inst)를 직접 받으므로, 트윈 대상을 얻기 위해 Ref나 네임스페이스드 조회가 필요하지 않음. Tween의 store-bind 핸들러는 "k는 무엇이든, v가 Store인 것"을 잡아내는 우선순위 매우 높은 핸들러로 등록되고, inst는 이미 파라미터로 주어짐. (Ref 자체는 도입되지만 전혀 다른 용도 — base/bind-system-plan.md의 Ref 절 참고.)

retract(구 cleanup)로 확정된 오버라이드 시맨틱

스토어 값은 항상 먼저 캐치한다 — 그래야 retract 호출이 가능(이름 변경 근거는 base/lifecycle-pattern.md). 이전 트윈을 취소하고 새 트윈을 만드는 게 맞지만, "취소" 시점의 동작이 여러 갈래로 갈릴 수 있음:

  1. 키 밸류를 받으면, 그로 인해 생성된 트윈을 얻어서 멈춰버리기.
  2. 트윈 뒤에 삭제하지 않고 오버라이드(새 트윈이 이전 트윈의 현재 값에서 시작, 이전 트윈 자체는 그대로 재사용/대체).
  3. 삭제 후 새로 시작.
  4. 트윈을 끝 지점으로 옮기고 새로운 트윈을 시작.

확정된 기본값: 멈춤(Cancel) — 새 트윈은 현재 보간된 값에서 자연스럽게 시작. 근거: Roblox TweenService:Cancel()은 프로퍼티를 되돌리지 않고 그 자리에서 멈추기만 하므로, 새 트윈이 시작될 때 이미 인스턴스 프로퍼티에 남아있는 현재 값에서 자연스럽게 이어짐 — 대부분의 UI 애니메이션이 기대하는 동작과 일치.

이 기본값 외 나머지 세 동작(오버라이드/삭제 후 재시작/끝점 이동 후 재시작)은 라이브러리가 강제하지 않고, [Tween(key, tweenData, {onOverride=...})]처럼 키 설정으로 사용자가 고를 수 있게 열어둠 — retract(inst, k, v)가 이전 값(v)을 받으므로 여기서 선택된 동작을 구현. retract가 접근해야 할 "이전에 생성한 실제 Tween 객체"는 base/bind-system-plan.md가 말하는 base 제공 범용 유틸(inst를 키로 하는 weak-keyed per-instance 상태 저장소)에 담아두면 됨.

retract는 완전 소멸(Destroy) 시엔 호출되지 않는다

base/lifecycle-pattern.md의 핵심 원칙: quad는 자신이 만든 Instance를 생명주기 끝까지 그대로 들고 있는 소유자라, Destroy 이후에 실행해야 할 정리 로직이 없다 — 오히려 Destroy된 대상에 :Cancel()/:Stop() 같은 메서드를 호출하면 에러남 (대상이 죽으면 그 대상에 묶인 Tween도 함께 죽은 상태가 되므로). 따라서 retract는 "같은 key에 새 값이 들어와 이전 트윈을 갈아치울 때"만 호출되고, Destroy 시점엔 아무 것도 안 함(라이프타임 Connected 체크로 처리 자체를 멈추는 것만으로 충분).

네임스페이스드 객체 (성능상 이유로 보류)

트윈 대상을 이름으로 찾는 별도 네임스페이스는 성능상 별로라고 판단 — TagService를 쓰는 게 나아 보이지만, 트윈 전용 네임스페이스가 따로 있을 필요가 있는지는 미정(단, 위 정정으로 이 절 자체의 필요성이 낮아짐 — 핸들러가 이미 대상을 직접 받으므로 "나중에 이름으로 찾아서 트윈"할 필요 자체가 잘 없을 수 있음).

열린 질문 (.claude/question.md에도 취합)

  • 기본값(Cancel)은 확정됨. 남은 건 나머지 세 동작(오버라이드/삭제 후 재시작/ 끝점 이동 후 재시작)을 선택하는 옵션 키의 정확한 이름/시그니처 — 구현 단계에서 확정.