Merge pull request #9 from qwreey-bot/main
Sync 08.19 13:36 KST - Dev docs
This commit is contained in:
commit
2b48bd0bf3
14 changed files with 667 additions and 234 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` 리턴은 파괴가 기본" 절이
|
||||
소스.
|
||||
|
||||
---
|
||||
|
||||
# 확인/결정 필요 목록
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ State와 같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것.
|
|||
지연시킬 수 있는 유일한 요소**임. 평범한 State는 신호를 받으면 자기
|
||||
`invalid` 상태와 무관하게 **항상** 아래로 전파하고(`base/source-state-plan.md`
|
||||
"전파 모델 확정" 절), 그 흐름을 붙잡아둘 수 있는 건 명시적으로 배선된
|
||||
게이트뿐 — 지금은 `Blocker`가 유일하고, 시간 기반 게이트(`research/
|
||||
debounce-throttle-plan.md`)가 추가되면 같은 자리에 들어옴. 이걸 못 박아
|
||||
게이트뿐 — 지금은 `Blocker`가 유일하고, 시간 기반 게이트(`base/
|
||||
debounce-throttle-plan.md`, 설계 확정·구현은 아직)가 추가되면 같은 자리에
|
||||
들어옴. 이걸 못 박아
|
||||
두는 이유: 과거에 "이미 `invalid`면 전파를 멈춘다"는 서술이 base에
|
||||
있었고, 그건 사실상 `Blocker`가 하는 일을 모든 State에 암묵적으로 심는
|
||||
것이라 `Blocker`의 존재 의의를 반쯤 지워버렸음(역전 경위는
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
# Debounce / Throttle — 시간 기반 전파 게이트
|
||||
|
||||
**상태**: research — **[2026-08-14 신설]** 사용자 요청("`Blocker`와 유사하게
|
||||
Debounce/Throttle를 만들어야 한다")으로 신설. 여기 적힌 건 **에이전트가
|
||||
먼저 전부 정의해본 초안**이고, 확정된 건 하나도 없음 — 사용자가 이 문서를
|
||||
읽고 판단하는 게 다음 단계. 열린 결정은 맨 아래 "사용자 판단 대기" 절에
|
||||
번호로 모아뒀음.
|
||||
**상태**: base — **[2026-08-19 세션, 전부 해소되어 `research/`에서 승격]**
|
||||
12절 "사용자 판단 대기"에 남아있던 마지막 항목(이름/의미론/제어 핸들/
|
||||
`Time = 0`)까지 전부 결론이 나서 열린 결정이 없음 — 논의 원문은
|
||||
`session/2026-08-19-03-debounce-throttle-final-close.md`. 이 라운드에서
|
||||
드러난 것: 제어 핸들 설계까지 확정되고 나니 이 프리미티브는 **quad-base에
|
||||
새 코어 메커니즘을 추가하지 않는 순수 슈가**로 귀결됨(기존 `Blocker`의
|
||||
게이트 개념 + `Ref` + 주입 op 2개 위에 전부 얹힘) — 13절이 이를 반영해
|
||||
갱신됨.
|
||||
|
||||
> **[2026-08-14 신설]** 사용자 요청("`Blocker`와 유사하게 Debounce/Throttle를
|
||||
> 만들어야 한다")으로 `research/`에 신설. 여기 적힌 건 **에이전트가 먼저
|
||||
> 전부 정의해본 초안**이었음 — 이후 네 라운드 리뷰를 거쳐 아래 배너들대로
|
||||
> 전부 확정됨.
|
||||
|
||||
> **[2026-08-14 1차 리뷰 반영]** 사용자가 스로틀의 trailing 동작을 짚어준
|
||||
> 뒤 세 가지가 바뀜: (1) **Q5(패키지 경계) 해소** — quad-base + 엔진별
|
||||
|
|
@ -33,6 +41,24 @@ Debounce/Throttle를 만들어야 한다")으로 신설. 여기 적힌 건 **에
|
|||
> **`base/source-state-plan.md`의 무효화 dedup 문장이 확정된 `Observer`
|
||||
> 계약과 모순**되고 `base/architecture.md`와도 어긋난다는 게 드러나,
|
||||
> base 정정 항목(Q10)이 새로 생김.
|
||||
>
|
||||
> **[2026-08-19 4차 리뷰 반영, 최종]** 남아있던 Q1/Q2/Q4/Q8을 전부 닫음.
|
||||
> (1) **Q1 이름** — `Debounce`/`Throttle` 유지 확정, Roblox 관용 "debounce"
|
||||
> (재진입 방지 불리언)와 다르다는 경고를 사용자 문서 첫 줄에 못박기로.
|
||||
> (2) **Q2 의미론 — (A) emit-gate로 확정, (B) value-hold는 철회**.
|
||||
> `:Get()`이 값을 지연시키려면 게이트가 "창이 열리기 전 캐시가 확실히
|
||||
> valid하다"를 보장해야 하는데, invalid로 남은 채 아무도 안 읽다가 새 창이
|
||||
> 열리는 경우(드물지 않음 — 컴포넌트가 한동안 안 읽다가 다시 읽는 경우 등)
|
||||
> 그 보장이 깨져 "held value" 계약이 조용히 무너짐 — 이걸 고치려면 창이
|
||||
> 열리는 순간 upstream을 강제로 pull해야 하는데, 그건 정확히 Throttle이
|
||||
> 막으려는 그 비싼 연산을 게이트 자신이 강제로 돌리는 셈이라 laziness와
|
||||
> 상충. **결과적으로 Q7도 자동 소멸**(A는 `Blocker`와 완전히 같은 메커니즘이라
|
||||
> `blocker-plan.md`의 기존 "`Get()`은 라이브 레퍼런스" 문구가 그대로 맞고
|
||||
> 명확화가 따로 필요 없음). (3) **Q4 제어 핸들 — 넣기로 확정**, 다만 모양은
|
||||
> 초안 S1/S2 둘 다 아니고 세 번째 형태로 수렴 — 5-4절 "제어 핸들" 신설
|
||||
> 참고. (4) **Q8 `Time = 0`** — 허용, "defer될 수 있음"만 문서화, 금지/에러
|
||||
> 안 함. 부수로 **Time/MaxTime을 `number | State<number>`로 확장**하는 것도
|
||||
> 이 라운드에 같이 확정(5-2절) — 원문은 위 세션 파일.
|
||||
|
||||
`research/operator-sugar-plan.md`가 "`Operator.*` 카탈로그 밖의 별도
|
||||
설계 질문"으로 분리해뒀던 항목이 이 문서의 출발점(그 문서 "열린 질문 —
|
||||
|
|
@ -320,42 +346,47 @@ quad의 전파 모델은 `base/source-state-plan.md`의 "전파 모델 확정"
|
|||
근거는 순회 비용 최적화였지만 지금은 실제 중복 *계산*이라서).
|
||||
---
|
||||
|
||||
## 4. 의미론 — 지연되는 건 "전파"인가 "값"인가
|
||||
## 4. 의미론 — 지연되는 건 "전파"인가 "값"인가 — **(A) emit-gate로 확정 (2026-08-19)**
|
||||
|
||||
두 갈래가 있고, 이게 이 문서에서 두 번째로 큰 결정.
|
||||
두 갈래를 놓고 두 라운드 동안 (B)를 권장안으로 들고 있었으나, **[2026-08-19]
|
||||
(A)로 확정하고 (B)는 철회**. 아래는 최종 결론과, 왜 (B)가 무너졌는지의 기록.
|
||||
|
||||
### (A) emit-gate — `Blocker`와 완전히 동일
|
||||
### (A) emit-gate — `Blocker`와 완전히 동일 — **채택**
|
||||
|
||||
게이트는 자기 `invalid`를 즉시 세우되 아래로 전파만 미룸. 창이 열려 있는
|
||||
동안 누가 `debounced:Get()`하면 **최신값**이 나옴.
|
||||
동안 누가 `debounced:Get()`하면 **최신값**이 나옴 — `Blocker`의 gated
|
||||
state와 글자 그대로 같은 동작.
|
||||
|
||||
### (B) value-hold — 값 자체가 지연됨 (권장)
|
||||
### ~~(B) value-hold — 값 자체가 지연됨~~ — **철회됨, laziness와 상충**
|
||||
|
||||
게이트는 창이 닫혀 있는 동안 **자기를 invalid로 만들지 않음**. 즉 캐시된
|
||||
직전 값을 계속 들고 있고, `debounced:Get()`은 **지연된 값**을 반환. 타이머가
|
||||
커밋할 때 비로소 invalid + 전파.
|
||||
게이트가 창이 닫혀 있는 동안 자기를 invalid로 만들지 않고 캐시된 직전
|
||||
값을 들고 있다가, `debounced:Get()`이 **지연된 값**을 반환하게 하려던
|
||||
안. 업계 선례(VueUse `useDebounce`/RxJS `debounceTime`)와의 일치, `Blocker`와의
|
||||
역할 분리를 근거로 두 라운드 동안 권장안이었음.
|
||||
|
||||
**(B)를 권하는 이유**:
|
||||
**왜 무너졌는가**: 이 계약("`:Get()`은 항상 창 열리기 전의 held value")을
|
||||
지키려면 게이트가 **창이 열리는 시점에 자기 캐시가 확실히 valid함**을
|
||||
보장해야 하는데, 실제로는 안 그런 경로가 있음 — 직전 커밋에서
|
||||
`invalid = true`로 세팅된 채 아무도 `:Get()`을 안 부르고 있다가(다운스트림이
|
||||
한동안 안 읽는 경우, 예: 언마운트됐다 재마운트) 그 상태에서 새 창이
|
||||
열리면, "창 안에선 invalid 세팅 안 함"이라는 (B)의 규칙이 이미 세팅돼
|
||||
있던 `invalid=true`를 못 되돌려서 `:Get()`이 곧바로 최신값을 계산해버림 —
|
||||
"held value" 계약이 조용히 깨짐. 이걸 고치려면 창이 열리는 순간 게이트가
|
||||
upstream을 강제로 pull해 캐시를 스냅샷 떠야 하는데, **그건 정확히
|
||||
Throttle이 막으려던 그 비싼 연산을 게이트 자신이 매 창마다 강제로 돌리는
|
||||
것**이라 laziness가 깨짐 — Throttle의 주 용례(랙 걸리는 연산 게이팅)에서
|
||||
치명적.
|
||||
|
||||
1. **업계 의미론과 일치.** VueUse `useDebounce`는 반환된 ref의 *값 자체*가
|
||||
늦게 따라오고, RxJS `debounceTime`도 값의 방출을 미룸. "debounce된
|
||||
상태"를 다른 곳에서 읽었을 때 debounce 안 된 값이 나오면 놀람.
|
||||
2. **`Blocker`와 역할이 깔끔히 갈림.** `Blocker`는 "지금 중간 상태니까
|
||||
구독자에게 알리지 않을 뿐, 진실은 이미 새 값"이고, Debounce는 "이
|
||||
노드의 값은 정의상 늦게 따라오는 값"임. 후자를 (A)로 만들면 두 도구가
|
||||
같은 자리에서 미묘하게만 다른 애매한 물건이 됨.
|
||||
3. **확정된 원칙과 충돌하지 않음.** `base/blocker-plan.md`가 인용하는
|
||||
"`Get()`은 라이브 레퍼런스를 준다" 원칙은 `base/source-state-plan.md`에서
|
||||
실제로는 **레퍼런스 의미론**(테이블을 복사본이 아니라 라이브 참조로
|
||||
준다, 그래서 `Get()` 결과를 캐시해두고 `==` 비교하면 안 됨)에 대한
|
||||
것이지 "항상 상류 최신값"이라는 뜻이 아님. 게이트 노드의 *자기* 값이
|
||||
지연된 값이라면 `:Get()`은 여전히 그 노드의 라이브 값을 주는 것.
|
||||
→ 다만 blocker-plan의 그 한 줄이 후자로 읽힐 여지가 있어, 확정되면
|
||||
그 문장에 한 줄 명확화를 넣는 게 좋음(열린 질문 Q7).
|
||||
부수로, (B)를 지지했던 업계 선례(VueUse/RxJS)도 재검토하면 그대로 옮겨올
|
||||
근거가 약함 — 둘 다 push/eager 모델(Vue reactivity의 effect는 즉시 도는
|
||||
push, RxJS는 애초에 eager push 스트림)이라 "값이 지연된다"가 공짜로
|
||||
성립하는 세계고, quad처럼 **pull-lazy가 원칙**인 곳엔 그대로 안 맞는
|
||||
선례였음.
|
||||
|
||||
**(B)의 캐비엇 하나**: 노드가 아직 한 번도 계산된 적 없으면(캐시 없음)
|
||||
붙잡고 있을 "이전 값"이 없어서 첫 `:Get()`은 그냥 최신값을 계산함.
|
||||
"지연은 두 번째 값부터"라는 뜻 — 자연스럽고 무해하지만 문서에 명시할 것.
|
||||
**(A) 채택의 부수 효과 — Q7 소멸**: (A)는 `Blocker`의 gated state와
|
||||
완전히 같은 메커니즘이라, `base/blocker-plan.md`가 이미 쓰고 있는
|
||||
"`Get()`은 라이브 레퍼런스를 준다" 문구가 그대로 맞고 별도 명확화가
|
||||
필요 없음(옛 Q7이 걱정했던 모순은 (B)를 택했을 때만 생기는 문제였음).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -383,24 +414,35 @@ local sampled = mousePos:Apply(Throttle{Time = 1 / 30})
|
|||
|
||||
```lua
|
||||
type DebounceOptions = {
|
||||
Time: number, -- 필수. 창 길이(초) — 신호마다 리셋됨
|
||||
Leading: boolean?, -- 기본 false. 버스트의 첫 신호를 즉시 통과시킬지
|
||||
Trailing: boolean?, -- 기본 true. 조용해진 뒤 한 번 통과시킬지
|
||||
MaxTime: number?, -- 기본 nil. 신호가 안 끊겨도 최대 이 간격마다 강제 통과
|
||||
Time: number | State<number>, -- 필수. 창 길이(초) — 신호마다 리셋됨
|
||||
Leading: boolean?, -- 기본 false. 버스트의 첫 신호를 즉시 통과시킬지
|
||||
Trailing: boolean?, -- 기본 true. 조용해진 뒤 한 번 통과시킬지
|
||||
MaxTime: (number | State<number>)?, -- 기본 nil. 신호가 안 끊겨도 최대 이 간격마다 강제 통과
|
||||
Handle: Ref<GateHandle>?, -- 기본 nil. 이 인스턴스 전용 제어 핸들(5-4절)
|
||||
}
|
||||
|
||||
type ThrottleOptions = {
|
||||
Time: number, -- 필수. 창 길이(초) — 신호가 리셋하지 못함
|
||||
Leading: boolean?, -- 기본 true. 창 밖 첫 신호를 즉시 통과시킬지
|
||||
Trailing: boolean?, -- 기본 true. 창 안에 눌러둔 게 있으면 창 끝에 통과시킬지
|
||||
Time: number | State<number>, -- 필수. 창 길이(초) — 신호가 리셋하지 못함
|
||||
Leading: boolean?, -- 기본 true. 창 밖 첫 신호를 즉시 통과시킬지
|
||||
Trailing: boolean?, -- 기본 true. 창 안에 눌러둔 게 있으면 창 끝에 통과시킬지
|
||||
Handle: Ref<GateHandle>?, -- 기본 nil. 이 인스턴스 전용 제어 핸들(5-4절)
|
||||
}
|
||||
```
|
||||
|
||||
- **모든 필드는 plain 값만** — `State`를 못 받음. `base/tween-plan.md`의
|
||||
"옵션 값 모양" 절이 `Tween{...}`에 대해 확정한 것과 같은 규칙, 같은
|
||||
이유(옵션 안에 두 번째 반응 경로를 만들지 않음). `Time`을 반응형으로
|
||||
바꾸고 싶으면 `state:Apply(...)` 자체를 다시 만드는 상위 구조가 필요한데,
|
||||
그건 이 프리미티브가 아니라 `State<State<T>>` 영역.
|
||||
- **[2026-08-19 확정] `Time`/`MaxTime`은 `number | State<number>` 허용** —
|
||||
`Leading`/`Trailing`은 여전히 plain만(정적 정책값이라 반응형일 이유가
|
||||
없음). `base/tween-plan.md`의 "옵션 값 모양" 절이 든 "옵션 안에 두 번째
|
||||
반응 경로를 만들지 않음" 근거는 여기 안 부딪힘 — Debounce/Throttle은
|
||||
`Time`을 **구독하지 않고**, `setTimeout`을 실제로 호출하는 그 순간에만
|
||||
`:Get()`으로 값을 읽는 폴링이라 새 무효화 채널이 안 생김(`Animate`가
|
||||
트윈 시작 시점에만 duration을 읽는 것과 같은 결).
|
||||
- **이미 스케줄된 타이머엔 반영 안 됨** — `setTimeout(fn, delay)`로
|
||||
한 번 예약된 delay는 못 바꾸므로, `Time`을 바꿔도 **다음 창부터만**
|
||||
적용됨(진행 중인 창은 그대로 끝까지 감). 7절 의사코드의 `openWindow`/
|
||||
`cap` 스케줄 지점이 유일한 읽기 지점.
|
||||
- `Time`을 State로 만들고 싶으면 `state:Apply(...)` 상위 구조
|
||||
(`State<State<T>>`)가 필요하다던 옛 서술은 무효 — 그런 상위 구조 없이
|
||||
바로 지원됨.
|
||||
- `Leading = true, Trailing = false` → "버스트 시작에 한 번만"
|
||||
- `Leading = false, Trailing = true` → 기본값, 일반적인 debounce
|
||||
- 둘 다 `false`는 아무 일도 안 하는 설정 → **즉시 error** 권장(`Slot:Add`의
|
||||
|
|
@ -432,7 +474,81 @@ type ThrottleOptions = {
|
|||
끊이지 않으면 영원히 발화 안 함"이 디바운스의 정의라 그 안전장치가
|
||||
필요하지만, 스로틀은 원래 주기적으로 발화하므로 무의미함.
|
||||
|
||||
→ 열린 질문 Q3(이 개정안 확인).
|
||||
**[2026-08-19 확정]** 이 개정안 그대로 채택 — 이후 라운드들이 전부 이
|
||||
`Reset` 한 비트 모델을 전제로 논의를 진행했고 별도 이의 없이 유지됨(구
|
||||
Q3).
|
||||
|
||||
## 5-4. 제어 핸들 — `Flush`/`Cancel`, 개별은 `Ref`로 · 전체는 팩토리로 (2026-08-19 확정, 구 Q4)
|
||||
|
||||
**결론**: 핸들을 넣는다. 초안이 제시했던 두 모양(S1: 핸들 없음, S2:
|
||||
`Blocker`와 똑같은 "재사용 가능한 외부 객체") 둘 다 아니고, **세 번째
|
||||
모양으로 수렴**했다 — `Debounce`/`Throttle`가 `Blocker`와 근본적으로 다른
|
||||
지점(이전 실행이 다음 실행에 영향을 주는 상태 기계라, `Blocker`처럼
|
||||
여러 파이프라인에 자유롭게 공유해도 안전한 물건이 아님)을 짚은 사용자
|
||||
지적에서 나왔다.
|
||||
|
||||
### 왜 State 자신에 메소드를 붙이지 않는가
|
||||
|
||||
가장 먼저 검토했던 대안(게이트가 반환하는 State 자체에 `:Flush()`/
|
||||
`:Cancel()`을 직접 붙임)은 기각됨 — 그러면 "디바운스로 만들어진 State"와
|
||||
"일반 State"가 구조적으로 다른 타입이 되어(메소드 유무로 타입이 갈림),
|
||||
State 계층에 조용히 서브타입 분기가 생긴다. quad가 피해온 OOP식 확장과
|
||||
같은 종류의 문제라 State 자신은 손대지 않기로 함.
|
||||
|
||||
### 왜 `Blocker`처럼 "먼저 만들어 공유하는 외부 객체"도 아닌가
|
||||
|
||||
`Blocker()`는 의도적으로 **여러 배치에 재사용 가능한 외부 객체** —
|
||||
`blocker:On()`/`Off()`가 그 블로커에 배선된 모든 gated state에 동시
|
||||
적용되는 게 정확히 원하는 동작. 그런데 Debounce/Throttle을 그대로
|
||||
따라하면(외부 `Debouncer{...}` 객체 하나를 여러 `state:Debounce(d)`에
|
||||
공유) **사용자가 지적한 문제가 그대로 재현됨** — 여러 파이프라인이 같은
|
||||
내부 타이머/pending 상태를 공유하게 되어, "누구의 신호가 창을 리셋하고
|
||||
누구의 값이 커밋되는가"가 불명확해진다. 5-1절이 이미 "재사용해도
|
||||
상태를 공유하지 않음, `:Apply`가 호출될 때마다 새 게이트"로 확정해둔
|
||||
것과도 정면으로 어긋남.
|
||||
|
||||
### 채택된 모양 — 개별은 `Ref` 아웃파라미터, 전체는 팩토리 자체
|
||||
|
||||
`:Apply()`의 기존 계약(`Operator` 관용구, "팩토리가 State 하나만 돌려준다")은
|
||||
안 건드리고, 옵션 필드로 핸들을 곁다리로 받는다 — `base/ref-plan.md`의
|
||||
`Ref`("채워지길 기다리는 빈 박스를 먼저 만들어 넘기고, 나중에 채워지면
|
||||
`:Callback()`/`.Value`로 받는" 이미 확정된 패턴)를 그대로 재사용:
|
||||
|
||||
```lua
|
||||
export type GateHandle = {
|
||||
Flush: () -> (), -- 이 인스턴스만 즉시 커밋
|
||||
Cancel: () -> (), -- 이 인스턴스만 pending을 버림(전파 없음)
|
||||
}
|
||||
|
||||
local h = Ref()
|
||||
local debounced = state:Apply(Debounce{Time = 0.3, Handle = h})
|
||||
-- 게이트가 실제로 만들어지는 시점(팩토리 호출 시)에 h가 채워짐:
|
||||
-- h.Value == { Flush = fn, Cancel = fn } -- 이 게이트 인스턴스 하나만 제어
|
||||
h.Value:Flush()
|
||||
|
||||
-- 이 인스턴스 하나만 즉시 커밋(pending이면 창 끝을 기다리지 않고 지금 통과)
|
||||
-- :Cancel()은 반대로 pending을 그냥 버림(통과 없이 타이머만 정리)
|
||||
```
|
||||
|
||||
- **개별 제어**: 위처럼 `Handle = Ref()`로 특정 `:Apply()` 호출 하나만
|
||||
겨냥.
|
||||
- **전체 브로드캐스트**: 팩토리 자신(`Debounce{...}`가 돌려주는 객체)에도
|
||||
`:Flush()`/`:Cancel()`을 붙임 — 그 팩토리로 만들어진 **모든** 게이트
|
||||
인스턴스에 한 번에 적용(저장 버튼 하나로 여러 debounce된 입력을 동시
|
||||
커밋하는 식의 용례). 새 객체 종류를 만드는 게 아니라 이미 `Debounce{...}`가
|
||||
돌려주던 팩토리 값에 메소드 두 개를 더하는 것뿐.
|
||||
- **팩토리는 자기가 만든 게이트를 weak 레지스트리로만 추적** — strong
|
||||
참조로 붙잡으면 다운스트림이 전부 죽어도 게이트가 팩토리에 살아있다는
|
||||
이유로 GC가 안 돼 `base/lifecycle-pattern.md`의 "정리(`retract`)는
|
||||
기본적으로 GC에 위임" 절과 충돌함. weak 등록이면 그 문제가 없음(코퍼스가
|
||||
gcconn/gchold 구분에서 이미 쓰는 것과 같은 종류의 장치).
|
||||
- `Flush`/`Cancel`은 게이트 인스턴스가 이미 갖고 있는 커밋/취소 내부
|
||||
함수(7절의 `onWindowEnd`/타이머 정리 로직)를 그대로 호출 — 새 로직이
|
||||
아니라 노출 방식만 다름.
|
||||
- **의미**: `Flush()`는 `pending`이면 창 끝을 기다리지 않고 즉시
|
||||
`onWindowEnd`가 하는 커밋(passThrough + 창 재개방)을 강제 실행,
|
||||
`pending`이 없으면 아무 일도 안 함(idempotent). `Cancel()`은 타이머를
|
||||
정리하고 `pending = false`로 되돌리되 **전파는 안 함**(버림).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -662,10 +778,17 @@ function clearTimeout(timeout: Timeout) timeout._native() end
|
|||
> **주의**: `Gate` 노드의 내부 훅(`onUpstreamSignal`, `commit`)은 아직
|
||||
> 이름도 계약도 확정되지 않은 가칭 — `Blocker`의 게이티드 노드가 이미
|
||||
> 필요로 하는 것과 같은 훅이라(1절), 실제 구현 시엔 그쪽과 같이 정의할 것.
|
||||
> 아래 코드의 `registry`/`Handle` 배선도 마찬가지로 스케치 수준 — 실제
|
||||
> 구현 시 이름은 자유.
|
||||
|
||||
```lua
|
||||
-- quad-base — 공용 코어. Reset 한 비트가 Debounce/Throttle을 가름(5-3절).
|
||||
|
||||
local function readTime(t: number | State<number>): number
|
||||
if type(t) == "number" then return t end
|
||||
return t:Get() -- setTimeout 호출 시점에만 읽음 — 이미 스케줄된 타이머엔 영향 없음
|
||||
end
|
||||
|
||||
local function makeGate(reset: boolean, opts)
|
||||
if opts.Leading == false and opts.Trailing == false then
|
||||
error("Leading/Trailing 둘 다 false면 아무것도 통과하지 않음")
|
||||
|
|
@ -673,66 +796,103 @@ local function makeGate(reset: boolean, opts)
|
|||
local leading = if reset then opts.Leading == true else opts.Leading ~= false
|
||||
local trailing = opts.Trailing ~= false
|
||||
|
||||
-- 팩토리 — :Apply가 state마다 한 번씩 호출하므로 아래 지역 상태는
|
||||
-- 게이트 노드 하나당 하나씩 새로 생김(팩토리 재사용해도 공유 안 됨)
|
||||
return function(self)
|
||||
local gate = Gate(self) -- Blocker가 쓰는 것과 같은 게이트 노드
|
||||
local pending = false -- 창 안에서 상류 신호가 있었는가
|
||||
local window = nil -- 살아있으면 "창 안", nil이면 idle
|
||||
local cap = nil -- MaxTime 타이머(Debounce 전용)
|
||||
-- 팩토리 레벨 상태 — makeGate 호출(=Debounce{...}/Throttle{...} 한 번)당 하나.
|
||||
-- weak 레지스트리라 여기 등록돼도 게이트의 GC를 막지 않음(5-4절).
|
||||
local instances = setmetatable({}, {__mode = "k"})
|
||||
|
||||
local openWindow, onWindowEnd
|
||||
local function flushAll()
|
||||
for gate in instances do gate._flush() end
|
||||
end
|
||||
local function cancelAll()
|
||||
for gate in instances do gate._cancel() end
|
||||
end
|
||||
|
||||
function openWindow()
|
||||
window = setTimeout(onWindowEnd, opts.Time)
|
||||
end
|
||||
-- 팩토리 자체 — :Apply(factory)가 호출할 수 있는 함수이면서, 동시에
|
||||
-- 전체 브로드캐스트 :Flush()/:Cancel()도 갖는 콜러블 객체(5-4절)
|
||||
local factory = setmetatable({}, {
|
||||
__call = function(_, self)
|
||||
local gate = Gate(self) -- Blocker가 쓰는 것과 같은 게이트 노드
|
||||
local pending = false -- 창 안에서 상류 신호가 있었는가
|
||||
local window = nil -- 살아있으면 "창 안", nil이면 idle
|
||||
local cap = nil -- MaxTime 타이머(Debounce 전용)
|
||||
|
||||
function onWindowEnd()
|
||||
window = nil
|
||||
if pending and trailing then
|
||||
-- Blocker와 같은 순서: 상태를 먼저 정리하고 그 다음 전파
|
||||
-- (전파 도중 소비자가 동기적으로 상류를 :Set()해서 재진입해도
|
||||
-- 방금 닫은 창의 잔여 상태를 다시 건드리지 않게)
|
||||
pending = false
|
||||
if cap then clearTimeout(cap); cap = nil end
|
||||
gate:passThrough() -- invalid 세팅 + 아래로 1회 전파
|
||||
openWindow() -- 통과했으니 창을 다시 엶 = 다음 통과까지 최소 Time
|
||||
local openWindow, onWindowEnd
|
||||
|
||||
function openWindow()
|
||||
window = setTimeout(onWindowEnd, readTime(opts.Time))
|
||||
end
|
||||
-- pending이 없으면 창을 안 열고 완전히 idle로 복귀
|
||||
end
|
||||
|
||||
gate.onUpstreamSignal = function()
|
||||
if window == nil then
|
||||
-- 창 밖(idle)
|
||||
if leading then gate:passThrough() else pending = true end
|
||||
openWindow()
|
||||
else
|
||||
-- 창 안
|
||||
pending = true
|
||||
if reset then -- ← Debounce만: 창을 뒤로 민다
|
||||
clearTimeout(window)
|
||||
function onWindowEnd()
|
||||
window = nil
|
||||
if pending and trailing then
|
||||
-- Blocker와 같은 순서: 상태를 먼저 정리하고 그 다음 전파
|
||||
-- (전파 도중 소비자가 동기적으로 상류를 :Set()해서 재진입해도
|
||||
-- 방금 닫은 창의 잔여 상태를 다시 건드리지 않게)
|
||||
pending = false
|
||||
if cap then clearTimeout(cap); cap = nil end
|
||||
gate:passThrough() -- invalid 세팅 + 아래로 1회 전파
|
||||
openWindow() -- 통과했으니 창을 다시 엶 = 다음 통과까지 최소 Time
|
||||
end
|
||||
-- pending이 없으면 창을 안 열고 완전히 idle로 복귀
|
||||
end
|
||||
|
||||
gate.onUpstreamSignal = function()
|
||||
if window == nil then
|
||||
-- 창 밖(idle)
|
||||
if leading then gate:passThrough() else pending = true end
|
||||
openWindow()
|
||||
else
|
||||
-- 창 안
|
||||
pending = true
|
||||
if reset then -- ← Debounce만: 창을 뒤로 민다
|
||||
clearTimeout(window)
|
||||
openWindow()
|
||||
end
|
||||
end
|
||||
|
||||
-- MaxTime: 창과 달리 절대 리셋되지 않는 두 번째 타이머.
|
||||
-- "신호가 안 끊기면 영원히 발화 안 함"(1-1절)을 위한 안전장치라
|
||||
-- Debounce에서만 의미 있음.
|
||||
if opts.MaxTime and cap == nil and pending then
|
||||
cap = setTimeout(function()
|
||||
cap = nil
|
||||
if pending and trailing then
|
||||
pending = false
|
||||
if window then clearTimeout(window) end
|
||||
gate:passThrough()
|
||||
openWindow()
|
||||
end
|
||||
end, readTime(opts.MaxTime))
|
||||
end
|
||||
end
|
||||
|
||||
-- MaxTime: 창과 달리 절대 리셋되지 않는 두 번째 타이머.
|
||||
-- "신호가 안 끊기면 영원히 발화 안 함"(1-1절)을 위한 안전장치라
|
||||
-- Debounce에서만 의미 있음.
|
||||
if opts.MaxTime and cap == nil and pending then
|
||||
cap = setTimeout(function()
|
||||
cap = nil
|
||||
if pending and trailing then
|
||||
pending = false
|
||||
if window then clearTimeout(window) end
|
||||
gate:passThrough()
|
||||
openWindow()
|
||||
end
|
||||
end, opts.MaxTime)
|
||||
-- 5-4절 제어 핸들 — Flush는 즉시 커밋(창 끝을 안 기다림), Cancel은 버림
|
||||
gate._flush = function()
|
||||
if pending and trailing then
|
||||
pending = false
|
||||
if window then clearTimeout(window) end
|
||||
if cap then clearTimeout(cap); cap = nil end
|
||||
gate:passThrough()
|
||||
openWindow()
|
||||
end
|
||||
end
|
||||
gate._cancel = function()
|
||||
pending = false
|
||||
if window then clearTimeout(window); window = nil end
|
||||
if cap then clearTimeout(cap); cap = nil end
|
||||
end
|
||||
end
|
||||
|
||||
return gate
|
||||
end
|
||||
instances[gate] = true
|
||||
if opts.Handle then
|
||||
opts.Handle:Set({ Flush = gate._flush, Cancel = gate._cancel })
|
||||
end
|
||||
|
||||
return gate
|
||||
end,
|
||||
__index = { Flush = flushAll, Cancel = cancelAll },
|
||||
})
|
||||
|
||||
return factory
|
||||
end
|
||||
|
||||
function Debounce(opts: DebounceOptions) return makeGate(true, opts) end
|
||||
|
|
@ -754,10 +914,14 @@ function Throttle(opts: ThrottleOptions) return makeGate(false, opts) end
|
|||
3.5 입력 → window == nil → leading 즉시 통과 ●
|
||||
```
|
||||
|
||||
**(B) value-hold 의미론이 붙는 자리**: `gate`는 `onUpstreamSignal`에서
|
||||
자기 `invalid`를 세우지 않고, 오직 `passThrough()`에서만 세움 — 그래서
|
||||
창이 열려 있는 동안 `gate:Get()`은 캐시된 직전 값을 반환. (A)를 택하면
|
||||
`onUpstreamSignal` 진입 시 invalid만 세우고 전파를 미루는 형태로 바뀜.
|
||||
**(A) emit-gate가 붙는 자리(확정, 4절)**: `gate`는 다른 평범한 State와
|
||||
똑같이 **매 `onUpstreamSignal` 진입 시 자기 `invalid`를 즉시 세운다** —
|
||||
`source-state-plan.md`의 "전파 모델 확정" 절이 정한 "emit은 항상 전파된다"
|
||||
규칙이 게이트 자신에게도 그대로 적용됨. 위 코드의 `gate:passThrough()`가
|
||||
실제로 미루는 건
|
||||
**다운스트림 통지(전파)뿐**이지 invalid 세팅이 아님 — 그래서 창이 열려
|
||||
있는 동안 `gate:Get()`을 불러도 항상 최신값이 계산됨(캐시가 stale한
|
||||
채로 안 남음).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -787,9 +951,9 @@ function Throttle(opts: ThrottleOptions) return makeGate(false, opts) end
|
|||
|
||||
---
|
||||
|
||||
## 9. 이름
|
||||
## 9. 이름 — `Debounce`/`Throttle` 확정 (2026-08-19)
|
||||
|
||||
### 9-1. ⚠️ Roblox 커뮤니티의 "debounce"와 충돌함
|
||||
### 9-1. ⚠️ Roblox 커뮤니티의 "debounce"와 충돌함 — 유지로 확정
|
||||
|
||||
Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리언**을
|
||||
가리킴(`local debounce = false ... if debounce then return end`). 웹
|
||||
|
|
@ -808,9 +972,9 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
|||
| `Coalesce` | "합친다"는 동작 자체 | nil 병합(`Alternative`)과 어휘 충돌 |
|
||||
| `RateLimit` (Throttle 자리) | 의미 명확 | 서버 레이트 리밋 뉘앙스 |
|
||||
|
||||
**권고**: `Debounce`/`Throttle` 유지 + **사용자 문서 첫 줄에 Roblox 관용
|
||||
"debounce"와 다르다는 걸 못박기**. 업계 표준 이름을 버리면 검색·이주
|
||||
비용이 더 큼. 단 이건 사용자 취향이 강하게 걸리는 영역이라 Q1로 넘김.
|
||||
**[2026-08-19 확정]** `Debounce`/`Throttle` 유지 + **사용자 문서 첫 줄에
|
||||
Roblox 관용 "debounce"와 다르다는 걸 못박기**. 업계 표준 이름을 버리면
|
||||
검색·이주 비용이 더 크다는 게 채택 근거 — 더 판단할 것 없음(구 Q1).
|
||||
|
||||
### 9-2. `-ed`를 안 붙이는 이유 (이건 코퍼스 규칙으로 결정됨)
|
||||
|
||||
|
|
@ -856,24 +1020,24 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
|||
|
||||
---
|
||||
|
||||
## 12. 사용자 판단 대기 — 열린 질문
|
||||
## 12. 사용자 판단 대기 — 전부 해소됨 (2026-08-19)
|
||||
|
||||
1. **이름** — `Debounce`/`Throttle` 유지(권장) vs 대안. Roblox 관용
|
||||
"debounce"(재진입 불리언)와의 충돌을 문서 경고로만 다룰지. (9-1절)
|
||||
2. **의미론** — (A) emit-gate(`Blocker`와 동일, `:Get()`은 최신값) vs
|
||||
**(B) value-hold(권장, `:Get()`도 지연된 값)**. (4절)
|
||||
3. **[2026-08-14 개정] 공개 생성자 2개 + 내부 구현 1개(`Reset` 한 비트)**
|
||||
— 초안의 "`Throttle`은 `Debounce{MaxTime}` 프리셋"은 이중 발화 버그가
|
||||
있어 폐기됨. 개정안 확인만 필요. (5-3절, 7절)
|
||||
4. **제어 핸들(`:Flush()`/`:Cancel()`)을 v1에 넣을지.** 세 가지 모양:
|
||||
- **S1(권장, v1)**: `state:Apply(Debounce{...})` — 핸들 없음.
|
||||
`Operator` 관용구와 완전 일치, 가장 단순.
|
||||
- **S2**: `local d = Debouncer{...}; state:Debounce(d)` + `d:Flush()`/
|
||||
`d:Cancel()` — **`Blocker`의 모양과 글자 그대로 같음**(외부 객체를
|
||||
들고 `state:Block(blocker)`로 배선). 구조적 추가 비용이 사실상 없고,
|
||||
"검색창에서 Enter 누르면 즉시 커밋" 같은 실사용 요구를 커버함.
|
||||
- **S3**: `__call`을 가진 객체로 S1/S2를 겸함 — 매직이라 비권장.
|
||||
실사용에서 Flush가 자주 필요하다고 보면 처음부터 S2가 나음.
|
||||
**[2026-08-19] 남아있던 Q1/Q2/Q4/Q8까지 전부 닫혀 열린 항목이 없음** —
|
||||
논의 원문은 `session/2026-08-19-03-debounce-throttle-final-close.md`.
|
||||
이력만 남겨둠(각 항목이 왜 그렇게 됐는지는 가리키는 절이 소스, 여기서
|
||||
반복 안 함):
|
||||
|
||||
1. ~~**이름**~~ **[2026-08-19 해소]** — `Debounce`/`Throttle` 유지,
|
||||
Roblox 관용 "debounce"와의 충돌은 문서 경고로만 대응. (9-1절)
|
||||
2. ~~**의미론**~~ **[2026-08-19 해소]** — **(A) emit-gate 채택**, (B)
|
||||
value-hold는 laziness와 상충해 철회. (4절)
|
||||
3. ~~**공개 생성자 2개 + 내부 구현 1개(`Reset` 한 비트)**~~
|
||||
**[2026-08-19 해소]** — 2026-08-14 개정안 그대로 채택, 이견 없었음.
|
||||
(5-3절, 7절)
|
||||
4. ~~**제어 핸들**~~ **[2026-08-19 해소]** — 넣기로 확정. 초안의 S1(핸들
|
||||
없음)/S2(`Blocker`와 같은 공유 외부 객체) 둘 다 아니고, 개별은
|
||||
`Ref` 아웃파라미터·전체는 팩토리 자체의 `:Flush()`/`:Cancel()`(weak
|
||||
레지스트리로 브로드캐스트)로 수렴. (5-4절)
|
||||
5. ~~**패키지 경계**~~ **[2026-08-14 해소]** — 사용자가 quad-base +
|
||||
엔진별 태스크 배선으로 확정. 주입 표면이 3개→5개로 늘어나는 비용은
|
||||
수용됨. op 이름/시그니처도 사용자가 지정
|
||||
|
|
@ -882,14 +1046,12 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
|||
전제(무효화 dedup이 신호를 삼킴)가 틀린 것으로 드러나 질문 자체가
|
||||
없어짐. emit은 항상 재전파되므로 게이트는 어디에 걸든 정상 동작함.
|
||||
(3절)
|
||||
7. **`base/blocker-plan.md` 한 줄 명확화** — 그 문서가 인용하는
|
||||
"`Get()`은 라이브 레퍼런스를 준다"는 실제로는 `base/source-state-plan.md`의
|
||||
**레퍼런스 의미론** 원칙이지 "항상 상류 최신값"이 아님. Q2에서 (B)를
|
||||
택하면 두 문서가 모순돼 보이므로 그 인용에 한 줄 주석을 넣는 게 좋음.
|
||||
(확정 문서 수정이라 사용자 승인 후 반영 — 이 세션에선 안 건드림.)
|
||||
8. **`Time = 0`을 허용할지** — "이번 스텝 합치기" 용도로 유용하지만
|
||||
스케줄러 타이밍에 의존하는 준-`Blocker`가 됨. 허용(권장) / 금지 /
|
||||
별도 이름 부여 중 선택. (1절 인용문)
|
||||
7. ~~**`base/blocker-plan.md` 한 줄 명확화**~~ **[2026-08-19 소멸]** — Q2가
|
||||
(A)로 확정되면서 이 항목이 걱정했던 모순(값-지연 의미론과 "`Get()`은
|
||||
라이브 레퍼런스" 문구의 충돌)이 애초에 안 생김 — (A)는 `Blocker`와
|
||||
완전히 같은 메커니즘이라 그 문구가 그대로 맞음. (4절)
|
||||
8. ~~**`Time = 0`을 허용할지**~~ **[2026-08-19 해소]** — 허용, "defer될
|
||||
수 있음"만 문서화, 금지/에러 안 함. (1절 인용문)
|
||||
9. ~~**`Timeout` 핸들의 타입**~~ **[2026-08-14 완전 해소]** — 사용자 결정으로
|
||||
**`type Timeout = { __type_timeout: true, _native: any }`**(마커는
|
||||
런타임에도 실제로 넣고, 백엔드 페이로드 자리도 타입에 미리 선언).
|
||||
|
|
@ -906,18 +1068,41 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
|||
전체 정정을 지시 — 같은 세션에 base/reference/research/ROADMAP/
|
||||
스파이크/audit까지 전부 반영했고, 역전 기록은
|
||||
`archive/invalidate-dedup-propagation-reversed.md`. 상세는 3절.
|
||||
11. ~~**`Time`/`MaxTime`을 `State`로 받을 수 있는가**~~ **[2026-08-19
|
||||
신설·같은 날 해소]** — 허용. 구독이 아니라 `setTimeout`/`cap` 스케줄
|
||||
시점의 폴링(`:Get()`)이라 새 무효화 채널이 안 생겨 5-2절이 인용하던
|
||||
`tween-plan.md` 선례("옵션에 두 번째 반응 경로를 만들지 않음")와
|
||||
안 부딪힘. 이미 스케줄된 타이머엔 미반영, 다음 창부터 적용. (5-2절)
|
||||
|
||||
---
|
||||
|
||||
## 13. 우선순위 / 마일스톤
|
||||
## 13. 우선순위 / 마일스톤 — **[2026-08-19 재평가] 결국 순수 슈가로 귀결**
|
||||
|
||||
**M0 착수를 막지 않음** — 이 문서의 어떤 결정도 디스패치/State 코어 계약을
|
||||
바꾸지 않음(11절에서 확인). 다만 `Operator` 슈가와 달리 **순수 슈가가
|
||||
아니라 실제 기능 갭**이라(타이머 없이는 사용자 코드로 재현하기 번거롭고,
|
||||
재현하면 laziness를 깨기 쉬움) 우선순위는 그쪽보다 위로 두는 게 맞아 보임.
|
||||
바꾸지 않음(11절에서 확인).
|
||||
|
||||
**의존성**: State 코어(`ROADMAP.md` M3) + 백엔드 주입 표면. 그 둘이
|
||||
서면 언제든 얹을 수 있고, `Blocker` 구현과 **같은 시점에 하는 게 확실히
|
||||
쌈** — 1절에서 봤듯 게이트 노드를 공유하므로 따로 하면 같은 걸 두 번
|
||||
설계하게 됨. **권고: `Blocker` 구현 시점(M3)에 게이트 노드를 공용으로
|
||||
빼두고, Debounce/Throttle 자체는 그 위에 나중에 얹기.**
|
||||
**[2026-08-19] 옛 서술("`Operator` 슈가와 달리 순수 슈가가 아니라 실제
|
||||
기능 갭이라 우선순위를 위로 둔다")은 이번 라운드로 뒤집힘.** 제어 핸들
|
||||
설계(5-4절)까지 확정하고 나니, `Debounce`/`Throttle`이 실제로 새로
|
||||
필요로 하는 quad-base 코어 표면은 **주입 op 2개(`setTimeout`/`clearTimeout`)
|
||||
뿐**이고 — 게이트 메커니즘은 `Blocker`가 이미 확정한 gated state 개념
|
||||
위에서(1절), 제어 핸들은 이미 확정된 `Ref` 위에서(5-4절) 전부 얹히는 것으로
|
||||
드러남. 즉 **quad-base에 새 코어 메커니즘을 추가하지 않는 순수 슈가**다 —
|
||||
`Animate`/`Operator.*`와 같은 성격.
|
||||
|
||||
**그래도 착수 시점이 뒤로 밀리진 않는다.** 설계 자체가 실제 기능 갭에서
|
||||
나온 요청(사용자가 "만들어야 한다"고 직접 지정, `research/
|
||||
additional-primitives-plan.md`가 원래 "안 만들어도 된다"고 판단했던 걸
|
||||
뒤집은 배경)이라는 사실은 안 바뀌고, 이 문서가 `research/`에서 `base/`로
|
||||
승격된 것도 별개로 유효 — 달라지는 건 오직 **구현 우선순위**뿐이다.
|
||||
순수 슈가라는 게 확인됐으니 `Operator` 콤비네이터 카탈로그와 같은 급으로
|
||||
맨 뒤로 미뤄도 됨(다른 기능이 이걸 의존하지 않고, 없어도 다른 기능이 안
|
||||
막힘).
|
||||
|
||||
**의존성**: State 코어(`ROADMAP.md` M3) + 백엔드 주입 표면(`setTimeout`/
|
||||
`clearTimeout`) + `Blocker`(gated state) + `Ref`. 전부 M3/M8 안에서
|
||||
확정되는 것들이라 그 이후 언제든 얹을 수 있다. **`Blocker` 구현
|
||||
시점(M3)에 게이트 노드를 공용으로 빼두는 것만은 여전히 그 시점에 해야
|
||||
함** — 1절에서 봤듯 같은 노드를 공유하므로 따로 하면 같은 걸 두 번
|
||||
설계하게 됨. 프리미티브 자체(`Debounce`/`Throttle` 함수)는 그 위에 아무
|
||||
때나 나중에 얹으면 된다.
|
||||
|
|
@ -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`가 하는 일 중 **실제 파괴와 자식
|
||||
|
|
|
|||
|
|
@ -194,9 +194,10 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
|||
전파를 제어하는 장치가 **아님**. `:Get()`이 호출되면 상류로 올라가
|
||||
재계산하고, 그 결과를 캐시에 넣고, `invalid`를 끈다.
|
||||
- **emit 전파를 늦추거나 흡수할 수 있는 건 명시적인 게이트 요소뿐** —
|
||||
지금은 `Blocker`(`base/blocker-plan.md`)가 유일하고, 앞으로 추가된다면
|
||||
`research/debounce-throttle-plan.md`의 시간 기반 게이트가 같은 자리에
|
||||
들어옴. **평범한 State는 절대 신호를 삼키지 않는다.**
|
||||
지금은 `Blocker`(`base/blocker-plan.md`)가 유일하고,
|
||||
`base/debounce-throttle-plan.md`의 시간 기반 게이트가 같은 자리에
|
||||
들어감(설계 확정, 구현은 아직). **평범한 State는 절대 신호를 삼키지
|
||||
않는다.**
|
||||
- **[2026-08-14 정정 — 중요]** 이 자리엔 원래 "이미 `invalid`였다면 그
|
||||
아래로 더 전파하지 않는다(다이아몬드 중복 워크 방지)"라고 적혀 있었으나
|
||||
**틀린 서술이라 뒤집힘**. 그대로 두면 `:Get()`을 호출하지 않는
|
||||
|
|
|
|||
|
|
@ -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 배경 있는 사람이
|
||||
헷갈릴 수 있음.
|
||||
|
|
@ -134,35 +139,13 @@
|
|||
몫. 같은 리서치에서 포함 범위도 새로 갈렸음 — 비트/비교 연산자 그룹과
|
||||
`Sub`/`Div`는 리액티브 콤비네이터로서 선례가 전혀 없어 드랍 후보로,
|
||||
`Clamp`/`Min`/`Max`는 선례가 강해 추가 후보로, Debounce/Throttle은
|
||||
업계에 흔하지만 `Blocker`와는 다른 시간 기반 메커니즘이라 이 카탈로그가
|
||||
아니라 quad-roblox 쪽 별도 프리미티브로 다룰지 판단이 필요한 별개 질문으로
|
||||
분리됨. **[2026-08-13 세션 신설]** `Alternative`(nil 대체값, coalesce/`??`/
|
||||
업계에 흔하지만 `Blocker`와는 다른 시간 기반 메커니즘이라 이 카탈로그
|
||||
밖 별개 질문으로 분리됐었음 — **[2026-08-19 해소]** 그 별개 질문은
|
||||
`base/debounce-throttle-plan.md`로 전부 해소·승격 완료, 더 이상 판단
|
||||
대기 아님. **[2026-08-13 세션 신설]** `Alternative`(nil 대체값, coalesce/`??`/
|
||||
엘비스 연산자류) 후보 추가 — Haskell 비교 리서치 중 나옴, 카탈로그 확정
|
||||
규칙에 그대로 맞아 포함 근거는 있음. 상세는 `research/operator-sugar-plan.md`.
|
||||
구현 자체는 맨 마지막 우선순위(순수 슈가, 없어도 무방) — 여전함.
|
||||
- **[신설, 2026-08-14] Debounce/Throttle — 남은 열린 질문(개수는
|
||||
`research/debounce-throttle-plan.md` 12절이 소스, 여기서 반복 안 함)** —
|
||||
사용자 요청("`Blocker`와 유사하게 만들어야 한다")으로
|
||||
`research/debounce-throttle-plan.md` 신설. 네 번의 리뷰 라운드로
|
||||
**패키지 경계·주입 op 시그니처·`Timeout` 타입·동작 정식화는 전부
|
||||
확정**됐고, 사용자 취향/방향이 갈리는 것만 남음 — 주요 4개:
|
||||
- **의미론** — 창이 열려 있는 동안 `:Get()`이 최신값을 주는가
|
||||
(`Blocker`와 동일) vs 지연된 값을 주는가(권장, VueUse/RxJS와 일치).
|
||||
- **제어 핸들** — `state:Apply(Debounce{...})`로 끝낼지, `Blocker`와
|
||||
글자 그대로 같은 모양(`Debouncer` 객체 + `state:Debounce(d)` +
|
||||
`d:Flush()`/`:Cancel()`)으로 갈지. "검색창에서 Enter 누르면 즉시
|
||||
커밋"류가 잦다고 보면 후자가 처음부터 나음.
|
||||
- **이름** — Roblox 관용 "debounce"(재진입 방지 불리언)와 정면 충돌.
|
||||
업계 표준 이름을 유지하고 문서로 경고할지, 다른 이름을 쓸지.
|
||||
- **`Time = 0` 허용 여부** — "이번 스텝 합치기"로 유용하지만 스케줄러
|
||||
타이밍에 의존하는 준-`Blocker`가 됨.
|
||||
이 외에 사소한 것 둘도 12절에 열려있음: 공개 생성자 2개 개정안이
|
||||
사용자 확인만 남은 상태, `base/blocker-plan.md`가 인용하는 "`Get()`은
|
||||
라이브 레퍼런스" 문구에 한 줄 명확화가 필요(확정 문서 수정이라 승인
|
||||
대기, 아직 안 건드림). 우선순위 낮음 — M0를 막지 않고 코어 계약도 안
|
||||
건드림. 다만 **M3에서
|
||||
`Blocker`를 구현할 때 게이티드 노드를 공용으로 빼두는 것**만은 그
|
||||
시점에 해야 함(따로 하면 같은 설계를 두 번 함).
|
||||
- **중첩 State 평탄화 `State<State<T>>` → `State<T>`(2026-08-13 여섯 번째
|
||||
세션 신설, 백로그)** — **[근거 축소, 열네 번째 세션]** 원래 이 항목의
|
||||
주 근거는 "깊은 체인에선 힌트가 `nil`로 전달돼 깜빡임 방지가 꺼진다"는
|
||||
|
|
@ -181,7 +164,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`에게 되돌려지지도 않고
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ State 메소드로 두려던 초기 폼팩터가 기각된 경위만 여전히
|
|||
세 레포 모두 없다는 것 자체가 "quad도 굳이 안 만들어도 된다"는 정황.~~
|
||||
**[2026-08-14 뒤집힘]** 사용자가 직접 "`Blocker`와 유사하게 만들어야
|
||||
한다"고 지정 — 위 "빈 자리 아님" 판정은 더 이상 유효하지 않음.
|
||||
설계 초안은 `research/debounce-throttle-plan.md`. (원 판정의 근거였던
|
||||
설계는 `base/debounce-throttle-plan.md`(2026-08-19 전부 해소돼
|
||||
`research/`에서 승격). (원 판정의 근거였던
|
||||
"세 레포에 없다"는 관찰 자체는 사실이지만, 2026-08-12 열아홉 번째 세션의
|
||||
외부 리서치에서 RxJS/VueUse 등 Roblox 밖에선 가장 흔한 콤비네이터
|
||||
카테고리 중 하나로 확인돼 근거로서의 무게가 이미 약해져 있었음.)
|
||||
|
|
|
|||
|
|
@ -237,8 +237,9 @@ introspection 로직을 추가하는 거라 라이브러리 복잡도가 늘어
|
|||
모양을 벗어남 — `Operator.*`(quad-base, 엔진 무종속)에 넣을 수 있는 게
|
||||
아니라 quad-roblox 쪽 별도 프리미티브(Tween과 비슷한 위치)로 다뤄야 할
|
||||
가능성이 큼. 이 문서 범위 밖의 별도 설계 질문으로 분리해서 판단 필요.
|
||||
**[2026-08-14, 분리 완료]** 사용자 요청으로 `research/debounce-throttle-plan.md`
|
||||
신설 — 이 항목은 그 문서로 이관됐고 여기선 더 이상 다루지 않음. 그
|
||||
**[2026-08-14, 분리 완료, 2026-08-19 설계 전부 해소돼 `base/debounce-throttle-plan.md`로
|
||||
승격]** 사용자 요청으로 신설 — 이 항목은 그 문서로 이관됐고 여기선 더
|
||||
이상 다루지 않음. 그
|
||||
문서의 결론 두 가지만 여기 관련 있음: (1) 붙이는 모양은 이 카탈로그와
|
||||
똑같이 `factory(self)` + `:Apply`라 위 "왜 `:Apply`인가" 규칙이 그대로
|
||||
적용됨, (2) 다만 배치는 위 추측과 달리 **quad-base + 주입 op 2개**로
|
||||
|
|
|
|||
|
|
@ -1504,3 +1504,34 @@ 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.
|
||||
|
||||
## 2026-08-19 — `Debounce`/`Throttle` 마지막 판단 대기 4개 닫음, `base/`로 승격
|
||||
|
||||
원문: `session/2026-08-19-03-debounce-throttle-final-close.md`
|
||||
|
||||
`research/debounce-throttle-plan.md` 12절에 남아있던 이름/의미론/제어
|
||||
핸들/`Time=0`을 리뷰 — 이름은 유지+문서 경고로 사용자가 먼저 확정, 나머지
|
||||
셋은 논의를 거쳐 (A) emit-gate 채택(값-지연 (B)는 laziness와 상충해
|
||||
철회), 제어 핸들은 개별 `Ref` 아웃파라미터+전체 팩토리 브로드캐스트(weak
|
||||
레지스트리)로 수렴, `Time=0`은 허용으로 확정. 부수로 `Time`/`MaxTime`을
|
||||
`number|State<number>`로 확장(스케줄 시점에만 폴링, 구독 아님)도 결정.
|
||||
전부 닫히면서 문서가 `research/`에서 `base/`로 승격됐고, 제어 핸들 설계가
|
||||
`Blocker`의 gated state + `Ref` + 주입 op 2개 위에 전부 얹힌다는 게
|
||||
드러나 **순수 슈가로 재평가**(옛 "실제 기능 갭이라 우선순위 위" 서술
|
||||
철회). `ROADMAP.md`/`question.md`/`todos.md`/`README.md`/
|
||||
`source-state-plan.md`/`blocker-plan.md` 전량 반영.
|
||||
|
|
|
|||
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`도
|
||||
같이 갱신, 이 문서가 그 포인터.
|
||||
100
.claude/session/2026-08-19-03-debounce-throttle-final-close.md
Normal file
100
.claude/session/2026-08-19-03-debounce-throttle-final-close.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# 2026-08-19 — `Debounce`/`Throttle` 마지막 판단 대기 4개 닫음, `base/`로 승격
|
||||
|
||||
**요청**: `research/debounce-throttle-plan.md` 12절에 남아있던 판단 대기
|
||||
항목(이름/의미론/제어 핸들/`Time = 0`) 중 사용자가 이미 답을 준 이름(Q1)을
|
||||
빼고 나머지를 리뷰. "지금은 다른 에이전트들이 있어서 읽기만 해달라"는
|
||||
조건으로 시작 — 다른 에이전트들이 끝난 뒤 반영·핸드오버까지 진행.
|
||||
|
||||
## 1. 리뷰 준비 — 문서 재확인, 새 질문 발견
|
||||
|
||||
문서를 다시 읽는 과정에서 12절에 없던 새 항목이 나옴 — **`Time`/`MaxTime`을
|
||||
`State`로 받을 수 있는가**(5-2절은 당시 "plain 값만"으로 확정돼 있었음).
|
||||
사용자가 먼저 판단을 제시: *"1. 이름 -> Debounce Throttle 유지를 하는게
|
||||
나도 맞다고 봄. 관용 표현과 다르다는 경고로 넣기로 문서화 계획만 되도록
|
||||
해줘 + debounce-throttle 에 시간은 state 로 받을 수 있을지는 확인 필요한
|
||||
부분으로 보임."*
|
||||
|
||||
## 2. Time as State
|
||||
|
||||
사용자: *"Throttle 과 Debounce 는 state 중간이나 말단에 들어가서 emit 을
|
||||
제어하는거라, tween 과 완전 다름. 그리고 한번 바인딩 된다면 다시 Time
|
||||
같은걸 바꿀 수 없게되어버림. (…) 필요 시마다 get 해서 쓰긴 하는거야."*
|
||||
즉 구독이 아니라 **`setTimeout` 호출 시점에만 폴링**하자는 제안 — 검토
|
||||
결과 5-2절이 인용하던 `tween-plan.md`의 "옵션에 두 번째 반응 경로를
|
||||
안 만든다" 근거와 안 부딪힘(폴링은 새 무효화 채널이 아님). 후속 확인:
|
||||
*"이미 스캐쥴 한건 반영 안함. Animate 슈거랑 비슷함. 오직 setTimeout
|
||||
수행할 때 읽음. MaxTime 같은 다른것도 number|state<number> 주는거
|
||||
가능해보임."* → `Time`/`MaxTime` 둘 다 `number | State<number>` 허용,
|
||||
이미 스케줄된 타이머엔 미반영·다음 창부터 적용으로 확정.
|
||||
|
||||
## 3. Q2 — 의미론, (B) value-hold 철회
|
||||
|
||||
사용자: *"Get 을 지연된 값으로 만들어낸다 하면, 이전에 Compute 안 했던걸
|
||||
다시 컴퓨팅 하기 어려움. 리프 바인딩이라 가정하면 그냥 Blocker 동작이랑
|
||||
동일하는게 맞다고 생각하는데. (…) value-hold 라는걸 해야하는 순간부터,
|
||||
Get을 명시적으로 호출하고 들고 있어준다는게 되지 않느냐는것. (…) 이전에
|
||||
Get하지 않았다면 아주 이전 값일 가능성이 있는데. 이는 어떻게 제어하는가?"*
|
||||
|
||||
검토 결과 지적이 정확함을 확인 — (B)의 "held value" 계약은 직전 커밋에서
|
||||
`invalid = true`로 세팅된 채 아무도 안 읽다가 새 창이 열리는 경로에서
|
||||
조용히 깨지고, 이걸 고치려면 창이 열리는 순간 upstream을 강제로 pull해야
|
||||
해서 laziness와 정면 충돌(Throttle의 주 용례인 "비싼 연산 게이팅"에서
|
||||
치명적). **(A) emit-gate로 확정, (B) 철회.** 부수로 Q7(blocker-plan.md
|
||||
명확화)도 (A)를 택하면서 자동 소멸.
|
||||
|
||||
## 4. Q4 — 제어 핸들, 세 라운드 만에 수렴
|
||||
|
||||
1차 제안(에이전트): 게이트가 반환하는 State 자체에 `:Flush()`/`:Cancel()`을
|
||||
직접 붙임. 사용자 반박: *"State 가 확장되거나 해야함. 상태 따라 debounce
|
||||
에 대한 메서드를 실행할 수 있거나 없고. 타입이 나뉘거나 함. 차라리
|
||||
Debounce() 할 때, 디바운싱을 진짜 하는건 state 따로이지만, Flush Cancel
|
||||
은 한 핸들에서 수행 가능하고(마치 On/Off 처럼) 그게 전파 되는건 어떻게
|
||||
봄?"* — State 서브타입 분기 문제를 정확히 짚음.
|
||||
|
||||
2차 제안: `base/ref-plan.md`의 `Ref`(채워지길 기다리는 빈 박스) 패턴을
|
||||
재사용 — 옵션 필드로 `Handle = Ref()`를 받아 게이트 생성 시 채움. 사용자
|
||||
동의, 이어서 대칭 확장 질문: *"Debounce{} 결과에 :Flush 하면 있는 전체가
|
||||
플러싱되고, 하나만 하고 싶으면 Handler 를 넣어 쓰게 하면 되고,
|
||||
정확해보임."* → 개별은 `Ref` 아웃파라미터, 전체는 팩토리 자신의
|
||||
`:Flush()`/`:Cancel()`(모든 인스턴스에 브로드캐스트)로 최종 확정.
|
||||
|
||||
`:Apply()`를 계속 쓰는 이유도 확인: *"왜 Apply 일 이유가 있어? (…) 내부적으로
|
||||
Blocker 로 동작하기 때문임?"* — 두 근거 다 유효(① `Operator` 관용구가
|
||||
이미 "factory(self) + `:Apply`"로 확정, ② 내부적으로 `Blocker`와 같은
|
||||
gated state를 공유) 확인 후 동의.
|
||||
|
||||
전체 브로드캐스트의 구현 함정(weak 레지스트리 필요, GC 방해 방지) 지적에
|
||||
사용자 동의: *"1. 은 동의, weak 로 전부 가지고 있으면 돼."* `Cancel`도
|
||||
`Flush`와 대칭 포함 확정: *"Cancel도 대칭으로 포함해줘."*
|
||||
|
||||
## 5. Q8 — `Time = 0`
|
||||
|
||||
사용자: *"Q8 은 금지할 이유가 없어보이긴 함. 그냥 defer 될 수 있다고만
|
||||
알리면 별 문제 없음."* → 허용, "defer될 수 있음"만 문서화, 별도 이름/에러
|
||||
없음.
|
||||
|
||||
## 6. 부수 발견 — 순수 슈가로 귀결
|
||||
|
||||
사용자: *"이건 어쩌다 보니 사실상 슈거가 되었고, setTimeout 만 나중에
|
||||
진짜 quad-base 에 추가적으로 들어가는 부분이 될듯. 표면 상 아주 나중에
|
||||
구현되어도 괜찮아 보이는데, 지금 구현 상 base 승격에 문제 없어보임."*
|
||||
제어 핸들까지 확정되면서 새로 필요한 quad-base 코어 표면이 주입 op
|
||||
2개(`setTimeout`/`clearTimeout`)뿐임이 드러남 — 게이트는 `Blocker`가 이미
|
||||
확정한 gated state 위, 핸들은 이미 확정된 `Ref` 위에 전부 얹힘. 옛
|
||||
13절의 "순수 슈가가 아니라 실제 기능 갭이라 우선순위를 위로 둔다"는
|
||||
서술을 뒤집고, `Operator.*`와 같은 급의 후순위로 재평가.
|
||||
|
||||
## 7. 반영
|
||||
|
||||
*"다른 에이전트들 다 끝났어, 이제 반영해줘. 세션 기록도 남기고 핸드오버
|
||||
준비해줘. 끝나면 커밋하면 돼."*
|
||||
|
||||
`base/debounce-throttle-plan.md`(구 `research/debounce-throttle-plan.md`,
|
||||
`research/`에서 승격 — 4차 리뷰 배너, 4/5-2/5-3/5-4/7/9-1/12/13절 갱신,
|
||||
`DebounceHandle`/`Timeout` 등 타입 확장, 의사코드에 `readTime`/weak
|
||||
레지스트리/`Flush`·`Cancel` 반영), `ROADMAP.md`(백로그 항목 경로+서술
|
||||
갱신), `.claude/todos.md`(4번 항목), `.claude/question.md`(3번 항목
|
||||
전체 제거 — 전량 해소), `.claude/README.md`(research→base 테이블 이동),
|
||||
`base/source-state-plan.md`/`base/blocker-plan.md`(경로 참조 갱신)에
|
||||
반영. `archive/`·`session/`의 과거 경로 참조는 히스토리 문서라 그대로
|
||||
둠(`doc-check.py`가 파일명 기준 폴백 매칭이라 깨지지 않음).
|
||||
|
|
@ -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순위).
|
||||
|
|
@ -174,15 +178,17 @@
|
|||
(`debug-tooling-plan.md`/`documentation-plan.md`/
|
||||
`documentation-content-map.md`/`framework-comparison-findings.md`/
|
||||
`operator-sugar-plan.md`).
|
||||
**[2026-08-14 추가, 성격이 다름]** 시간 기반 전파 게이트
|
||||
`Debounce`/`Throttle`(`research/debounce-throttle-plan.md`)도 백로그이긴
|
||||
하나 위 항목들과 달리 **사용자가 직접 요청한 실제 기능 갭**이고 순수
|
||||
슈가가 아님 — M0/M3를 막지는 않지만, **M3에서 `Blocker`를 구현할 때
|
||||
게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 해야 함**(따로
|
||||
하면 같은 설계를 두 번 함). 주입 op 2개(`setTimeout`/`clearTimeout`)가
|
||||
백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 설계는
|
||||
네 라운드로 대부분 확정됐고 남은 열린 질문은 `question.md` 3번(개수는
|
||||
거기도 반복 안 함 — 소스는 `research/debounce-throttle-plan.md` 12절).
|
||||
**[2026-08-14 추가, 2026-08-19 설계 전부 해소 후 `base/`로 승격]** 시간
|
||||
기반 전파 게이트 `Debounce`/`Throttle`(`base/debounce-throttle-plan.md`)도
|
||||
백로그이지만 위 항목들과는 발단이 다름 — **사용자가 직접 요청한 실제
|
||||
기능 갭**에서 시작됨(그 문서 13절). 다만 제어 핸들 설계까지 닫히고 나니
|
||||
실제로 quad-base에 새 코어 메커니즘을 추가하지 않는 **순수 슈가**로
|
||||
확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M3를 막지
|
||||
않고, **M3에서 `Blocker`를 구현할 때 게이티드 노드를 공용 `Gate`로
|
||||
빼두는 것만은 그 시점에 해야 함**(따로 하면 같은 설계를 두 번 함).
|
||||
주입 op 2개(`setTimeout`/`clearTimeout`)가 백엔드 팩토리 표면에
|
||||
추가될 예정이라는 것도 M1 설계 시 인지. 남은 열린 질문 없음(구
|
||||
`question.md` 3번, 전량 해소로 항목 자체가 빠짐).
|
||||
**[2026-08-18 추가]** 사용자 아이디어 메모 두 건도 같은 성격의 백로그로
|
||||
신설 — 스크롤 최적화 외부 유틸 `quad-roblox-fastscroll`
|
||||
(`research/fastscroll-plan.md`, 선행으로 `Visible=false`일 때
|
||||
|
|
|
|||
31
ROADMAP.md
31
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` 배열
|
||||
|
|
@ -907,10 +909,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
테스트는 2026-08-13 세 번째 세션에 불필요로 해소됨 —
|
||||
`archive/question-resolved.md` 참고, v2엔 대응 개념 자체가 없음)
|
||||
- [ ] Slot 형제 순서 보장(다중 백엔드 관점) — Roblox만이면 급하지 않음
|
||||
- [ ] **[2026-08-14 신설]** 시간 기반 전파 게이트 `Debounce`/`Throttle`
|
||||
(`research/debounce-throttle-plan.md`) — **M3에서 `Blocker`를 구현할
|
||||
때 게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 할 것**
|
||||
(둘이 같은 노드를 공유하므로 따로 하면 같은 설계를 두 번 함).
|
||||
- [ ] **[2026-08-14 신설, 2026-08-19 설계 전부 해소 후 `base/`로 승격]**
|
||||
시간 기반 전파 게이트 `Debounce`/`Throttle`(`base/debounce-throttle-plan.md`)
|
||||
— 제어 핸들 설계까지 닫히면서 quad-base에 새 코어 메커니즘을
|
||||
추가하지 않는 **순수 슈가**로 확인됨(`Blocker`의 gated state + `Ref` +
|
||||
아래 주입 op 2개 위에 전부 얹힘, 그 문서 13절). **M3에서 `Blocker`를
|
||||
구현할 때 게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 할
|
||||
것**(둘이 같은 노드를 공유하므로 따로 하면 같은 설계를 두 번 함).
|
||||
프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M3를 막지 않음.
|
||||
주입 op 2개(`setTimeout(func, delay) -> Timeout` / `clearTimeout`,
|
||||
Roblox는 `task.delay`/`task.cancel`로 배선 — **인자 순서가 반대라
|
||||
|
|
|
|||
Loading…
Reference in a new issue