design: PopOnly 가칭을 Detach로 리네임 확정 + 공개 표면 위치 확정

`:List` reconcile의 비파괴 반환 sentinel 이름을 사용자와 후보 검토(Bench/
Stash/Hold/Detach 등) 끝에 Detach로 확정 — 이미 있는 Extract(호출자 직접
호출, 명령형 소유권 이관)와 동사가 겹쳐도 "화면에서만 떼고 관리 주체는
reconcile"이라는 의미가 자연스럽게 구분됨. 공개 표면 위치도 같이 확정 —
Slot이 함수라 Slot.Detach로 못 붙이므로, None sentinel의 선례(공개 표면은
패키지 최상위 export, 정의는 관련 로직 옆)를 그대로 따름.

base/slot-plan.md 전량 반영(가칭 표기 제거, 이름/배치 두 결정 불릿 신설),
question.md/todos.md/ROADMAP.md/archive/question-resolved.md/README.md
인덱스 갱신, session/2026-08-19-02-*.md로 논의 원문 남김. 키 소멸 시 홀드
중이던 요소 처분 문제는 이름과 무관한 별개 항목으로 여전히 미결.

quad-doc-auditor 1라운드(무발견) 거쳐 doc-check.py ERROR 0 확인 후 커밋.

Co-authored-by: qwreey <me@qwreey.moe>
This commit is contained in:
qwreey-agent-selene 2026-08-19 01:46:18 +09:00
parent 93cf408eb3
commit 2348ea8058
No known key found for this signature in database
8 changed files with 193 additions and 48 deletions

File diff suppressed because one or more lines are too long

View file

@ -128,6 +128,32 @@ pre-implementation-qa-round3.md`가 원본) — 트레이싱 중 `attachSlot`이
절이 소스. 논의 원문(최초 분석·사용자 정정·확인 질문과 답변 전문)은
`qa-request/pre-implementation-qa-round3.md`.
## [해소됨, 2026-08-19] `PopOnly``Detach` 리네임 + 공개 표면 위치 확정
`:List` reconcile의 "파괴하지 말고 자리만 비우라" 반환 센티널은 2026-08-18
가칭 `PopOnly`로 도입되며 사용자가 "이름은 변경될 수 있음, 더 생각해봐야함"
이라 남긴 채 `question.md` 용어 정리 항목에 열려 있었음. 사용자가 후속
세션에서 이름 아이디어를 요청 → 여러 후보(`Bench`/`Stash`/`Hold`/`Detach`
등)를 검토한 뒤 `Detach`로 좁혔고, 이어서 "이걸 어디에 던져줘야 하나"(공개
표면 위치)까지 같이 확정됨. 원문은
`session/2026-08-19-02-detach-naming-and-placement.md`.
- **이름 — `Detach`.** 근거 둘: (1) 이미 있는 `Extract`(호출자가 직접
부르는 명령형 추출)와 동사가 겹치면 헷갈리는데, `Detach`는 "화면(부모
계층)에서만 떼어낼 뿐 관리 주체는 여전히 reconcile"이라는 뜻이라
`Extract`의 "소유권을 통째로 호출자에게 넘긴다"와 자연스럽게 구분됨.
(2) `nil`(파괴)과의 대비도 더 직접적으로 드러남.
- **공개 표면 위치 — 패키지 최상위 export.** `Slot`이 함수(팩토리)라
`Slot.Detach`처럼 붙이려면 callable-table+메타테이블이 새로 필요한데,
sentinel 상수 하나 때문에 그 구조를 들이는 건 "실제로 관측된 문제에만
구조를 쓴다" 원칙에 안 맞음(사용자 지적). 대신 `None`이 이미 쓰는 패턴을
그대로 따름 — `None`도 여러 곳(Slot 요소, Attribute, offsetSource)에서
쓰이지만 공개 표면은 패키지 최상위(`quad-base/src/init.luau` 재노출)이고
실제 정의는 관련 로직 옆(`Dispatch/None.luau`)에 있음. `Detach`도 같은
패턴 — 정의는 Slot 관련 파일 옆에 두고 `init.luau`에서 최상위로 재노출.
- 지금 유효한 설계는 `base/slot-plan.md`의 "`nil` 리턴은 파괴가 기본" 절이
소스.
---
# 확인/결정 필요 목록

View file

@ -850,7 +850,7 @@ fail-fast 톤으로 그 자리에서 막음 — `keyFn` 작성자(주로 위 `it
"지금 이 key는 렌더 안 함"(filter 탈락 등) — `prev`가 있었다면
**[정정, 2026-08-13 3차 감사, 그러나 2026-08-18 구현 전 QA로 재역전
— "`nil` 리턴은 파괴가 기본" 절 참고] 파괴됨**(단순 `Visible =
false`도 아니고, 언마운트도 아님 — `rawRemove`. `PopOnly`를 명시
false`도 아니고, 언마운트도 아님 — `rawRemove`. `Detach`를 명시
반환해야 대신 언마운트+재사용됨). 편의상
`nil` 권장(반환값이 raw Slot 요소로 직접 들어가는 게 아니라
`:List`의 reconcile이 해석만 하므로 "요소 타입 제약"의 raw
@ -987,15 +987,15 @@ end
**⚠️ [재정정, 2026-08-18 구현 전 QA, `/code-review high`로 이 절의 stale
서술 발견] 바로 위 두 문단은 "filter 탈락 = 언마운트(비파괴)"를 결론으로
쓰고 있는데, 그 결론은 이후 재역전됐다.** 지금 유효한 규칙은 "`nil`
리턴은 파괴가 기본 — `PopOnly`(가칭)로만 비파괴" 절(SL-3 해소,
리턴은 파괴가 기본 — `Detach`로만 비파괴" 절(SL-3 해소,
`question.md`/`archive/question-resolved.md` 참고) — filter 탈락으로
`updateFn`이 그냥 `nil`을 반환하면 이제 **파괴**(`rawRemove`)가 기본이고,
"Instance.new/Destroy 비용을 아끼고 싶다"는 이 절의 동기를 살리려면
`nil` 대신 명시적으로 `PopOnly`를 반환해야 언마운트+재사용이 된다. 이
`nil` 대신 명시적으로 `Detach`를 반환해야 언마운트+재사용이 된다. 이
절의 **동기**(matched-item 애니메이션/이벤트가 계속 돌면 안 된다는 문제
자체)는 여전히 유효하지만, "그래서 nil이 곧 언마운트"라는 결론 문장은
`PopOnly` 신설로 대체됐다 — 이 절을 읽고 filter를 구현할 땐 반드시 위
"`nil` 리턴은 파괴가 기본" 절도 같이 볼 것.
`Detach`(당시 가칭 `PopOnly`) 신설로 대체됐다 — 이 절을 읽고 filter를
구현할 땐 반드시 위 "`nil` 리턴은 파괴가 기본" 절도 같이 볼 것.
**"이전 상태를 다음 호출에 어떻게 넘기냐" 문제는 `userdata`가 그 채널** —
item이 plain table이라 매번 `Source`를 새로 안 만들고 재사용하려면 그
@ -1097,10 +1097,10 @@ function activateList(self, inst)
local candidateIndex = pos + 1 -- "이 item이 살아남으면 차지할" 압축 위치(생존 여부와 무관하게 계산 가능)
local result, ud = updateFn(item, candidateIndex, offset, prev, userdata[key])
if result == None then result = nil end -- 편의: None도 nil과 동일 취급
-- [2026-08-18] PopOnly(가칭)는 "이 자리를 비우되 죽이지는 말라"는 지시.
-- 아래 "PopOnly" 절 — 자리 계산 관점에선 nil과 똑같이 취급된다.
local popOnly = (result == PopOnly)
if popOnly then result = nil end
-- [2026-08-18, 이름 2026-08-19 확정] Detach는 "이 자리를 비우되 죽이지는 말라"는 지시.
-- 아래 "Detach" 절 — 자리 계산 관점에선 nil과 똑같이 취급된다.
local detach = (result == Detach)
if detach then result = nil end
if result ~= nil then
-- [2026-08-11 일곱 번째 세션] result가 nested Slot이면 그
@ -1115,10 +1115,10 @@ function activateList(self, inst)
-- "`nil` 리턴은 파괴가 기본" 절이 소스:
-- (a) 교체(result ~= nil): 밀려난 prev는 **언마운트만**
-- — state<Frame> 교체와 동형, 지우라고 한 적이 없음.
-- (b) PopOnly: **언마운트만**, 재사용은 ud가 홀드.
-- (b) Detach: **언마운트만**, 재사용은 ud가 홀드.
-- (c) 그냥 nil/None: **파괴**(rawRemove) — "지워라"라는 지시.
if prev ~= nil then
if result ~= nil or popOnly then rawUnmount(self, prev)
if result ~= nil or detach then rawUnmount(self, prev)
else rawRemove(self, prev) end
end
if result ~= nil then rawAdd(self, result, pos) end -- 새로 배치, 압축 위치 기준
@ -1128,7 +1128,7 @@ function activateList(self, inst)
end
userdata[key] = ud -- result와 무관, 그대로 기록
-- (PopOnly 재사용은 여기 담긴 { old = ... }가 담당)
-- (Detach 재사용은 여기 담긴 { old = ... }가 담당)
newKeyIndex[key] = pos
end
for key in pairs(keyIndex) do -- 직전 사이클에 존재했던 전체 key
@ -1211,7 +1211,7 @@ raw `i`를 그대로 위치 인자로 썼는데, 앞쪽 item이 filter로 마운
`rawMove`** (**[재정정, 2026-08-18 구현 전 QA]** 2026-08-13 여섯 번째
세션에 "reconcile의 제거는 전부 비파괴 언마운트"로 바꿨던 것을
**부분적으로 되돌림**`nil` 리턴/키 소멸은 다시 **파괴**가 기본이고,
값 교체와 `PopOnly`만 비파괴. 아래 "`nil` 리턴은 파괴가 기본" 절이
값 교체와 `Detach`만 비파괴. 아래 "`nil` 리턴은 파괴가 기본" 절이
소스) — `rawExtract`/`rawSwap`/`rawClear`도 (위 "모든 공개 CRUD는
가드+위임" 구조상) 당연히 존재하지만, `:List`의 reconcile 알고리즘
자체가 그 셋을 직접 호출할 일이 없을 뿐. `rawUnmount``rawRemove`
@ -1228,7 +1228,7 @@ raw `i`를 그대로 위치 인자로 썼는데, 앞쪽 item이 filter로 마운
저비용 경로. 최소-이동 알고리즘(LIS 기반 등) 자체는 구현 시점 최적화로
미룸, 여기선 계약(파괴 없이 위치만 바뀜)만 확정.
### `nil` 리턴은 파괴가 기본 — `PopOnly`(가칭)로만 비파괴 (2026-08-18 구현 전 QA, 확정 뒤집기)
### `nil` 리턴은 파괴가 기본 — `Detach`로만 비파괴 (2026-08-18 구현 전 QA, 확정 뒤집기; 이름 2026-08-19 확정)
**[재정정]** 2026-08-13 여섯 번째 세션은 "자동 경로는 언마운트, 명시적으로
지우라고 한 것만 파괴"라는 일반 규칙을 세우면서 `:List`의 reconcile까지
@ -1242,17 +1242,17 @@ raw `i`를 그대로 위치 인자로 썼는데, 앞쪽 item이 filter로 마운
|---|---|---|
| 새 값(`result ~= nil`) | **언마운트만** | 밀려난 것뿐이지 "지워라"가 아님. `state<Frame>` 교체와 동형이고, `Slot { State<Slot> }` sugar(`:Single`)가 이 경로를 타므로 아래 "`State<Slot>` 교체" 절의 확정도 그대로 유지됨 |
| `nil` / `None` | **파괴**(`rawRemove`) | `updateFn`이 명시적으로 "이 자리를 지워라"라고 말한 것 |
| `PopOnly`(가칭) | **언마운트만** + 재사용 대기 | 아래 |
| `Detach` | **언마운트만** + 재사용 대기 | 아래 |
| 키가 데이터에서 사라짐 | **파괴**(`rawRemove`) | `nil` 리턴과 같은 의미(그 아이템은 이제 없음) |
**`PopOnly`(가칭)`Instance.new`/`Destroy` 비용을 아끼는 재사용 경로.**
**`Detach``Instance.new`/`Destroy` 비용을 아끼는 재사용 경로.**
`filter` 용도처럼 "지금은 안 보이지만 곧 다시 필요할" 요소를 매번
파괴/재생성하는 건 비싸다. 그래서 `updateFn`**`PopOnly`와 함께 userdata를
파괴/재생성하는 건 비싸다. 그래서 `updateFn`**`Detach`와 함께 userdata를
반환**하면 그 자리는 파괴 없이 `Parent = nil`로만 내려오고 Slot에서 빠진다:
```lua
-- filter에서 걸러진 아이템 — 죽이지 말고 들고 있다가 나중에 되쓴다
return PopOnly, { old = prev, source = ... }
return Detach, { old = prev, source = ... }
```
- **보존 주체는 `userdata`** — reconcile은 `mounted[key]`에서만 뺄 뿐
@ -1264,7 +1264,7 @@ return PopOnly, { old = prev, source = ... }
전까지 안 지워진다** — 즉 "언제 진짜로 버릴지"를 `updateFn`이 결정한다.
- **⚠️ 단, 키가 데이터에서 아예 사라지면 얘기가 다르다(2026-08-18 감사에서
발견한 갭).** 그 경우 reconcile의 소멸 루프가 `mounted[key]`/`userdata[key]`를
**둘 다** 지우는데, `PopOnly`로 홀드 중이던 요소는 `mounted[key]`가 이미
**둘 다** 지우는데, `Detach`로 홀드 중이던 요소는 `mounted[key]`가 이미
`nil`이라 `rawRemove`(파괴) 대상이 아니다 — 결과적으로 그 요소는
**파괴되지도, `updateFn`에게 되돌려지지도 않고 참조만 끊겨 GC 대상이
된다**(Parent는 이미 `nil`). 이건 같은 절의 표가 "키가 사라지면 파괴"라고
@ -1274,12 +1274,33 @@ return PopOnly, { old = prev, source = ... }
(b) 지금처럼 참조만 끊고 GC에 맡김(단 표와 서술을 그 사실에 맞게 고침),
(c) `updateFn`을 마지막으로 한 번 더 불러 처분을 묻는다.
**지금 문서는 (a)를 기본으로 가정하지 않는다** — 결정 전이므로 구현 금지.
- **이름은 가칭** — 사용자 확정: *"PopOnly 확정. 다만 이름은 변경될 수
있음. 이름에 대해서는 더 생각해보아야함"*. `question.md` 용어 정리 항목에
올려둠. 메커니즘(반환 규약 + userdata 홀드 + 재마운트)은 확정.
- **이름 확정 — `Detach`(2026-08-19).** 처음엔 가칭 `PopOnly`로 도입됐고
사용자가 *"PopOnly 확정. 다만 이름은 변경될 수 있음. 이름에 대해서는 더
생각해보아야함"*이라고 남겨 `question.md` 용어 정리 항목에 올라가
있었음 — 이후 세션에서 후보들을 검토하다 `Detach`로 확정. 근거는 둘:
(1) 이미 있는 `Extract`(바로 아래 불릿, 호출자가 직접 부르는 명령형
추출)와 동사가 겹치면 헷갈리는데, `Detach`는 "화면(부모 계층)에서만
떼어낼 뿐 관리 주체는 여전히 reconcile"이라는 의미라 `Extract`
"소유권을 통째로 호출자에게 넘긴다"는 것과 자연스럽게 구분됨. (2)
`nil`(파괴)과의 대비도 더 직접적으로 드러남. 메커니즘(반환 규약 +
userdata 홀드 + 재마운트)은 그대로 — 원문은
`session/2026-08-19-02-detach-naming-and-placement.md`.
- **공개 표면 위치 확정 — 패키지 최상위 export(2026-08-19).** `Slot`
함수(팩토리)라 `Slot.Detach`처럼 붙이려면 callable-table+메타테이블이
새로 필요한데, sentinel 상수 하나 때문에 그 구조를 들이는 건 과함
(`conventions.md`의 "드문 오용이나 가상의 미래 요구까지 방어/최적화하려고
구조를 복잡하게 만들지 않는다" 원칙). 대신 `None`과 같은 선례를 따른다 —
`None`도 여러 곳(Slot 요소, Attribute, offsetSource)에서 쓰이는
sentinel이지만 공개 표면은 패키지 최상위(`quad-base/src/init.luau`
재노출)이고 실제 정의는 관련 로직 옆(`Dispatch/None.luau`)에 있다.
`Detach`도 같은 패턴 — **정의는 Slot 관련 파일(`Slot.luau` 또는
`Dispatch/Slot.luau`) 옆에 두고, `init.luau`에서 최상위로 재노출**한다.
지금은 `:List` reconcile 한 곳에서만 쓰이지만 `None`도 처음엔 그렇게
시작해 이후 재사용됐으므로 최상위에 두는 게 자연스럽다. 정확한 파일
배치는 M6 구현 시점에 확정.
- **`Slot`의 다른 비파괴 API와의 관계**: `Extract`/`ExtractAll`/`Splice`가
이미 비파괴 추출을 제공하지만(위 "CRUD API 확정" 절) 그건 **호출자가
직접 부르는 명령형 경로**다. `PopOnly`는 같은 일을 **reconcile 안에서
직접 부르는 명령형 경로**다. `Detach`는 같은 일을 **reconcile 안에서
선언적으로** 하기 위한 것이라 서로 대체 관계가 아니다.
### 구독 시점 — `:List()` 호출이 아니라 Slot 마운트 시점, lazy `bindLifetime`
@ -2114,7 +2135,7 @@ quad-roblox는 `inst:Destroy()`로 구현. 웹 등 다른 백엔드는 자기
코드가 `unmountSlotTree` + `setOffsetSource(None)`/`setLength(0)` +
`unbindLifetime` + `releaseOwner`를 부름.
2. **`:List``reconcile`** — **[재정정, 2026-08-18 구현 전 QA]** 여기서
비파괴가 되는 건 **값 교체와 `PopOnly`뿐**이다. `updateFn``nil`/`None`을
비파괴가 되는 건 **값 교체와 `Detach`뿐**이다. `updateFn``nil`/`None`을
반환하거나 키가 데이터에서 사라진 경우는 **다시 파괴가 기본**(사용자
판정) — 상세와 이유는 위 "`nil` 리턴은 파괴가 기본" 절이 소스.
2026-08-13에 이 항목이 "교체/소멸 시 전부 비파괴"로 적혔던 것은
@ -2125,7 +2146,7 @@ quad-roblox는 `inst:Destroy()`로 구현. 웹 등 다른 백엔드는 자기
`:List` 소멸 경로. 즉 일반 규칙은 **"자동 경로는 언마운트, 명시적으로
지우라고 한 것만 파괴"**이되, **`:List`에서 `nil`을 반환하는 것 자체가
"지우라고 한 것"으로 센다** — `Ref`/`Attribute`의 "지울 거면 명시적으로"
철학과 같은 결이고, `updateFn`이 지우지 않길 원하면 `PopOnly`로 그 의도를
철학과 같은 결이고, `updateFn`이 지우지 않길 원하면 `Detach`로 그 의도를
명시한다.
`unmountSlotTree``destroySlotTree`가 하는 일 중 **실제 파괴와 자식

View file

@ -48,11 +48,16 @@
"문서에서 처음 나올 때 항상 `D`(Declarative)로 풀어쓴다"는 표기 규약으로
보완하기로 같이 확정. 코퍼스 반영 완료 —
`base/bind-system-plan.md`의 "인스턴스 생성 / 이벤트 네이밍 인체공학" 절.
- **`PopOnly`(가칭, 2026-08-18 신설)**: `:List` reconcile에서 "파괴하지 말고
자리만 비우라"를 지시하는 반환 센티널(`base/slot-plan.md`의 "`nil` 리턴은
파괴가 기본" 절). 메커니즘은 확정됐고 **이름만 열려 있음** — 사용자:
*"PopOnly 확정. 다만 이름은 변경될 수 있음. 이름에 대해서는 더
생각해보아야함"*.
- **[해소됨, 2026-08-19] `PopOnly``Detach` 확정** — 원문과 근거는
`archive/question-resolved.md`. 요지: 이미 있는
`Extract`(호출자가 직접 부르는 명령형 추출)와 동사가 겹치면 헷갈리는데,
`Detach`는 "화면(부모 계층)에서만 떼어낼 뿐 관리 주체는 여전히
reconcile"이라는 뜻이라 `Extract`의 "소유권을 통째로 넘긴다"와 자연스럽게
구분되고, `nil`(파괴)과의 대비도 더 직접적으로 드러남. 공개 표면 위치도
같이 확정 — `Slot`이 함수(팩토리)라 `Slot.Detach`처럼 붙이려면
callable-table+메타테이블이 새로 필요해서 과함, `None`과 같은 선례를 따라
**패키지 최상위 export**로. 코퍼스 반영 완료 — `base/slot-plan.md`
"`nil` 리턴은 파괴가 기본" 절.
- **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가
다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이
헷갈릴 수 있음.
@ -181,7 +186,7 @@
`getDynamic(store, name)` — "특정 프리미티브에 안 묶인 범용 유틸은 소문자
탑레벨"이라는 기존 네이밍 규칙에는 오히려 더 맞는다. **M3/M4 착수 전
필요**, `base/store-plan.md`의 "타입 추론 문제" 절.
- **[신설, 2026-08-18 커밋 전 `/code-review high`] `PopOnly`로 홀드 중이던
- **[신설, 2026-08-18 커밋 전 `/code-review high`] `Detach`로 홀드 중이던
요소의 키가 데이터에서 사라지면 어떻게 처분하는가** — 지금 의사코드대로면
`mounted[key]`가 이미 `nil`이라 파괴 대상이 아니고, 소멸 루프가
`userdata[key]`까지 지워서 **파괴되지도 `updateFn`에게 되돌려지지도 않고

View file

@ -1504,3 +1504,18 @@ key로 완료 여부 기록) 방식으로 직접 해소하는 아이디어도
포함)가 있었는데 그날 session/ 파일은 1개뿐, 2026-08-19는 이 세션 전까지
(QA 3라운드 커밋 1개가 있었음에도) 0개였음. 과거 대화 트랜스크립트에 접근 불가라 그 공백을 사후 재구성하는 건
허위 기록 위험이 있어 보류 — 처리 방침은 사용자 확인 대기.
## 2026-08-19 — `PopOnly``Detach` 리네임 + 공개 표면 위치 확정
원문: `session/2026-08-19-02-detach-naming-and-placement.md`
가칭으로 남아있던 `:List` reconcile의 비파괴 반환 sentinel `PopOnly`
이름을 사용자 요청으로 브레인스토밍(풀링/보관 은유 계열, "언마운트+보류"
계열, 기존 조어 구조 계열 후보 제시) → 사용자가 이미 있는 `Extract`(명령형
추출)와 동사가 안 겹치면서 "화면에서만 떼고 관리 주체는 그대로"라는 뜻을
살릴 수 있다는 이유로 `Detach`를 골라 확정. 이어서 공개 표면 위치("Slot이
함수라 `Slot.Detach`로 못 붙임")도 논의 — `None` sentinel의 선례(공개
표면은 패키지 최상위 export, 실제 정의는 관련 로직 옆)를 그대로 따르기로
확정. `base/slot-plan.md`/`question.md`/`todos.md`/
`archive/question-resolved.md`/`ROADMAP.md` 전량 반영, `doc-check.py`
ERROR 0.

View file

@ -0,0 +1,72 @@
# 2026-08-19 — `PopOnly``Detach` 리네임 + 공개 표면 위치 확정
**요청**: `base/slot-plan.md`에 가칭으로 남아있던 `PopOnly`(`:List`
reconcile에서 "파괴하지 말고 자리만 비우라"는 반환 센티널)의 이름을 사용자가
직접 못 정해 아이디어를 요청 — "지금은 다른 에이전트들이 있어서 쓰지는
말아달라"는 조건으로 브레인스토밍만 먼저 진행.
## 1. 메커니즘 재확인
`base/slot-plan.md`의 "`nil` 리턴은 파괴가 기본" 절을 다시 읽고 의미를
정리: `:List` reconcile에서 `updateFn`이 반환하는 sentinel로, 이 자리를
언마운트(`Parent = nil`)는 하되 **파괴는 하지 말고 userdata를 들고 있다가
나중에 재사용**하라는 지시 — 목적은 `Instance.new`/`Destroy` 비용을 아끼는
재사용(오브젝트 풀링에 가까움). `nil` 반환(파괴)과도, 새 값 반환(언마운트만,
즉시 교체)과도 구분되는 세 번째 경로.
## 2. 이름 후보 제시
풀링/보관 은유 계열(`Bench`/`Stash`/`Shelve`/`Park`), "언마운트+보류" 계열
(`Hold`/`Detach`/`Suspend`), 기존 조어 구조를 살리는 계열(`PopKeep`/
`KeepOnly`/`SoftPop`)을 제시하고 `Bench`(가장 직관적인 은유) 또는 `Hold`
(문서가 이미 "홀드"라는 용어를 쓰고 있어 도입 마찰이 적음) 둘 중 하나를
추천.
## 3. 사용자가 `Detach` 선택 — 근거 검증
사용자: *"Detach 많이 괜찮아보이는데, Slot 에서 뽑아내다라는 Extract 를
지금 쓰고 있으니까, 이미 있는 Extract 를 쓰긴 뭐하니 Detach 를 넣는건
어때보임?"*
`Extract`/`ExtractAll`은 호출자가 직접 부르는 **명령형** 추출(소유권을
통째로 호출자에게 넘김)인 반면, `Detach`(구 `PopOnly`)는 reconcile 내부에서
여전히 userdata로 추적하며 화면(부모 계층)에서만 떼는 **선언적** sentinel —
"소유권 이관" vs "내부 보류 상태"로 성격이 갈려 `Extract`와 이름이 겹쳐도
혼동 소지가 적다고 판단, 채택 추천. Roblox 개발자가 `Attachment`/
`WeldConstraint`류의 물리적 "detach"를 먼저 연상할 여지는 있지만 "부모
계층에서 떼어낸다"는 실제 동작과 크게 어긋나지 않아 큰 문제 아니라고 정리.
`Detach` 채택 확정.
## 4. 공개 표면 위치 — 최상위 export
사용자: *"근데 그러면, Detach 는 어디에서 던져줘야한다 생각함? 그냥 base
모듈 바로 최상위? Slot 이랑 연관인데 Slot 옆에 존재하게 되는게 뭔가 느낌이
애매한거 같긴 함. 그런데 Slot 은 또 함수라서, 뭔가 넣기 애매해. 넣으려면
메타테이블이 되어야하기도 하고. 그냥 최상위가 좋음?"*
`architecture.md`의 패키지 트리를 확인해 `None` sentinel의 선례를 찾음 —
`None`도 Slot 요소/Attribute/offsetSource 등 여러 곳에서 쓰이는 sentinel
이지만, 공개 표면은 패키지 최상위(`quad-base/src/init.luau` 재노출)이고
실제 정의는 관련 로직 옆(`Dispatch/None.luau`)에 있음(공개 표면과 구현
위치가 분리돼 있는 기존 패턴). `Detach`도 같은 성격의 sentinel이므로 같은
자리(최상위)에 두는 게 일관적이라고 판단 — `Slot`이 함수라 `Slot.Detach`
형태로 붙이려면 callable-table+메타테이블이 새로 필요한데, sentinel 상수
하나 때문에 그 구조를 들이는 건 `conventions.md`의 "드문 오용이나 가상의
미래 요구까지 방어/최적화하려고 구조를 복잡하게 만들지 않는다" 원칙에
안 맞음. 정의 파일 위치(`Slot.luau` 또는 `Dispatch/Slot.luau`)는 `None`
같은 패턴(정의는 관련 로직 옆, 재노출만 `init.luau`에서)을 따르되 정확한
배치는 M6 구현 시점에 확정하기로 함.
## 5. 반영
사용자: *"이거 base/slot-plan.md에 정리해서 반영해줘. 세션 기록도
남겨주고, 핸드오버 준비해줘. 적절히 정해진것 같아서 커밋해줘도 될듯."*
`base/slot-plan.md`("이름은 가칭" 불릿을 "이름 확정"/"공개 표면 위치 확정"
두 불릿으로 교체, 본문 전체의 `PopOnly` 표기를 `Detach`로 치환하되 사용자
발언 직접 인용과 히스토리 서술은 유지), `question.md`(용어 정리 1번 항목을
`DI`→`D`와 같은 형식의 `[해소됨]` 항목으로 교체, 3번의 남은 열린 항목 이름만
치환), `todos.md`(00번/2번 항목 갱신), `archive/question-resolved.md`(새
`[해소됨, 2026-08-19]` 절 추가), `ROADMAP.md`(M6 체크리스트의 `PopOnly`
표기 치환)에 반영. 인덱스 레이어(`README.md`)와 `session-summary.md`
같이 갱신, 이 문서가 그 포인터.

View file

@ -59,12 +59,15 @@
시그니처에 영향이 갈 수 있어 M3 착수 전 방향만이라도.
- **dedup 경로의 process/retract 대칭 확인**(`base/effect-plan.md`
`:Unsubscribe()` 절) — M3 착수 전 확인.
- **`PopOnly` 이름**(`base/slot-plan.md`) — 메커니즘은 확정, 이름만 열림.
- **`PopOnly` 홀드 중 키가 사라졌을 때의 처분**(`base/slot-plan.md`) —
지금 의사코드대로면 파괴도 반환도 안 되고 참조만 끊김. **[정정,
2026-08-18 `/code-review high``ROADMAP.md`의 M6 PopOnly 체크박스와
대조해 발견] M6(`:List`가 있는 마일스톤) 착수 전 필요** — M8(`Ref`)
아님, 이전엔 마일스톤을 잘못 적어 M6를 그냥 지나칠 위험이 있었음.
- **[2026-08-19 해소]** `PopOnly` 이름 — **`Detach`로 확정**(공개 표면
위치도 `None`과 같은 최상위 export로 같이 확정). 원문은
`archive/question-resolved.md`.
- **`Detach`(구 `PopOnly`) 홀드 중 키가 사라졌을 때의 처분**
(`base/slot-plan.md`) — 지금 의사코드대로면 파괴도 반환도 안 되고
참조만 끊김. **[정정, 2026-08-18 `/code-review high``ROADMAP.md`
M6 `Detach` 체크박스와 대조해 발견] M6(`:List`가 있는 마일스톤) 착수
전 필요** — M8(`Ref`) 아님, 이전엔 마일스톤을 잘못 적어 M6를 그냥
지나칠 위험이 있었음.
- **`store:GetDynamic`을 콜론 메소드로 둘지 탑레벨 함수로 둘지**
(`base/store-plan.md`) — 콜론이면 `GetDynamic`이 모든 Store의 예약 키가
됨(lazy `__index`와 충돌). M3/M4 착수 전 필요.
@ -142,7 +145,8 @@
2026-08-12 스무 번째 세션에 현재 이름 그대로 유지로 이미 확정됐음(이
목록이 "위험도 높음, 1순위 open"으로 stale하게 남아있던 걸 발견해 수정)
— 아직 진짜로 열려있는 것만 짚으면(**[2026-08-18] `DI`→`D`는 확정·반영
완료로 목록에서 빠짐**, 대신 `PopOnly`(가칭)가 새로 들어옴): `Slot`(2순위),
완료로 목록에서 빠짐**, **[2026-08-19] `PopOnly`→`Detach`도 확정·반영
완료로 목록에서 빠짐**): `Slot`(2순위),
`canExecute`(3순위 — `isAlive`는 검토 후 기각, `can` 계열 접두 유지
방향으로 기울었으나 구체 대안 미정), `Brand`(3순위), `Tag`/`Added`/
`Removed`/`Merged`(3순위), `Attribute`/`AttributeKey`(3순위).

View file

@ -395,7 +395,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
차이는 딱 둘: 실제 `Destroy()`를 안 하고, 자식 `releaseOwner`도 안 함
(자식은 계속 그 slot 소유라 통째로 재마운트 가능 = 포탈).
**쓰는 자리**: `SlotHandler.process`가 반환하는 클로저, 그리고
`:List``reconcile`**값 교체와 `PopOnly`(가칭) 경로만**.
`:List``reconcile`**값 교체와 `Detach` 경로만**.
**여전히 파괴인 것**: 명시적 `Remove`/`Clear`/`dispose`, 그리고
**[재정정, 2026-08-18 구현 전 QA] `:List`에서 `updateFn`
`nil`/`None`을 반환하거나 키가 데이터에서 사라진 경로**(2026-08-13의
@ -479,14 +479,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
(filter/toggle 지원 — 첫 반환값 `nil` 시 실제 파괴, `Visible` 토글
아님, 200+ 항목에서 lazy하지 않은 문제 회피), `prev` 그대로 반환하면
저비용 재사용 경로.
**[2026-08-18 신설] `PopOnly`(가칭) 반환 경로** — `updateFn`
`PopOnly, { old = ..., source = ... }`를 반환하면 그 자리는 **파괴하지
않고 `Parent = nil`로만 내려와** Slot에서 빠지고, 보존은 반환한
userdata가 담당(다음 사이클에 거기서 `old`를 꺼내 반환하면 재마운트).
`Instance.new`/`Destroy` 비용을 아끼는 filter용 경로.
**⚠️ 이름은 가칭이고, "키가 데이터에서 사라졌을 때 PopOnly로 홀드
중이던 요소를 어떻게 처분하는가"는 미결** — 착수 전 결론 필요
(`base/slot-plan.md`의 "`nil` 리턴은 파괴가 기본" 절, `question.md` 3번). 파라미터 순서는 반환값 순서(`prev`류 먼저,
**[2026-08-18 신설, 이름 2026-08-19 확정] `Detach` 반환 경로** —
`updateFn``Detach, { old = ..., source = ... }`를 반환하면 그
자리는 **파괴하지 않고 `Parent = nil`로만 내려와** Slot에서 빠지고,
보존은 반환한 userdata가 담당(다음 사이클에 거기서 `old`를 꺼내
반환하면 재마운트). `Instance.new`/`Destroy` 비용을 아끼는 filter용
경로. 공개 표면은 `None`과 같이 패키지 최상위 export(`base/
slot-plan.md`의 "`nil` 리턴은 파괴가 기본" 절).
**⚠️ "키가 데이터에서 사라졌을 때 `Detach`로 홀드 중이던 요소를
어떻게 처분하는가"는 미결**(이름과 무관한 별개 항목) — 착수 전 결론
필요(같은 절, `question.md` 3번). 파라미터 순서는 반환값 순서(`prev`류 먼저,
`userdata`류 나중)와 맞춤(2026-08-11 세션 정정, 원래 `userdata`
`prev`보다 앞이었음).
**`updateFn``index``keyFn`의 raw `index`(원본 `data` 배열