decide(attribute): per-name ownership via private rawNew keys, not a new registry

Group Attribute(...) delegating through the public memoized AttributeKey(name)
cache meant a direct [AttributeKey "name"]=v write and a group field (or two
independent groups) targeting the same name silently converged on the same
dispatch slot -- last write wins, no error. Fix: groups use rawNew(name) to
mint a private per-name key cached on their own (inst,index) relation instead
of the shared cache, so AttributeKeyHandler can detect a conflicting claimant
by simple object-identity comparison -- no separate ownership registry needed.
Persisting names reuse the same cached key across group value swaps, so the
existing diff (nil only what left, leave the rest alone) keeps working.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-12 15:23:24 +09:00
parent 27adad9c1a
commit 6b7af54e0d
Signed by: qwreey
GPG key ID: D28DB79297A214BD
4 changed files with 203 additions and 26 deletions

View file

@ -116,10 +116,89 @@ end
`Tag(...)`↔`nil`이 실사례 — 2026-08-10 세션부터 Tween은 더 이상 이 `Tag(...)`↔`nil`이 실사례 — 2026-08-10 세션부터 Tween은 더 이상 이
패턴의 예시가 아님, `base/tween-plan.md`)에 해당 안 함 — 패턴의 예시가 아님, `base/tween-plan.md`)에 해당 안 함 —
`bind-system-plan.md` "확정된 디스패치 모델" 절이 한때 Attribute도 `bind-system-plan.md` "확정된 디스패치 모델" 절이 한때 Attribute도
retract 필요 예시로 들었던 걸 여기서 바로잡음. retract 필요 예시로 들었던 걸 여기서 바로잡음. **[정정, 2026-08-12
열 번째 세션] 그룹 위임 경로가 생기면서 `retract`가 다시 의미 있어짐 —
아래 "이름 소유권" 절 참고, 이 문단이 말하는 "값이 뭐든 같은 핸들러"는
여전히 맞지만 그룹이 이름을 놓을 때는 그 이름 전용 키 객체 자체가
통째로 폐기되므로 그 키 슬롯 기준으로는 `retract`가 실행됨.**
- store-bind 가능(일반 프로퍼티와 동일하게 취급, `Store<T>`/`State<T>` - store-bind 가능(일반 프로퍼티와 동일하게 취급, `Store<T>`/`State<T>`
값도 받음). 값도 받음).
### 이름 소유권 — 그룹/직접 쓰기 충돌 방지, `rawNew`와 per-name 전용 키 (2026-08-12 열 번째 세션)
**문제**: `AttributeKey(name)`이 이름별 weak 캐시로 항상 같은 객체를
리턴하고, 그룹 `Attribute(...)`가 그 경로를 그대로 재사용(위 "메커니즘"
절)하다 보니, **서로 다른 원래 위치(해시파트 직접 쓰기 `[AttributeKey
"name"]=value` vs 배열파트 `Attribute(store)`, 또는 서로 다른 두
`Attribute(...)` 그룹)가 같은 이름을 동시에 관리하려 하면 정확히 같은
`(inst, k=AttributeKey(name))` 자리로 수렴해 조용히 마지막 쓰기가
이기는 충돌이 생김.** Modifier 필드는 정적이라 override로 이미 해소되지만
(같은 해시 키는 한 Modifier 안에 하나뿐), 그룹의 이름 집합은 런타임에
동적이라 이 해소망 밖에 있음.
**해법 — 그룹은 공개 `AttributeKey(name)` 캐시를 안 쓰고, 이름당 자기
전용 키 객체를 만들어 씀.** `AttributeKey`의 내부 구현을 캐시 조회
(`rawNew`가 없으면 만들어서 캐시)와 순수 객체 생성(`rawNew(name)`,
브랜드 태그/`Name` 필드는 있지만 캐시를 거치지 않는 raw 생성자)로 분리 —
공개 `AttributeKey(name)`은 지금처럼 캐시를 거치고, **그룹 Handler(roblox
글루)만 `rawNew`를 직접 써서 이름마다 자기만의 키 객체를 만듦.**
```lua
-- AttributeKeyHandler(quad-roblox) 전용, (inst,name)별 현재 이 이름을 쓰는 키 객체
local owners = Relate() -- {[inst(weak)] = {[name]: keyObject}}
function AttributeKeyHandler.process(inst, k, v)
local name = k.Name
local map = owners:GetStrong(inst) or {}
local current = map[name]
if current ~= nil and current ~= k then
error(("attribute \"%s\"는 이미 다른 AttributeKey가 관리 중"):format(name))
end
inst:SetAttribute(name, v)
if v == nil then
map[name] = nil -- nil로 귀결되면 소유권도 같이 반납 — 다른 claimant가 다시 쓸 수 있게
else
map[name] = k
end
owners:SetStrong(inst, map)
end
function AttributeKeyHandler.retract(inst, k, v)
local name = k.Name
local map = owners:GetStrong(inst)
if map and map[name] == k then
map[name] = nil
inst:SetAttribute(name, nil)
end
end
```
- **직접 리터럴 쓰기**(`[AttributeKey<<T>> "name"] = value`)는 공개
`AttributeKey(name)`을 그대로 씀 — 한 Modifier 안에 같은 해시 키가
중복될 수 없어 이 경로의 claimant는 항상 유일, 별도 캐싱 불필요.
- **그룹**은 자기가 이미 갖고 있던 "(inst, 자기 배열 위치)별 마지막으로
쓴 attribute 상태" 릴레이션(위 "메커니즘" 절)의 저장 형태를 **이름
문자열 집합 → `{[name]: 그 이름 전용 키 객체}` 맵으로 확장**만 하면 됨 —
새 릴레이션 불필요, 이미 있던 걸 재사용. 이름을 처음 보면 `rawNew(name)`
만들어 이 맵에 캐싱하고 그 키로 위임, 이미 맵에 있으면(이전 사이클에
이미 관리 중이던, 즉 "남아있는" 이름) **그 캐싱된 같은 객체를 그대로
재사용**해서 위임.
- **왜 이러면 "새 Attribute 셋 비교"가 저절로 맞아떨어지는지(사용자 확인,
2026-08-12 열 번째 세션)**: 그룹이 이름 집합을 diff할 때 — **남아있는
이름은 캐싱된 같은 키 객체로 재위임하므로 `owners` 맵에서 `current == k`
성립해 통과, `retract` 자체가 안 불림**(값만 갱신) — **사라진 이름만
`Dispatch.retractUnder`가 그 이름 전용 키를 타서 `retract`가 불리고
`nil`화됨.** 새로 들어온 이름은 `rawNew`로 갓 만든 키라 `owners`에 없어
그냥 새로 클레임. 즉 "진짜 새 셋과 비교해 사라진 것만 nil화, 나머지는
건드리지 않고 갱신"이 diff 로직을 하나도 안 고치고 위 `process`/`retract`
구현만으로 자연히 나옴 — **캐시가 그룹 값 교체를 넘어 계속 유지돼야만
성립**(매 교체마다 키를 새로 만들면 남아있는 이름도 `owners`엔 옛
객체가 남아있어 새 객체와 비교 시 오탐 충돌이 남).
- **패키지 경계**: `AttributeKey` 자체가 이미 quad-roblox 소속(Tag와
달리 base/roblox로 안 쪼갬, 아래 "패키지 배치" 절)이고 그룹의 실제
위임 로직도 이미 roblox 쪽 글루라 `rawNew` 호출이 새 역의존을 안 만듦 —
base쪽 `Attribute(...)` 값 객체 자신은 이 메커니즘을 전혀 모름.
## 그룹 `Attribute(...)` — 여러 Store를 한 번에 attribute로 (2026-08-11 아홉 번째 세션 신설) ## 그룹 `Attribute(...)` — 여러 Store를 한 번에 attribute로 (2026-08-11 아홉 번째 세션 신설)
### 동기 ### 동기
@ -154,48 +233,59 @@ Frame { Attribute(styleStore), Attribute(stateStore) } -- 여러 개 나란히
슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것 — 아래 "레이어드 슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것 — 아래 "레이어드
Store 기각과 안 부딪히나" 참고. Store 기각과 안 부딪히나" 참고.
### 메커니즘 — 메모이즈된 키로 기존 단일 키 경로에 재귀 위임 ### 메커니즘 — per-name 전용 키로 기존 단일 키 경로에 재귀 위임
**[2026-08-11 아홉 번째 세션 후속, 개정]** 최초안은 "자기 완결형 Handler, **[2026-08-11 아홉 번째 세션 후속, 개정]** 최초안은 "자기 완결형 Handler,
Dispatch 재진입 없이 직접 `SetAttribute`+수동 per-field StoreBind 구독" Dispatch 재진입 없이 직접 `SetAttribute`+수동 per-field StoreBind 구독"
이었으나, 위 "동등성" 절의 이름별 weak 캐시가 확정되며 그 회피 이유 이었으나, 위 "동등성" 절의 이름별 weak 캐시가 확정되며 그 회피 이유
자체가 없어짐 — `AttributeKey(name)`을 몇 번을 다시 불러도 항상 같은 자체가 없어짐 — 그래서 그룹 Handler는 **자기만의 SetAttribute/구독 로직을
객체가 나오므로, Dispatch의 `(inst, k)`별 핸들러 체인 추적이 깨질
위험이 없음. 그래서 그룹 Handler는 **자기만의 SetAttribute/구독 로직을
새로 만들지 않고, 각 필드를 기존 단일 키 `AttributeKey` 경로에 그대로 새로 만들지 않고, 각 필드를 기존 단일 키 `AttributeKey` 경로에 그대로
재귀 위임** — `None`/`retract`/store-bind 전부 이미 확정된 단일 키 재귀 위임** — `None`/`retract`/store-bind 전부 이미 확정된 단일 키
메커니즘을 100% 재사용, 중복 구현 없음: 메커니즘을 100% 재사용, 중복 구현 없음. **[정정, 2026-08-12 열 번째
세션] 위임에 쓰는 키가 공개 `AttributeKey(name)`이 아니라 `rawNew(name)`
매번 그룹 전용으로 만드는 키로 바뀜** — 이유·정확한 소유권 판정 방식은
위 "이름 소유권" 절 참고, 이 절은 그 위에서 diff 로직이 어떻게 도는지만
설명:
- **그룹 값이 (재)할당될 때**(마운트 시점, 또는 `:Compute`가 그룹 값 - **그룹 값이 (재)할당될 때**(마운트 시점, 또는 `:Compute`가 그룹 값
자체를 교체하는 드문 경우 — 흔한 필드 하나 변경과는 다른 경로, 아래 자체를 교체하는 드문 경우 — 흔한 필드 하나 변경과는 다른 경로, 아래
참고): `(inst, index)`(array-part 위치, `Tag``relate:GetStrong(inst,k)` 참고): `(inst, index)`(array-part 위치, `Tag``relate:GetStrong(inst,k)`
동일 키잉 — `k`는 배열 인덱스)로 찾은 릴레이션에 저장된 **"이전에 쓴 동일 키잉 — `k`는 배열 인덱스)로 찾은 릴레이션에 저장된 **"이전에 쓴
attribute 이름 문자열 집합"**과 새 값의 키 집합을 diff: attribute 이름 → 그 이름 전용 키 객체 맵"**(구 "이름 문자열 집합" —
- 사라진 이름만 `Dispatch.retractUnder(inst, AttributeKey(name))` 이름 존재 여부뿐 아니라 그때 쓴 키 객체 자체까지 같이 들고 있어야 위
메모이즈된 키라 그 이름이 살아있는 동안 만들어졌던 원래 체인과 "이름 소유권" 절의 동일 객체 재사용이 성립)과 새 값의 키 집합을 diff:
정확히 같은 슬롯을 가리켜 정리됨. - 사라진 이름만, 그 이름이 맵에 들고 있던 **그 전용 키 객체**로
- 남아있거나 새로 들어온 이름은 **값 비교 없이 전부** `Dispatch.retractUnder(inst, key)` — 그 이름이 살아있는 동안 만들어졌던
`Dispatch.process(inst, AttributeKey(name), source)`로 넘김 — 작성자가 원래 체인과 정확히 같은 슬롯을 가리켜 정리됨. 맵에서도 그 이름 제거.
직접 `[AttributeKey<<T>> name] = source`를 쓴 것과 완전히 같은 경로를 - 남아있는 이름은 **맵에 이미 캐싱된 같은 키 객체를 그대로 재사용**,
타므로, `source``State`/`Source`면 `Dispatch/StoreBind`가 알아서 새로 들어온 이름은 `rawNew(name)`로 갓 만든 키를 맵에 새로 캐싱 —
언랩+구독까지 다 해줌(그룹 Handler가 따로 구독 관리 안 함). **값 비교 없이 전부** `Dispatch.process(inst, key, source)`로 넘김,
작성자가 직접 `[AttributeKey<<T>> name] = source`를 쓴 것과 거의 같은
경로를 타므로(단, 공개 캐시가 아니라 그룹 전용 키를 씀) `source`
`State`/`Source`면 `Dispatch/StoreBind`가 알아서 언랩+구독까지 다
해줌(그룹 Handler가 따로 구독 관리 안 함).
- **값 비교(`:Get()`으로 old/new 비교)는 하지 않음** — State 계약("값은 - **값 비교(`:Get()`으로 old/new 비교)는 하지 않음** — State 계약("값은
항상 선언된 Compute 재실행 결과, 캐시 비교 금지", `store-semantics.md` 항상 선언된 Compute 재실행 결과, 캐시 비교 금지", `store-semantics.md`
"하드 경계" 절)과 어긋나고, 릴레이션 키를 문자열 이름 집합(존재 여부)만 "하드 경계" 절)과 어긋나고, 릴레이션 키를 문자열 이름 집합(존재 여부)만
들고 있으면 충분해서 굳이 값까지 비교할 이유가 없음. 들고 있으면 충분해서 굳이 값까지 비교할 이유가 없음.
- **캐시(맵)는 그룹 값 교체를 넘어 계속 유지돼야 함** — 매 교체마다
남아있는 이름의 키까지 새로 만들면, "이름 소유권" 절의 `owners`
레지스트리엔 옛 키가 남아있어 새로 만든 키와 비교 시 오탐 충돌이
남(자기 자신과 충돌하는 꼴). 이 릴레이션이 `(inst, index)`
영속되는 건 이미 확정돼 있던 설계라 새로 챙길 것 없음, 저장 값
형태만 위처럼 바뀜.
- **필드 하나만 바뀌는 흔한 경우**(`storeA.foo:Set(v)`, 그룹 자체는 - **필드 하나만 바뀌는 흔한 경우**(`storeA.foo:Set(v)`, 그룹 자체는
안 바뀜)는 위 그룹 재처리를 아예 거치지 않음 — 마운트 시 이미 걸린 안 바뀜)는 위 그룹 재처리를 아예 거치지 않음 — 마운트 시 이미 걸린
단일 키 `AttributeKeyHandler`의 store-bind 구독이 바로 단일 키 `AttributeKeyHandler`의 store-bind 구독이 바로
`SetAttribute("foo", v)`를 호출(그룹 재진입 없이 그 경로 스스로). `SetAttribute("foo", v)`를 호출(그룹 재진입 없이 그 경로 스스로).
**`AttributeChanged`/`GetAttributeChangedSignal` 남발 걱정 없음** — **`AttributeChanged`/`GetAttributeChangedSignal` 남발 걱정 없음** —
키 집합이 안 바뀌는 한 diff 로직 자체가 안 돎. 키 집합이 안 바뀌는 한 diff 로직 자체가 안 돎.
- **`retract`**: 자기가 쓴 키 전부 `Dispatch.retractUnder(inst, - **`retract`**: 자기가 쓴 키(전용 키 객체) 전부 `Dispatch.retractUnder`
AttributeKey(name))`로 정리 — 단일 키의 `SetAttribute(name, nil)` 정리 — 단일 키의 `SetAttribute(name, nil)`이 그대로 실행되므로 결과적으로
그대로 실행되므로 결과적으로 `Tag`와 동일하게 확실히 청소됨. Roblox는 `Tag`와 동일하게 확실히 청소됨. Roblox는 Instance 풀링/재사용이 흔해서,
Instance 풀링/재사용이 흔해서, 이전 컴포넌트가 남긴 attribute가 이전 컴포넌트가 남긴 attribute가 재사용된 Instance에 방치되면
재사용된 Instance에 방치되면 selector/스타일시트 로직에 실제 버그가 selector/스타일시트 로직에 실제 버그가 됨. ("소진돼도 부작용 없으니
됨. ("소진돼도 부작용 없으니 방치해도 된다"는 초안은 기각 — Tag의 방치해도 된다"는 초안은 기각 — Tag의 확실한 청소 원칙과 통일.)
확실한 청소 원칙과 통일.)
**그룹 Handler에 남는 자기 로직은 사실상 "이름 집합 diff"뿐** — 실제 **그룹 Handler에 남는 자기 로직은 사실상 "이름 집합 diff"뿐** — 실제
`SetAttribute` 호출/`None` 처리/store-bind 구독은 전부 기존 단일 키 `SetAttribute` 호출/`None` 처리/store-bind 구독은 전부 기존 단일 키

View file

@ -121,10 +121,18 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
바뀌므로 `retract`가 의미 있어짐(전체 삭제), 같은 Tag끼리 바뀌는 바뀌므로 `retract`가 의미 있어짐(전체 삭제), 같은 Tag끼리 바뀌는
diff는 `process`가 담당(`base/tag-plan.md`, 2026-08-08 세 번째 세션 diff는 `process`가 담당(`base/tag-plan.md`, 2026-08-08 세 번째 세션
— array-part 값 객체 재설계 이후, 구 모델은 `archive/ — array-part 값 객체 재설계 이후, 구 모델은 `archive/
tag-hash-key-model-reversed.md`). **Attribute는 여기 해당 안 함** tag-hash-key-model-reversed.md`). **Attribute(단일 키 직접 쓰기 경로)는
UICorner 숏핸드와 같은 패턴(값의 참/거짓/nil 여부와 무관하게 항상 여기 해당 안 함** — UICorner 숏핸드와 같은 패턴(값의 참/거짓/nil
같은 핸들러가 계속 담당, 추가/제거를 전부 `process` 자신이 처리)이라 여부와 무관하게 항상 같은 핸들러가 계속 담당, 추가/제거를 전부
핸들러 교체 자체가 안 일어남 — `base/attribute-plan.md`. **[정정, `process` 자신이 처리)이라 핸들러 교체 자체가 안 일어남 —
`base/attribute-plan.md`. **[추가, 2026-08-12 열 번째 세션] 단, 그룹
`Attribute(...)`가 개별 이름을 놓을 때는 그 이름 전용 키 객체의 체인
자체가 통째로 정리되는 거라 `retract`가 실제로 불림** — "핸들러
타입이 안 바뀌면 retract 없이 process"라는 이 절의 원칙과 안 어긋남,
그룹이 이름을 잃는 건 "그 이름 전용 키가 이 인스턴스를 더 이상
관리 안 하게 됨"이라 오히려 `Tag(...)→nil`과 같은 급의 "완전히
사라짐" 케이스임 — `attribute-plan.md` "이름 소유권" 절 참고.
**[정정,
2026-08-10 세션] Tween도 더 이상 여기 해당하지 않음** — 원래는 이 2026-08-10 세션] Tween도 더 이상 여기 해당하지 않음** — 원래는 이
패턴의 대표 예시("Tween 핸들러가 매치돼 애니메이션 실행 중이었는데 패턴의 대표 예시("Tween 핸들러가 매치돼 애니메이션 실행 중이었는데
값이 더 이상 Tween 대상이 아니게 되어 일반 PropertyHandler로 매치가 값이 더 이상 Tween 대상이 아니게 되어 일반 PropertyHandler로 매치가

View file

@ -0,0 +1,63 @@
# 2026-08-12 열 번째 세션 — Attribute 이름 소유권, `rawNew` 전용 키로 그룹/직접 쓰기 충돌 방지
## 배경
앞선 세션들(여덟/아홉 번째, `Ref`/`Slot`의 store 재바인드 정정)의 흐름을
이어, 사용자가 `Attribute`도 같은 종류의 문제가 있는지 확인 요청 —
그룹 `Attribute(...)``State<Attribute>`로 동적으로 바뀔 수 있는데, 이미
확정된 diff 메커니즘("사라진 이름만 nil, 남은/새 이름은 그대로 설정")
자체는 맞지만, **서로 다른 두 원본(직접 리터럴 `[AttributeKey "name"]=v`
배열파트 `Attribute(store)`, 또는 서로 다른 두 그룹)이 같은 이름을 동시에
관리하려 하면 어떻게 되는가**가 빠져있다는 지적.
## 진단
`AttributeKey(name)`이 이름별 weak 캐시로 항상 같은 객체를 리턴하고,
그룹의 위임 메커니즘이 `Dispatch.process(inst, AttributeKey(name), source)`
그 공개 캐시를 그대로 씀 — 즉 서로 다른 원본이 같은 이름을 다루면 정확히
같은 `(inst, k)` 디스패치 자리로 수렴함. Modifier 필드는 override로 정적
충돌이 이미 해소되지만(한 Modifier 안에 같은 해시 키 중복 불가), 그룹의
이름 집합은 런타임에 동적이라 이 해소망 밖에 있다는 게 확인됨 — 실제로
일어날 수 있는 구조적 충돌.
## 설계 논의
1차로 Claude가 별도 `Relate` 기반 `AttributeManageMap`(claimant 타입을
구분해 등록/체크)을 제안했으나, 사용자가 더 단순한 안을 제시: **그룹이
공개 `AttributeKey(name)` 캐시를 안 쓰고, 캐시를 우회하는 `rawNew(name)`
이름마다 자기 전용 키 객체를 만들어 자기 자신(그룹의 릴레이션)에
캐싱** — 그러면 "이 이름에 지금 어느 키 객체가 적용돼 있는가"를 보는
것만으로 소유권 판정이 되고, `AttributeKeyHandler`에서 바로 처리 가능.
Claude가 이게 별도 claimant 타입 없이 AttributeKey 객체 identity 자체를
재사용하는 더 적은 부품의 설계임을 확인, 채택.
사용자가 추가로 확인 요청한 지점: 그룹 값이 교체될 때 diff가 "진짜
새 셋과 비교해 사라진 것만 nil화, 남은/새 이름은 그대로 갱신"으로
동작해야 이 메커니즘이 충돌 없이 맞물림 — 이미 확정된 diff 로직이 정확히
이 모양이라는 걸 재확인, 단 **전용 키 캐시가 그룹 값 교체를 넘어 계속
유지돼야만** 성립함을 짚음(매 교체마다 키를 새로 만들면 남아있는
이름조차 옛 키와 새 키가 달라 자기 자신과 충돌하는 꼴이 됨).
## 결정
- 그룹의 기존 "(inst, 배열 위치)별 마지막으로 쓴 attribute 이름 문자열
집합" 릴레이션을 **"이름 → 그 이름 전용 키 객체 맵"**으로 확장(새
릴레이션 아님, 저장 형태만 바뀜) — 이름이 살아있는 동안 캐싱된 같은
키 객체를 재사용, 사라지면 그 키로 `retractUnder`, 새로 생기면
`rawNew`로 새 키.
- `AttributeKeyHandler`는 per-inst `owners: Relate() -- {[inst]={[name]:
현재 키 객체}}`를 들고, `process`에서 다른 키 객체가 이미 그 이름을
쓰고 있으면 즉시 error, `retract`/nil 귀결 시 소유권 반납.
- 패키지 경계 문제 없음 — `AttributeKey`도, 그룹의 실제 위임 로직도 이미
quad-roblox 소속이라 `rawNew` 호출이 새 역의존을 안 만듦.
## 반영
- `base/attribute-plan.md` — "이름 소유권" 새 절 신설(메커니즘 전문),
"메커니즘 — per-name 전용 키로..." 절 갱신(공개 캐시→그룹 전용 키로
정정, 캐시 영속성 조건 명시), "retract 불필요" 문단에 정정 각주 추가.
- `base/bind-system-plan.md` — 일반 retract 계약 절의 "Attribute는
해당 안 함" 서술이 단일 키 경로에만 해당함을 명확화, 그룹의 이름 이탈은
오히려 `Tag→nil`급 retract 케이스라고 추가.
- 다른 stale 참조(`onchange-plan.md`의 `AttributeKey(name)` 언급 등)는
이 변경과 무관해 손 안 댐 — 확인만 하고 넘어감.

View file

@ -539,3 +539,19 @@ store-bind retract 경로에 연결된 적 없는 진짜 갭). `Ref`와 같은 `
no-op 가드는 Tag/Ref보다 Slot에서 훨씬 중요함(가드 없으면 재귀 재emit마다 no-op 가드는 Tag/Ref보다 Slot에서 훨씬 중요함(가드 없으면 재귀 재emit마다
마운트된 서브트리 전체가 파괴됐다 재생성돼 자식의 스크롤/포커스/애니메이션 마운트된 서브트리 전체가 파괴됐다 재생성돼 자식의 스크롤/포커스/애니메이션
상태가 전부 유실됨). 상태가 전부 유실됨).
**2026-08-12 열 번째 세션 — Attribute 이름 소유권, `rawNew` 전용 키로
그룹/직접 쓰기 충돌 방지** (`session/2026-08-12-10-attribute-name-ownership.md`)
`Ref`/`Slot`에 이어 `Attribute`도 확인 — 그룹 `Attribute(...)`의 위임
메커니즘이 공개 `AttributeKey(name)` 캐시(이름별 weak 캐시로 항상 같은
객체)를 그대로 쓰다 보니, 직접 리터럴 `[AttributeKey "name"]=v`와 배열파트
`Attribute(store)`(또는 서로 다른 두 그룹)가 같은 이름을 동시에 관리하면
같은 `(inst,k)` 자리로 수렴해 조용히 마지막 쓰기가 이기는 충돌이 실제로
가능함을 확인. Claude가 처음 제안한 별도 `Relate` claimant 레지스트리 대신,
사용자가 더 단순한 안을 제시해 채택: 그룹이 캐시를 우회하는 `rawNew(name)`
이름마다 자기 전용 키 객체를 만들어 자기 릴레이션에 캐싱하면(그룹 값 교체를
넘어 유지), "이 이름에 지금 어느 키 객체가 적용돼 있는가" 조회만으로
`AttributeKeyHandler`에서 바로 소유권 판정(다르면 error) 가능 — 별도
claimant 타입 없이 AttributeKey 객체 identity 자체를 재사용하는 더 적은
부품의 설계. 기존 diff 로직(사라진 이름만 nil, 남은/새 이름은 갱신)은
그대로 맞물림, 캐시가 그룹 값 교체를 넘어 영속돼야 한다는 조건만 명시.