decide(base): With도 새 State 노드로 확정, 가변인자로 체인 남발 방지

clone 기반 빌더 대안은 디버그 그래프 1:1 매핑을 깨고, Compute 노드 위에
clone하면 캐시 슬롯이 복사되어 계산이 중복 실행됨(State 체인 플래튼
기각과 같은 실패 모드). With(...)를 가변인자로 만들어 노드 남발 걱정을
해소.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-07 11:34:22 +09:00
parent 9609c7cd57
commit ec7469f74f
Signed by: qwreey
GPG key ID: D28DB79297A214BD
2 changed files with 85 additions and 0 deletions

View file

@ -635,6 +635,56 @@ State는 구독 등록 자체가 안 일어남. 다이아몬드에서 중복 워
Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사용자를 위한
좋은 심화 콘텐츠 소재.
### `:With`도 새 State 노드로 확정, 가변인자로 체인 남발 방지 (2026-08-07)
**문제 제기(사용자)**: `:With(...)`가 문서상 가변인자 표기이긴 한데, 실제로
호출마다(`:With(a):With(b):With(c)`처럼 체이닝할 때) 매번 새 State 노드를
만드는 게 맞는지, 아니면 값 없이 의존성 목록만 clone-then-append로 누적하는
가벼운 빌더로 만들어 노드 증식을 피해야 하는지가 불명확했음.
**"빌더" 대안은 기각.** 세 가지 이유:
1. **디버그 그래프가 꼬임.** `quad-debug`의 핵심 UX는 "무엇이 무엇에
연결됐는가" 그래프(`research/debug-tooling-plan.md`). With/Compute를
전부 실제 노드로 두면 코드상의 호출 체인이 그래프 엣지와 1:1로 그대로
대응됨. 빌더로 만들면 그래프 툴이 "이건 노드가 아니라 나중에 갈라지는
지점"이라는 가상의 분기 모양을 따로 합성해야 함 — 그럴 이유가 없음.
2. **다이아몬드 dedup을 못 타고 특수 케이스가 생김.** With가 진짜 노드면
`w = key1:With(key2)`에서 갈라지는 `c1 = w:Compute(g1)`, `c2 =
w:Compute(g2)` 같은 흔한 fan-out이 이미 확정된 "invalid 플래그로
다이아몬드 중복 워크 방지" 장치(위 "전파 모델 확정" 절)를 그대로
재사용함. 빌더면 c1/c2가 key1/key2에 각자 직접 구독을 걸어야 해서
기존 dedup 경로를 매번 우회하는 특수 케이스가 생김.
3. **clone 기반 구현은 Compute 노드 위에서 실제로 깨짐(사용자 지적,
검증 완료).** `c = a:Compute(f)` 뒤에 `w = c:With(b)`를 clone으로
구현하면, `table.clone``c`의 캐시 슬롯(계산된 값 + `invalid`
플래그)까지 그대로 복사해 `w``c`와 별개의 독립 캐시를 갖는 사실상
다른 노드가 됨. `c``w`가 각자 관측되면 `f`가 두 번 따로
실행/캐싱됨 — 바로 위 "왜 State 체인을 Modifier처럼 플래튼하지
않는가" 절에서 이미 기각한 것과 정확히 같은 실패 모드(공유돼야 할
계산이 소비자 수만큼 중복 실행). Modifier의 clone-then-append 패턴을
State 쪽에 그대로 가져오면 안 되는 이유가 바로 이것.
**결정**: `:With(...)`는 호출마다 self+주어진 인자들을 구독하는 **새 State
노드**를 만든다(레퍼런스 기반 구독, clone 아님) — 계산 함수는 없고 값은
`self`를 그대로 통과(pass-through)시키되 구독 목록만 넓힌 얇은 노드. 이
노드는 Observer와 같은 패턴(외부 weak table)으로 상위 노드의 구독자 목록에
등록됨.
**노드 증식 걱정은 가변인자로 해소.** 처음 문제 제기("With 하나마다 노드가
하나씩 늘어나는 게 낭비 아니냐")는 노드 자체를 없애는 대신, `:With(...)`
여러 의존성을 한 번에 받을 수 있게 해서 해소함:
- `key1:With(a, b, c):Compute(fn)` — 노드 1개(구독 3개)로 끝남.
- `key1:With(a):With(b):With(c):Compute(fn)` — 여전히 가능하지만 노드
3개가 만들어짐. 이건 나쁜 게 아니라 각 노드가 dedup/디버그 그래프에서
실제 역할(구독 fan-in 지점)을 하는 저렴한 노드(계산 없음, Modifier
clone과 같은 급의 비용)라 걱정할 비용이 아님.
- 그래도 **가변인자 스타일을 권장 관례로 삼음** — 그래프로 그릴 때도
`:With(a, b, c)``:With(a):With(b):With(c)`보다 단순(노드 1개에 들어오는
엣지 3개 vs 노드 3개가 순서대로 이어지는 모양)해서 디버그하기 쉬움
(사용자 확인).
**`:With`/`:Compute` — self 인자도 lazy 핸들로 통일**
- 최초안(self 값은 포지셔널 raw 값, with한 값만 클로저로 읽음)에는 실제

View file

@ -569,3 +569,38 @@ architecture.md` 소스트리 주석 두 곳 — `Store.luau`가 여전히 옛 `
**다음 세션이 할 일**: M0 착수 전에 `pre-implementation-audit.md` 우선순위1
항목(특히 위 6개)부터 확인 — "지금 할 일" 1번 참고. `.claude/question.md`
2번에 사용자 판단이 필요한 항목 요약이 반영돼 있음.
## 2026-08-07 세션 — `:With`도 새 State 노드로 확정
사용자 질문에서 시작: `:With(...)`가 문서상 가변인자 표기이긴 한데, 체이닝
(`:With(a):With(b):With(c)`)할 때마다 실제로 새 State 노드를 만드는 게
맞는지, 아니면 값 없이 의존성 목록만 clone-then-append로 누적하는 가벼운
빌더로 만들어 "노드가 With 호출마다 하나씩 증가하는" 낭비를 피해야 하는지가
불명확했음. 처음엔 "빌더" 대안(진짜 State가 아닌 clone 기반 누적 객체)을
검토했으나, 사용자가 두 가지 반례를 직접 제시하며 기각함:
1. **디버그 그래프가 꼬임**`quad-debug`의 핵심 UX가 "무엇이 무엇에
연결됐는가" 그래프인데, With/Compute가 전부 실제 노드면 코드 호출
체인이 그래프 엣지와 1:1 대응되지만, 빌더로 만들면 그래프 툴이 가상의
분기 지점을 따로 합성해야 함.
2. **clone 기반 구현이 Compute 노드 위에서 실제로 깨짐** — `c =
a:Compute(f)` 뒤에 `w = c:With(b)`를 clone으로 구현하면 `c`의 캐시
슬롯까지 그대로 복사되어 `w``c`와 별개의 독립 캐시를 갖게 되고,
`c`/`w`가 각자 관측되면 `f`가 두 번 따로 실행됨 — `bind-system-plan.md`
이미 기각해둔 "State 체인 플래튼"과 정확히 같은 실패 모드.
**결정**: `:With`는 호출마다 self+인자들을 레퍼런스로 구독하는 새 State
노드를 만든다(clone 아님, 계산 없는 pass-through 노드). 원래 문제 제기
(노드 남발)는 노드를 없애는 대신 `:With(...)`를 진짜 가변인자로 만들어
해소 — `:With(a, b, c)` 한 번으로 노드 1개(구독 3개)를 만들 수 있고,
디버그 그래프도 이쪽이 더 단순해 권장 관례로 삼음. 체이닝 스타일도 여전히
가능하나 그건 저렴한 노드가 늘어나는 것뿐이라 문제 삼을 비용이 아님.
`base/bind-system-plan.md`의 "왜 State 체인을 Modifier처럼 플래튼하지
않는가" 절 바로 뒤에 새 소절로 반영 완료. 다른 문서(`question.md`/
`ROADMAP.md`/`modifier-plan.md`)엔 이 결정과 모순되거나 갱신이 필요한
서술 없음을 확인함(감사 완료) — `modifier-plan.md`가 이미 "State가
`:With`/`:Compute`마다 새 노드를 할당"이라고 서술해뒀던 것과도 정합적.
다음 세션이 할 일은 안 바뀜(위 2026-08-06 네 번째 세션 절 참고) — 이
결정은 M0 스파이크(Store/State propagation 검증)가 실제로 짜볼 때
참고할 구체 스펙이 하나 더 생긴 것뿐.