콘텐츠로 이동

Ref

Ref<T>값 상자입니다. 값 하나와 리비전 하나를 들고, 값이 바뀔 때 등록된 콜백을 부릅니다. State가 아닙니다 — 전파도, :Get도, :Compute도 없습니다. quad가 Ref에 주는 유일한 추가 의미는 “직전과 구별되는 표식을 나른다”(= Epoch)입니다.

Ref가 실제로 빛나는 자리는 컴포넌트가 만든 실물 인스턴스를 밖으로 꺼내오는 통로입니다. props의 배열 부분에 Ref를 놓으면 quad가 그 자리에 만들어진 인스턴스를 :Set 해줍니다. 언제 채워지느냐가 셋을 가릅니다 — q.PreRef는 배열 위치와 무관하게 가장 먼저, q.Ref자기 배열 자리가 처리될 때(형제 자식들과 같은 순서 위에서), q.PostRef는 배열 부분과 해시 부분이 전부 끝난 뒤 채워집니다.

이 페이지의 심볼: q.Ref · q.PreRef · q.PostRef · ref.Value · ref.Revision · ref.Callbacks · ref.WeakCallbacks · ref:Set · ref:Callback · ref:WeakCallback · ref:Uncallback · ref:Wait · ref:Unwrap

이 페이지의 모든 예제는 아래 프롤로그를 전제합니다.

-- 설치 경로는 프로젝트 구성에 따라 다르다(00-installation 참고)
local Quad = require(<quad-base 모듈 경로>)
local QuadRoblox = require(<quad-roblox 모듈 경로>).QuadRoblox
local q = Quad:UseProvider(QuadRoblox) -- quad-roblox 백엔드 설치: D/Tween/Animate/OnChange가 생긴다
local D = q.D
type RefCallback<T> = (value: T, ref: Ref<T>) -> ()
type Ref<T> = {
read __quadRefAccepts: (T) -> (), -- 타입 전용 반공변 팬텀 필드(children 자리의 클래스 검사용)
Value: T,
Revision: number,
Callbacks: { [RefCallback<T> | thread]: true },
WeakCallbacks: { [RefCallback<T>]: true },
Set: <Self>(self: Self, value: T) -> Self,
Callback: <Self>(self: Self, fn: RefCallback<T>) -> Self,
WeakCallback: <Self>(self: Self, fn: RefCallback<T>) -> Self,
Uncallback: <Self>(self: Self, fn: RefCallback<T>) -> Self,
Wait: <Self>(self: Self, thread: thread?) -> Self,
Unwrap: (self: Ref<T>) -> StripNil<T>,
}
type PreRef<T> = Ref<T> & { read __quadPreRef: true }
type PostRef<T> = Ref<T> & { read __quadPostRef: true }

콜백의 두 번째 인자는 Ref 자신입니다(= 그 Epoch). 평범한 사용자 콜백은 그냥 무시하면 됩니다.

변경 메소드는 전부 self를 돌려주므로 체이닝됩니다. 반환 타입이 <Self> 제네릭이라 q.PreRef(x):Callback(fn)도 여전히 PreRef입니다.

PreRef/PostRef는 props의 배열 부분 리터럴 항목으로만 놓을 수 있습니다. 해시 키의 값으로 두거나 Source/Store 값에 담아 배열 자리에 닿게 하면 전용 가드가 그 자리에서 던집니다(PostRef도 주어만 바뀐 같은 문구).

  • PreRef: must be an array item, not the value of a {typeof(k)} key
  • PreRef: must be a literal array item — it reached array index {k} through a State/Store value, which the pre-pass cannot see

평범한 Ref에는 그 가드가 없습니다. 배열 자리에 닿기만 하면 되므로 Source/Store 값에 담아 넣어도 그대로 채워지고, 다른 자리에 두면 “Ref를 잘못 놓았다”는 진단 대신 그 자리의 주인이 내는 에러를 봅니다.

  • Modifier 필드 — Modifier: field "{k}" cannot hold a handler-layer value (Ref/Observer/Effect/Slot/Modifier)
  • 아무 핸들러도 맡지 않는 해시 키 — Dispatch: no handler matched key {k} (value: {typeof(v)}, brand: Ref)
  • 반영 프로퍼티 키 — 생성된 props 타입이 그 값 자리에서 Ref를 거부합니다.

하나의 Ref한 자리에만 놓을 수 있습니다. 생명주기 결합이 그 자리에서 던집니다 — 같은 인스턴스의 두 자리면 bindLifetime: value is already bound to this Instance (the same handle at two positions?), 다른 인스턴스면 bindLifetime: value is already bound to another Instance(생명주기는 백엔드가 심으므로 이 두 문구는 quad-roblox의 것입니다).

PreRef/PostRef는 그 위에 일회용이기까지 합니다.

  • PreRef: already fired — a PreRef is one-shot, make a new one for each instance

시그니처

Ref: <T>(default: T) -> Ref<T>

인자

이름 타입 설명
default T 초기 .Value

반환 — 새 Ref<T>. .Revision은 0, 콜백 테이블 둘은 비어 있습니다.

동작

  • 타입 파라미터는 하나입니다. nil이 들어올 수 있는 자리는 호출자가 넓힙니다 — q.Ref(nil :: Frame?)처럼 씁니다. props 배열 부분에 놓는 Ref는 인스턴스가 채워지기 전까지 비어 있으므로 사실상 항상 T? 형태입니다.
  • 배열 부분에 놓으면, 그 배열 자리가 처리되는 시점:Set(inst)가 불립니다. 디스패치는 배열 부분을 인덱스 순서로 돌기 때문에, 앞 자리의 자식이 먼저 놓인 뒤 이 Ref가 채워지고, 뒤 자리는 그다음입니다. 그 자리가 철거될 때는 :Set(nil)로 되돌아갑니다.
  • RefEpoch이기도 합니다(다중 태깅) — q.isRefq.isEpoch가 둘 다 참입니다.

예제

local boxRef = q.Ref(nil :: Frame?)
local panel = D.Frame {
boxRef, -- 배열부: 만들어진 Frame이 여기 담긴다
D.TextLabel { Text = "제목" },
}

관련컴포넌트 조합

시그니처

PreRef: <T>(default: T) -> PreRef<T>

동작 — 런타임은 Ref와 완전히 같고, 브랜드와 마커 필드만 다릅니다. 차이는 발화 시점입니다.

PreRef는 그 인스턴스에 아무 일도 일어나기 전에 채워집니다. 디스패치의 pre-pass가 배열 부분을 한 번 훑으면서 위치와 무관하게 전부 먼저 발화시킵니다(PreRef끼리의 상대 순서는 배열 인덱스 순서). 그래서 같은 props 안의 이벤트 핸들러나 프로퍼티 계산이 이미 채워진 .Value를 볼 수 있습니다.

일회용입니다 — 한 번 발화한 PreRef를 다른 인스턴스에 다시 놓으면 에러입니다. 인스턴스마다 새로 만드세요.

예제

local inputRef = q.PreRef(nil :: TextBox?)
local form = D.Frame {
D.TextBox { inputRef },
D.TextButton {
Text = "지우기",
Activated = function()
inputRef:Unwrap().Text = "" -- pre-pass가 이미 채웠다
end,
},
}

관련컴포넌트 조합

시그니처

PostRef: <T>(default: T) -> PostRef<T>

동작PreRef의 거울입니다. 이 인스턴스의 배열 부분(자식·서브트리)와 해시 부분(프로퍼티·이벤트)가 전부 끝난 뒤 채워집니다. 여러 개면 배열 인덱스 순서대로 발화합니다.

“자식이 전부 붙었다”까지가 계약입니다 — 부모에 붙었는지는 계약이 아닙니다(어느 쪽으로도 보장하지 않습니다). 완성된 서브트리를 재는 코드(레이아웃 측정 등)가 이 자리입니다.

PreRef와 마찬가지로 일회용이고, 배열 부분 리터럴 전용입니다.

예제

local doneRef = q.PostRef(nil :: Frame?)
local card = D.Frame {
doneRef,
D.TextLabel { Text = "A" },
D.TextLabel { Text = "B" },
}
doneRef:Callback(function(inst)
if inst then
-- 자식 둘이 이미 다 붙어 있다
end
end)

시그니처

Value: T

동작 — 지금 담긴 값. 직접 읽습니다. :Set이 가장 먼저 갱신하는 것이 이 필드이므로, 콜백 안에서 ref.Value를 읽으면 이미 새 값입니다.

직접 대입해도 막지는 않지만 리비전도 콜백도 돌지 않습니다 — 쓸 때는 항상 :Set을 쓰세요.

props 배열 부분에 놓은 Ref는 채워지기 전까지 nil이라 타입이 사실상 Ref<T?>입니다. 런타임 보장이 있는 자리라면 :Unwrap()이 그 nil을 벗겨줍니다.

시그니처

Revision: number

동작:Set 때마다 바뀌는 표식입니다. RefEpoch으로 만드는 필드입니다.

계약은 같다/다르다뿐입니다. 크기 비교나 증가 방향에 의존하지 마세요 — 32비트 범위를 순환합니다.

시그니처

Callbacks: { [RefCallback<T> | thread]: true }

동작 — 강하게 붙들린 콜백들의 집합(배열이 아닙니다). 키가 콜백 함수 자신이라 중복 등록은 자동으로 하나로 합쳐집니다 — “몇 번 등록했나”라는 질문이 존재하지 않습니다.

:Wait()의 대기 코루틴도 여기 thread 키로 들어갔다가 :Set이 소진합니다.

시그니처

WeakCallbacks: { [RefCallback<T>]: true }

동작 — 같은 집합이되 weak 키 테이블입니다. 여기에만 등록된 콜백은 다른 곳에서 붙들지 않으면 GC 대상이 됩니다.

약한 등록이 프리미티브이고, :Callback은 그 위에 “GC로부터 지켜주기”만 더한 것입니다. :Set은 두 테이블을 합쳐 순회하므로 발화 동작은 둘이 같습니다.

시그니처

Set: <Self>(self: Self, value: T) -> Self

인자

이름 타입 설명
value T 새 값

반환self.

동작 — 순서가 계약입니다.

  1. .Value를 먼저 확정합니다.
  2. .Revision을 바꿉니다.
  3. CallbacksWeakCallbacks스냅샷으로 합쳐 순회합니다. 순회 중 등록/해제를 해도 이번 순회는 흔들리지 않고, 순회 중 해제된 콜백은 건너뜁니다.

콜백이 순회 도중 다시 :Set을 부르면(재진입), 안쪽 순회가 이미 모두에게 더 새 값을 전달했으므로 바깥 순회는 남은 콜백을 건너뛰고 멈춥니다 — 콜백이 낡은 값을 받는 일은 없습니다.

thread 키(= :Wait 대기자)는 소진되고 Ref 자신을 인자로 resume됩니다. 대기 중인 코루틴 자신이나 그 코루틴이 resume한 코루틴에서 :Set을 부르면 그 자리에서 던집니다.

  • Ref: cannot :Set from the coroutine that is waiting on this Ref, nor from one it resumed (Wait(thread) registered a {status} coroutine)

예제

local ref = q.Ref(0)
ref:Callback(function(v) print("값", v) end) -- 등록 즉시 0으로 한 번 불린다
ref:Set(1):Set(2) -- 체이닝

시그니처

Callback: <Self>(self: Self, fn: RefCallback<T>) -> Self

인자

이름 타입 설명
fn (value: T, ref: Ref<T>) -> () 값이 바뀔 때 부를 함수

반환self.

동작

  • 등록하는 그 자리에서 한 번 즉시 호출됩니다 — 지금 담긴 값으로. 아직 비어 있으면 nil로 불립니다(nil 가드는 호출자 몫입니다).
  • 콜백을 강하게 붙듭니다(Callbacks). 중복 등록은 집합 의미로 한 번입니다.
  • 인자 검증: Ref:Callback: callback must be a function (got {typeof(fn)}).

시그니처

WeakCallback: <Self>(self: Self, fn: RefCallback<T>) -> Self

동작:Callback과 모든 동작이 같고, GC 보호만 없습니다(WeakCallbacks에 weak 키로 들어갑니다). 등록 즉시 한 번 호출되는 것도 같습니다.

콜백을 다른 곳에서 붙들고 있고 그 수명에 Ref 구독을 맞추고 싶을 때 씁니다. 인자 검증: Ref:WeakCallback: callback must be a function (got {typeof(fn)}).

시그니처

Uncallback: <Self>(self: Self, fn: RefCallback<T>) -> Self

동작 — 등록을 해제합니다. 두 테이블 모두에서 지웁니다 — 약하게 등록한 콜백도 이걸로만 뗄 수 있습니다.

등록이 집합이라 “몇 번 뗄지”를 셀 필요가 없습니다. 인자 검증: Ref:Uncallback: callback must be a function (got {typeof(fn)}).

시그니처

Wait: <Self>(self: Self, thread: thread?) -> Self

인자

이름 타입 설명
thread thread? 주면 등록만 하고 즉시 반환합니다. 생략하면 지금 코루틴을 등록하고 여기서 yield합니다

반환self. 그래서 ref:Wait().Value가 갓 도착한 값을 읽는 관용구입니다.

동작

  • 항상 다음 :Set을 기다립니다. 이미 값이 차 있어도 기다립니다 — Ref<T?>에서는 nil도 정당한 값이라 “차 있는가”를 여기서 판정할 수 없기 때문입니다. 미리 확인하고 싶으면 if ref.Value then … else ref:Wait().Value end 관용구를 쓰세요.
  • 인자 없이 부르려면 yield 가능한 코루틴 안이어야 합니다.
    • Ref: Wait() must be called from a yieldable coroutine (pass a thread to register a waiter without yielding)
  • 명시적 thread를 주면 등록만 합니다 — 코루틴은 자기 자신만 멈출 수 있지 남의 코루틴을 대신 멈출 수 없기 때문입니다.
    • Ref: Wait(thread) expects a thread (got {typeof(thread)})
  • 대기자는 Callbacksthread 키로 들어가고, :Set이 그 키를 소진하며 resume합니다. 중복 등록은 집합이라 무해합니다.

예제

local ref = q.Ref(nil :: string?)
task.spawn(function()
local value = ref:Wait().Value -- 다음 :Set 까지 여기서 멈춘다
print("도착", value)
end)
ref:Set("hello")

시그니처

Unwrap: (self: Ref<T>) -> StripNil<T> -- StripNil은 T에서 nil 성분만 벗기는 타입 함수

반환 — 담긴 값. 타입에서는 nil만 제거됩니다(Frame?Frame).

동작 — 배열 부분에 놓은 PreRef는 이벤트 콜백이 돌기 전에 반드시 채워지는데, 타입은 여전히 Ref<Frame?>라 매번 if inst then 가드를 써야 했습니다. :Unwrap()은 그 자리를 위한 것입니다.

규약이지 강제가 아닙니다 — 런타임 보장이 있는 자리에서만 쓰세요. 비어 있으면 호출한 줄을 blame하며 던집니다.

  • Ref:Unwrap: the Ref is empty (Value is nil) — not filled yet, or never placed

Ref/PreRef/PostRef 셋 다 같은 동작입니다.

예제

local btnRef = q.PreRef(nil :: TextButton?)
local button = D.TextButton {
btnRef,
Text = "보내기",
Activated = function()
btnRef:Unwrap().BackgroundTransparency = 0.5 -- 가드 없이
end,
}

관련디버깅과 문제 해결