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:
parent
93cf408eb3
commit
2348ea8058
8 changed files with 193 additions and 48 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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` 리턴은 파괴가 기본" 절이
|
||||
소스.
|
||||
|
||||
---
|
||||
|
||||
# 확인/결정 필요 목록
|
||||
|
|
|
|||
|
|
@ -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`가 하는 일 중 **실제 파괴와 자식
|
||||
|
|
|
|||
|
|
@ -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`에게 되돌려지지도 않고
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
72
.claude/session/2026-08-19-02-detach-naming-and-placement.md
Normal file
72
.claude/session/2026-08-19-02-detach-naming-and-placement.md
Normal 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`도
|
||||
같이 갱신, 이 문서가 그 포인터.
|
||||
|
|
@ -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순위).
|
||||
|
|
|
|||
20
ROADMAP.md
20
ROADMAP.md
|
|
@ -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` 배열
|
||||
|
|
|
|||
Loading…
Reference in a new issue