Tag / Attr
Tag와 Attr은 props의 배열 부분에 놓는 값 객체입니다. 인스턴스에 이름표를 붙이고(Tag), 이름 붙은 값을 심습니다(Attr). 둘 다 불변이고, 모든 연산이 새 값을 돌려줍니다.
Modifier와 달리 이 둘은 프로퍼티가 아니라 엔진의 별도 채널로 갑니다. quad-base는 그 채널을 직접 건드리지 않고 주입된 엔진 op 셋(addTag/removeTag/setAttr)에 넘깁니다 — 백엔드가 그걸 채웁니다. Roblox 백엔드는 각각 CollectionService와 SetAttribute로 이어집니다.
이 페이지의 심볼: q.Tag · tag:Added · tag:Removed · tag:Contains · tag:Names · tag:Apply · q.Tag.Merged · q.Attr · attr:NameMap · q.Attr.Merged · q.Attr.Overridden · q.AttrKey · q.StringAttr · q.NumberAttr · q.BooleanAttr
이 페이지의 모든 예제는 아래 프롤로그를 전제합니다.
-- 설치 경로는 프로젝트 구성에 따라 다르다(00-installation 참고)local Quad = require(<quad-base 모듈 경로>)local QuadRoblox = require(<quad-roblox 모듈 경로>).QuadRobloxlocal QuadTypes = require(<quad-types 모듈 경로>) -- 타입 주석용(`QuadTypes.None` 등)local q = Quad:UseProvider(QuadRoblox) -- quad-roblox 백엔드 설치: D/Tween/Animate/OnChange가 생긴다local D = q.DTag
섹션 제목: “Tag”Tag는 이름의 집합입니다. 순서가 없고 중복이 없습니다.
type TagNames = string | { read [number]: TagNames } | TagMarker
type Tag = { read __quadTag: true, Added: (self: Tag, names: TagNames) -> Tag, Removed: (self: Tag, names: TagNames) -> Tag, Contains: (self: Tag, ...string) -> boolean, Names: (self: Tag) -> () -> string?, Apply: <U>(self: Tag, factory: (Tag) -> U) -> U,}이름 자리에는 어디서나 셋 중 하나가 옵니다 — 문자열, 다른 Tag(그 집합 전체), 또는 그것들의 평범한 리스트(중첩 가능). 세 문을 하나가 지키므로 생성자든 :Added든 규칙이 같습니다.
Tag: names must be strings, Tags, or a plain {...} list of those (got {typeof(v)})빈 문자열은 태그 이름이 아닙니다 — Tag: names must be strings — an empty string is not a tag name.
참조 계수로 붙습니다. 한 인스턴스의 여러 자리에서 같은 이름을 얹으면 자리마다 세어지고, 마지막 자리가 물러날 때 비로소 엔진에서 떨어집니다. 같은 불변 Tag 객체를 두 자리에 놓아도 두 번으로 셉니다. 엔진 호출은 이름별로 묶여 한 번에 나갑니다.
q.Tag(...names)
섹션 제목: “q.Tag(...names)”시그니처
Tag: setmetatable<{ Merged: (...TagMarker) -> Tag }, { __call: (self: any, ...TagNames) -> Tag }>인자
| 이름 | 타입 | 설명 |
|---|---|---|
...names |
TagNames |
문자열 / Tag / 그것들의 평범한 리스트. 개수 제한 없음 |
반환 — 새 Tag(frozen).
동작
- 인자가 없어도 됩니다 —
q.Tag()는 빈 태그 집합입니다. - 인자마다 같은 문을 지납니다.
q.Tag(otherTag, "focus", { "a", "b" })처럼 섞어 쓸 수 있습니다. - nil 슬롯은 조용히 무시되지 않고 검증 에러가 됩니다.
- 리스트는 배열이어야 합니다 — 해시 키가 섞이거나 nil 구멍이 있으면 에러입니다.
Tag: names must be strings, Tags, or a plain {...} list of those — a name list is an array, not a hash tableTag: names must be strings, Tags, or a plain {...} list of those — a name list cannot have nil holesTag: names must be strings, Tags, or a plain {...} list of those (got a table with a metatable)Tag: names must be strings, Tags, or a plain {...} list of those (got an AttrKey/Mapper descriptor)
예제
local interactive = q.Tag("clickable", "focusable")local button = D.TextButton { interactive, q.Tag("primary"), Text = "확인",}관련 — 컴포넌트 조합, 확장 가능한 디스패치 엔진
tag:Added(names)
섹션 제목: “tag:Added(names)”시그니처
Added: (self: Tag, names: TagNames) -> Tag반환 — 이름이 더해진 새 Tag. 원본은 그대로입니다.
동작 — 인자 하나를 받고, 그 하나가 문자열이든 Tag든 리스트든 상관없습니다(생성자와 같은 문). 이미 있는 이름을 더하면 집합이라 변화가 없습니다.
예제
local base = q.Tag("card")local selected = base:Added("selected")local both = base:Added({ "selected", "hovered" })tag:Removed(names)
섹션 제목: “tag:Removed(names)”시그니처
Removed: (self: Tag, names: TagNames) -> Tag반환 — 이름이 빠진 새 Tag.
동작 — :Added와 똑같이 검증합니다. 없는 이름을 빼는 것은 조용한 no-op이지만, 이름이 아닌 것을 넘기면 에러입니다.
tag:Contains(...names)
섹션 제목: “tag:Contains(...names)”시그니처
Contains: (self: Tag, ...string) -> boolean인자
| 이름 | 타입 | 설명 |
|---|---|---|
...names |
string |
확인할 이름들 |
반환 — 준 이름이 전부 있으면 true.
동작 — 가변 인자입니다. 이름을 하나도 안 주면 공허하게 참입니다(동적으로 언팩한 목록이 비었을 때를 위한 것). 문자열이 아닌 인자는 조용한 false가 아니라 에러입니다.
Tag:Contains: names must be strings (got {typeof(name)} at argument {i})
예제
local tag = q.Tag("card", "selected")print(tag:Contains("card")) -- trueprint(tag:Contains("card", "selected")) -- true (전부 있어야 참)print(tag:Contains("card", "missing")) -- falsetag:Names()
섹션 제목: “tag:Names()”시그니처
Names: (self: Tag) -> () -> string?반환 — 이름을 하나씩 돌려주고 끝나면 nil을 주는 이터레이터 함수.
동작 — 집합이라 순서 보장이 없습니다. for 문의 제너릭 자리에 그대로 넣어 씁니다.
예제
local tag = q.Tag("a", "b")for name in tag:Names() do print(name) -- 순서는 보장되지 않는다endtag:Apply(factory)
섹션 제목: “tag:Apply(factory)”시그니처
Apply: <U>(self: Tag, factory: (Tag) -> U) -> U반환 — factory(self)의 결과.
동작 — Modifier:Apply와 같은 순수 호출 슈거입니다. 계약도 추가 의미도 없습니다. 함수가 아닌 것을 넘기면 그 자리에서 던집니다.
Tag: Apply factory must be a function (got {typeof(factory)})
q.Tag.Merged(...tags)
섹션 제목: “q.Tag.Merged(...tags)”시그니처
Merged: (...TagMarker) -> Tag반환 — 모든 입력의 이름을 합친 새 Tag.
동작 — 손실 없는 합집합입니다. q.Tag(tag1, tag2, …)와 결과가 같고, Tag만 받는 엄격한 철자라는 점만 다릅니다 — 문자열이나 리스트를 섞어 넘길 수 없습니다.
Tag.Merged: arguments must be Tag values
예제
local a, b = q.Tag("x"), q.Tag("y")local merged = q.Tag.Merged(a, b) -- {x, y}local same = q.Tag(a, b) -- 같은 결과, 느슨한 철자Attr
섹션 제목: “Attr”Attr은 이름 → 값 맵입니다. 값은 원시 값이거나 State/Source이거나 q.None입니다.
type Attr = { read __quadAttr: true, NameMap: (self: Attr) -> { [string]: any },}
type AttrConstructor = setmetatable<{ Merged: (...Attr) -> Attr, Overridden: (...Attr) -> Attr,}, { __call: (self: any, ...any) -> Attr }>삭제 규칙 — None만이 지운다
섹션 제목: “삭제 규칙 — None만이 지운다”세 가지를 구별하세요.
| 한 일 | 결과 |
|---|---|
값에 q.None을 둔다 |
엔진에서 그 속성이 삭제됩니다 |
바인딩된 State가 q.None이 된다 |
같습니다 — 삭제됩니다 |
그 Attr 값 객체를 다른 것으로 교체한다 |
옛 속성 값이 엔진에 그대로 남습니다 |
세 번째가 중요합니다. 자리에서 물러나는 Attr은 구독을 끊고 이름 소유권만 반납할 뿐, 엔진 쪽에는 손대지 않습니다. 지우고 싶으면 명시적으로 q.None을 보내야 합니다. nil은 그 자리에서 거부되므로(평범한 테이블은 nil을 담을 수 없어 항목이 조용히 사라지기 때문) 지우기의 유일한 표현이 q.None입니다.
q.Attr(...)
섹션 제목: “q.Attr(...)”시그니처
__call: (self: any, ...any) -> Attr인자
| 이름 | 타입 | 설명 |
|---|---|---|
... |
Store / Attr / 평범한 테이블 |
하나의 이름 맵으로 평탄화됩니다 |
반환 — 새 Attr(frozen).
동작
- **
Store**를 주면 그 스토어가 선언한 키 전부가 이름이 되고, 값은 각Source슬롯 자체입니다. 스토어를 그대로 속성으로 내보내는 관용구입니다. - **다른
Attr**을 주면 그 이름 맵이 합쳐집니다. - 평범한 테이블(메타테이블 없는)을 주면
{ [name] = value }가 그대로 항목이 됩니다. 값은 원시 값 /State/q.None입니다. - 이름이 겹치면 뒤 인자가 조용히 이깁니다(=
Attr.Overridden의 정책). 겹침을 에러로 만들고 싶으면q.Attr.Merged를 쓰세요.
에러
Attr: arguments must be Stores, Attr values or plain tablesAttr: attribute name cannot be emptyAttr: plain-table keys must be non-empty stringsAttr: attribute "{name}" cannot be a function — attribute values are raw values or StateAttr: attribute "{name}" cannot be a table — attribute values are raw values, None or State (got {typeof(v)})
한 인스턴스의 두 자리에 같은 그룹 값 객체를 놓으면 그 자리에서 던집니다 — Attr: the same group value is placed at two positions of this instance.
예제
local hp = q.Source(100 :: number | QuadTypes.None)local stats = D.Frame { q.Attr({ Hp = hp, Name = "hero" }), D.TextLabel { Text = "체력" },}hp:Set(80) -- 속성 Hp가 따라 바뀐다hp:Set(q.None) -- 속성 Hp가 삭제된다관련 — 확장 가능한 디스패치 엔진
attr:NameMap()
섹션 제목: “attr:NameMap()”시그니처
NameMap: (self: Attr) -> { [string]: any }반환 — 평탄화된 이름 맵(frozen). 값은 저장된 그대로 — Source 슬롯은 핸들 자체로, None은 None으로 나옵니다.
동작 — 읽기용입니다. 합성 결과를 검사하거나 테스트에서 확인할 때 씁니다.
예제
local attr = q.Attr({ A = 1, B = q.Source("x"), C = q.None })local map = attr:NameMap()print(map.A) -- 1print(map.C == q.None) -- true (:Get()을 부르지 않는다)q.Attr.Merged(...)
섹션 제목: “q.Attr.Merged(...)”시그니처
Merged: (...Attr) -> Attr반환 — 합쳐진 새 Attr.
동작 — Attr 값만 받고, 이름이 겹치면 에러입니다. 두 출처가 같은 속성을 주장하는 사고를 조용히 넘기지 않으려는 자리입니다.
Attr.Merged: arguments must be Attr valuesAttr.Merged: attribute name "{name}" appears more than once
q.Attr.Overridden(...)
섹션 제목: “q.Attr.Overridden(...)”시그니처
Overridden: (...Attr) -> Attr반환 — 합쳐진 새 Attr.
동작 — Attr 값만 받되, 겹치면 뒤 인자가 조용히 이깁니다. 생성자 q.Attr(...)의 정책과 같고, 입력을 Attr로만 제한한 철자입니다.
Attr.Overridden: arguments must be Attr values
예제
local theme = q.Attr({ Accent = "blue", Size = 1 })local override = q.Attr({ Accent = "red" })
local final = q.Attr.Overridden(theme, override) -- Accent = "red"-- q.Attr.Merged(theme, override) 였다면 겹침 에러q.AttrKey(name)
섹션 제목: “q.AttrKey(name)”시그니처
AttrKey: (name: string) -> AttrKeyObjecttype AttrKeyObject = { Name: string }인자
| 이름 | 타입 | 설명 |
|---|---|---|
name |
string |
속성 이름(빈 문자열 불가) |
반환 — 그 이름의 키 객체.
동작 — 속성 단일 키 프리미티브입니다. props의 해시 키 자리에 놓아 씁니다 — D.Frame { [q.AttrKey("Hp")] = 100 }.
- 값 타입을 모르는 무타입 프리미티브입니다. 값 검증은 백엔드의
setAttr몫입니다. 패밀리 슈가(StringAttr등)가 못 덮는 엔진 고유 타입(Color3,UDim2,Instance…)이 이 키의 자리입니다. - 이름별 weak 캐시를 지납니다 — 무언가가 붙들고 있는 동안
q.AttrKey("Hp") == q.AttrKey("Hp")가 성립합니다. - 값에
q.None을 두면 그 속성이 삭제됩니다. - 한 인스턴스의 같은 이름을 서로 다른 키 객체가 주장하면 그 자리에서 던집니다 —
AttrKey: attribute "{k.Name}" is already bound by another owner.
에러
AttrKey: name must be a non-empty string
예제
local marker = D.Frame { [q.AttrKey("SpawnColor")] = Color3.new(1, 0, 0), -- 패밀리가 못 덮는 엔진 타입}q.StringAttr(name, value)
섹션 제목: “q.StringAttr(name, value)”시그니처
StringAttr: AttrSugar<string>type AttrSugar<T> = (name: string, value: T | StateMarker<T> | None) -> Attr인자
| 이름 | 타입 | 설명 |
|---|---|---|
name |
string |
속성 이름(빈 문자열 불가) |
value |
string / State<string> / q.None |
값. nil은 거부됩니다 |
반환 — 항목이 하나뿐인 Attr(즉 q.Attr({ [name] = value })).
동작 — 타입드 스칼라 슈가입니다. 자기 핸들러를 갖지 않고 그룹 경로를 그대로 씁니다. 차이는 값 검증뿐입니다 — 패밀리는 자기 타입을 알기 때문에 원시 값의 타입을 여기서 확인합니다(State와 q.None은 그대로 통과합니다).
배열 부분에 놓으므로 strict 모드에서도 타입이 섭니다. AttrKey의 해시 키 형태를 대신하는 자리입니다.
에러
StringAttr: name must be a non-empty stringStringAttr: value of "{name}" must not be nil — use None to deleteStringAttr: value of "{name}" must be a string, State or None (got {typeof(value)})
예제
local label = q.Source("hero")local card = D.Frame { q.StringAttr("Label", label), q.StringAttr("Kind", "unit"),}label:Set("villain") -- 속성 Label이 따라 바뀐다local blank = D.Frame { q.StringAttr("Label", q.None) } -- 값 자리의 None: 그 속성을 지운다q.NumberAttr(name, value)
섹션 제목: “q.NumberAttr(name, value)”시그니처
NumberAttr: AttrSugar<number>동작 — q.StringAttr과 모든 것이 같고 원시 값 타입만 number입니다.
에러 — NumberAttr: name must be a non-empty string / NumberAttr: value of "{name}" must not be nil — use None to delete / NumberAttr: value of "{name}" must be a number, State or None (got {typeof(value)})
q.BooleanAttr(name, value)
섹션 제목: “q.BooleanAttr(name, value)”시그니처
BooleanAttr: AttrSugar<boolean>동작 — q.StringAttr과 모든 것이 같고 원시 값 타입만 boolean입니다.
에러 — BooleanAttr: name must be a non-empty string / BooleanAttr: value of "{name}" must not be nil — use None to delete / BooleanAttr: value of "{name}" must be a boolean, State or None (got {typeof(value)})
예제
local hp = q.Source(100)local alive = D.Frame { q.NumberAttr("Hp", hp), q.BooleanAttr("Friendly", true),}