- state:Observer(fn)는 등록 즉시 1회 실행되는 것으로 확정 — 초기화 순서 디버깅 문제를 피하고, store-bind 프로퍼티 핸들러가 "초기값 적용"과 "이후 변경 반영"을 같은 코드 경로로 통일할 수 있게 됨. - Effect(fn, state?)로 확정 — state 생략 시 기존 스펙(설치 1회 + 확정 정리) 유지, state 지정 시 내부적으로 state:Observer(...)를 조합해 재실행 + 자동 cleanup 배선(React useEffect와 동형). 다수 의존성은 :With(...)로 묶어서 넘김. 여전히 자유 함수(메소드 아님) — leaf 생명주기 바인딩을 state가 소유하지 않아서. - fn 커링 스타일을 Effect/Observer 공통 모듈화 관용구로 권장, state:Apply 커링 조합기 아이디어는 백로그로만 기록. - question.md 0번의 Effect/Observer 열린 질문 해소. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.9 KiB
Effect — 설치 + 확정 정리, state 있으면 Observer를 감싸 재실행도 지원
상태: base — research/additional-primitives-plan.md(다른 프레임워크
대비 갭 분석)에서 갈라져 나온 확정 프리미티브. base/blocker-plan.md(같은
조사에서 나온 다른 확정 프리미티브)와는 서로 무관 — Store/State 작업이나
Ref/PreRef와 파생 관계는 아니라 별도 파일로 둔다(2026-08-07 문서 정리에서
한 파일로 합쳤던 걸 다시 분리). 단 state 인자를 받는 형태는 내부적으로
Observer를 조합해서 만들어짐(아래 참고, 2026-08-07 여섯 번째 세션 확정) —
"Observer와 무관한 완전 독립 프리미티브"였던 이전 서술은 정정됨.
Effect와 Observer의 관계 확정(2026-08-07 여섯 번째 세션): 별개의
독립 프리미티브이되, state를 받는 형태의 Effect는 내부적으로 Observer를
**조합(compose)**해서 만들어짐 — Ref/PreRef처럼 브랜드 태그만 다른 재사용이
아니라, Observer(재실행 신호) 위에 자동 cleanup 배선을 얹은 한 단계 위
계층. 자유 함수인 이유는 여전히 유효: state 없이도 성립하는
mount/unmount 전용 유스케이스가 있고, 실제 leaf 생명주기 바인딩은 (Observer와
마찬가지로) children 배열 위치에 거는 것이라 state가 그 바인딩을 소유하지
않음 — Roblox엔 task.spawn으로 코루틴에 반복문/타이머를 돌리는 패턴이
흔하고, Luau 테이블엔 __gc 같은 GC 시점 훅이 없어서 "이게 진짜 사라지는
순간"을 아는 유일한 방법은 Instance.Destroying류 명시적 신호뿐 — 이런
케이스(타이머 시작 → leaf가 죽을 때 반드시 정지)를 위한 별도 primitive로
합의됨.
Effect(fn, state?) -> EffectHandle
state 생략 시: fn()을 즉시 1회 실행, 리턴값(nil | () -> ())은
이 Effect가 바인드된 leaf가 죽을 때 정확히 1회 호출. 재실행 없음
(mount/unmount 전용, React useEffect(fn, [])와 동형).
state 지정 시(2026-08-07 여섯 번째 세션 확정): Effect는 내부적으로
state:Observer(...)를 감싸는 걸로 구현 — fn은 포지셔널 인자로 state를
받고(fn(state), :Compute의 fn(self) 포지셔널-self 패턴 재사용,
모듈화 목적 — 클로저 캡처 없이 fn을 독립적으로 정의/재사용 가능),
Observer가 이제 등록 즉시 1회 실행되므로(아래 Observer 절 참고) 그 첫
실행이 "설치"를 겸함. 이후 state가 무효화될 때마다 직전 fn 호출이
리턴한 cleanup을 먼저 호출한 뒤 fn을 재호출, 그리고 Effect가 바인드된
leaf가 죽을 때 마지막 cleanup을 한 번 더 호출. 결과적으로 React
useEffect(fn, [dep])와 동형(설치+재실행 사이/최종 cleanup 전부 같은
반환 계약 하나로 처리).
- 다수 의존성은
:With(...)로 먼저 하나의 State로 묶어서 넘길 것 — React식 별도 deps 배열을 새로 만들지 않음, quad가 이미 가진 다중 의존성 결합 관용구(base/bind-system-plan.md":With+:Compute" 절)를 그대로 재사용해 같은 일 하는 두 번째 경로를 안 만듦. fn은 커링 스타일도 권장(2026-08-07 여섯 번째 세션, 사용자 제안) —Effect(makeLogger("mount"), state)처럼 팩토리 함수가 실제fn(state)를 만들어 반환하는 패턴,Modifier의Boldify(10)커링 관용구(modifier-plan.md8번)와 같은 결.state:Observer(fn)도 동일하게 커링 스타일을 권장 대상으로 같이 문서화(아래 Observer 절 참고) — 모듈화가 필요하면 둘 다 이 패턴을 쓸 것.- 재실행이 필요 없는 케이스와 혼동하지 말 것: 값 변화와 무관하게 설치+최종
정리만 필요하면
state없이Effect(fn)을 씀 —state를 굳이 넘겨서 재실행을 유발할 필요 없음.
children 배열에 leaf로 놓는 기존 Observer 바인딩 패턴을 그대로 재사용(그 leaf가 살아있는 동안만 유효, leaf가 죽으면 최종 정리 콜백 호출). 비용은 leaf당 실제 Destroying 바인딩 하나(공유 weak table로 되는 Observer보다 비쌈) — 필요할 때만 쓰는 걸로 충분.
Observer 자체에 cleanup 반환 계약을 추가하는 안은 여전히 기각 — React
useEffect류로 fn이 nil | () -> ()를 반환하면 다음 재실행 직전에 그걸
불러주는 안을 검토했으나, 클로저 업밸류로 이미 쉽게 되고 잘 작동해서(local lastConn; state:Observer(function() if lastConn then lastConn:Disconnect() end; lastConn = ... end)) Observer 자체가 이걸 대신해줄 이유는 여전히
약함. 이 기각과 위 Effect 설계는 상충하지 않는다 — 그때 기각한 건
"Observer 자체에 이 복잡도를 넣지 말자"였지 "이 패턴 자체가 무용하다"가
아니었음. 자동 cleanup 배선이 필요한 사람만 opt-in으로 쓰는 별도 계층
(Effect)으로 분리해 얹었을 뿐, Observer의 기본 계약(재실행 신호만, cleanup은
클로저로 직접)은 그대로 가볍게 유지됨.
해결됨 — Effect/Observer 관계 (2026-08-07 여섯 번째 세션, 이전 미해결 절 대체)
과거 미해결이었던 두 질문 모두 확정:
- Effect는 자유 함수로 확정(
state:Effect(fn)메소드 아님) — 위 "Effect와 Observer의 관계 확정" 절 참고.state인자가 있어도 실제 leaf 생명주기 바인딩을state가 소유하지 않아서 메소드로 만들 필연성이 없었음. state:Observer(fn)는 등록 즉시 1회 실행되는 것으로 확정(base/ bind-system-plan.md의 Observer 절 참고) — 이 덕에 Effect가state를 받을 때 Observer를 그대로 조합해 재사용할 수 있게 됨(별도 "설치 시 1회 실행" 로직을 Effect가 따로 만들 필요 없음).
.claude/question.md 0번의 관련 항목도 해소됨으로 갱신 완료.