콘텐츠로 이동

Source

Source는 값을 직접 쓸 수 있는 유일한 반응형 노드입니다. 파생 노드인 State는 읽기 전용이고, 전파는 언제나 어떤 Source의 :Set/:Emit에서 시작합니다.

이 페이지의 심볼: q.Source(v) · source:Set(v) · source:SetIfChanged(v) · source:SetAndDispose(v) · source:Emit() · source.Revision · Source가 물려받는 State 메소드

-- 01장의 설정 모듈: quad_base에 quad_roblox를 설치하고 타입을 다시 내보낸다(시작하기 01 참고)
local q = require("@game/ReplicatedStorage/Client/UI/Quad")

Source의 생성·읽기·쓰기와 :Compute/:Depend 같은 파생 자체는 백엔드가 없어도 동작합니다. 백엔드가 필요한 것은 이 노드에 붙는 구독 핸들의 생명주기입니다 — :Observer로 만든 핸들을 실제로 살리는 것(:Subscribe, 인스턴스 바인딩)부터가 백엔드 몫입니다(05-observer-effect 참고).


시그니처

Source: <T>(v: T) -> Source<T>
export type Source<T> = State<T> & {
read Revision: number,
Set: (self: Source<T>, v: T) -> Source<T>,
SetIfChanged: (self: Source<T>, v: T) -> Source<T>,
SetAndDispose: (self: Source<T>, v: T) -> Source<T>,
Emit: (self: Source<T>) -> Source<T>,
}

인자

이름 타입 설명
v T 초기값. nil도 값으로 허용됩니다(q.Source<<number?>>(nil) — 타입 인자는 명시해야 Source<number?>가 됩니다).

반환 — Source<T>. 만들어진 시점의 Revision은 0입니다.

동작

  • 값으로 Modifier는 담을 수 없습니다. 생성자에서 걸리면 Quad0181 Source: cannot hold a Modifier as a Source value
  • 판정 술어는 셋 다 참입니다 — q.isSource(s), q.isEpoch(s), q.isState(s). 세 번째는 술어 합성의 결과이고, 값 자체가 State 브랜드에 등록되지는 않습니다.
  • print(source)는 Source(<현재 값>) 모양으로 찍힙니다. 읽기만 하며 계산을 유발하지 않습니다.

예제

local hp = q.Source(100)
print(hp:Get()) --> 100

관련 — Getting Started 03 값이 흐르게 하기 · Quadnomicon Vol. 1


시그니처

Set: (self: Source<T>, v: T) -> Source<T>

인자

이름 타입 설명
v T 새 값. Modifier는 거부됩니다.

반환 — self(체이닝 가능).

동작

  • 같은 값을 다시 넣어도 언제나 리비전을 올리고 전파합니다. 값 비교로 중복을 죽이지 않습니다 — 중복 제거는 하류의 판단이고(에폭 판정·게이트), == 비교는 테이블을 제자리에서 고친 변경을 조용히 삼켜버리기 때문입니다. 값이 같으면 쓰기를 건너뛰는 프레임워크에서 오셨다면 기대가 반대일 수 있습니다 — 같은 값을 무시하려면 :SetIfChanged(v)를 쓰세요.
  • :Get()은 넣은 참조를 그대로 돌려줍니다(사본이 아닙니다). 테이블을 담아 두고 제자리에서 고쳤다면 :Emit()으로 신호만 보내면 됩니다.
  • Modifier를 넣으면 Quad0181 Source: cannot Set a Modifier as a Source value
  • 전파를 받는 하류(파생 State·Observer·Effect)끼리의 순서는 정해져 있지 않습니다 — 등록 순서도 아닙니다.

예제

local hp = q.Source(100)
hp:Set(80):Set(60) -- self를 돌려주므로 이어 쓸 수 있다
print(hp:Get()) --> 60

시그니처

SetIfChanged: (self: Source<T>, v: T) -> Source<T>

인자

이름 타입 설명
v T 새 값. Modifier는 거부됩니다(:Set과 같은 검사).

반환 — self(체이닝 가능). 값을 실제로 썼는지와 상관없이 같습니다 — 썼는지가 궁금하면 if v ~= src:Get() then src:Set(v) end로 풀어 쓰면 됩니다.

동작

  • v == 현재 값이면 아무것도 하지 않습니다 — 값도 Revision도 그대로이고 하류에 아무것도 전파하지 않습니다. 다르면 :Set(v)과 똑같습니다.
  • 비교는 Luau의 == 그대로입니다. 테이블은 참조로 비교하므로 제자리에서 고친 테이블을 다시 넣으면 “같다”로 보고 건너뜁니다 — 그때는 :Emit()을 쓰세요. 두 값이 같은 __eq 메타메서드를 가지면 그 메타메서드가 같음을 정합니다(값 객체가 자기 뜻대로 같음을 정의할 수 있습니다; 메타메서드가 서로 다르면 Luau는 그것을 부르지 않고 다르다고 봅니다).
  • NaN은 자기 자신과 같지 않으므로 NaN을 넣으면 늘 씁니다.
  • q.Tween{…} 값은 만들 때마다 새 테이블이라 늘 다른 값입니다 — 목표가 같은 Tween을 다시 넣어도 이 메서드는 씁니다. 같은 목표로의 재발행을 접는 것은 프로퍼티 핸들러의 Dedup 몫입니다.
  • 값 검사는 비교보다 먼저 합니다 — Modifier를 넣으면 현재 값과 같든 다르든 Quad0181 Source: cannot Set a Modifier as a Source value.
  • q.Store의 필드도 Source이므로 store.hp:SetIfChanged(v)처럼 그대로 씁니다.

쓰는 자리 — 기본은 :Set이고, 이 메서드는 되먹임 고리를 끊어야 하는 좁은 자리를 위한 것입니다.

  • Effect가 자기 의존성을 다듬어 되쓸 때 — :Set으로 쓰면 같은 값도 전파해 지연 재실행이 수렴하지 않고(정의되지 않은 고리) 늘 켜진 한도 Quad0263으로 끝납니다. :SetIfChanged면 값이 자리 잡는 순간 반복이 끝납니다.
  • 엔진 → Source 브릿지와 입력 핸들러 — 프로퍼티 변경 신호나 입력 이벤트가 이미 들고 있는 값을 다시 올려 보낼 때. q.Out이 안에서 이것으로 되씁니다.
  • Observer 콜백이 자기 상류를 되쓰는 것은 이 메서드를 써도 여전히 정의되지 않은 동작입니다 — state:Observer(fn)의 “Observer는 최종 소비자입니다” 문단이 정본입니다.

예제

local hp = q.Source(120)
q.Effect(function()
hp:SetIfChanged(math.clamp(hp:Get(), 0, 100)) -- 100으로 자리 잡으면 더 쓰지 않는다
end, hp)
print(hp:Get()) --> 100

시그니처

SetAndDispose: (self: Source<T>, v: T) -> Source<T>

인자

이름 타입 설명
v T 새 값. Modifier는 거부됩니다(:Set과 같은 검사).

반환 — self(체이닝 가능).

동작

  • :Set(v)을 한 뒤, 바뀌기 전 값을 q.dispose로 파괴합니다. 옛 값을 :Get()으로 미리 잡아 둘 필요가 없습니다.
  • 순서가 이 메서드의 요점입니다. 전파가 끝나야 그 값을 앉혀 두었던 하류(숫자 키 자리, Slot)가 자리를 놓아 주고, 그 뒤라야 dispose가 받아 줍니다 — 앉아 있는 값을 먼저 q.dispose하면 Quad0179로 거부되고, :Set 전에 inst:Destroy()로 직접 지우는 것은 정의되지 않은 동작입니다.
  • 값 검사는 쓰기보다 먼저 합니다 — Modifier를 넣으면 Quad0181 Source: cannot Set a Modifier as a Source value이고, 값도 옛 값도 그대로입니다.
  • 쓰기는 :Set과 같아서 같은 값도 전파합니다.
  • 옛 값을 파괴하지 않고 넘어가는 경우는 둘입니다.
    • 옛 값이 nil이나 q.None일 때 — 빈 자리라 지울 것이 없습니다. 처음 채우는 호출도 그대로 쓸 수 있습니다.
    • 옛 값이 방금 넣은 v와 같은 참조일 때 — 지우면 방금 넣은 값을 지우게 됩니다.
  • 그 밖의 옛 값은 q.dispose의 계약 그대로입니다. Slot이면 트리째, 백엔드 요소(Roblox면 Instance)면 파괴합니다. dispose가 받지 않는 값이면 그 에러가 그대로 납니다 — 숫자·문자열·평범한 테이블처럼 파괴할 수 없는 값이면 Quad0180 dispose: this backend cannot dispose this value, 아직 다른 자리가 쥐고 있으면 Quad0179 dispose: this value is still held by ….
  • ⚠️ 이 에러들은 쓰기가 이미 끝난 뒤에 납니다. 새 값은 들어가 있고 하류도 전파를 받았으며, 되돌리지 않습니다.
  • ⚠️ Source와 값이 놓인 자리 사이에 Debounce나 Blocker처럼 전파를 늦추거나 막는 것이 있으면, :SetAndDispose가 돌아올 때 옛 값은 아직 자리에 있어 Quad0179가 납니다. 그런 경로에서는 자리가 실제로 빈 뒤에 q.dispose를 직접 부르세요.
  • q.Store의 필드도 Source이므로 store.panel:SetAndDispose(v)처럼 그대로 씁니다.

예제

local panel = q.Source<<Instance?>>(nil)
local screen = q.Elem "Frame" { panel } -- panel이 쥔 Frame이 이 자리에 앉는다
panel:SetAndDispose(q.Elem "Frame" {}) -- 처음: 옛 값이 nil이라 지울 것이 없다
panel:SetAndDispose(q.Elem "Frame" {}) -- 바꾸기: 새 Frame이 앉은 뒤 옛 Frame이 파괴된다
panel:SetAndDispose(nil) -- 비우기: 자리를 비운 뒤 마지막 Frame을 파괴한다

시그니처

Emit: (self: Source<T>) -> Source<T>

반환 — self.

동작

  • 값은 건드리지 않고 리비전만 올려 하류에 전파합니다. 담아둔 테이블·배열을 제자리에서 고친 뒤 알릴 때 쓰는 문입니다.
  • 루트 Source에만 있는 개념입니다. 파생 State에는 :Emit이 없습니다 — 파생 노드의 값은 자기 것이 아니기 때문입니다.

예제

local items = q.Source({ "a" })
table.insert(items:Get(), "b") -- 제자리 변경 — 이것만으로는 아무도 모른다
items:Emit() -- 이제 하류가 안다

시그니처

read Revision: number

동작

  • :Set/:Emit이 일어날 때마다(:SetIfChanged는 실제로 쓸 때만) 바뀌는 불투명한 표식입니다. 하류는 “이 값을 마지막으로 본 뒤에 움직였는가”를 이 값의 일치/불일치로만 판정합니다.
  • 크고 작음을 비교하지 마세요 — 갱신 방향은 계약이 아니며 32비트 범위를 순환합니다. 저장해 둔 값과 같으면 그 사이 아무 일도 없었다는 뜻, 다르면 움직였다는 뜻, 그게 전부입니다.
  • 갓 만든 Source의 값은 0입니다.

관련 — Quadnomicon Vol. 1 — Revision과 EpochMap


Source는 타입으로도 런타임으로도 State를 포함합니다 — 파생 메소드는 별도 변환 없이 Source 위에서 바로 부를 수 있습니다.

메소드 문서
:Get() 03-state
:Compute(fn, ...deps) 03-state
:Depend(...) 03-state
:Apply(factory) 03-state
:Observer(fn) 05-observer-effect
:Gate(setup) 03-state

한 가지 예외는 :Get()입니다. Source는 자기 값을 그대로 돌려주는 자기만의 :Get()을 갖습니다(캐시도 재계산도 없습니다). 나머지는 State의 구현을 그대로 씁니다.

local hp = q.Source(100)
local label: q.State<string> = hp:Compute(function(self)
return `HP {self:Get()}`
end)

관련 — 03-state · 04-store · How-To 02 폼 검증