Observer / Effect
This content is not available in your language yet.
반응형 그래프의 말단에 붙어 바깥세상에 부수효과를 내는 핸들 둘입니다.
Observer— 노드 하나를 관측합니다. 값을 실어주지 않고, cleanup 개념도 없습니다.Effect— 의존성 여럿(State/Source/Ref)을 걸고, 함수가 돌려준 cleanup을 생명주기에 맞춰 소진합니다.
둘은 이름이 같은 네 진입점(:Subscribe / :WeakSubscribe / :Unsubscribe / :WeakUnsubscribe)과 .Subscribed 플래그를 공유하지만, 본문은 각자의 것입니다 — 아래 각 절이 그 차이를 적습니다.
반응형 콜백은 파동 도중 yield하지 않고, 꼬인 파동을 풀어 줄 도구가 없는 곳에서 자기 입력을 되먹이지 않습니다. :Compute 계산 함수는 어떤 State도 :Set하지 않습니다 — 읽어서 조합해 돌려줄 뿐입니다. Effect가 자기 의존성이나 그 상류를 :Set하면 끝난 뒤 한 번 더 도는데, 이것은 지연 재실행이 그 꼬임을 푸는 정의된 예외입니다. 이 밖은 정의되지 않습니다. 다른 대상을 :Set하는 것은 두 핸들의 본업입니다.
같은 되쓰기가 Effect에서는 정의되고 Observer에서는 정의되지 않는 이유는 파동을 받는 방식에 있습니다. Effect는 자기 fn이 도는 동안 도착한 변경을 그 자리에서 돌리지 않고, fn이 돌아온 뒤로 미뤄 한 번 더 돌립니다. 그래서 fn 안에서 자기 의존성이나 그 상류를 :Set해도 — 직접이든, 다른 Effect를 거쳐서든(서로의 의존성을 맞추는 두 Effect), 그 의존성이 Blocker·Debounce 같은 게이트 뒤에 있든 — 새 파동은 바깥 실행과 얽히지 않고 다음 실행 하나로 접힙니다. 이 고리는 수렴할 때 정의됩니다: 값이 자리 잡아 더는 :Set이 일어나지 않으면 끝납니다. 수렴하지 않는 고리는 정의되지 않은 동작이고, 무한히 돌지 않도록 늘 켜진 한도가 Quad0263으로 끝냅니다(아래 q.Effect). Observer는 파동 안에서 변경이 도착할 때마다 그 자리에서 콜백이 불리고, 자기 재실행 루프도, 무엇을 이미 받았는지의 기록도 없습니다. 그래서 콜백이 자기 상류를 되쓰면 새 파동이 아직 끝나지 않은 바깥 파동 안에서 같은 핸들을 다시 부르고 두 파동이 얽힙니다 — 같은 장면이 Observer에서는 정의되지 않는 이유입니다(상류가 게이트 뒤에 있어도 같습니다). 긴 설명은 Quadnomicon 12 — 파동을 받는 두 방식.
이 페이지의 심볼: state:Observer(fn) · observer.Subscribed · observer:Subscribe() · observer:WeakSubscribe() · observer:Unsubscribe() · observer:WeakUnsubscribe() · q.Effect(fn, ...deps) · effect:Rerun() · effect:Subscribe() · effect:WeakSubscribe() · effect:Unsubscribe() · effect:WeakUnsubscribe()
-- 01장의 설정 모듈: quad_base에 quad_roblox를 설치하고 타입을 다시 내보낸다(시작하기 01 참고)local q = require("@game/ReplicatedStorage/Client/UI/Quad")핸들은 두 가지로 살아난다
섹션 제목: “핸들은 두 가지로 살아난다”만들어진 직후의 핸들은 아직 실행 자격이 없습니다(.Subscribed는 false). 자격을 얻는 경로는 둘이고, 둘 중 하나만 골라야 합니다.
- props의 숫자 키 자리에 핸들을 넣어 인스턴스에 매다는 것 — 그 인스턴스가 사는 동안만 실행 자격을 유지합니다. 인스턴스가 파괴되면
Observer는 관측을 멈추고(그 뒤로는 값이 바뀌어도 콜백이 불리지 않습니다 — 기본 Deferred 시그널 설정에선 파괴 즉시; Immediate 설정에선 파괴 파동 안에서 죽는 쪽의 cleanup·OnDestroyed가 부른Set이 어떤 핸들을 한 번 더 돌리는지는 엔진의 파괴·연결 해제 순서에 달려 있습니다 — 관측이며 계약이 아닙니다),Effect은 cleanup이 한 번 돈 뒤 멈춥니다. UI에 딸린 부수효과는 대개 이쪽입니다. - 전역 구독 —
:Subscribe()(강한 유지) 또는:WeakSubscribe()(약한 유지). 인스턴스와 무관하게 사는 구독입니다.
파괴 순서에 관해 정해 둔 것은 없습니다. 서브트리 안 인스턴스들의 파괴 순서는 정하지 않습니다 — 한 인스턴스 안의 cleanup·OnDestroyed가 다른 인스턴스의 것보다 먼저 도는지에 기대지 마세요. 또 죽어 가는 호스트의 Slot을 cleanup·OnDestroyed 안에서 CRUD하는 것은 정의되지 않은 동작입니다(죽은 Slot과 마운트 규칙).
둘을 겹칠 수는 없습니다. 이미 한쪽으로 살아 있는 핸들을 다른 쪽으로 다시 살리려 하면 거절합니다 — 묶인 핸들에 :Subscribe()/:WeakSubscribe()를 부르면 Observer: already bound to an Instance(Effect도 주어만 바뀐 같은 문구), 반대로 구독된 핸들을 숫자 키 자리(q.Elem "Frame" { h })에 놓으면 Quad0104 bindLifetime: value is already subscribed([2026-09-27 mock 실측] — 강한·약한 구독 둘 다).
숫자 키 자리에 넣기
섹션 제목: “숫자 키 자리에 넣기”local hp = q.Source(100)
local label = q.Elem "TextLabel" { Text = hp:Compute(function(self: q.StateData<number>) return `HP {self:Get()}` end),
-- 숫자 키 자리: 이 인스턴스의 수명에 매달린다 -- (strict: 이 자리의 무주석 콜백은 target의 타입을 잃습니다 — 값을 쓰려면 `target: q.StateData<number>`처럼 주석을 달 것) hp:Observer(function(target) print("hp is now", target:Get()) end), q.Effect(function() print("effect sees", hp:Get()) end, hp),}문자 키 자리에 넣으면 타입 우회 실수로 보고 거절합니다:
Quad0068 Observer: must be an array item, not the value of a {typeof(k)} keyQuad0068 Effect: must be an array item, not the value of a {typeof(k)} key
단 그 문자 키가 그 클래스의 반영 프로퍼티 이름이면 프로퍼티 핸들러가 먼저 받아 이 거절에 닿지 않습니다(생성된 props 타입이 strict에서 그 자리를 막는 것이 첫 방어입니다). q.debug를 켜면 그런 자리마다 경고 한 줄이 나옵니다(같은 요소·키는 한 번, 동작은 그대로).
state:Observer(fn)
섹션 제목: “state:Observer(fn)”시그니처
Observer: (self: StateData<T>, fn: ObserverFn<T>?) -> Observer
export type ObserverFn<T> = (targetState: StateData<T>, self: Observer, emitFrom: (Epoch | EpochSet)?) -> ()export type Observer = { read __quadObserver: true, read Subscribed: boolean, Subscribe: (self: Observer) -> Observer, WeakSubscribe: (self: Observer) -> Observer, Unsubscribe: (self: Observer) -> Observer, WeakUnsubscribe: (self: Observer) -> Observer,}인자
| 이름 | 타입 | 설명 |
|---|---|---|
fn |
ObserverFn<T>? |
관측 콜백. 생략하면 “언제나 관측만 하는” 유틸이 됩니다(내부적으로 대상의 값을 읽기만 합니다). |
콜백이 받는 셋:
| 자리 | 타입 | 설명 |
|---|---|---|
targetState |
StateData<T> |
관측 대상 노드의 핸들. 값은 :Get()으로 읽습니다 — 값이 인자로 오지 않습니다. |
self |
Observer |
이 핸들 자신. |
emitFrom |
(Epoch | EpochSet)? |
이 발화의 출처. 설치 발화와 보류분 재생 때는 **nil**이고, 게이트를 지나온 발화에서는 하나가 아니라 집합입니다. |
반환 — Observer 핸들. 아직 구독되지 않은 상태입니다.
Observer는 최종 소비자입니다. 관측한 값을 화면·로그·바깥 세계로 내보내는 끝자리이지, 값을 만들어 그래프에 다시 공급하는 자리가 아닙니다. 콜백이 다른 대상을 :Set하는 것은 정상입니다. 콜백이 자기 상류(관측 대상이나 그 원천)를 직접, 또는 다른 핸들을 거쳐 다시 :Set하는 것은 정의되지 않은 동작입니다 — Compute 안에서 :Set하는 것과 같은 종류이고, 그 공급 때문에 이 핸들과 같은 상류를 보는 핸들들이 몇 번, 어떤 순서로, 어떤 값으로 도는지 quad는 정하지 않습니다(같은 값으로 중복해 도는 것도 포함). 예를 들어 같은 Compute를 보는 형제 Observer 둘이 각자 그 원천을 되쓰면, 어느 쪽이 몇 번 어떤 값으로 도는지 정해져 있지 않습니다. 값을 이어 만들려면 Compute로 파생하세요 — 필요한 State를 ...deps로 선언하고 계산 함수 안에서 :Get()으로 읽습니다(03-state). 자기 입력을 되써서 바로잡아야 하면 Effect로 옮기세요 — 지연 재실행이 그 꼬임을 푸는 정의된 경로입니다(Effect에서 정의되는 같은 장면도 Observer에서는 정의되지 않습니다 — 상류가 Blocker·Debounce 같은 게이트 뒤에 있어도, 위 “파동을 받는 방식” 문단). 범위를 강제하려면 값을 받는 자리(입력 핸들러)에서 깎아 :Set하거나 src:Compute(...)로 깎은 값을 따로 두세요. Slot의 Offset/Length를 보는 콜백이 Slot을 CRUD해 그 값이 다시 바뀌는 고리도 여기 해당하지 않습니다 — 그 고리는 State가 아니라 Slot의 자리 계산을 거치고, Slot이 스스로 다시 계산해 맞춥니다(수렴할 때 정의된 경로 — 예: 옆 Slot의 길이에 따라 헤딩·그리드 칸을 넣고 빼기; 넣고 빼기를 값마다 뒤집는 콜백처럼 수렴하지 않는 고리는 정의되지 않은 동작이고, Offset을 보는 고리는 늘 켜진 한도 Quad0267로, Length를 보는 고리는 스택 오버플로로 끝납니다 — 06-slot). 바깥세상을 한 바퀴 도는 왕복도 여기 해당하지 않습니다 — Observer가 쓴 프로퍼티를 엔진이 q.Out·OnChange로 다시 올려 보내는 경로는 정상입니다(엔진은 같은 값을 다시 넣으면 신호를 내지 않고, q.Out은 같은 값을 거르므로 한 바퀴에서 멈춥니다). 정의되는 범위는 엔진이 quad가 쓴 값을 되돌려 보내는 왕복입니다 — 엔진 신호가 즉시 배달되는 환경에서 그 값을 처리하는 도중 돌아와도, 엔진이 값을 자기 표현으로 다듬어 돌려줘 한 바퀴를 더 돌아도 정의된 경로입니다(상류에 Animate가 있어 되돌아온 값이 Tween이 되어 같은 프로퍼티로 들어와도 — 그 프로퍼티 처리는 나중에 들어온 쪽에 자리를 내줍니다). 콜백이 스스로 바꾼 다른 값을 자기가 불린 그 자리로 되넣는 것(OnChange 콜백이 값을 깎아 같은 원천에 :Set)은 이 왕복이 아니라 위의 자기 상류 되먹임입니다 — 깎은 값은 src:Compute(...)로 따로 두세요. q.debug를 켜면 콜백이 도는 도중 같은 핸들에 변경이 다시 도착하는 것 — 자기 상류를 되먹였다는 신호 — 이 핸들마다 경고 한 줄로 드러납니다(던지지 않고, 동작은 그대로; 첫 발화 도중 보류되는 변경도 포함; 위 Slot 자리 계산을 거친 고리는 마운트·다시 앉히기까지 알리지 않습니다). props 자리에 놓인 State도 같습니다 — 그 값을 처리하는 동안 불린 콜백(Ref·Tween 콜백, 이벤트 연결 등)이 그 State를 되쓰면 알립니다. 정의된 경로 둘은 알리지 않습니다: 위의 q.Out·OnChange 엔진 왕복(엔진 신호가 즉시 배달되는 환경에서는 그 값을 처리하는 도중 동기로 돌아옵니다)과, Tween의 Completed에서 다음 트윈을 잇는 연쇄(스냅이라 동기로 불려도 — roblox/06). 이 둘이 빼 주는 것은 그 경로를 부른 그 State의 처리뿐입니다 — 사용자 Observer의 콜백이 그 State를 :Set했고 그 Completed(또는 왕복)가 이 Observer의 상류를 되쓰면, 그 고리는 알립니다.
동작
- 등록 즉시 한 번 발화합니다(
emitFrom = nil). 초기값 반영을 따로 적을 필요가 없습니다. - 핸들은 먼저 상류의 구독자 집합에 합류하고, 그다음 첫 발화를 합니다. 첫 발화 도중 대상 노드가 바뀌면 그 변경은 버려지지 않고 보류됩니다 — 첫 발화 안에서 콜백을 다시 돌리지 않고, 핸들이 묶이거나 구독될 때 한 번
emitFrom = nil로 반영됩니다(아직 살아나지 않은 핸들에 온 변경과 같은 규칙 — 아래 항목). 첫 발화 도중 변경이 오는 길은 콜백 자신(이나 콜백이 깨운 다른 코드)이 자기 상류를 되먹이는 것뿐이고, 그건 위 단락대로 정의되지 않은 동작입니다(q.debug면 이것도 경고 한 줄 — 한 번 되쓰고 멈추는 clamp 모양 포함). - 첫 발화의 콜백이 던지면 예외는
:Observer(...)호출 밖으로 나가고 핸들은 돌려받지 못합니다. 그 핸들은 상류의 구독자 집합(약한 참조)에 남아 있다가 수거되고, 그동안 변경이 와도 콜백은 돌지 않습니다. - 아직 살아나지 않은(구독도 바인딩도 안 된) 핸들에도 emit은 도착합니다. 다만 실행 자격이 없어 보류되고, 그 사이 몇 번이 왔든 살아나는 시점에 정확히 한 번,
emitFrom = nil로 재생됩니다. Source를 직접 관측하는 핸들은 이미 돈 리비전으로 다시 돌지 않습니다. 파동 도중 다른 콜백이 이 핸들을 살려(구독·바인딩) 재생이 먼저 돌았거나, 다른 콜백이 같은Source를 다시:Set해 안쪽 파동이 먼저 이 핸들에 닿았다면, 뒤늦게 도착하는 같은 리비전의 통지는 건너뜁니다. 이 보장은Source를 직접 관측할 때뿐입니다 — 파생 State(:Compute등)를 관측하는 핸들은 같은 두 경우에 같은 값으로 한 번 더 돌 수 있습니다([2026-09-30 mock 실측]; 몇 번째로 닿는지는 발화 순서처럼 정해져 있지 않습니다).- 콜백은 자기 자신을 구독하거나 묶을 수 없습니다. 콜백 안에서
:Subscribe()/:WeakSubscribe()를 부르면Observer: cannot change subscription from inside its own fn (or after its fn threw - a throw leaves the in-run flag up)(그 핸들 자신의 콜백이 도는 동안 같은 핸들을 숫자 키 자리에 놓으면Quad0087 Observer: cannot bind an Observer from inside its own fn (or after its fn threw - a throw leaves the in-run flag up)— 콜백 안에서 새로 만든Observer를 놓는 것은 통과합니다). 해제(:Unsubscribe()/:WeakUnsubscribe())는 콜백 안에서도 됩니다 — 해제는 콜백을 다시 부르지 않습니다. - 살아난 뒤의 콜백이 자기 관측 대상을
:Set하는 것도 자기 상류 되먹임이라 정의되지 않은 동작입니다. 관측(계약 아님): 지금은 새 파동이 콜백 안에서 곧바로 이 핸들을 다시 부르고(Effect처럼 끝난 뒤로 미루지 않습니다), 바깥 실행의 나머지는 되쓰기 전에 읽은 옛 값을 들고 이어집니다. 조건 없이 되쓰는 콜백은Source가 같은 값도 전파하므로 스택이 자라 넘칩니다. 대상을 되써 수렴시켜야 하면Effect로 옮기세요. - 콜백이 예외를 던지면 그 핸들은 굳습니다 — 이후 (재)구독과 다른 인스턴스에 묶기는 위 문구로 거부됩니다. 발화 자체는 막히지 않아 상류가 바뀌면 콜백은 계속 돕니다(quad는 예외를 잡지 않으므로 “던졌다”는 사실을 따로 기억하지 못합니다). 정리하려면
:Unsubscribe()(강한 구독) 또는 묶인 인스턴스의 파괴로 놓아주세요. - 인자 검증:
Quad0190 State: Observer fn must be a function or nil (got {typeof(fn)}) - nil makes the always-observe utility - 같은 원천을 구독한 Observer·Effect끼리의 발화 순서는 정해져 있지 않습니다 — 등록 순서도 아닙니다. 순서가 필요하면 한 콜백 안에서 차례대로 부르세요.
- 콜백 안에서 yield하지 마세요(Roblox의
task.wait등) — 정의되지 않은 동작입니다. 파동이 그 자리에서 멈추거나 같은 콜백이 겹쳐 돌 수 있고, 겹치면 나중 값의 콜백이 먼저 끝나 마지막으로 처리한 값이 낡은 값이 됩니다. 기다릴 일은 따로 띄운 코루틴으로 떼어 내세요. - 숫자 키 자리에 놓여 묶이는 순간 보류돼 있던 통지가 한 번 재생되는데, 그 콜백이 던지면 요소 생성자는 에러로 끝나지만 그 핸들은 실패한 인스턴스에 묶인 채 남습니다(정의되지 않은 동작 —
Effect도 같습니다). 같은 핸들로 다시 놓으면 원인 대신Quad0232 bindLifetime: value is already bound to another Instance가 납니다. 재시도는 새 핸들로 하세요 —q.Elem생성자 테이블 안에서 인라인으로 만드는 보통 코드는 자연히 그렇게 됩니다.
예제
local hp = q.Source(100)local log = {}
local observer = hp:Observer(function(target, self, emitFrom) table.insert(log, target:Get())end)print(#log) --> 1 (등록 즉시 1회)
hp:Set(80)print(#log) --> 1 (아직 살아나지 않았다 — 보류)
observer:Subscribe()print(#log) --> 2 (보류분 1회 재생, 최신값 80)
hp:Set(60)print(#log) --> 3관련 — 03-state · Quadnomicon Vol. 6
observer.Subscribed
섹션 제목: “observer.Subscribed”boolean 필드. 강한 구독뿐 아니라 약한 구독에서도 true가 됩니다 — “전역 구독(:Subscribe/:WeakSubscribe)이 붙어 있는가”를 나타내는 플래그이지 “강하게 잡혀 있는가”가 아닙니다. 숫자 키 자리에 묶여 살아 있는 핸들은 이 필드가 false입니다(그 경로는 인스턴스 수명이 자격을 주므로) — “지금 실행 자격이 있는가”를 묻는 것이 아닙니다. 강·약 구분은 해제할 때 어느 문을 써야 하는지로 드러납니다(아래 두 쌍).
Effect도 같은 이름의 플래그를 같은 뜻으로 갖습니다.
strict 캐비엇 — 한 함수 안에서 .Subscribed를 비교한 뒤 :Subscribe()/:Unsubscribe()를 부르고 같은 필드를 다시 비교하면, 타입 검사기가 앞 비교로 좁힌 값을 그대로 들고 있어 두 번째 비교를 모순으로 봅니다(런타임 값은 정상). 다시 읽어야 하면 함수를 거쳐 읽으세요 — local function subscribed(h) return h.Subscribed end.
observer:Subscribe()
섹션 제목: “observer:Subscribe()”시그니처 — Subscribe: (self: Observer) -> Observer (self 반환)
동작 — 강한 구독. 전역 레지스트리가 핸들을 강하게 잡으므로, 참조를 어디에도 남기지 않아도 수거되지 않습니다. .Subscribed를 올리고, 보류된 변경이 있으면 그 자리에서 한 번 재생합니다.
에러 — 이미 살아 있으면 Quad0089 Observer: already subscribed / Quad0230 Observer: already bound to an Instance. 콜백 안에서 부르면 Quad0088 Observer: cannot change subscription from inside its own fn (or after its fn threw - a throw leaves the in-run flag up).
observer:WeakSubscribe()
섹션 제목: “observer:WeakSubscribe()”시그니처 — WeakSubscribe: (self: Observer) -> Observer
동작 — 약한 구독. .Subscribed를 올리고 보류분을 재생하는 것은 :Subscribe()와 같지만, 레지스트리가 핸들을 잡아주지 않습니다 — 당신이 참조를 들고 있어야 살아 있습니다. 놓으면 수거되고 발화도 멈춥니다.
에러 — :Subscribe()와 같습니다.
observer:Unsubscribe()
섹션 제목: “observer:Unsubscribe()”시그니처 — Unsubscribe: (self: Observer) -> Observer
동작 — 강한 구독을 해제합니다(강·약 등록을 둘 다 지우고 .Subscribed를 내립니다). 엄격합니다 — 강하게 구독한 적이 없으면 거절합니다:
Quad0093 Observer: only a strongly subscribed Observer can be released with :Unsubscribe() - a weakly subscribed one must be released with :WeakUnsubscribe(), and one held only by a seat must be released by clearing that seat
콜백 안에서, 그리고 콜백이 던져 굳은 뒤에도 됩니다 — 해제는 콜백을 부르지 않습니다.
observer:WeakUnsubscribe()
섹션 제목: “observer:WeakUnsubscribe()”시그니처 — WeakUnsubscribe: (self: Observer) -> Observer
동작 — 약한 구독을 해제합니다. 관대합니다 — 구독한 적이 없거나 이미 풀린 핸들에 불러도 조용히 통과합니다. 단, 강한 유지가 남아 있으면 반쯤 풀린 핸들이 되므로 거절합니다:
Quad0092 Observer: subscribed strongly; use :Unsubscribe()
q.Effect(fn, ...deps)
섹션 제목: “q.Effect(fn, ...deps)”시그니처
Effect: (fn: EffectFn, ...any) -> Effect
export type EffectFn = (self: Effect) -> ...((dying: boolean) -> ())export type Effect = { read __quadEffect: true, read Subscribed: boolean, Rerun: (self: Effect) -> Effect, Subscribe: (self: Effect) -> Effect, WeakSubscribe: (self: Effect) -> Effect, Unsubscribe: (self: Effect) -> Effect, WeakUnsubscribe: (self: Effect) -> Effect,}인자
| 이름 | 타입 | 설명 |
|---|---|---|
fn |
EffectFn |
부수효과 본문. 인자로 핸들 자신을 받습니다(의존성 값은 넘어오지 않습니다 — 클로저로 직접 읽으세요). cleanup 함수를 돌려줄 수 있습니다. |
...deps |
State / Source / Ref |
이것들이 움직일 때마다 fn이 다시 돕니다 — 한 Set이 서로 다른 여러 dep을 거쳐 와도 1회이고(다이아몬드 흡수), 게이트를 거친 사본이 나중에 풀려도 이미 통지로 받은 리비전이면 0회입니다([2026-09-27 mock 실측]). 게이트가 막고 있는 동안 만든 Effect는 첫 실행에서 값을 읽었더라도 그 통지는 받은 적이 없으므로, 게이트가 풀리면 한 번 더 돕니다([2026-09-30 mock 실측]). |
반환 — Effect. 아직 구독되지 않은 상태입니다.
동작
- 생성하는 그 자리에서
fn이 한 번 돕니다. 구독·바인딩보다 먼저이며, 이 첫 실행은 미룰 수 없습니다. - 의존성은 생성자에서 한 번 검증합니다. 같은 것을 여러 번 넘기면 조용히 하나로 합쳐집니다.
Quad0094 Effect: fn must be a function (got {typeof(fn)})Quad0095 Effect: dep #{i} is nilQuad0096 Effect: dep #{i} is not a State/Source/Ref(번호는...deps에서의 자리입니다)
- cleanup은 함수 하나입니다. 타입의 가변 반환 표기는 “아무것도 안 돌려줘도 된다”를 위한 것이고(단 strict에서 한 갈래는 cleanup을, 다른 갈래는
return nil을 하는fn은 타입이 거부합니다 —return nil :: any또는 빈return function() end로 우회, [2026-09-27 실측]), 런타임이 소진하는 것은 첫 번째 반환 하나뿐입니다. 정리할 게 여럿이면 한 클로저로 묶으세요 —return function() a() b() end. 함수도nil도 아닌 값(숫자, 연결 객체 등)을 돌려주면 그 자리에서 던지고,fn이 던진 것과 같이 그Effect는 죽습니다 — 연결은return function() conn:Disconnect() end처럼 감싸세요.Quad0086 Effect: fn must return a cleanup function or nothing (got {typeof(cleanup)})
- cleanup 안에서 의존성을
:Set해도 됩니다 — 바로 뒤에 도는fn이 그 새 값을 읽고, 그것으로 한 사이클입니다(같은 입력으로 한 번 더 돌지 않습니다).fn안에서 의존성을:Set하면 한 번 더 돕니다. - cleanup이 도는 자리는 넷입니다 — 다음
fn실행 직전,:Unsubscribe(), 매달린 인스턴스가 파괴될 때, 그리고 인스턴스는 살아 있는데 그 숫자 키 자리가 다른 값으로 재구동될 때(자리를State<Effect?>로 잡아 두고 갈아 끼우는 경우 — 그 자리를 떠나는Effect의 cleanup이 한 번 돕니다). cleanup은 인자 하나dying을 받습니다 — 셋째 자리(인스턴스가 죽어서)에서만true이고 나머지 셋에서는false입니다. 죽을 때만 해야 할 정리와 자리를 떠날 때마다 할 정리를 이걸로 가릅니다. 인자를 안 받는function() … end도 그대로 됩니다.- Immediate 시그널 설정에서 파괴 파동 도중 그 자리를 갈아 끼우는 것은 정의되지 않은 동작입니다. 같은 파괴 파동 안에서 다른 파괴 처리(형제의 cleanup이나
OnDestroyed)가 죽어 가는 인스턴스의State<Effect?>자리를 다른 값으로Set하면, 떠나는 옛 값의 cleanup은 “자리를 떠남”으로 처리돼dying = false를 받고, 새 값은 이미 죽어 가는 인스턴스에 막 묶여 그 파괴 통지를 놓치므로 cleanup(과OnDestroyed)이 한 번도 돌지 않습니다([2026-09-27 mock 실측 — 관측, 계약 아님]). 기본 Deferred 설정(Roblox 기본값)에서는 파괴 처리가 돌기 전에 자리의 구독이 먼저 끊겨 이 창이 없습니다.
- Immediate 시그널 설정에서 파괴 파동 도중 그 자리를 갈아 끼우는 것은 정의되지 않은 동작입니다. 같은 파괴 파동 안에서 다른 파괴 처리(형제의 cleanup이나
- 살아나기 전의 의존성 변경은
Observer와 같이 보류됐다가 살아나는 시점에 한 번 재생됩니다.Source·Ref의존성은fn이 돌 때마다 그 리비전을 본 것으로 기록하므로, 파동 도중 되살아나 재생이 먼저 돌았다면 같은 파동의 뒤늦은 통지로 다시 돌지 않습니다 —Observer의 같은 항목과 같은 뜻이고, 파생 State 의존성은 이 경우 같은 값으로 한 번 더 돌 수 있다는 한계도 같습니다([2026-09-30 mock 실측]). fn이나 cleanup 안에서 자기를 (재)구독할 수 없습니다:Effect: cannot change subscription from inside fn or cleanup, or after this Effect's fn or cleanup has thrown - a thrown Effect stays dead; create a new one(그 핸들 자신의fn이나 cleanup이 도는 동안 같은 핸들을 숫자 키 자리에 놓으면Quad0087 Effect: cannot bind an Effect from inside its own fn or cleanup—fn/cleanup 안에서 새로 만든Effect를 놓는 것은 통과합니다). 해제(:Unsubscribe()/:WeakUnsubscribe())는fn안에서도 됩니다 —fn을 다시 부르지 않고,fn이 그 뒤 돌려준 cleanup은 저장되지 않고 즉시 소진됩니다(핸들이 더는 실행 자격이 없으므로).fn안에서 의존성(이나 그 상류 — 다른Effect를 거쳐도)을:Set하면 그 실행이 끝난 뒤 한 번 더 도는 지연 재실행이 됩니다. 재실행이 다시:Set하면 또 돌고, 그렇게 이어지다 값이 자리 잡아 끝나는 재실행은 정의된 동작입니다. 끝나지 않는 고리는 정의되지 않은 동작이고, 무한히 돌지 않도록 한 번의 반복 안에서fn이 1만 번 돈 뒤에도 또 다시 돌아야 하면(1만 1번째 실행 직전)Quad0263으로 멈추고 던집니다(늘 켜진 한도 —q.debug와 무관합니다; 그Effect는fn이 던진 것과 같이 죽습니다). 첫 실행이든 이후 실행이든 의존성을 계속 바꿔 수렴하지 않는fn이 여기 걸리고,source:Set은 같은 값을 다시 넣어도 전파하므로 조건 없이 같은 값을 되쓰는fn도 여기 해당합니다. 자기 의존성을 다듬어 되쓰는fn은source:SetIfChanged(v)로 쓰세요 — 값이 같으면(==) 아무것도 하지 않아 값이 자리 잡는 순간 반복이 끝납니다.Quad0263 Effect: fn re-ran 10000 times in one loop - the fn keeps setting one of its own deps (Source:Set propagates even an equal value; write with Source:SetIfChanged to skip it)
fn이 예외를 던지면 그Effect는 죽습니다 — 이후 재실행과 (재)구독이 막힙니다. cleanup이 던져도 같습니다 — 재실행·:Unsubscribe·호스트 파괴·:WeakUnsubscribe어느 자리에서 던졌든 그 뒤 재실행은 보류되고 (재)구독은 “cannot change subscription from inside fn or cleanup”으로 거부됩니다(던진 자리에 따라 강한 구독이 남거나, 호스트 파괴 자리였다면 재바인드도 막힙니다). 남는 일은:Unsubscribe()(강한 구독을 풀어 핸들과 상류를 놓아줌 — 그때 소진할 cleanup은 없습니다, 던진fn은 돌려준 게 없으니까) 또는:WeakUnsubscribe()뿐입니다. 죽은 핸들을 강한 구독에 둔 채 버리면 모듈 수명 동안 남습니다.fn과 cleanup 안에서 yield하지 마세요 — 정의되지 않은 동작입니다. 증상은Observer와 다릅니다:Effect는 겹친 요청을fn이 돌아온 뒤 한 번 더 도는 것으로 흡수하지만, yield하는 사이 묶인 인스턴스가 죽거나 자리에서 내려가면 죽음의 cleanup은 이미 지나간 뒤입니다. 그래도fn이 돌아오며 돌려준 cleanup은 버려지지 않습니다 — 핸들이 더는 실행 자격이 없으므로 그 자리에서 즉시 소진됩니다(인스턴스가 죽어서면dying = true, 자리에서 내려가서면false). 그 사이에fn이 잡은 자원은 그렇게 정리되지만, 순서와 타이밍은 보장하지 않습니다. 네트워크 왕복은fn밖(따로 띄운 코루틴)에서 하고 결과를State로 넣으세요.
예제
local hp = q.Source(100)
local effect = q.Effect(function(self) local value = hp:Get() print("hp:", value) return function() print("정리:", value) -- 다음 실행 직전 / 해제 / 파괴 때 정확히 한 번 endend, hp)-- 여기서 이미 한 번 돌았다
effect:Subscribe()hp:Set(80) -- 정리 → 재실행effect:Unsubscribe() -- 마지막 정리 1회관련 — How-To 04 네트워크·입력 브리지 · Quadnomicon Vol. 6
effect:Rerun()
섹션 제목: “effect:Rerun()”시그니처 — Rerun: (self: Effect) -> Effect (인자 없음, self 반환)
동작 — fn을 지금 다시 돌립니다(직전 cleanup을 먼저 소진하고). 실행 자격이 없으면(구독·바인딩 전, 파괴 파동 안, cleanup 실행 중) 돌지 않고 요청을 보류했다가 살아나는 시점에 한 번 재생합니다. 이미 fn이 도는 중이면 그 실행이 끝난 뒤로 미뤄집니다 — 생성 직후의 첫 실행도 마찬가지라, 첫 실행의 fn 안에서 부른 Rerun()(또는 의존성 :Set)은 아직 구독·바인딩 전이어도 보류되지 않고 그 실행이 끝난 자리에서 한 번 더 돕니다. 실행 자격은 “의존성 변경으로 다시 돌 수 있는가”를 가르는 조건이지, 명시적으로 요청한 재실행을 미루는 장치가 아닙니다.
effect:Subscribe()
섹션 제목: “effect:Subscribe()”시그니처 — Subscribe: (self: Effect) -> Effect
동작 — 강한 구독. 레지스트리가 핸들을 강하게 잡습니다. .Subscribed를 올린 뒤, 보류된 변경이 있으면 한 번 재생합니다.
에러 — Quad0089 Effect: already subscribed / Quad0230 Effect: already bound to an Instance / Quad0088 Effect: cannot change subscription from inside fn or cleanup, or after this Effect's fn or cleanup has thrown - a thrown Effect stays dead; create a new one
effect:WeakSubscribe()
섹션 제목: “effect:WeakSubscribe()”시그니처 — WeakSubscribe: (self: Effect) -> Effect
동작 — 약한 구독. 나머지는 :Subscribe()와 같지만 레지스트리가 핸들을 잡아주지 않습니다 — 참조를 놓으면 수거됩니다. 이때 의존성 쪽에 걸려 있던 등록(Ref 콜백, State·Source의 구독자 집합 항목 — 둘 다 약한 참조이고 그 등록을 살려 두는 것은 Effect입니다)도 함께 사라집니다.
에러 — Quad0089 Effect: already subscribed / Quad0230 Effect: already bound to an Instance / Quad0088 Effect: cannot change subscription from inside fn or cleanup, or after this Effect's fn or cleanup has thrown - a thrown Effect stays dead; create a new one
effect:Unsubscribe()
섹션 제목: “effect:Unsubscribe()”시그니처 — Unsubscribe: (self: Effect) -> Effect
동작 — 강한 구독을 해제하고 마지막 cleanup을 정확히 한 번 소진합니다. 엄격합니다:
Quad0093 Effect: only a strongly subscribed Effect can be released with :Unsubscribe() - a weakly subscribed one must be released with :WeakUnsubscribe(), and one held only by a seat must be released by clearing that seat
(이 문에서 거절당하면 cleanup은 건드려지지 않습니다.) fn 안에서도, fn이 던져 죽은 뒤에도 됩니다 — fn을 부르지 않습니다. cleanup 안에서 자기를 해제하는 것은 그 cleanup이 다음 fn 직전·인스턴스 파괴·자리 교체로 소진될 때만 뜻이 있습니다 — 이 메소드가 소진한 cleanup 안에서 다시 :Unsubscribe()를 부르면 강한 구독은 이미 풀린 뒤라 위 문구로 던집니다(이중 해제).
effect:WeakUnsubscribe()
섹션 제목: “effect:WeakUnsubscribe()”시그니처 — WeakUnsubscribe: (self: Effect) -> Effect
동작 — 약한 구독을 해제합니다. 관대하며(구독한 적 없어도 통과) cleanup을 건드리지 않습니다. 강한 유지가 남아 있으면 거절합니다:
Quad0092 Effect: subscribed strongly; use :Unsubscribe()
관련 — 03-state · sugar/06-blocker · Quadnomicon Vol. 3 — 메모리 토폴로지
Answered: 0 / 0 (0%)