decide(base): Compute 커링, state:Apply 확정, Effect Subscribe/Unsubscribe, 이중 바인딩 금지

- :Compute(fn)에도 Observer/Effect와 동일한 커링 권장 노트 추가
- state:Apply(factory) 확정 — ":With"/":Compute" 자동 등록 조합기였던
  백로그안 기각, Modifier:Apply와 동일한 순수 체이닝 설탕으로 재정의
- EffectHandle:Subscribe()/:Unsubscribe() 신설 — leaf 없이 쓰는 독립
  Effect 지원, :Unsubscribe()는 마지막 cleanup을 1회 트리거해야 함
- Observer/Effect 이중 바인딩(leaf 부착 + 수동 Subscribe) 금지 확정,
  Bound 플래그로 즉시 error
- ROADMAP.md M3/question.md에 반영, effect-plan.md 오기 정정
This commit is contained in:
qwreey 2026-08-07 16:59:40 +09:00
parent b7ce11cf7f
commit 71729db816
Signed by: qwreey
GPG key ID: D28DB79297A214BD
5 changed files with 217 additions and 9 deletions

View file

@ -515,6 +515,13 @@ Connect가 도는 숨은 churn 비용도 있음(Store Set은 dedup 안 함,
lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/ lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/
`:Compute`" 부분 참고). `:Compute`" 부분 참고).
**`fn`을 커링 스타일로 짜는 것도 권장(2026-08-07 일곱 번째 세션)** —
`key:Compute(makeFormatter("ko-KR"))`처럼 팩토리가 실제 `fn`을 만들어
반환하는 패턴, Observer/Effect의 동일 관용구(아래 "`fn`을 커링 스타일로
짜는 것도 모듈화 관용구로 권장" 절, `base/effect-plan.md`)와 같은 결 —
`:Compute`가 원래부터 이 셋 중 제일 먼저 있던 자리라 뒤늦게 문서화된
것뿐, 새 결정이라기보다 이미 있던 패턴을 명문화한 것.
### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06) ### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06)
**배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진 **배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진
@ -630,14 +637,56 @@ retract/Destroy되면 자동으로 정리됨.
이 State가 계속 재계산되게만 강제하고 싶을 때 씀. 문서화만 확실히 이 State가 계속 재계산되게만 강제하고 싶을 때 씀. 문서화만 확실히
하면 별문제 없음(사용자 판단). 하면 별문제 없음(사용자 판단).
### 백로그(미확정) — `state:Apply(...)`: `:With`+`:Compute` 등록을 커링으로 자동화 (2026-08-07 여섯 번째 세션, 사용자 제안) ### `state:Apply(factory)` — Modifier와 동일한 순수 체이닝 설탕으로 확정 (2026-08-07 일곱 번째 세션)
Effect/Observer의 `fn` 커링 관용구 논의 중 나온 인접 아이디어 — `Modifier` **처음 제안됐던 "`:With`/`:Compute` 등록을 커링으로 자동화하는 조합기"
`:Apply(factory)`(팩토리 체이닝, `modifier-plan.md` 8번)와 비슷하게, 여러 방향은 기각됨 — 사용자가 재확인한 실제 의도는 그보다 훨씬 단순함.**
개를 커링으로 받아 알아서 `:With`/`:Compute` 등록을 대신 해주는 `Modifier:Apply(factory)`도 매번 새 값을 만들어내는 체이닝 설탕일 뿐이듯,
`state:Apply(...)` 같은 조합기가 있으면 편리할 수 있다는 제안. **지금 결정 State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양이라 —
필요 없음 — 백로그로만 기록.** 구체 시그니처/필요성 검증 없음, 나중에 `state:Apply(factory)`는 그냥 `factory(state)`를 메소드 체이닝 문법으로
`:With`/`:Compute` 관용구가 실제로 자주 반복되는 게 확인되면 다시 논의. 쓴 것뿐이고 그 이상의 계약은 없음(`Modifier:Apply`와 완전히 동일한
정의: `function(self, factory) return factory(self) end`).
- **동기**: 커링 팩토리 두 개 이상을 이미 있는 문법만으로 이으면 바깥에서
안으로 겹쳐 읽어야 하는 중첩 호출이 됨 — 실제 형태로 예를 들면,
```lua
-- Apply 없이: 안쪽(가장 최근에 만든 것)부터 거꾸로 읽어야 함
local capped = capAt(100)(withLocale(localeStore.locale)(rawScore))
-- state:Apply로: 왼쪽에서 오른쪽, 만든 순서 그대로 읽힘
local capped = rawScore
:Apply(withLocale(localeStore.locale))
:Apply(capAt(100))
```
팩토리가 세 개, 네 개로 늘어날수록 앞쪽 버전은 괄호 깊이와 읽는 방향이
코드 작성 순서와 반대로 꼬여 diff/리뷰에서 특히 안 좋음 — `:Apply`
버전은 각 줄이 "그다음 뭘 했는지"를 순서대로 나열하므로 Modifier
체이닝(`mod:FontSize(14):Apply(Boldify(10)):Apply(Italicify)`)과 읽는
방식이 완전히 통일됨. `:With`/`:Compute` 자체를 대신 호출해주는
자동화가 아니므로, 여전히 팩토리 본문 안에서 `:With`/`:Compute`를
직접 호출하는 건 팩토리 작성자 몫.
- **구현 비용 거의 0**: Modifier와 달리 State/Source는 제네릭 `__index`
필드 setter를 즉석 합성하는 메커니즘이 없어서(고정된 메소드 표면만
존재), Modifier의 `Apply`처럼 "필드 이름으로 예약해야 하는" 충돌
자체가 없음 — 그냥 고정 메소드 하나 추가하는 것.
- **타입은 `factory: (State<T>) -> U): U`로 완전히 열어둠** — Modifier의
`Apply``factory: (M) -> M`으로 같은 타입을 유지해야 체이닝이
이어지지만, State의 `:Apply`는 팩토리가 State가 아닌 값(예: 최종
요약된 plain 값)을 반환해 반응형 그래프를 벗어나는 탈출구로 쓰는 것도
막을 이유가 없음 — Modifier보다 오히려 더 자유로운 시그니처.
- **Source도 자동 포함**: Source가 State를 구조적으로 만족하는 기존
델리게이션(`__index`로 `:With`/`:Compute` 위임)에 `:Apply`도 그대로
얹히므로 별도 구현 불필요.
- **Effect/Observer/Compute의 `fn` 커링 권장(위 절들)과 같은 스레드지만
별개 기능** — 커링은 "`fn` 자체를 팩토리로 짜는 관용구" 권장이고,
`:Apply`는 그렇게 만든 팩토리를 체이닝 문법으로 적용하는 수단. 둘이
합쳐지면 `state:Apply(makeFormatter("ko-KR"))`처럼 자연스럽게 이어짐.
**Observer/Effect의 `:Subscribe()`/`:Unsubscribe()`는 이 절과 무관한
별개 주제** — 아래 새 절로 분리(이전에 이 헤더 아래 잘못 걸려 있던
문서 버그 수정, 내용 자체는 이미 확정된 것 그대로).
### Observer의 `:Subscribe()`/`:Unsubscribe()` — children 배열 밖 독립 구독 (2026-08-06 후속 세션)
**문제**: children 배열에 넣는 자동 라이프사이클 바인딩은 Observer가 **문제**: children 배열에 넣는 자동 라이프사이클 바인딩은 Observer가
"어딘가 leaf에 붙어있다"는 걸 전제함. 근데 흔한 실사용 패턴 하나가 이 "어딘가 leaf에 붙어있다"는 걸 전제함. 근데 흔한 실사용 패턴 하나가 이
@ -692,6 +741,50 @@ Effect/Observer의 `fn` 커링 관용구 논의 중 나온 인접 아이디어
객체를 mutate하고 그대로 돌려주는 것)지만 표면 문법은 비슷하게 객체를 mutate하고 그대로 돌려주는 것)지만 표면 문법은 비슷하게
체이닝 가능. 체이닝 가능.
### 이중 바인딩 금지 — leaf 부착과 `:Subscribe()`는 상호 배타적, `Bound` 플래그로 즉시 에러 (2026-08-07 일곱 번째 세션)
**규칙**: 같은 Observer/Effect 핸들 하나는 라이프사이클 바인딩 경로를
딱 하나만 가질 수 있음 — children 배열에 놓여 leaf에 자동 부착되거나
(위 weak table 경로) `:Subscribe()`로 수동 등록되거나(위 강참조
레지스트리 경로), 둘 중 하나만. **둘 다 동시에 걸리는 건 UB로 확정**
이미 leaf에 부착된 핸들을 다시 `:Subscribe()`하는 것도, 이미
`:Subscribe()`한 핸들을 children 배열에 놓아 leaf로도 부착시키는 것도
둘 다 금지.
**UB를 조용한 오동작이 아니라 즉시 에러로 만든다** — 판별 비용이 사실상
0(불리언 필드 하나 확인)이라, 조용히 이상하게 동작하게 두는 것보다
바로 에러를 던져 버그를 그 자리에서 잡는 게 엔지니어링상 훨씬 쌈:
```lua
-- :Subscribe() 진입부, children 배열 leaf 부착부 — 둘 다 진입 전 동일하게 확인
if self.Bound then
error("Observer/Effect가 이미 다른 경로로 바인딩됨 — leaf 부착과 :Subscribe()는 동시에 쓸 수 없음")
end
self.Bound = true
```
- **`Bound`는 가칭** — 용어 정리 라운드에서 최종 이름 재검토 대상
(`.claude/question.md`에 반영).
- 이 플래그는 어느 경로가 먼저 왔는지와 무관하게 "이미 바인딩됨"만
표시 — 두 진입점이 똑같이 확인/설정하므로 순서와 무관하게 대칭적으로
막힘.
- **`:Unsubscribe()`는 여전히 "어떤 경로로 바인딩됐든 그 계약을 끊는다"는
뜻으로 통일** — `Bound`가 어느 경로로 세워졌는지와 무관하게,
`:Unsubscribe()` 한 번으로 그 바인딩(leaf의 Destroying 연결이든 수동
강참조 등록이든)을 끝내고 최종 정리를 수행. 위 "`:Unsubscribe()`는
자동(리프) 케이스에도 동일하게 씀" 절과 정합 — 이중 바인딩 금지 규칙과
별개로, "단일 바인딩을 끊는" `:Unsubscribe()` 자체의 계약은 안 바뀜.
- **Effect도 동일 규칙 적용** — 내부적으로 Observer를 조합하는 경우든
`state` 없는 경우든 같은 `Bound` 게이트를 그대로 재사용
(`base/effect-plan.md`). 이전에 그 문서에 적어뒀던 "leaf 부착과
`:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이 규칙으로
대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야 하는
조합이었음.
- **문서화 경고 대상(api/심화)**: "한 Effect/Observer 핸들을 children
배열에 놓았다면 그걸 다시 `:Subscribe()`하지 말 것, 반대도 마찬가지 —
두 경로를 동시에 쓰고 싶으면 각각 독립된 새 `Effect(...)`/
`state:Observer(...)` 호출로 따로 만들 것"을 명시할 것.
## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨 ## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨
**배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐. **배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.

View file

@ -69,6 +69,52 @@ end; lastConn = ... end)`) **Observer 자체**가 이걸 대신해줄 이유는
(Effect)으로 분리해 얹었을 뿐, Observer의 기본 계약(재실행 신호만, cleanup은 (Effect)으로 분리해 얹었을 뿐, Observer의 기본 계약(재실행 신호만, cleanup은
클로저로 직접)은 그대로 가볍게 유지됨. 클로저로 직접)은 그대로 가볍게 유지됨.
## `EffectHandle:Subscribe()`/`:Unsubscribe()` — leaf 없이 쓰는 독립 Effect (2026-08-07 일곱 번째 세션)
**동기**: 지금까지 Effect의 유일한 생애주기 경로는 children 배열의 leaf
부착뿐이었음 — leaf 없이 `Effect(fn)`/`Effect(fn, state)`를 호출하면
설치(1회 실행)는 되지만 반환된 `EffectHandle`엔 아무 인터페이스도 없어서
cleanup을 트리거할 방법이 없는 막다른 길이었음. `state:Observer(fn)`
이미 `:Subscribe()`/`:Unsubscribe()`(위 bind-system-plan.md 절)로 "children
배열 밖, 모듈/스크립트 레벨에서 독립적으로 켜고 끄는" 경로를 갖고 있는데,
Effect도 모듈/스크립트 사이드 이펙트(백그라운드 시스템, non-UI 코드가
quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로 쓰일 수 있어서
같은 결로 필요 — `Effect`도 leaf 없이 독립적으로 켜고 끌 수 있어야 함.
**확정**: `EffectHandle`에도 `:Subscribe()`/`:Unsubscribe()` 추가, 둘 다
`self` 반환(Observer와 동일한 fluent 대칭).
- **`:Subscribe()`** — Observer가 쓰는 것과 같은 강참조 레지스트리에
자신(또는 `state` 있는 경우 내부 Observer)을 등록 — 새 메커니즘 아님,
기존 레지스트리 재사용. 이후 로컬 변수로 참조를 안 들고 있어도 계속
살아있음(Observer와 동일 관용구).
- **`:Unsubscribe()`는 Observer의 것을 그냥 위임하지 않는다 — Effect
계층에서 의미가 확장됨.** Observer의 `:Unsubscribe()`는 "미래 재실행만
끊는다"(Observer 자체엔 정리할 상태가 없음)로 충분하지만, Effect의
계약은 "생애주기가 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"
이고 leaf 사망은 그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`
동일하게 "지금 끝났다"는 신호로 취급해야 계약이 일관됨:
1. `state`가 있으면 내부 Observer도 `:Unsubscribe()`해서 향후 재실행을
끊고,
2. **직전(또는 유일한) cleanup을 정확히 1회 호출** — leaf가 죽을 때
하던 것과 정확히 같은 이벤트를 수동으로 앞당기는 것.
3. **idempotent, 그리고 이후 leaf가 실제로 죽어도 cleanup이 중복
호출되면 안 됨** — 새 메커니즘 불필요, Observer가 이미 확정해둔
"`Subscribed` 필드 우선 liveness 체크"가 자동(리프)/수동(Unsubscribe)
두 경로를 하나의 게이트로 OR 묶어주므로 여기 그대로 얹힘.
- **`state` 없는 mount-only Effect엔 특별한 분기 불필요** — install은 이미
`Effect(fn)` 호출 시점에 끝나 있으므로, `:Unsubscribe()`는 그냥 "지금
leaf-사망 cleanup을 수동으로 트리거"하는 것과 완전히 동치.
- **leaf 부착과 `:Subscribe()`를 동시에 쓰는 건 UB — 정정(2026-08-07
일곱 번째 세션 후속)**: 처음엔 "같은 liveness 게이트를 공유하니
동시에 써도 안전"으로 적었으나, 애초에 한 핸들은 라이프사이클 바인딩
경로를 하나만 가져야 한다는 게 맞는 방향이라 판단이 뒤집힘 — 상세
규칙과 `Bound` 플래그 기반 즉시-에러 메커니즘은
`base/bind-system-plan.md`의 "이중 바인딩 금지" 절 참고. leaf 부착
**후** `:Unsubscribe()`로 조기 해제하는 것(위 "Observer의
`:Unsubscribe()`는 자동 케이스에도 동일하게 씀" 패턴)은 여전히 정상 —
금지되는 건 leaf 부착과 `:Subscribe()`**같이** 쓰는 것뿐.
## 해결됨 — Effect/Observer 관계 (2026-08-07 여섯 번째 세션, 이전 미해결 절 대체) ## 해결됨 — Effect/Observer 관계 (2026-08-07 여섯 번째 세션, 이전 미해결 절 대체)
**과거 미해결이었던 두 질문 모두 확정**: **과거 미해결이었던 두 질문 모두 확정**:

View file

@ -87,6 +87,10 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
Source 판별 predicate 세 개의 이름 — 동작은 전부 확정(`base/ Source 판별 predicate 세 개의 이름 — 동작은 전부 확정(`base/
modifier-plan.md` 9번, `base/bind-system-plan.md``isState` 절), modifier-plan.md` 9번, `base/bind-system-plan.md``isState` 절),
이름만 다른 가칭들과 같이 용어 정리 라운드에서 재검토. 이름만 다른 가칭들과 같이 용어 정리 라운드에서 재검토.
- **`Bound`(3순위, 사소함, 2026-08-07 일곱 번째 세션 추가)**: Observer/
Effect 핸들이 leaf 부착과 `:Subscribe()` 중 이미 어느 한쪽으로
바인딩됐는지 표시하는 내부 플래그 이름(`base/bind-system-plan.md`
"이중 바인딩 금지" 절) — 동작은 확정, 이름만 가칭.
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md` - **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로 "provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/ 스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/

View file

@ -930,9 +930,64 @@ setter를 단발로 직접 호출하는 흔한 경로는 여전히 mutable이라
- **백로그로만 기록, 결정 안 함**: `state:Apply(...)`처럼 여러 개를 커링으로 - **백로그로만 기록, 결정 안 함**: `state:Apply(...)`처럼 여러 개를 커링으로
받아 `:With`/`:Compute` 등록을 자동화하는 조합기 아이디어(사용자 제안, 받아 `:With`/`:Compute` 등록을 자동화하는 조합기 아이디어(사용자 제안,
`Modifier:Apply`의 State판 대응물) — `base/bind-system-plan.md`에 백로그 `Modifier:Apply`의 State판 대응물) — `base/bind-system-plan.md`에 백로그
절로만 남김, 시그니처/필요성 미검증. 절로만 남김, 시그니처/필요성 미검증. **(2026-08-07 일곱 번째 세션에서
이 방향 자체가 기각되고 훨씬 단순한 형태로 확정됨 — 아래 참고.)**
- 이걸로 `question.md` 0번(추가 프리미티브 논의)의 열린 항목은 "키 기반 - 이걸로 `question.md` 0번(추가 프리미티브 논의)의 열린 항목은 "키 기반
동적 컬렉션 재조정" 하나만 남음. 동적 컬렉션 재조정" 하나만 남음.
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미 **다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미
설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로. 설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로.
## 2026-08-07 일곱 번째 세션 — `:Compute` 커링, `state:Apply` 확정(백로그안 기각), Effect `:Subscribe`/`:Unsubscribe` 신설, 이중 바인딩 금지
짧은 대화형 세션, 네 가지를 순서대로 처리 — 전부 `base/bind-system-plan.md`/
`base/effect-plan.md`/`ROADMAP.md`/`question.md`에 반영 완료:
1. **`:Compute(fn)`에도 커링 권장 노트 추가.** 여섯 번째 세션에서 Observer/
Effect의 `fn`에만 문서화됐던 "팩토리가 실제 `fn`을 만들어 반환하는
커링 스타일 권장"이 `:Compute`엔 빠져 있었음 — 같은 결이라 자연스럽게
확장, `bind-system-plan.md` "`:With`+`:Compute`" 절에 추가.
2. **`state:Apply(factory)` 확정 — 원래 백로그였던 "`:With`/`:Compute`
등록을 커링으로 자동화하는 조합기" 방향은 기각.** 사용자가 재확인한
실제 의도는 훨씬 단순함: `Modifier:Apply`와 똑같이 `factory(self)`
체이닝 문법으로 부르는 순수 설탕(`function(self, factory) return
factory(self) end`) — `fnb(c,d)(fn(a,b)(state))`처럼 팩토리를 안에서
밖으로 겹쳐 읽어야 하는 중첩을 `state:With(a,b):Compute(fn(a,b))
:Apply(fnb(c,d))`로 펴는 게 유일한 목적. 구현 비용 거의 0(State는
Modifier와 달리 제네릭 `__index` 필드 setter 합성이 없어 이름 예약
충돌도 없음), 타입은 `factory: (State<T>) -> U): U`로 Modifier보다
더 열어둠(팩토리가 State 밖 plain 값을 반환해 반응형 그래프를 벗어나는
것도 허용). Source는 기존 `:With`/`:Compute` 델리게이션에 얹혀 자동
포함. `bind-system-plan.md` "`state:Apply(factory)`" 절, 구체 전/후
코드 예시까지 반영. 부수적으로 같은 헤더 아래 잘못 걸려 있던 Observer
`:Subscribe`/`:Unsubscribe` 내용(무관한 주제)을 별도 절로 분리하는
문서 버그도 수정.
3. **`EffectHandle:Subscribe()`/`:Unsubscribe()` 신설.** 지금까지 Effect의
유일한 생애주기 경로는 children 배열 leaf 부착뿐이라, leaf 없이 쓰는
모듈/스크립트 레벨 사이드 이펙트(백그라운드 시스템 등)엔 반환된
`EffectHandle`이 막다른 길이었음 — Observer가 이미 가진 `:Subscribe`/
`:Unsubscribe`와 같은 결로 확정. **핵심 주의점**: Effect의
`:Unsubscribe()`는 Observer의 것을 그냥 위임하면 안 됨 — Observer의
계약은 "미래 재실행만 끊는다"로 충분하지만, Effect의 계약은 "생애주기가
끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"이고 leaf 사망은
그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`도 동일하게 "지금
끝났다"는 신호로 취급해 마지막 cleanup을 트리거해야 계약이 일관됨(leaf
가 살아있어도 마찬가지). idempotent 보장은 기존 `Subscribed` 필드
liveness 체크 재사용으로 공짜. `base/effect-plan.md` 신규 절.
4. **Observer/Effect 이중 바인딩 금지 — `Bound`(가칭) 플래그로 즉시
`error`.** 처음엔 "leaf 부착과 `:Subscribe()`를 동시에 써도 같은
liveness 게이트를 공유하니 안전"이라고 적었으나, 사용자가 애초에 한
핸들은 라이프사이클 바인딩 경로를 하나만 가져야 한다고 정정 — 동시
바인딩은 UB로 확정하되, 판별 비용이 사실상 0(불리언 필드 하나)이라
조용한 오동작 대신 그 자리에서 `error`를 던지는 쪽으로 결정
(엔지니어링 비용 대비 디버깅 이득이 명확). 두 진입점(`:Subscribe()`
호출부, children 배열 leaf 부착부)이 똑같이 확인/설정하는 대칭적 게이트
— 순서 무관. `bind-system-plan.md` "이중 바인딩 금지" 절 신설,
`effect-plan.md`의 3번 항목 서술은 이 규칙으로 대체(정정 표시 남김).
**부수 정리**: `ROADMAP.md` M3에 `state:Apply`/Effect `:Subscribe`·
`:Unsubscribe`/이중 바인딩 금지 체크박스 추가. `question.md``Bound`
이름을 용어 정리 대상(3순위)으로 추가.
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수
설계 확정이라 M0 착수 우선순위 자체는 그대로.

View file

@ -67,13 +67,23 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를 - [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive, 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발) State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
- [ ] `state:Apply(factory)`(`base/bind-system-plan.md` "`state:Apply(factory)`"
절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로
부르는 순수 설탕, `factory: (State<T>) -> U): U`로 열린 타입. Source도
기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함
- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회 - [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회
실행 확정**(`base/bind-system-plan.md`의 Observer 절), `isObserver` 실행 확정**(`base/bind-system-plan.md`의 Observer 절), `isObserver`
판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()` 판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`
- [ ] `Effect(fn, state?)`(`base/effect-plan.md`) — `state` 생략 시 설치 - [ ] `Effect(fn, state?)`(`base/effect-plan.md`) — `state` 생략 시 설치
1회+leaf 사망 시 확정 정리, `state` 지정 시 내부적으로 1회+leaf 사망 시 확정 정리, `state` 지정 시 내부적으로
`state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React
`useEffect` 동형). Observer 구현 이후에 착수(의존 관계) `useEffect` 동형). Observer 구현 이후에 착수(의존 관계).
`EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션)
- [ ] Observer/Effect 이중 바인딩 금지 — `Bound`(가칭) 플래그로 leaf 부착과
`:Subscribe()`가 동시에 걸리면 즉시 `error`(`base/bind-system-plan.md`
"이중 바인딩 금지" 절, 2026-08-07 일곱 번째 세션)
- [ ] mock 대상 테스트 - [ ] mock 대상 테스트
## M4 — 첫 end-to-end 반응형 업데이트 ## M4 — 첫 end-to-end 반응형 업데이트