Skip to content

OnChange·OnEvent·Out

This content is not available in your language yet.

GetPropertyChangedSignal과 엔진 이벤트 바인딩을 props의 숫자 키 자리에 놓는 값으로 만든 것입니다. Tag나 생명주기 훅과 같은 자리에 놓입니다. 셋 다 클래스를 먼저 받고, 그다음 이름, 마지막에 콜백(또는 되쓸 Source)을 받습니다.

[3.4.0] 3.3까지는 q.OnChange "TextBox" "Text" (fn)·q.Out "TextBox" "Text" (src)처럼 이름부터 받았고, 이름 집합은 생성 범위 전체를 합친 맵이었습니다(클래스마다 타입이 다른 이름은 any). 3.4.0부터 클래스를 먼저 받아(q.OnChange "TextLabel" "Text" (fn)) 그 클래스의 타입으로 검사합니다. 옛 두 인자 꼴은 Quad0281로 거부됩니다. q.OnEvent는 3.4.0에 새로 생겼습니다.

이 페이지의 심볼: q.OnChange "Class" "Name" (fn) · q.OnEvent "Class" "Name" (fn) · q.Out "Class" "Name" (src)

-- 01장의 설정 모듈: quad_base에 quad_roblox를 설치하고 타입을 다시 내보낸다(시작하기 01 참고)
local q = require("@game/ReplicatedStorage/Client/UI/Quad")
local Frame = q.Elem "Frame"
local OnFrameChange = q.OnChange "Frame" -- 클래스 단계도 꺼내 둘 수 있다

시그니처

OnChange: <C>(class: C & keyof<ModClassMap>) -> <K>(name: K & keyof<읽기 표면>) -> (fn: (값) -> ()) -> OnChangeDescriptor<K>

인자 — 세 단계로 받습니다.

단계 받는 것 설명
클래스 클래스 이름 Modifier·Spread와 같은 맵(조상 클래스 포함). 놓을 요소의 클래스나 그 조상
이름 프로퍼티 이름 그 클래스의 읽기 표면(쓰기 가능 + 읽기 전용) — 오타는 타입 에러
콜백 (newValue) -> () 그 프로퍼티가 바뀔 때 불린다. 파라미터 타입은 클래스와 이름에서 정해진다 — 주석을 생략해도 잘못 쓰면 타입 에러가 나지만, 편집기의 hover·자동완성은 주석을 달아야 나온다

값이 Instance인 프로퍼티의 콜백 인자는 T?입니다. 예를 들어 "Parent"는 Instance? — 인스턴스가 트리에서 빠지는 순간 엔진이 nil을 주고, 그 순간을 잡는 것이 Parent를 지켜보는 주된 이유입니다. function(p: Instance)로 주석하면 타입 에러이고, 본문에서 nil을 먼저 걸러야 합니다. 같은 이유로 AncestryChanged 이벤트의 둘째 인자 parent도 Instance?입니다(02-elem 이벤트 절).

반환 — frozen 디스크립터 { Name, Callback }(타입은 OnChangeDescriptor<K>). 캐시가 없어 호출마다 새 값입니다.

예제

local size = q.Source(UDim2.fromScale(1, 0))
local box = Frame {
Size = size,
OnFrameChange "AbsoluteSize" (function(v: Vector2) -- 읽기 전용 프로퍼티도 된다
print(v.X, v.Y)
end),
OnFrameChange "Visible" (function(v) -- 주석을 생략해도 검사는 된다(v를 string으로 쓰면 에러)
local _: boolean = v
end),
}

동작

  • 콜백은 새 값을 받습니다 — 핸들러가 inst:GetPropertyChangedSignal(name)에 연결하고, 신호가 올 때 그 시점의 inst[name]을 읽어 넘깁니다. Deferred 신호 모드에서는 쓰기 한 번당 전달 한 번이고 값은 전달 시점에 읽힙니다.

  • 초기값도 콜백에 닿습니다 — 단, 쓰는 값이 엔진 기본값과 다를 때만. props의 숫자 키가 문자 키보다 먼저 처리되므로, 같은 props에 적은 프로퍼티 쓰기가 이미 연결된 바인딩에 도착합니다. 값이 기본값과 같으면(Frame { Visible = true }) 엔진이 동일값 대입에 시그널을 쏘지 않아 발화가 없습니다(엔진 0회 — 테스트용 mock 백엔드는 값 비교 없이 쏘므로 1회).

    local seen = {}
    q.Elem "TextLabel" {
    Text = "a", -- 문자 키: 아래 바인딩이 연결된 뒤에 쓰인다
    q.OnChange "TextLabel" "Text" (function(v)
    table.insert(seen, v) -- seen[1] == "a" (SignalBehavior가 Deferred면 프레임 끝에 — 만든 직후엔 비어 있음)
    end),
    }
  • 같은 이름을 두 번 적으면 둘 다 연결됩니다. 문자 키 형태였다면 조용히 마지막 것만 남았을 자리라, 일부러 숫자 키 자리 형태로 만든 것입니다.

  • 숫자 키 자리를 차지하지만 길이는 0입니다 — 물리 자식이 아니라서 형제 자식의 오프셋에 기여하지 않습니다.

  • State에 담아 반응형으로 바꿔 끼울 수 있습니다.

    local desc = q.Source(OnFrameChange "Visible" (function(v: boolean) end))
    local box = Frame { desc }

    그 자리에 q.None을 발행하면 연결이 끊기고, 새 디스크립터를 발행하면 하나만 다시 연결됩니다. 같은 값 dedup은 없습니다(Connect가 멱등이 아니라서, 재발행마다 Disconnect+Connect 한 번).

  • 클래스 단계는 놓을 요소의 클래스나 그 조상을 적으세요. 디스크립터의 타입은 이름만 기억하므로 다른 클래스의 요소에 놓아도 타입은 막지 않지만, 그 클래스에 없는 프로퍼티면 런타임에 엔진이 자기 에러를 냅니다. 한 모듈에 디스크립터가 천 개를 넘기면 Studio 기본 한도에서 타입 검사가 무너지기 시작합니다([2026-10-20 실측] — 02-elem).

읽기 표면

OnChange가 받는 이름 집합은 그 클래스의 읽기 표면입니다. 그래서 props의 문자 키로는 쓸 수 없는 읽기 전용 프로퍼티도 여기서는 유효합니다.

q.OnChange "Frame" "AbsoluteSize" (function(v: Vector2) end) -- OK
q.OnChange "Frame" "AbsolutePosition" (function(v: Vector2) end) -- OK
q.OnChange "TextLabel" "TextBounds" (function(v: Vector2) end) -- OK

이름이 그 클래스에 없으면 타입 검사에서 걸립니다. 타입을 우회해 넘긴 이름은 런타임에 엔진이 자기 에러를 냅니다 — quad는 프로퍼티 존재 여부를 다시 검사하지 않습니다.

이름의 타입은 클래스가 정합니다 — UIStroke의 Color는 Color3, UIGradient의 Color는 ColorSequence입니다(3.3에서는 이런 이름 여섯이 any였습니다).

에러

상황 문구
클래스 이름이 문자열이 아니거나 "" Quad0278 OnChange: the first argument must be a class name string (got {…})
클래스 단계나 이름 단계에 인자를 둘 이상(3.3의 q.OnChange("Text", fn), 또는 손으로 옮기다 만 q.OnChange "TextLabel" ("Text", fn)) Quad0281 OnChange: each stage takes one argument - write q.OnChange "Class" "Name" (…) (3.3's q.OnChange(name, …) form is gone)
이름이 문자열이 아니거나 "" Quad0221 OnChange: property name must be a non-empty string
콜백이 함수가 아님 Quad0222 OnChange: callback for "{name}" must be a function (got {typeof(fn)})

시그니처

OnEvent: <C>(class: C & keyof<ModClassMap>) -> <K>(name: K & keyof<이벤트>) -> (fn: 그 이벤트의 콜백) -> OnEventDescriptor<K>

엔진 이벤트를 숫자 키 자리 디스크립터로 연결합니다. 문자 키 MouseEnter = fn과 같은 연결이지만, 문자 키는 한 이벤트에 콜백 하나인 데 비해 디스크립터는 몇 개든 놓을 수 있습니다 — 훅 둘이 각자 MouseLeave를 거는 경우, 또는 Spread로 묶어 넘기는 핸들러가 요소의 자기 핸들러와 겹치는 경우에 씁니다.

local hovered = q.Source(false)
local OnGui = q.OnEvent "GuiObject"
local hoverHook = q.Spread "GuiObject" {
OnGui "MouseEnter" (function(x: number, y: number) hovered:Set(true) end),
OnGui "MouseLeave" (function() hovered:Set(false) end),
}
Frame {
hoverHook,
MouseLeave = function() print("요소 자신의 핸들러도 같이 불린다") end,
}
  • 콜백은 엔진 인자만 받습니다(self·Instance 없음) — 문자 키 이벤트와 같습니다. 파라미터 타입은 클래스와 이벤트 이름에서 정해집니다.
  • 철거될 때 연결을 끊습니다. 길이 0, State에 담아 바꿔 끼우기, 같은 이벤트 여럿 — OnChange와 같습니다.
  • 이벤트 이름이 그 클래스에 없으면 타입 에러이고, 타입을 우회한 이름은 연결하는 순간 엔진이 자기 에러를 냅니다.

에러

상황 문구
클래스·이름 단계(Quad0278·Quad0281) OnChange와 같습니다
이벤트 이름이 문자열이 아니거나 "" Quad0279 OnEvent: event name must be a non-empty string
콜백이 함수가 아님 Quad0280 OnEvent: callback for "{name}" must be a function (got {typeof(fn)})

시그니처

Out: <C>(class: C & keyof<ModClassMap>) -> <K>(name: K & keyof<읽기 표면>) -> (src: Source<값>) -> OnChangeDescriptor<K>
-- src는 쓸 수 있는 값(Source)만 — :Compute 결과는 여기서 거부된다

엔진이 바꾼 프로퍼티 값을 Source에 되쓰는 슈거입니다. OnChange 위에 얹힌 팩토리라 돌려주는 값도 같은 디스크립터이고, 같은 숫자 키 자리에 놓습니다. 내려가는 방향은 평범한 문자 키 그대로라, 두 방향을 나란히 적습니다.

local text = q.Source("")
local box = q.Elem "TextBox" {
Text = text, -- in: Source → 프로퍼티
q.Out "TextBox" "Text" (text), -- out: 프로퍼티 → Source
}
-- 사용자가 타이핑하면 text:Get()이 따라오고, text:Set("…")하면 입력창이 따라간다

인자

단계 받는 것 설명
클래스·이름 OnChange와 같은 읽기 표면
src Source 되쓸 곳. 값 타입은 그 프로퍼티의 읽기 타입이고, 값이 Instance인 프로퍼티는 Source<T?>도 받습니다(Adornee 등). :Compute 결과 같은 파생 State는 안 됩니다 — Set이 없어 주석 달린 변수(local s: q.State<UDim2>)면 타입 검사에서 걸리고(무주석 인라인 :Compute 결과는 타입이 비어 검사를 지나칩니다), 런타임이 거부합니다

동작

  • 신호가 올 때 새 값이 src:Get()과 같으면 쓰지 않습니다 — 되쓰기가 src:SetIfChanged(v)입니다. 왕복을 끊기 위해서가 아닙니다 — 엔진은 같은 값을 다시 대입해도 변경 신호를 내지 않아 Source → 프로퍼티 → 신호 → Source 고리는 저절로 멈춥니다. 건너뛰는 이유는 메아리입니다: 숫자 키가 문자 키보다 먼저 처리되므로 Text = text의 첫 쓰기가 이미 연결된 이 콜백에 닿는데, Source:Set은 같은 값에도 늘 전파하므로(source:Set(v)) 건너뛰지 않으면 text의 모든 구독자가 바인딩마다 한 번 더 헛돕니다.
  • 그 밖은 OnChange와 같습니다 — 길이 0, 같은 이름 여럿 가능, State에 담아 바꿔 끼우기.
  • 포커스나 커서는 건드리지 않습니다. 타이핑 중에 바깥에서 text:Set하면 입력창이 그 값으로 덮입니다 — 그런 정책이 필요하면 FocusLost 이벤트로 확정하는 쪽을 직접 짜세요.
  • 입력을 다듬어(앞뒤 공백 깎기 등) 쓰는 권장 경로는 how-to 02 §6 입력 다듬기에 있습니다.
  • 그래서 잘라서 되쓰는 자리에는 q.Out이 맞지 않습니다. 최대 길이처럼 엔진 값을 잘라 원천에 적을 때, 원천이 이미 한도 값이면 잘린 값이 원천과 같아 q.Out은 건너뛰고 입력창에는 넘친 글자가 남습니다. 그 자리는 q.OnChange 콜백에서 같은 값도 전파하는 src:Set으로 적으세요 — 그래야 Text = src 바인딩이 엔진 쪽을 되돌립니다(how-to 02 §6 길이 제한).

에러

상황 문구
클래스·이름 단계(Quad0278·Quad0281) OnChange와 같습니다
이름이 문자열이 아니거나 "" Quad0223 Out: property name must be a non-empty string
셋째 인자가 Source가 아님 Quad0224 Out: "{name}" needs a Source to write back into (got {…}) — :Compute 결과면 got a read-only State (a :Compute result?)

관련