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.
5.9 KiB
2026-08-11 일곱 번째 세션 — 반응형 raw 요소: Slot이 State<T>/Source<T>도
요소로 허용(:Single sugar로), :List의 index-스킵 보강
짧은 세션. 사용자가 "Slot이 state<slot>/state<frame>을 안 받는데,
setLength/setOffsetSource를 재사용하면 쉽게 구현 가능하지 않냐"고
제기하며 시작 — 최초 검토안이 사용자의 즉각적인 반례로 기각되고 훨씬
단순한 최종안으로 수렴한 단일 스레드라, 시행착오 없이 최종 확정만
정리(경위는 아래 "기각된 최초안" 절에 압축 보존).
확정: Slot:Add(및 Slot(initial) 생성자 sugar)가 받는 element의
타입이 T | State<T> | Source<T>(T = Instance | Slot<Instance>, 여섯
번째 세션에 확정된 자기 참조 제네릭과 합성)로 확장 — 임의 깊이 nesting
가능(사용자: "이게 얼마나 nesting되는지는 유저 마음, 다 처리가 가능함").
구현은 새 메커니즘이 아니라 순수 :Single sugar: isState(element)면
그 자리에 내부적으로 Slot():Single(element)(nested Slot, updateFn
생략 시 identity 기본값)를 대신 삽입 —
function Slot:Add(element, index)
if isState(element) then
local sub = Slot()
sub:Single(element) -- updateFn 생략 → identity 기본값
element = sub
end
return rawAdd(self, element, index)
end
이 sugar가 성립하도록 Slot:Single(state, updateFn?)도 같이 확정 —
updateFn을 선택 인자로 완화(기본값 function(item) return item end).
기각된 최초안 — position-keyed StoreBind 구독 + state:Compute로
파생한 Length. 처음엔 rawAdd가 그 위치에 대해 별도 재-dispatch
구독을 걸고 Length 기여도도 state:Compute(...)로 파생시켜 setLength에
넘기는 방식을 검토했으나, 사용자가 즉시 반례를 제시해 기각: "state
언랩 시에 None 되는게 복잡함. 그럼 slot 사이에 None이 존재할 수 있게
되는 거 아님? Length 계산도 달라져야 하고, Add/Remove 동작성이 문제가
됨 — 사실상 Slot:Single이랑 정확히 같은 구현이 돼야 함." 검증 결과
정확함 — (1) State<T?>(nilable)를 지원하려면 _elements에 None을
다시 끌어들여야 함, (2) Length 계산에 예외가 생김, (3) Move/Swap이
인덱스를 옮길 때마다 그 위치의 구독도 같이 옮겨야 하는 인덱스-구독
동기화 부담이 생김 — 정확히 :List가 element가 아니라 key 기준으로
설계된 이유와 정면 충돌하는 회귀였음.
위 확정된 sugar가 이 세 문제를 전부 없애는 이유: 바깥 _elements엔
항상 안정적인 sub Slot 레퍼런스만 있어 None이 안 들어가고, sub가
비어있는 것 자체가 이미 지원되는 정상 상태(Length 0 자동 기여)이며,
Remove/Move/Swap도 그 sub 레퍼런스 하나만 다루면 끝이라 인덱스-구독
동기화가 필요 없음. raw State<T> 요소는 결국 "updateFn 생략한
:Single"일 뿐이고, :Single에 updateFn을 직접 주면 prev/userdata
patch-reuse + offset 접근이 되는 상위 호환 — 둘은 대체 관계가 아니라
같은 메커니즘의 다른 updateFn.
:Single이 애초에 생긴 이유가 정확히 이 offset 접근(사용자 직접
확인): "Single은 정확히는 updateFn에서 렌더할 때 offset이 필요해서
만들어진 것" — raw State<T> 요소(identity updateFn)는 값이 바뀔
때마다 이전 mount를 통째로 버리고 새로 만드는 coarse swap(quad가 prev
재사용을 안 해줌)인 반면, updateFn을 직접 지정한 :Single은 같은
Instance를 유지하며 속성만 patch하는 fine-grained 갱신 + offset을
updateFn에 직접 전달. 사용자가 제시한 조합 예시(Slot{ State<Frame> --[[리스트헤더]], Slot():List() --[[아이템]] })를 문서에 그대로 반영 —
LayoutOrder(offset) 참여가 필요 없는 요소는 raw State<T>로 가볍게,
개수/순서가 동적이라 offset이 필요한 그룹은 Slot():List(...)로
감싸는 식으로 한 Slot 안에서 자유롭게 섞어 씀.
부수 발견(사용자) — nested-Slot 결과를 반환하는 :List 아이템의
index 스킵. "상위 입장에서의 인덱스는, Slot이 있으면 크게 건너뛰겠네
아마 의도된 동작이긴 할듯"라는 지적 — 검증 결과 맞음: updateFn이
nested Slot(멀티루트 컴포넌트 결과 등, Length=N)을 반환하면 그 아이템은
물리적으로 1개가 아니라 N개를 차지하므로, reconcile의 pos 커밋이
고정 +1이 아니라 +result.Length여야 다음 형제의 index가 LayoutOrder
계산에서 안 겹침 — pos = candidateIndex - 1 + (isSlot(result) and result.Length:Get() or 1)로 수정, 의도된 동작으로 확정. 남는 캐비엇(그
Length가 outer :List의 reconcile 없이 나중에 바뀌면 index가 스냅샷인
채로 안 갱신됨)은 이미 확정된 "index는 raw, 실시간 반응은 updateFn
몫" 원칙의 당연한 연장이라 새 문제로 취급 안 함.
반영된 파일: base/slot-plan.md(요소 타입 제약 절 갱신, "반응형 raw
요소" 신규 절, :Single의 updateFn 선택 인자화, reconcile의 pos
커밋 공식 수정)/ROADMAP.md(M6)/.claude/README.md/.claude/question.md.
다음 세션이 할 일: 안 바뀜(ROADMAP.md M0부터, luau-test 결과 확인
우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로.