quad/.claude/session/2026-08-10-03-tween-redesign.md
qwreey 1f56c75978
chore(docs): split CLAUDE.md session log into .claude/session/, keep 2-4 line summaries
CLAUDE.md had grown to 3196 lines of accumulated session logs, causing
context bloat. Full session narratives (including trial-and-error and
later-corrected reasoning — quadnomicon devlog raw material) now live
as 39 individual files under .claude/session/. CLAUDE.md keeps only a
short "지금 할 일" (re-synced against question.md/pre-implementation-audit.md,
stale detail dropped) and a compact per-session summary+link table.
No design decisions changed; base/research/question.md were already
in sync with every session (verified against README.md/question.md
before archiving), so no unreflected content needed migrating first.
2026-08-11 14:40:43 +09:00

9 KiB

2026-08-10 두 번째 세션 — Tween 구조 전면 재설계: 독립 Dispatch 핸들러 →

값-레벨 Tween<T> 래퍼, pre-implementation-audit.md 1-1 완전 해소

사용자가 "트윈도 타입 문제가 있다 — 키 타입을 어떻게 하냐, Property setter가 더 분발해서 VisTween이면 트윈 넣는 게 낫지 않냐"고 제기하며 시작된 긴 단일 스레드. 기존 확정 모델([Tween(key, tweenData...)] = storeValue, v가 Store인 아무 k나 잡는 우선순위 최상위 Dispatch 핸들러, 2026-08-04부터 확정)이 실은 research/pre-implementation-audit.md 우선순위1-1이 이미 지적해뒀던 구조적 모호함("애니메이션 없는 일반 반응형 프로퍼티 바인딩도 결국 이름이 Tween인 파일을 거쳐가는가")을 안고 있었다는 걸 사용자 제안이 정확히 겨냥한 것으로 드러나, 세션 내내 살을 붙여 완전히 재설계까지 감. 구 모델은 archive/tween-special-bind-key-reversed.md로 이전, research/tween-plan.md는 전면 재작성됨 — 상세 근거는 그 두 문서가 최종 소스, 여기는 결정 흐름만 요약.

핵심 재설계: State/Source 언랩(범용 Dispatch/StoreBind.luau, k/v 타입 무관)과 "이 값이 트윈 대상인가" 판단을 완전히 분리 — 후자는 별도 Dispatch 핸들러/우선순위 경쟁이 아니라, PropertyHandler가 realv를 다 풀어낸 뒤 직접 하는 값-레벨 분기(isTween(realv))로 옮김. Tween(opts: {Value: T, ease...}) -> Tween<T>Store({...})와 같은 Type(args) 테이블 팩토리. 이 전환 하나로 우선순위1-1이 구조적으로 성립 불가능해짐 — 범용 반응형 바인딩과 Tween이 애초에 같은 핸들러를 놓고 경쟁할 지점 자체가 없어짐.

세션 중 순서대로 다듬어진 세부 결정들(전부 최종적으로 research/ tween-plan.md에 반영):

  1. Tween.Value는 plain T만, 자체 반응 경로 없음 — 처음엔 ValueT|State<T>를 받아 내부에 별도 Observer를 걸어야 하나 검토했으나, 바깥 :Compute가 소스 변경마다 Tween{Value=v,...}를 통째로 재생성해 StoreBind 재귀를 타므로 불필요함을 확인 — "같은 일 하는 두 번째 경로를 안 만든다" 원칙 재적용, Tween<T> = {Value: T, ease...}로 확정.
  2. 3-상태 릴레이션 슬롯으로 hasBeenSet과 활성 엔진 트윈 저장을 통합relate:GetStrong(inst,k)RobloxTween | true | nil 중 하나: nil=이 키 첫 세팅(애니메이션 없이 즉시 스냅, 기본값→목표값으로 날아오는 진입 애니메이션 버그 방지), true=세팅된 적 있음/활성 트윈 없음(정상 애니메이션 시작 가능), 엔진 객체=활성 트윈 있음(override 정책대로 정리 먼저). 사용자가 직접 "hasBeenSet은 어차피 트윈에만 쓰이니 트윈 저장 슬롯 하나로 합치자"고 제안해 확정.
  3. 활성 트윈이 있는데 plain 값이 들어오는 경우의 순서 규칙 신설 — 먼저 override 정책대로 이전 트윈을 정리(멈추거나 끝냄)하고, 그 정리가 끝난 뒤에만 새 값을 세팅. 순서가 뒤바뀌면 이전 트윈의 다음 인터폴레이션 프레임이 방금 세팅한 값을 덮어쓸 위험이 있어서 — 사용자가 직접 짚은 시퀀싱 버그.
  4. 타입 대수: T' = T | Tween<T> 치환만으로 해결, 새 타입 기계 불필요 — 지금 프로퍼티류 필드가 전부 T | State<T> 모양으로 통일돼 있는데, 여기서 "이 필드의 T" 자체를 T' = T | Tween<T>로 치환하면 자동으로 T | Tween<T> | State<T | Tween<T>>가 나옴 — Modifier/State/Source/ StoreBind 코드엔 Tween 인지 로직이 전혀 안 들어감(StoreBind는 원래도 페이로드 타입에 무관하게 isState만 봄), 타입 생성 스크립트가 필드 타입 문자열만 바꾸면 끝. 사용자가 직접 대수적으로 도출.
  5. useTween 우회 — 새 옵션 필드 없이 해소. 이전엔 Tween{useTween=state<boolean>}처럼 별도 필드가 필요하다고 열어뒀으나, 2026-08-07 일곱 번째 세션에 확정된 state:Apply(factory) sugar 위에 someState:Apply(Animate(reduceMotion, opts))처럼 조건부로 Tween{...}를 씌우거나 안 씌우는 :Compute 팩토리 하나로 공짜로 풀림 — 새 base 메커니즘 불필요.
  6. Animate 콤비네이터는 quad-roblox 유틸, base 프리미티브 아님Tween<T> 값 타입/isTween만 base(quad-base/Tween.luau)에 있고, Animate는 이미 있는 :Apply/:Compute/Tween{...}를 조합한 편의 함수라 나중에 이름/모양을 자유롭게 바꿔도 base 계약에 영향 없음 — 사용자 표현으로 "저비용 고효율 엔지니어링".
  7. 패키지 경계는 Tag가 이미 밟은 분리를 그대로 재사용 — quad-base: Tween.luau(값 타입만). quad-roblox: Handlers/Property.luau(isTween 분기+3-상태 저장+override 정책 흡수, 기존 독립 Handlers/Tween.luau 폐기) + Animate.luau(신규).
  8. 부수 발견 — retract가 Tween 경로에서 사실상 필요 없어짐. 기존 모델에서 "Tween↔프로퍼티 핸들러 타입 교체"가 retract가 실제로 의미를 갖는 유일한 대표 예시였는데, 새 모델에선 매치되는 Dispatch 핸들러가 항상 PropertyHandler 하나뿐이라 이 케이스 자체가 사라짐 — 트윈 취소/전환은 PropertyHandler 내부의 3-상태 슬롯 로직으로 대체(Tag가 이미 하는 "diff는 process 자신이 담당" 패턴과 같은 모양). retract 필드 자체는 "생략 불가" 일반 규칙이라 여전히 정의는 해두되, 실제 호출은 거의 없어짐. Tag(핸들러 타입이 실제로 바뀌게 재설계되어 retract가 필요해진 사례)와 Tween(핸들러 타입이 안 바뀌게 재설계되어 retract 필요성이 사라진 사례)을 서로 반대 방향 사례로 archive 문서에 대비해둠 — quadnomicon 소재.
  9. Tween<T>의 핸들러 계층 분류 정정base/modifier-plan.md가 원래 Tween을 Slot/Tag/Attribute와 같은 "dispatch 참가자"(State/Source에 담겨도 재귀 재-dispatch가 그대로 처리해주는 부류)로 묶어뒀는데, 이제 Tween<T>process/retract가 없는 순수 raw 데이터 값이라 None과 같은 분류로 정정 — Modifier 필드/State<Modifier>가 막는 "핸들러 계층 값 → error" 규칙에 안 걸린다는 결론은 안 바뀜(그냥 raw 값이라서로 근거가 바뀜).
  10. initValue(진입 애니메이션)와 hasBeenSet의 긴장 관계를 기록만 해둠 — hasBeenSet이 "첫 세팅은 무조건 스냅"을 기본 동작으로 확정했으므로, 나중에 initValue(다이얼로그 슬라이드-인 등)가 실제로 필요해지면 이 억제 동작을 명시적으로 우회하는 방법까지 같이 설계해야 함 — 새 결정 없이 상충 관계만 research/tween-plan.md에 남김.

여전히 열려있는 것(M11 착수 시 확정): override 정책 4가지 중 기본값 Cancel 외 세 옵션의 정확한 키 이름/시그니처, Tween→plain 전환에 5번째 옵션이 필요한지, 트윈 옵션 값 모양(TweenInfo 그대로 vs 편의 필드 — 소견은 후자), Animate의 정확한 시그니처(조건/옵션 분리 vs 통합).

반영된 파일: research/tween-plan.md(전면 재작성, 최종 소스), archive/tween-special-bind-key-reversed.md(신규, 구 모델 원문+역전 사유), base/bind-system-plan.md(9곳 — "확정된 디스패치 모델"의 대표 예시를 Tween에서 StoreBind로, retract 필요 패턴 예시를 Tag로 교체, "Dispatch는 프리미티브가 아니다"/"Dispatch 체인" 절의 핸들러 목록에서 Tween 제거, None 센티널 절 예시 갱신, Ref/Brand 절 문구 정정), base/architecture.md(소스트리 — quad-base/Tween.luau 신설, quad-roblox/Handlers/Tween.luau 삭제하고 Handlers/Property.luau 설명에 흡수, Animate.luau 신설), base/modifier-plan.md(핸들러 계층 분류에서 Tween 제외 + 신규 "10. Tween<T>와의 타입 합성" 절), research/pre-implementation-audit.md(우선순위1-1 해소 표시), ROADMAP.md(M11 전면 재작성, M2/M7 체크박스 갱신), .claude/question.md/ .claude/README.md(참조 동기화).

다음 세션이 할 일: 안 바뀜(ROADMAP.md M0부터, luau-test 결과 확인 우선) — 이번 세션도 순수 설계 확정/문서 정리라 M0 착수 우선순위 자체는 그대로. M11 착수 시점이 오면 위 "여전히 열려있는 것" 목록부터 확인.