design: Debounce/Throttle 마지막 판단 대기 4개 닫고 base/로 승격
의미론은 (A) emit-gate로 확정(value-hold 안은 laziness와 상충해 철회), 제어 핸들은 개별 Ref 아웃파라미터 + 전체 팩토리 브로드캐스트(weak 레지스트리)로 수렴, Time/MaxTime은 number|State<number> 허용(스케줄 시점에만 폴링), 이름은 Debounce/Throttle 유지로 확정. 전부 닫히면서 quad-base에 새 코어 메커니즘을 안 더하는 순수 슈가로 재평가됨(Blocker의 gated state + Ref + 주입 op 2개 위에 전부 얹힘) — 우선순위 서술도 갱신. Co-authored-by: qwreey <me@qwreey.moe>
This commit is contained in:
parent
2348ea8058
commit
14f733dde1
11 changed files with 474 additions and 186 deletions
|
|
@ -56,6 +56,7 @@
|
||||||
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 |
|
| `purity-and-effects-plan.md` | 컴포넌트 "순수성"이 아니라 "이식성" 문제로 재정의 — 문서 경고 수준으로 확정 |
|
||||||
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 |
|
| `component-composition-plan.md` | 컴포넌트=플레인 함수, State/Source 읽기·쓰기 경계, Source가 State를 구조적으로 만족 — modifier/Ref 컴포넌트 경계 통과까지 전부 확정, 남은 건 API 이름뿐. **[2026-08-07 정리]** 폐기된 `StoreSource` 프록시 설계로의 역전 이력은 본문에서 빼고 `archive/store-source-proxy-reversed.md` 포인터로 압축 |
|
||||||
| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤(M3)과 함께 개발. 메커니즘+이름 확정. **[2026-08-18 구현 전 QA 2라운드 후속]** `IsOn()`/`OffWithoutEmit()` 신설(`RC-1` 해결 과정에서 나옴) — `state:Block()` 없이 Blocker를 직접 쓰는 두 번째 용례(base 내부 Length/Offset 배치 게이팅)도 추가. **[2026-08-18 구현 전 QA 3라운드]** 이 용례의 존재 이유 정정 — `RC-1`의 원래 크래시는 사라졌고(`bk.N` 수명주기 재정의로), 지금 필요한 이유는 배치 등록 비용(O(N²)→O(N)) |
|
| `blocker-plan.md` | **[2026-08-07 신설]** `Blocker` — 여러 Source를 한꺼번에 바꿔도 파생값 재계산이 한 번만 되게, State 마일스톤(M3)과 함께 개발. 메커니즘+이름 확정. **[2026-08-18 구현 전 QA 2라운드 후속]** `IsOn()`/`OffWithoutEmit()` 신설(`RC-1` 해결 과정에서 나옴) — `state:Block()` 없이 Blocker를 직접 쓰는 두 번째 용례(base 내부 Length/Offset 배치 게이팅)도 추가. **[2026-08-18 구현 전 QA 3라운드]** 이 용례의 존재 이유 정정 — `RC-1`의 원래 크래시는 사라졌고(`bk.N` 수명주기 재정의로), 지금 필요한 이유는 배치 등록 비용(O(N²)→O(N)) |
|
||||||
|
| `debounce-throttle-plan.md` | **[2026-08-14 신설, 2026-08-19 전부 해소돼 `research/`에서 승격]** 시간 기반 전파 게이트 `Debounce`/`Throttle` — 사용자 요청("`Blocker`와 유사하게")으로 신설. 요지: (1) `Blocker`가 이미 쓰는 게이트 노드의 **릴리스 트리거만 타이머로 바꾼 것**이라 새 전파 메커니즘이 아님, (2) 무효화 채널만 만지므로 laziness 안 깨짐, (3) **Debounce/Throttle의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐** — 공개 생성자는 둘, 구현은 하나, (4) 알고리즘은 quad-base + 주입 op 2개 `setTimeout(func, delay) -> Timeout`/`clearTimeout`(Roblox `task.delay`/`task.cancel`로 배선 — **인자 순서 반대라 주의**), `Timeout`은 `{ __type_timeout: true, _native: any }`. **[2026-08-19 마지막 라운드]** 의미론은 **(A) emit-gate**(`Blocker`와 동일, `:Get()`은 항상 최신값)로 확정 — 검토했던 값-지연 안은 laziness와 상충해 철회. 제어 핸들은 개별은 `Ref` 아웃파라미터·전체는 팩토리 자체의 `:Flush()`/`:Cancel()`(weak 레지스트리)로 확정, `Time`/`MaxTime`은 `number \| State<number>`(스케줄 시점에만 폴링) 허용. 이름은 `Debounce`/`Throttle` 유지 + Roblox 관용 "debounce"와 다르다는 문서 경고. **결과적으로 quad-base에 새 코어 메커니즘을 안 더하는 순수 슈가로 귀결**(`Blocker`의 gated state + `Ref` + 주입 op 2개 위에 전부 얹힘) — 우선순위는 `Operator.*`와 같은 급으로 재평가됨. **부수 성과**: 이 설계 중 `source-state-plan.md`의 무효화 dedup 서술이 `Observer` 계약과 모순되는 게 발견돼 base 전면 정정(`archive/invalidate-dedup-propagation-reversed.md`) |
|
||||||
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, state?)` — `state` 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음(그 dedup 경로의 process/retract 대칭은 **미확인**, M3 착수 전 확인) |
|
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, state?)` — `state` 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료. **[2026-08-18 구현 전 QA 반영]** **`:Unsubscribe()`는 `:Subscribe()`의 짝으로 축소** — leaf 바인딩 경로에서 cleanup을 앞당기면 dedup 때문에 재바인딩이 안 일어나 Effect가 조용히 죽음(그 dedup 경로의 process/retract 대칭은 **미확인**, M3 착수 전 확인) |
|
||||||
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님). **[2026-08-14 세션] Tween 지원 추가** — 자식 프로퍼티를 직접 대입하지 않고 `Dispatch.process(child, prop, ..., 1)`로 위임하는 것으로 확정(프로세스 중 `inst`를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님, `dispatch-core-plan.md`에 일반 규칙으로 명문화) — Tween 해석 코드가 `PropertyHandler` 하나에만 남는다는 불변식이 유지되고, 이 문서가 새로 정할 건 스칼라→프로퍼티 `wrap`을 `Tween<T>.Value`에만 적용되도록 들어올리는 헬퍼 하나뿐. 옛 "트윈까지 지원할 필요 없음" 서술은 역전됨(그때는 Tween이 독립 Dispatch 핸들러였음). ROADMAP M10에 빠져 있던 체크리스트 항목도 이 세션에 보강. **[2026-08-18 구현 전 QA 반영]** 만든 자식을 다시 찾을 때 **`FindFirstChild` 대신 `Relate` 저장**(이름은 표시·판정용, 릴레이션은 조회용), 자식 프로퍼티 세팅도 `Dispatch.process`로 위임해 Tween이 공짜로 따라오게 |
|
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님). **[2026-08-14 세션] Tween 지원 추가** — 자식 프로퍼티를 직접 대입하지 않고 `Dispatch.process(child, prop, ..., 1)`로 위임하는 것으로 확정(프로세스 중 `inst`를 바꾸는 건 키를 바꾸는 것과 같은 층위라 UB 아님, `dispatch-core-plan.md`에 일반 규칙으로 명문화) — Tween 해석 코드가 `PropertyHandler` 하나에만 남는다는 불변식이 유지되고, 이 문서가 새로 정할 건 스칼라→프로퍼티 `wrap`을 `Tween<T>.Value`에만 적용되도록 들어올리는 헬퍼 하나뿐. 옛 "트윈까지 지원할 필요 없음" 서술은 역전됨(그때는 Tween이 독립 Dispatch 핸들러였음). ROADMAP M10에 빠져 있던 체크리스트 항목도 이 세션에 보강. **[2026-08-18 구현 전 QA 반영]** 만든 자식을 다시 찾을 때 **`FindFirstChild` 대신 `Relate` 저장**(이름은 표시·판정용, 릴레이션은 조회용), 자식 프로퍼티 세팅도 `Dispatch.process`로 위임해 Tween이 공짜로 따라오게 |
|
||||||
| `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계, 2026-08-12 열한 번째 세션 메커니즘 정정]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. `retract`가 이전 Tag가 걸었던 이름을 이름별 참조 카운트 맵에서 빼고(다른 위치가 겹쳐 쓰면 실제 `RemoveTag`는 skip), `process`가 새 Tag의 이름을 등록 — 여러 위치가 같은 이름을 겹쳐 가져도(웹 `className`류 합집합) 안전. 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md`, 구 `assert(v==nil)` 메커니즘은 `archive/retract-always-fires-reversed.md`. **[2026-08-12 열다섯 번째 세션]** `Added`/`Removed`가 vararg가 아니라 `string | {string}`으로 정정 — `table.unpack`이 인자 목록 tail 위치에서만 완전히 펼쳐지는 Lua 문법 제약 때문에 여러 개의 독립된 동적 이름 테이블을 한 vararg 호출로 못 합치는 경우가 생김이 발견됨, `Tag(...)` 생성자 자체는 정적 리터럴 호출이라 vararg 유지. **[2026-08-13 세션]** 참조 카운트 `holders`가 Tag 객체 identity로 키잉돼 있어서 같은 Tag 객체를 여러 위치에서 재사용하면(immutable이라 흔한 관례) 한 위치만 retract돼도 다른 위치가 쓰는 태그가 지워지는 실제 버그 발견·수정 — holders를 위치(`k`) 기준으로 재키잉, `oldv==newv`면 retract 스킵하는 최적화도 추가. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `TagHandler.process`가 자기 retract 클로저를 반환하는 계약으로 전환되며 `kTagMap`(위치별 마지막 Tag)이 완전히 불필요해짐(클로저가 `v`를 직접 캡처) — `tagNameMap`(이름별 위치 집합)만 남음, `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고 **[2026-08-13 열네 번째 세션]** 하강 diff 반영(`isTag(hintValue)` 방어 가드 폐지 — 클로저 인자의 타입이 계약으로 보장됨, 깜빡임 방지가 깊은 체인에서도 유지) + **패키지 재배치**(참조 카운트 Handler까지 quad-base, 백엔드는 `addTag`/`removeTag(inst, {string})`만 주입 — 웹 `className` 대응 때문에, vararg 아닌 테이블인 이유는 `Tag:Added`와 동일) |
|
| `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계, 2026-08-12 열한 번째 세션 메커니즘 정정]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. `retract`가 이전 Tag가 걸었던 이름을 이름별 참조 카운트 맵에서 빼고(다른 위치가 겹쳐 쓰면 실제 `RemoveTag`는 skip), `process`가 새 Tag의 이름을 등록 — 여러 위치가 같은 이름을 겹쳐 가져도(웹 `className`류 합집합) 안전. 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md`, 구 `assert(v==nil)` 메커니즘은 `archive/retract-always-fires-reversed.md`. **[2026-08-12 열다섯 번째 세션]** `Added`/`Removed`가 vararg가 아니라 `string | {string}`으로 정정 — `table.unpack`이 인자 목록 tail 위치에서만 완전히 펼쳐지는 Lua 문법 제약 때문에 여러 개의 독립된 동적 이름 테이블을 한 vararg 호출로 못 합치는 경우가 생김이 발견됨, `Tag(...)` 생성자 자체는 정적 리터럴 호출이라 vararg 유지. **[2026-08-13 세션]** 참조 카운트 `holders`가 Tag 객체 identity로 키잉돼 있어서 같은 Tag 객체를 여러 위치에서 재사용하면(immutable이라 흔한 관례) 한 위치만 retract돼도 다른 위치가 쓰는 태그가 지워지는 실제 버그 발견·수정 — holders를 위치(`k`) 기준으로 재키잉, `oldv==newv`면 retract 스킵하는 최적화도 추가. **[2026-08-13 세션, 다섯 번째, 전면 반영]** `TagHandler.process`가 자기 retract 클로저를 반환하는 계약으로 전환되며 `kTagMap`(위치별 마지막 Tag)이 완전히 불필요해짐(클로저가 `v`를 직접 캡처) — `tagNameMap`(이름별 위치 집합)만 남음, `base/dispatch-core-plan.md` "Dispatch 체인" 절 참고 **[2026-08-13 열네 번째 세션]** 하강 diff 반영(`isTag(hintValue)` 방어 가드 폐지 — 클로저 인자의 타입이 계약으로 보장됨, 깜빡임 방지가 깊은 체인에서도 유지) + **패키지 재배치**(참조 카운트 Handler까지 quad-base, 백엔드는 `addTag`/`removeTag(inst, {string})`만 주입 — 웹 `className` 대응 때문에, vararg 아닌 테이블인 이유는 `Tag:Added`와 동일) |
|
||||||
|
|
@ -87,8 +88,7 @@
|
||||||
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 못 고치는 트레이드오프(의도된 설계) 정리. **[2026-08-12 열여덟 번째 세션]** "고칠 만한 것" 절에 남아있던 마지막 두 항목(use-after-destroy 검증 안전망 부재, `:With` 동적 의존성 미지원)도 사용자가 "고칠 필요 없음"으로 최종 판단해 3번 절(못 고치는 트레이드오프)로 이전 — 2번 절은 이제 해소된 항목만 남음 | 하 — 배경 자료, 더 이상 사용자 판단 대기 상태 아님 |
|
| `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 못 고치는 트레이드오프(의도된 설계) 정리. **[2026-08-12 열여덟 번째 세션]** "고칠 만한 것" 절에 남아있던 마지막 두 항목(use-after-destroy 검증 안전망 부재, `:With` 동적 의존성 미지원)도 사용자가 "고칠 필요 없음"으로 최종 판단해 3번 절(못 고치는 트레이드오프)로 이전 — 2번 절은 이제 해소된 항목만 남음 | 하 — 배경 자료, 더 이상 사용자 판단 대기 상태 아님 |
|
||||||
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
| `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 |
|
||||||
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 |
|
| `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 |
|
||||||
| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리, `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 **[2026-08-13 여섯 번째 세션]** `State<State<T>|T>` → `State<T>` 평탄화 항목 신설(백로그) — `State<State<T>>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 |
|
| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리(**[2026-08-19]** 그 질문은 `base/debounce-throttle-plan.md`로 전부 해소·승격 완료), `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 **[2026-08-13 여섯 번째 세션]** `State<State<T>|T>` → `State<T>` 평탄화 항목 신설(백로그) — `State<State<T>>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 |
|
||||||
| `debounce-throttle-plan.md` | **[2026-08-14 신설]** 시간 기반 전파 게이트 `Debounce`/`Throttle` — 사용자 요청("`Blocker`와 유사하게")으로 신설. 요지: (1) `Blocker`가 이미 쓰는 게이트 노드의 **릴리스 트리거만 타이머로 바꾼 것**이라 새 전파 메커니즘이 아님 — `Blocker` 구현(M3) 시점에 게이트를 공용으로 빼두는 게 쌈, (2) 무효화 채널만 만지므로 laziness 안 깨짐, (3) **Debounce/Throttle의 차이는 "신호가 창 타이머를 리셋하는가" 한 비트뿐** — 공개 생성자는 둘, 구현은 하나(초안의 lodash식 `maxWait` 공식엔 trailing 통과 직후 이중 발화 버그가 있었음), (4) 알고리즘은 quad-base + 주입 op 2개 `setTimeout(func, delay) -> Timeout`/`clearTimeout`(Roblox `task.delay`/`task.cancel`로 배선 — **인자 순서 반대라 주의**). `os.clock()`은 Luau 표준 라이브러리라 주입 대상 아님(단 절대 시각이 아니라 **diff 전용**), 취소 없는 엔진도 래핑+유효 플래그로 대응 가능, `Timeout`은 `{ __type_timeout: true, _native: any }`. **부수 성과**: 이 설계 중 `source-state-plan.md`의 무효화 dedup 서술이 `Observer` 계약과 모순되는 게 발견돼 base 전면 정정(`archive/invalidate-dedup-propagation-reversed.md`) | 하 — M0 안 막음(코어 계약 변경 없음), 다만 순수 슈가가 아니라 실제 기능 갭이라 `operator-sugar-plan.md`보다는 위. 의존은 M3(State)+백엔드 주입. 남은 열린 질문: 이름(Roblox 관용 debounce와 충돌)/값 지연 의미론/제어 핸들 `Flush`/`Time=0` 허용 |
|
|
||||||
| `quad-recursive-acronym.md` | **[2026-08-14 신설]** GNU/WINE류로 `Quad`를 재귀 약어화하는 카피 브레인스토밍 — 설계 결정도 착수 게이팅도 아니고 나중에 README.md 헤딩 등에 쓸 캐치프레이즈 후보 모음. 자학 개그 방향(기각)과 지연평가/재귀·커링/펑터/클로저를 자랑하는 방향(채택 후보, 미확정) 정리 | 하 — 카피 소재, 설계 상의 필요 없음. 사용자가 최종 문구 고르면 반영 |
|
| `quad-recursive-acronym.md` | **[2026-08-14 신설]** GNU/WINE류로 `Quad`를 재귀 약어화하는 카피 브레인스토밍 — 설계 결정도 착수 게이팅도 아니고 나중에 README.md 헤딩 등에 쓸 캐치프레이즈 후보 모음. 자학 개그 방향(기각)과 지연평가/재귀·커링/펑터/클로저를 자랑하는 방향(채택 후보, 미확정) 정리 | 하 — 카피 소재, 설계 상의 필요 없음. 사용자가 최종 문구 고르면 반영 |
|
||||||
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
|
| `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 |
|
||||||
| `doc-include-plan.md` | **[2026-08-14 신설]** 문서 stale 감소용 include 도구 `doc-include.py`(가칭) — 원본 파일에 `<!--#summary-->` 류 마커로 요약 구간을 표시해두면 인용하는 문서가 그 구간을 기계적으로 추출해 붙여넣게 하는 도구. `doc-check.py`(사후 탐지)와 짝을 이루는 사전 차단 장치. AsciiDoc tagged include/markdown-magic이 선례, build vs buy 검토 후 Python 표준 라이브러리로 직접 제작(~100줄) 채택. 파일럿은 `.claude/session-summary.md` ← `.claude/session/*.md` 요약 마커부터(CLAUDE.md 분할로 목적지가 "통째로 생성되는 파일"이 돼 단방향 생성으로 단순화됨) | 하 — M0/설계 게이트와 무관한 메타 도구. **[2026-08-16 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) |
|
| `doc-include-plan.md` | **[2026-08-14 신설]** 문서 stale 감소용 include 도구 `doc-include.py`(가칭) — 원본 파일에 `<!--#summary-->` 류 마커로 요약 구간을 표시해두면 인용하는 문서가 그 구간을 기계적으로 추출해 붙여넣게 하는 도구. `doc-check.py`(사후 탐지)와 짝을 이루는 사전 차단 장치. AsciiDoc tagged include/markdown-magic이 선례, build vs buy 검토 후 Python 표준 라이브러리로 직접 제작(~100줄) 채택. 파일럿은 `.claude/session-summary.md` ← `.claude/session/*.md` 요약 마커부터(CLAUDE.md 분할로 목적지가 "통째로 생성되는 파일"이 돼 단방향 생성으로 단순화됨) | 하 — M0/설계 게이트와 무관한 메타 도구. **[2026-08-16 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) |
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ State와 같은 마일스톤(`ROADMAP.md` M3)에서 함께 구현할 것.
|
||||||
지연시킬 수 있는 유일한 요소**임. 평범한 State는 신호를 받으면 자기
|
지연시킬 수 있는 유일한 요소**임. 평범한 State는 신호를 받으면 자기
|
||||||
`invalid` 상태와 무관하게 **항상** 아래로 전파하고(`base/source-state-plan.md`
|
`invalid` 상태와 무관하게 **항상** 아래로 전파하고(`base/source-state-plan.md`
|
||||||
"전파 모델 확정" 절), 그 흐름을 붙잡아둘 수 있는 건 명시적으로 배선된
|
"전파 모델 확정" 절), 그 흐름을 붙잡아둘 수 있는 건 명시적으로 배선된
|
||||||
게이트뿐 — 지금은 `Blocker`가 유일하고, 시간 기반 게이트(`research/
|
게이트뿐 — 지금은 `Blocker`가 유일하고, 시간 기반 게이트(`base/
|
||||||
debounce-throttle-plan.md`)가 추가되면 같은 자리에 들어옴. 이걸 못 박아
|
debounce-throttle-plan.md`, 설계 확정·구현은 아직)가 추가되면 같은 자리에
|
||||||
|
들어옴. 이걸 못 박아
|
||||||
두는 이유: 과거에 "이미 `invalid`면 전파를 멈춘다"는 서술이 base에
|
두는 이유: 과거에 "이미 `invalid`면 전파를 멈춘다"는 서술이 base에
|
||||||
있었고, 그건 사실상 `Blocker`가 하는 일을 모든 State에 암묵적으로 심는
|
있었고, 그건 사실상 `Blocker`가 하는 일을 모든 State에 암묵적으로 심는
|
||||||
것이라 `Blocker`의 존재 의의를 반쯤 지워버렸음(역전 경위는
|
것이라 `Blocker`의 존재 의의를 반쯤 지워버렸음(역전 경위는
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
# Debounce / Throttle — 시간 기반 전파 게이트
|
# Debounce / Throttle — 시간 기반 전파 게이트
|
||||||
|
|
||||||
**상태**: research — **[2026-08-14 신설]** 사용자 요청("`Blocker`와 유사하게
|
**상태**: base — **[2026-08-19 세션, 전부 해소되어 `research/`에서 승격]**
|
||||||
Debounce/Throttle를 만들어야 한다")으로 신설. 여기 적힌 건 **에이전트가
|
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 동작을 짚어준
|
> **[2026-08-14 1차 리뷰 반영]** 사용자가 스로틀의 trailing 동작을 짚어준
|
||||||
> 뒤 세 가지가 바뀜: (1) **Q5(패키지 경계) 해소** — quad-base + 엔진별
|
> 뒤 세 가지가 바뀜: (1) **Q5(패키지 경계) 해소** — quad-base + 엔진별
|
||||||
|
|
@ -33,6 +41,24 @@ Debounce/Throttle를 만들어야 한다")으로 신설. 여기 적힌 건 **에
|
||||||
> **`base/source-state-plan.md`의 무효화 dedup 문장이 확정된 `Observer`
|
> **`base/source-state-plan.md`의 무효화 dedup 문장이 확정된 `Observer`
|
||||||
> 계약과 모순**되고 `base/architecture.md`와도 어긋난다는 게 드러나,
|
> 계약과 모순**되고 `base/architecture.md`와도 어긋난다는 게 드러나,
|
||||||
> base 정정 항목(Q10)이 새로 생김.
|
> 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.*` 카탈로그 밖의 별도
|
`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`를 즉시 세우되 아래로 전파만 미룸. 창이 열려 있는
|
게이트는 자기 `invalid`를 즉시 세우되 아래로 전파만 미룸. 창이 열려 있는
|
||||||
동안 누가 `debounced:Get()`하면 **최신값**이 나옴.
|
동안 누가 `debounced:Get()`하면 **최신값**이 나옴 — `Blocker`의 gated
|
||||||
|
state와 글자 그대로 같은 동작.
|
||||||
|
|
||||||
### (B) value-hold — 값 자체가 지연됨 (권장)
|
### ~~(B) value-hold — 값 자체가 지연됨~~ — **철회됨, laziness와 상충**
|
||||||
|
|
||||||
게이트는 창이 닫혀 있는 동안 **자기를 invalid로 만들지 않음**. 즉 캐시된
|
게이트가 창이 닫혀 있는 동안 자기를 invalid로 만들지 않고 캐시된 직전
|
||||||
직전 값을 계속 들고 있고, `debounced:Get()`은 **지연된 값**을 반환. 타이머가
|
값을 들고 있다가, `debounced:Get()`이 **지연된 값**을 반환하게 하려던
|
||||||
커밋할 때 비로소 invalid + 전파.
|
안. 업계 선례(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의 *값 자체*가
|
부수로, (B)를 지지했던 업계 선례(VueUse/RxJS)도 재검토하면 그대로 옮겨올
|
||||||
늦게 따라오고, RxJS `debounceTime`도 값의 방출을 미룸. "debounce된
|
근거가 약함 — 둘 다 push/eager 모델(Vue reactivity의 effect는 즉시 도는
|
||||||
상태"를 다른 곳에서 읽었을 때 debounce 안 된 값이 나오면 놀람.
|
push, RxJS는 애초에 eager push 스트림)이라 "값이 지연된다"가 공짜로
|
||||||
2. **`Blocker`와 역할이 깔끔히 갈림.** `Blocker`는 "지금 중간 상태니까
|
성립하는 세계고, quad처럼 **pull-lazy가 원칙**인 곳엔 그대로 안 맞는
|
||||||
구독자에게 알리지 않을 뿐, 진실은 이미 새 값"이고, Debounce는 "이
|
선례였음.
|
||||||
노드의 값은 정의상 늦게 따라오는 값"임. 후자를 (A)로 만들면 두 도구가
|
|
||||||
같은 자리에서 미묘하게만 다른 애매한 물건이 됨.
|
|
||||||
3. **확정된 원칙과 충돌하지 않음.** `base/blocker-plan.md`가 인용하는
|
|
||||||
"`Get()`은 라이브 레퍼런스를 준다" 원칙은 `base/source-state-plan.md`에서
|
|
||||||
실제로는 **레퍼런스 의미론**(테이블을 복사본이 아니라 라이브 참조로
|
|
||||||
준다, 그래서 `Get()` 결과를 캐시해두고 `==` 비교하면 안 됨)에 대한
|
|
||||||
것이지 "항상 상류 최신값"이라는 뜻이 아님. 게이트 노드의 *자기* 값이
|
|
||||||
지연된 값이라면 `:Get()`은 여전히 그 노드의 라이브 값을 주는 것.
|
|
||||||
→ 다만 blocker-plan의 그 한 줄이 후자로 읽힐 여지가 있어, 확정되면
|
|
||||||
그 문장에 한 줄 명확화를 넣는 게 좋음(열린 질문 Q7).
|
|
||||||
|
|
||||||
**(B)의 캐비엇 하나**: 노드가 아직 한 번도 계산된 적 없으면(캐시 없음)
|
**(A) 채택의 부수 효과 — Q7 소멸**: (A)는 `Blocker`의 gated state와
|
||||||
붙잡고 있을 "이전 값"이 없어서 첫 `:Get()`은 그냥 최신값을 계산함.
|
완전히 같은 메커니즘이라, `base/blocker-plan.md`가 이미 쓰고 있는
|
||||||
"지연은 두 번째 값부터"라는 뜻 — 자연스럽고 무해하지만 문서에 명시할 것.
|
"`Get()`은 라이브 레퍼런스를 준다" 문구가 그대로 맞고 별도 명확화가
|
||||||
|
필요 없음(옛 Q7이 걱정했던 모순은 (B)를 택했을 때만 생기는 문제였음).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -383,24 +414,35 @@ local sampled = mousePos:Apply(Throttle{Time = 1 / 30})
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
type DebounceOptions = {
|
type DebounceOptions = {
|
||||||
Time: number, -- 필수. 창 길이(초) — 신호마다 리셋됨
|
Time: number | State<number>, -- 필수. 창 길이(초) — 신호마다 리셋됨
|
||||||
Leading: boolean?, -- 기본 false. 버스트의 첫 신호를 즉시 통과시킬지
|
Leading: boolean?, -- 기본 false. 버스트의 첫 신호를 즉시 통과시킬지
|
||||||
Trailing: boolean?, -- 기본 true. 조용해진 뒤 한 번 통과시킬지
|
Trailing: boolean?, -- 기본 true. 조용해진 뒤 한 번 통과시킬지
|
||||||
MaxTime: number?, -- 기본 nil. 신호가 안 끊겨도 최대 이 간격마다 강제 통과
|
MaxTime: (number | State<number>)?, -- 기본 nil. 신호가 안 끊겨도 최대 이 간격마다 강제 통과
|
||||||
|
Handle: Ref<GateHandle>?, -- 기본 nil. 이 인스턴스 전용 제어 핸들(5-4절)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThrottleOptions = {
|
type ThrottleOptions = {
|
||||||
Time: number, -- 필수. 창 길이(초) — 신호가 리셋하지 못함
|
Time: number | State<number>, -- 필수. 창 길이(초) — 신호가 리셋하지 못함
|
||||||
Leading: boolean?, -- 기본 true. 창 밖 첫 신호를 즉시 통과시킬지
|
Leading: boolean?, -- 기본 true. 창 밖 첫 신호를 즉시 통과시킬지
|
||||||
Trailing: boolean?, -- 기본 true. 창 안에 눌러둔 게 있으면 창 끝에 통과시킬지
|
Trailing: boolean?, -- 기본 true. 창 안에 눌러둔 게 있으면 창 끝에 통과시킬지
|
||||||
|
Handle: Ref<GateHandle>?, -- 기본 nil. 이 인스턴스 전용 제어 핸들(5-4절)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **모든 필드는 plain 값만** — `State`를 못 받음. `base/tween-plan.md`의
|
- **[2026-08-19 확정] `Time`/`MaxTime`은 `number | State<number>` 허용** —
|
||||||
"옵션 값 모양" 절이 `Tween{...}`에 대해 확정한 것과 같은 규칙, 같은
|
`Leading`/`Trailing`은 여전히 plain만(정적 정책값이라 반응형일 이유가
|
||||||
이유(옵션 안에 두 번째 반응 경로를 만들지 않음). `Time`을 반응형으로
|
없음). `base/tween-plan.md`의 "옵션 값 모양" 절이 든 "옵션 안에 두 번째
|
||||||
바꾸고 싶으면 `state:Apply(...)` 자체를 다시 만드는 상위 구조가 필요한데,
|
반응 경로를 만들지 않음" 근거는 여기 안 부딪힘 — Debounce/Throttle은
|
||||||
그건 이 프리미티브가 아니라 `State<State<T>>` 영역.
|
`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 = true, Trailing = false` → "버스트 시작에 한 번만"
|
||||||
- `Leading = false, Trailing = true` → 기본값, 일반적인 debounce
|
- `Leading = false, Trailing = true` → 기본값, 일반적인 debounce
|
||||||
- 둘 다 `false`는 아무 일도 안 하는 설정 → **즉시 error** 권장(`Slot:Add`의
|
- 둘 다 `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`)은 아직
|
> **주의**: `Gate` 노드의 내부 훅(`onUpstreamSignal`, `commit`)은 아직
|
||||||
> 이름도 계약도 확정되지 않은 가칭 — `Blocker`의 게이티드 노드가 이미
|
> 이름도 계약도 확정되지 않은 가칭 — `Blocker`의 게이티드 노드가 이미
|
||||||
> 필요로 하는 것과 같은 훅이라(1절), 실제 구현 시엔 그쪽과 같이 정의할 것.
|
> 필요로 하는 것과 같은 훅이라(1절), 실제 구현 시엔 그쪽과 같이 정의할 것.
|
||||||
|
> 아래 코드의 `registry`/`Handle` 배선도 마찬가지로 스케치 수준 — 실제
|
||||||
|
> 구현 시 이름은 자유.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
-- quad-base — 공용 코어. Reset 한 비트가 Debounce/Throttle을 가름(5-3절).
|
-- 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)
|
local function makeGate(reset: boolean, opts)
|
||||||
if opts.Leading == false and opts.Trailing == false then
|
if opts.Leading == false and opts.Trailing == false then
|
||||||
error("Leading/Trailing 둘 다 false면 아무것도 통과하지 않음")
|
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 leading = if reset then opts.Leading == true else opts.Leading ~= false
|
||||||
local trailing = opts.Trailing ~= false
|
local trailing = opts.Trailing ~= false
|
||||||
|
|
||||||
-- 팩토리 — :Apply가 state마다 한 번씩 호출하므로 아래 지역 상태는
|
-- 팩토리 레벨 상태 — makeGate 호출(=Debounce{...}/Throttle{...} 한 번)당 하나.
|
||||||
-- 게이트 노드 하나당 하나씩 새로 생김(팩토리 재사용해도 공유 안 됨)
|
-- weak 레지스트리라 여기 등록돼도 게이트의 GC를 막지 않음(5-4절).
|
||||||
return function(self)
|
local instances = setmetatable({}, {__mode = "k"})
|
||||||
local gate = Gate(self) -- Blocker가 쓰는 것과 같은 게이트 노드
|
|
||||||
local pending = false -- 창 안에서 상류 신호가 있었는가
|
|
||||||
local window = nil -- 살아있으면 "창 안", nil이면 idle
|
|
||||||
local cap = nil -- MaxTime 타이머(Debounce 전용)
|
|
||||||
|
|
||||||
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()
|
-- 팩토리 자체 — :Apply(factory)가 호출할 수 있는 함수이면서, 동시에
|
||||||
window = setTimeout(onWindowEnd, opts.Time)
|
-- 전체 브로드캐스트 :Flush()/:Cancel()도 갖는 콜러블 객체(5-4절)
|
||||||
end
|
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()
|
local openWindow, onWindowEnd
|
||||||
window = nil
|
|
||||||
if pending and trailing then
|
function openWindow()
|
||||||
-- Blocker와 같은 순서: 상태를 먼저 정리하고 그 다음 전파
|
window = setTimeout(onWindowEnd, readTime(opts.Time))
|
||||||
-- (전파 도중 소비자가 동기적으로 상류를 :Set()해서 재진입해도
|
|
||||||
-- 방금 닫은 창의 잔여 상태를 다시 건드리지 않게)
|
|
||||||
pending = false
|
|
||||||
if cap then clearTimeout(cap); cap = nil end
|
|
||||||
gate:passThrough() -- invalid 세팅 + 아래로 1회 전파
|
|
||||||
openWindow() -- 통과했으니 창을 다시 엶 = 다음 통과까지 최소 Time
|
|
||||||
end
|
end
|
||||||
-- pending이 없으면 창을 안 열고 완전히 idle로 복귀
|
|
||||||
end
|
|
||||||
|
|
||||||
gate.onUpstreamSignal = function()
|
function onWindowEnd()
|
||||||
if window == nil then
|
window = nil
|
||||||
-- 창 밖(idle)
|
if pending and trailing then
|
||||||
if leading then gate:passThrough() else pending = true end
|
-- Blocker와 같은 순서: 상태를 먼저 정리하고 그 다음 전파
|
||||||
openWindow()
|
-- (전파 도중 소비자가 동기적으로 상류를 :Set()해서 재진입해도
|
||||||
else
|
-- 방금 닫은 창의 잔여 상태를 다시 건드리지 않게)
|
||||||
-- 창 안
|
pending = false
|
||||||
pending = true
|
if cap then clearTimeout(cap); cap = nil end
|
||||||
if reset then -- ← Debounce만: 창을 뒤로 민다
|
gate:passThrough() -- invalid 세팅 + 아래로 1회 전파
|
||||||
clearTimeout(window)
|
openWindow() -- 통과했으니 창을 다시 엶 = 다음 통과까지 최소 Time
|
||||||
|
end
|
||||||
|
-- pending이 없으면 창을 안 열고 완전히 idle로 복귀
|
||||||
|
end
|
||||||
|
|
||||||
|
gate.onUpstreamSignal = function()
|
||||||
|
if window == nil then
|
||||||
|
-- 창 밖(idle)
|
||||||
|
if leading then gate:passThrough() else pending = true end
|
||||||
openWindow()
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
-- MaxTime: 창과 달리 절대 리셋되지 않는 두 번째 타이머.
|
-- 5-4절 제어 핸들 — Flush는 즉시 커밋(창 끝을 안 기다림), Cancel은 버림
|
||||||
-- "신호가 안 끊기면 영원히 발화 안 함"(1-1절)을 위한 안전장치라
|
gate._flush = function()
|
||||||
-- Debounce에서만 의미 있음.
|
if pending and trailing then
|
||||||
if opts.MaxTime and cap == nil and pending then
|
pending = false
|
||||||
cap = setTimeout(function()
|
if window then clearTimeout(window) end
|
||||||
cap = nil
|
if cap then clearTimeout(cap); cap = nil end
|
||||||
if pending and trailing then
|
gate:passThrough()
|
||||||
pending = false
|
openWindow()
|
||||||
if window then clearTimeout(window) end
|
end
|
||||||
gate:passThrough()
|
end
|
||||||
openWindow()
|
gate._cancel = function()
|
||||||
end
|
pending = false
|
||||||
end, opts.MaxTime)
|
if window then clearTimeout(window); window = nil end
|
||||||
|
if cap then clearTimeout(cap); cap = nil end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
return gate
|
instances[gate] = true
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
function Debounce(opts: DebounceOptions) return makeGate(true, opts) 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 즉시 통과 ●
|
3.5 입력 → window == nil → leading 즉시 통과 ●
|
||||||
```
|
```
|
||||||
|
|
||||||
**(B) value-hold 의미론이 붙는 자리**: `gate`는 `onUpstreamSignal`에서
|
**(A) emit-gate가 붙는 자리(확정, 4절)**: `gate`는 다른 평범한 State와
|
||||||
자기 `invalid`를 세우지 않고, 오직 `passThrough()`에서만 세움 — 그래서
|
똑같이 **매 `onUpstreamSignal` 진입 시 자기 `invalid`를 즉시 세운다** —
|
||||||
창이 열려 있는 동안 `gate:Get()`은 캐시된 직전 값을 반환. (A)를 택하면
|
`source-state-plan.md`의 "전파 모델 확정" 절이 정한 "emit은 항상 전파된다"
|
||||||
`onUpstreamSignal` 진입 시 invalid만 세우고 전파를 미루는 형태로 바뀜.
|
규칙이 게이트 자신에게도 그대로 적용됨. 위 코드의 `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`는 압도적으로 **재진입 방지 불리언**을
|
Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리언**을
|
||||||
가리킴(`local debounce = false ... if debounce then return end`). 웹
|
가리킴(`local debounce = false ... if debounce then return end`). 웹
|
||||||
|
|
@ -808,9 +972,9 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
||||||
| `Coalesce` | "합친다"는 동작 자체 | nil 병합(`Alternative`)과 어휘 충돌 |
|
| `Coalesce` | "합친다"는 동작 자체 | nil 병합(`Alternative`)과 어휘 충돌 |
|
||||||
| `RateLimit` (Throttle 자리) | 의미 명확 | 서버 레이트 리밋 뉘앙스 |
|
| `RateLimit` (Throttle 자리) | 의미 명확 | 서버 레이트 리밋 뉘앙스 |
|
||||||
|
|
||||||
**권고**: `Debounce`/`Throttle` 유지 + **사용자 문서 첫 줄에 Roblox 관용
|
**[2026-08-19 확정]** `Debounce`/`Throttle` 유지 + **사용자 문서 첫 줄에
|
||||||
"debounce"와 다르다는 걸 못박기**. 업계 표준 이름을 버리면 검색·이주
|
Roblox 관용 "debounce"와 다르다는 걸 못박기**. 업계 표준 이름을 버리면
|
||||||
비용이 더 큼. 단 이건 사용자 취향이 강하게 걸리는 영역이라 Q1로 넘김.
|
검색·이주 비용이 더 크다는 게 채택 근거 — 더 판단할 것 없음(구 Q1).
|
||||||
|
|
||||||
### 9-2. `-ed`를 안 붙이는 이유 (이건 코퍼스 규칙으로 결정됨)
|
### 9-2. `-ed`를 안 붙이는 이유 (이건 코퍼스 규칙으로 결정됨)
|
||||||
|
|
||||||
|
|
@ -856,24 +1020,24 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. 사용자 판단 대기 — 열린 질문
|
## 12. 사용자 판단 대기 — 전부 해소됨 (2026-08-19)
|
||||||
|
|
||||||
1. **이름** — `Debounce`/`Throttle` 유지(권장) vs 대안. Roblox 관용
|
**[2026-08-19] 남아있던 Q1/Q2/Q4/Q8까지 전부 닫혀 열린 항목이 없음** —
|
||||||
"debounce"(재진입 불리언)와의 충돌을 문서 경고로만 다룰지. (9-1절)
|
논의 원문은 `session/2026-08-19-03-debounce-throttle-final-close.md`.
|
||||||
2. **의미론** — (A) emit-gate(`Blocker`와 동일, `:Get()`은 최신값) vs
|
이력만 남겨둠(각 항목이 왜 그렇게 됐는지는 가리키는 절이 소스, 여기서
|
||||||
**(B) value-hold(권장, `:Get()`도 지연된 값)**. (4절)
|
반복 안 함):
|
||||||
3. **[2026-08-14 개정] 공개 생성자 2개 + 내부 구현 1개(`Reset` 한 비트)**
|
|
||||||
— 초안의 "`Throttle`은 `Debounce{MaxTime}` 프리셋"은 이중 발화 버그가
|
1. ~~**이름**~~ **[2026-08-19 해소]** — `Debounce`/`Throttle` 유지,
|
||||||
있어 폐기됨. 개정안 확인만 필요. (5-3절, 7절)
|
Roblox 관용 "debounce"와의 충돌은 문서 경고로만 대응. (9-1절)
|
||||||
4. **제어 핸들(`:Flush()`/`:Cancel()`)을 v1에 넣을지.** 세 가지 모양:
|
2. ~~**의미론**~~ **[2026-08-19 해소]** — **(A) emit-gate 채택**, (B)
|
||||||
- **S1(권장, v1)**: `state:Apply(Debounce{...})` — 핸들 없음.
|
value-hold는 laziness와 상충해 철회. (4절)
|
||||||
`Operator` 관용구와 완전 일치, 가장 단순.
|
3. ~~**공개 생성자 2개 + 내부 구현 1개(`Reset` 한 비트)**~~
|
||||||
- **S2**: `local d = Debouncer{...}; state:Debounce(d)` + `d:Flush()`/
|
**[2026-08-19 해소]** — 2026-08-14 개정안 그대로 채택, 이견 없었음.
|
||||||
`d:Cancel()` — **`Blocker`의 모양과 글자 그대로 같음**(외부 객체를
|
(5-3절, 7절)
|
||||||
들고 `state:Block(blocker)`로 배선). 구조적 추가 비용이 사실상 없고,
|
4. ~~**제어 핸들**~~ **[2026-08-19 해소]** — 넣기로 확정. 초안의 S1(핸들
|
||||||
"검색창에서 Enter 누르면 즉시 커밋" 같은 실사용 요구를 커버함.
|
없음)/S2(`Blocker`와 같은 공유 외부 객체) 둘 다 아니고, 개별은
|
||||||
- **S3**: `__call`을 가진 객체로 S1/S2를 겸함 — 매직이라 비권장.
|
`Ref` 아웃파라미터·전체는 팩토리 자체의 `:Flush()`/`:Cancel()`(weak
|
||||||
실사용에서 Flush가 자주 필요하다고 보면 처음부터 S2가 나음.
|
레지스트리로 브로드캐스트)로 수렴. (5-4절)
|
||||||
5. ~~**패키지 경계**~~ **[2026-08-14 해소]** — 사용자가 quad-base +
|
5. ~~**패키지 경계**~~ **[2026-08-14 해소]** — 사용자가 quad-base +
|
||||||
엔진별 태스크 배선으로 확정. 주입 표면이 3개→5개로 늘어나는 비용은
|
엔진별 태스크 배선으로 확정. 주입 표면이 3개→5개로 늘어나는 비용은
|
||||||
수용됨. op 이름/시그니처도 사용자가 지정
|
수용됨. op 이름/시그니처도 사용자가 지정
|
||||||
|
|
@ -882,14 +1046,12 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
||||||
전제(무효화 dedup이 신호를 삼킴)가 틀린 것으로 드러나 질문 자체가
|
전제(무효화 dedup이 신호를 삼킴)가 틀린 것으로 드러나 질문 자체가
|
||||||
없어짐. emit은 항상 재전파되므로 게이트는 어디에 걸든 정상 동작함.
|
없어짐. emit은 항상 재전파되므로 게이트는 어디에 걸든 정상 동작함.
|
||||||
(3절)
|
(3절)
|
||||||
7. **`base/blocker-plan.md` 한 줄 명확화** — 그 문서가 인용하는
|
7. ~~**`base/blocker-plan.md` 한 줄 명확화**~~ **[2026-08-19 소멸]** — Q2가
|
||||||
"`Get()`은 라이브 레퍼런스를 준다"는 실제로는 `base/source-state-plan.md`의
|
(A)로 확정되면서 이 항목이 걱정했던 모순(값-지연 의미론과 "`Get()`은
|
||||||
**레퍼런스 의미론** 원칙이지 "항상 상류 최신값"이 아님. Q2에서 (B)를
|
라이브 레퍼런스" 문구의 충돌)이 애초에 안 생김 — (A)는 `Blocker`와
|
||||||
택하면 두 문서가 모순돼 보이므로 그 인용에 한 줄 주석을 넣는 게 좋음.
|
완전히 같은 메커니즘이라 그 문구가 그대로 맞음. (4절)
|
||||||
(확정 문서 수정이라 사용자 승인 후 반영 — 이 세션에선 안 건드림.)
|
8. ~~**`Time = 0`을 허용할지**~~ **[2026-08-19 해소]** — 허용, "defer될
|
||||||
8. **`Time = 0`을 허용할지** — "이번 스텝 합치기" 용도로 유용하지만
|
수 있음"만 문서화, 금지/에러 안 함. (1절 인용문)
|
||||||
스케줄러 타이밍에 의존하는 준-`Blocker`가 됨. 허용(권장) / 금지 /
|
|
||||||
별도 이름 부여 중 선택. (1절 인용문)
|
|
||||||
9. ~~**`Timeout` 핸들의 타입**~~ **[2026-08-14 완전 해소]** — 사용자 결정으로
|
9. ~~**`Timeout` 핸들의 타입**~~ **[2026-08-14 완전 해소]** — 사용자 결정으로
|
||||||
**`type Timeout = { __type_timeout: true, _native: any }`**(마커는
|
**`type Timeout = { __type_timeout: true, _native: any }`**(마커는
|
||||||
런타임에도 실제로 넣고, 백엔드 페이로드 자리도 타입에 미리 선언).
|
런타임에도 실제로 넣고, 백엔드 페이로드 자리도 타입에 미리 선언).
|
||||||
|
|
@ -906,18 +1068,41 @@ Roblox 생태계에서 `debounce`는 압도적으로 **재진입 방지 불리
|
||||||
전체 정정을 지시 — 같은 세션에 base/reference/research/ROADMAP/
|
전체 정정을 지시 — 같은 세션에 base/reference/research/ROADMAP/
|
||||||
스파이크/audit까지 전부 반영했고, 역전 기록은
|
스파이크/audit까지 전부 반영했고, 역전 기록은
|
||||||
`archive/invalidate-dedup-propagation-reversed.md`. 상세는 3절.
|
`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 코어 계약을
|
**M0 착수를 막지 않음** — 이 문서의 어떤 결정도 디스패치/State 코어 계약을
|
||||||
바꾸지 않음(11절에서 확인). 다만 `Operator` 슈가와 달리 **순수 슈가가
|
바꾸지 않음(11절에서 확인).
|
||||||
아니라 실제 기능 갭**이라(타이머 없이는 사용자 코드로 재현하기 번거롭고,
|
|
||||||
재현하면 laziness를 깨기 쉬움) 우선순위는 그쪽보다 위로 두는 게 맞아 보임.
|
|
||||||
|
|
||||||
**의존성**: State 코어(`ROADMAP.md` M3) + 백엔드 주입 표면. 그 둘이
|
**[2026-08-19] 옛 서술("`Operator` 슈가와 달리 순수 슈가가 아니라 실제
|
||||||
서면 언제든 얹을 수 있고, `Blocker` 구현과 **같은 시점에 하는 게 확실히
|
기능 갭이라 우선순위를 위로 둔다")은 이번 라운드로 뒤집힘.** 제어 핸들
|
||||||
쌈** — 1절에서 봤듯 게이트 노드를 공유하므로 따로 하면 같은 걸 두 번
|
설계(5-4절)까지 확정하고 나니, `Debounce`/`Throttle`이 실제로 새로
|
||||||
설계하게 됨. **권고: `Blocker` 구현 시점(M3)에 게이트 노드를 공용으로
|
필요로 하는 quad-base 코어 표면은 **주입 op 2개(`setTimeout`/`clearTimeout`)
|
||||||
빼두고, Debounce/Throttle 자체는 그 위에 나중에 얹기.**
|
뿐**이고 — 게이트 메커니즘은 `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` 함수)는 그 위에 아무
|
||||||
|
때나 나중에 얹으면 된다.
|
||||||
|
|
@ -194,9 +194,10 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa
|
||||||
전파를 제어하는 장치가 **아님**. `:Get()`이 호출되면 상류로 올라가
|
전파를 제어하는 장치가 **아님**. `:Get()`이 호출되면 상류로 올라가
|
||||||
재계산하고, 그 결과를 캐시에 넣고, `invalid`를 끈다.
|
재계산하고, 그 결과를 캐시에 넣고, `invalid`를 끈다.
|
||||||
- **emit 전파를 늦추거나 흡수할 수 있는 건 명시적인 게이트 요소뿐** —
|
- **emit 전파를 늦추거나 흡수할 수 있는 건 명시적인 게이트 요소뿐** —
|
||||||
지금은 `Blocker`(`base/blocker-plan.md`)가 유일하고, 앞으로 추가된다면
|
지금은 `Blocker`(`base/blocker-plan.md`)가 유일하고,
|
||||||
`research/debounce-throttle-plan.md`의 시간 기반 게이트가 같은 자리에
|
`base/debounce-throttle-plan.md`의 시간 기반 게이트가 같은 자리에
|
||||||
들어옴. **평범한 State는 절대 신호를 삼키지 않는다.**
|
들어감(설계 확정, 구현은 아직). **평범한 State는 절대 신호를 삼키지
|
||||||
|
않는다.**
|
||||||
- **[2026-08-14 정정 — 중요]** 이 자리엔 원래 "이미 `invalid`였다면 그
|
- **[2026-08-14 정정 — 중요]** 이 자리엔 원래 "이미 `invalid`였다면 그
|
||||||
아래로 더 전파하지 않는다(다이아몬드 중복 워크 방지)"라고 적혀 있었으나
|
아래로 더 전파하지 않는다(다이아몬드 중복 워크 방지)"라고 적혀 있었으나
|
||||||
**틀린 서술이라 뒤집힘**. 그대로 두면 `:Get()`을 호출하지 않는
|
**틀린 서술이라 뒤집힘**. 그대로 두면 `:Get()`을 호출하지 않는
|
||||||
|
|
|
||||||
|
|
@ -139,35 +139,13 @@
|
||||||
몫. 같은 리서치에서 포함 범위도 새로 갈렸음 — 비트/비교 연산자 그룹과
|
몫. 같은 리서치에서 포함 범위도 새로 갈렸음 — 비트/비교 연산자 그룹과
|
||||||
`Sub`/`Div`는 리액티브 콤비네이터로서 선례가 전혀 없어 드랍 후보로,
|
`Sub`/`Div`는 리액티브 콤비네이터로서 선례가 전혀 없어 드랍 후보로,
|
||||||
`Clamp`/`Min`/`Max`는 선례가 강해 추가 후보로, Debounce/Throttle은
|
`Clamp`/`Min`/`Max`는 선례가 강해 추가 후보로, Debounce/Throttle은
|
||||||
업계에 흔하지만 `Blocker`와는 다른 시간 기반 메커니즘이라 이 카탈로그가
|
업계에 흔하지만 `Blocker`와는 다른 시간 기반 메커니즘이라 이 카탈로그
|
||||||
아니라 quad-roblox 쪽 별도 프리미티브로 다룰지 판단이 필요한 별개 질문으로
|
밖 별개 질문으로 분리됐었음 — **[2026-08-19 해소]** 그 별개 질문은
|
||||||
분리됨. **[2026-08-13 세션 신설]** `Alternative`(nil 대체값, coalesce/`??`/
|
`base/debounce-throttle-plan.md`로 전부 해소·승격 완료, 더 이상 판단
|
||||||
|
대기 아님. **[2026-08-13 세션 신설]** `Alternative`(nil 대체값, coalesce/`??`/
|
||||||
엘비스 연산자류) 후보 추가 — Haskell 비교 리서치 중 나옴, 카탈로그 확정
|
엘비스 연산자류) 후보 추가 — Haskell 비교 리서치 중 나옴, 카탈로그 확정
|
||||||
규칙에 그대로 맞아 포함 근거는 있음. 상세는 `research/operator-sugar-plan.md`.
|
규칙에 그대로 맞아 포함 근거는 있음. 상세는 `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 여섯 번째
|
- **중첩 State 평탄화 `State<State<T>>` → `State<T>`(2026-08-13 여섯 번째
|
||||||
세션 신설, 백로그)** — **[근거 축소, 열네 번째 세션]** 원래 이 항목의
|
세션 신설, 백로그)** — **[근거 축소, 열네 번째 세션]** 원래 이 항목의
|
||||||
주 근거는 "깊은 체인에선 힌트가 `nil`로 전달돼 깜빡임 방지가 꺼진다"는
|
주 근거는 "깊은 체인에선 힌트가 `nil`로 전달돼 깜빡임 방지가 꺼진다"는
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,8 @@ State 메소드로 두려던 초기 폼팩터가 기각된 경위만 여전히
|
||||||
세 레포 모두 없다는 것 자체가 "quad도 굳이 안 만들어도 된다"는 정황.~~
|
세 레포 모두 없다는 것 자체가 "quad도 굳이 안 만들어도 된다"는 정황.~~
|
||||||
**[2026-08-14 뒤집힘]** 사용자가 직접 "`Blocker`와 유사하게 만들어야
|
**[2026-08-14 뒤집힘]** 사용자가 직접 "`Blocker`와 유사하게 만들어야
|
||||||
한다"고 지정 — 위 "빈 자리 아님" 판정은 더 이상 유효하지 않음.
|
한다"고 지정 — 위 "빈 자리 아님" 판정은 더 이상 유효하지 않음.
|
||||||
설계 초안은 `research/debounce-throttle-plan.md`. (원 판정의 근거였던
|
설계는 `base/debounce-throttle-plan.md`(2026-08-19 전부 해소돼
|
||||||
|
`research/`에서 승격). (원 판정의 근거였던
|
||||||
"세 레포에 없다"는 관찰 자체는 사실이지만, 2026-08-12 열아홉 번째 세션의
|
"세 레포에 없다"는 관찰 자체는 사실이지만, 2026-08-12 열아홉 번째 세션의
|
||||||
외부 리서치에서 RxJS/VueUse 등 Roblox 밖에선 가장 흔한 콤비네이터
|
외부 리서치에서 RxJS/VueUse 등 Roblox 밖에선 가장 흔한 콤비네이터
|
||||||
카테고리 중 하나로 확인돼 근거로서의 무게가 이미 약해져 있었음.)
|
카테고리 중 하나로 확인돼 근거로서의 무게가 이미 약해져 있었음.)
|
||||||
|
|
|
||||||
|
|
@ -237,8 +237,9 @@ introspection 로직을 추가하는 거라 라이브러리 복잡도가 늘어
|
||||||
모양을 벗어남 — `Operator.*`(quad-base, 엔진 무종속)에 넣을 수 있는 게
|
모양을 벗어남 — `Operator.*`(quad-base, 엔진 무종속)에 넣을 수 있는 게
|
||||||
아니라 quad-roblox 쪽 별도 프리미티브(Tween과 비슷한 위치)로 다뤄야 할
|
아니라 quad-roblox 쪽 별도 프리미티브(Tween과 비슷한 위치)로 다뤄야 할
|
||||||
가능성이 큼. 이 문서 범위 밖의 별도 설계 질문으로 분리해서 판단 필요.
|
가능성이 큼. 이 문서 범위 밖의 별도 설계 질문으로 분리해서 판단 필요.
|
||||||
**[2026-08-14, 분리 완료]** 사용자 요청으로 `research/debounce-throttle-plan.md`
|
**[2026-08-14, 분리 완료, 2026-08-19 설계 전부 해소돼 `base/debounce-throttle-plan.md`로
|
||||||
신설 — 이 항목은 그 문서로 이관됐고 여기선 더 이상 다루지 않음. 그
|
승격]** 사용자 요청으로 신설 — 이 항목은 그 문서로 이관됐고 여기선 더
|
||||||
|
이상 다루지 않음. 그
|
||||||
문서의 결론 두 가지만 여기 관련 있음: (1) 붙이는 모양은 이 카탈로그와
|
문서의 결론 두 가지만 여기 관련 있음: (1) 붙이는 모양은 이 카탈로그와
|
||||||
똑같이 `factory(self)` + `:Apply`라 위 "왜 `:Apply`인가" 규칙이 그대로
|
똑같이 `factory(self)` + `:Apply`라 위 "왜 `:Apply`인가" 규칙이 그대로
|
||||||
적용됨, (2) 다만 배치는 위 추측과 달리 **quad-base + 주입 op 2개**로
|
적용됨, (2) 다만 배치는 위 추측과 달리 **quad-base + 주입 op 2개**로
|
||||||
|
|
|
||||||
|
|
@ -1519,3 +1519,19 @@ key로 완료 여부 기록) 방식으로 직접 해소하는 아이디어도
|
||||||
확정. `base/slot-plan.md`/`question.md`/`todos.md`/
|
확정. `base/slot-plan.md`/`question.md`/`todos.md`/
|
||||||
`archive/question-resolved.md`/`ROADMAP.md` 전량 반영, `doc-check.py`
|
`archive/question-resolved.md`/`ROADMAP.md` 전량 반영, `doc-check.py`
|
||||||
ERROR 0.
|
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` 전량 반영.
|
||||||
|
|
|
||||||
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`가 파일명 기준 폴백 매칭이라 깨지지 않음).
|
||||||
|
|
@ -178,15 +178,17 @@
|
||||||
(`debug-tooling-plan.md`/`documentation-plan.md`/
|
(`debug-tooling-plan.md`/`documentation-plan.md`/
|
||||||
`documentation-content-map.md`/`framework-comparison-findings.md`/
|
`documentation-content-map.md`/`framework-comparison-findings.md`/
|
||||||
`operator-sugar-plan.md`).
|
`operator-sugar-plan.md`).
|
||||||
**[2026-08-14 추가, 성격이 다름]** 시간 기반 전파 게이트
|
**[2026-08-14 추가, 2026-08-19 설계 전부 해소 후 `base/`로 승격]** 시간
|
||||||
`Debounce`/`Throttle`(`research/debounce-throttle-plan.md`)도 백로그이긴
|
기반 전파 게이트 `Debounce`/`Throttle`(`base/debounce-throttle-plan.md`)도
|
||||||
하나 위 항목들과 달리 **사용자가 직접 요청한 실제 기능 갭**이고 순수
|
백로그이지만 위 항목들과는 발단이 다름 — **사용자가 직접 요청한 실제
|
||||||
슈가가 아님 — M0/M3를 막지는 않지만, **M3에서 `Blocker`를 구현할 때
|
기능 갭**에서 시작됨(그 문서 13절). 다만 제어 핸들 설계까지 닫히고 나니
|
||||||
게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 해야 함**(따로
|
실제로 quad-base에 새 코어 메커니즘을 추가하지 않는 **순수 슈가**로
|
||||||
하면 같은 설계를 두 번 함). 주입 op 2개(`setTimeout`/`clearTimeout`)가
|
확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M3를 막지
|
||||||
백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 설계는
|
않고, **M3에서 `Blocker`를 구현할 때 게이티드 노드를 공용 `Gate`로
|
||||||
네 라운드로 대부분 확정됐고 남은 열린 질문은 `question.md` 3번(개수는
|
빼두는 것만은 그 시점에 해야 함**(따로 하면 같은 설계를 두 번 함).
|
||||||
거기도 반복 안 함 — 소스는 `research/debounce-throttle-plan.md` 12절).
|
주입 op 2개(`setTimeout`/`clearTimeout`)가 백엔드 팩토리 표면에
|
||||||
|
추가될 예정이라는 것도 M1 설계 시 인지. 남은 열린 질문 없음(구
|
||||||
|
`question.md` 3번, 전량 해소로 항목 자체가 빠짐).
|
||||||
**[2026-08-18 추가]** 사용자 아이디어 메모 두 건도 같은 성격의 백로그로
|
**[2026-08-18 추가]** 사용자 아이디어 메모 두 건도 같은 성격의 백로그로
|
||||||
신설 — 스크롤 최적화 외부 유틸 `quad-roblox-fastscroll`
|
신설 — 스크롤 최적화 외부 유틸 `quad-roblox-fastscroll`
|
||||||
(`research/fastscroll-plan.md`, 선행으로 `Visible=false`일 때
|
(`research/fastscroll-plan.md`, 선행으로 `Visible=false`일 때
|
||||||
|
|
|
||||||
11
ROADMAP.md
11
ROADMAP.md
|
|
@ -909,10 +909,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
테스트는 2026-08-13 세 번째 세션에 불필요로 해소됨 —
|
테스트는 2026-08-13 세 번째 세션에 불필요로 해소됨 —
|
||||||
`archive/question-resolved.md` 참고, v2엔 대응 개념 자체가 없음)
|
`archive/question-resolved.md` 참고, v2엔 대응 개념 자체가 없음)
|
||||||
- [ ] Slot 형제 순서 보장(다중 백엔드 관점) — Roblox만이면 급하지 않음
|
- [ ] Slot 형제 순서 보장(다중 백엔드 관점) — Roblox만이면 급하지 않음
|
||||||
- [ ] **[2026-08-14 신설]** 시간 기반 전파 게이트 `Debounce`/`Throttle`
|
- [ ] **[2026-08-14 신설, 2026-08-19 설계 전부 해소 후 `base/`로 승격]**
|
||||||
(`research/debounce-throttle-plan.md`) — **M3에서 `Blocker`를 구현할
|
시간 기반 전파 게이트 `Debounce`/`Throttle`(`base/debounce-throttle-plan.md`)
|
||||||
때 게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 할 것**
|
— 제어 핸들 설계까지 닫히면서 quad-base에 새 코어 메커니즘을
|
||||||
(둘이 같은 노드를 공유하므로 따로 하면 같은 설계를 두 번 함).
|
추가하지 않는 **순수 슈가**로 확인됨(`Blocker`의 gated state + `Ref` +
|
||||||
|
아래 주입 op 2개 위에 전부 얹힘, 그 문서 13절). **M3에서 `Blocker`를
|
||||||
|
구현할 때 게이티드 노드를 공용 `Gate`로 빼두는 것만은 그 시점에 할
|
||||||
|
것**(둘이 같은 노드를 공유하므로 따로 하면 같은 설계를 두 번 함).
|
||||||
프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M3를 막지 않음.
|
프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M3를 막지 않음.
|
||||||
주입 op 2개(`setTimeout(func, delay) -> Timeout` / `clearTimeout`,
|
주입 op 2개(`setTimeout(func, delay) -> Timeout` / `clearTimeout`,
|
||||||
Roblox는 `task.delay`/`task.cancel`로 배선 — **인자 순서가 반대라
|
Roblox는 `task.delay`/`task.cancel`로 배선 — **인자 순서가 반대라
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue