콘텐츠로 이동

Tag / Attr

TagAttr은 props의 배열 부분에 놓는 값 객체입니다. 인스턴스에 이름표를 붙이고(Tag), 이름 붙은 값을 심습니다(Attr). 둘 다 불변이고, 모든 연산이 새 값을 돌려줍니다.

Modifier와 달리 이 둘은 프로퍼티가 아니라 엔진의 별도 채널로 갑니다. quad-base는 그 채널을 직접 건드리지 않고 주입된 엔진 op 셋(addTag/removeTag/setAttr)에 넘깁니다 — 백엔드가 그걸 채웁니다. Roblox 백엔드는 각각 CollectionServiceSetAttribute로 이어집니다.

이 페이지의 심볼: 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 모듈 경로>).QuadRoblox
local QuadTypes = require(<quad-types 모듈 경로>) -- 타입 주석용(`QuadTypes.None` 등)
local q = Quad:UseProvider(QuadRoblox) -- quad-roblox 백엔드 설치: D/Tween/Animate/OnChange가 생긴다
local D = q.D

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 객체를 두 자리에 놓아도 두 번으로 셉니다. 엔진 호출은 이름별로 묶여 한 번에 나갑니다.

시그니처

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 table
    • Tag: names must be strings, Tags, or a plain {...} list of those — a name list cannot have nil holes
    • Tag: 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 = "확인",
}

관련컴포넌트 조합, 확장 가능한 디스패치 엔진

시그니처

Added: (self: Tag, names: TagNames) -> Tag

반환 — 이름이 더해진 Tag. 원본은 그대로입니다.

동작 — 인자 하나를 받고, 그 하나가 문자열이든 Tag든 리스트든 상관없습니다(생성자와 같은 문). 이미 있는 이름을 더하면 집합이라 변화가 없습니다.

예제

local base = q.Tag("card")
local selected = base:Added("selected")
local both = base:Added({ "selected", "hovered" })

시그니처

Removed: (self: Tag, names: TagNames) -> Tag

반환 — 이름이 빠진 새 Tag.

동작:Added똑같이 검증합니다. 없는 이름을 빼는 것은 조용한 no-op이지만, 이름이 아닌 것을 넘기면 에러입니다.

시그니처

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")) -- true
print(tag:Contains("card", "selected")) -- true (전부 있어야 참)
print(tag:Contains("card", "missing")) -- false

시그니처

Names: (self: Tag) -> () -> string?

반환 — 이름을 하나씩 돌려주고 끝나면 nil을 주는 이터레이터 함수.

동작 — 집합이라 순서 보장이 없습니다. for 문의 제너릭 자리에 그대로 넣어 씁니다.

예제

local tag = q.Tag("a", "b")
for name in tag:Names() do
print(name) -- 순서는 보장되지 않는다
end

시그니처

Apply: <U>(self: Tag, factory: (Tag) -> U) -> U

반환factory(self)의 결과.

동작Modifier:Apply와 같은 순수 호출 슈거입니다. 계약도 추가 의미도 없습니다. 함수가 아닌 것을 넘기면 그 자리에서 던집니다.

  • Tag: Apply factory must be a function (got {typeof(factory)})

시그니처

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이름 → 값 맵입니다. 값은 원시 값이거나 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 }>

세 가지를 구별하세요.

한 일 결과
값에 q.None을 둔다 엔진에서 그 속성이 삭제됩니다
바인딩된 Stateq.None이 된다 같습니다 — 삭제됩니다
Attr 값 객체를 다른 것으로 교체한다 옛 속성 값이 엔진에 그대로 남습니다

세 번째가 중요합니다. 자리에서 물러나는 Attr은 구독을 끊고 이름 소유권만 반납할 뿐, 엔진 쪽에는 손대지 않습니다. 지우고 싶으면 명시적으로 q.None을 보내야 합니다. nil은 그 자리에서 거부되므로(평범한 테이블은 nil을 담을 수 없어 항목이 조용히 사라지기 때문) 지우기의 유일한 표현이 q.None입니다.

시그니처

__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 tables
  • Attr: attribute name cannot be empty
  • Attr: plain-table keys must be non-empty strings
  • Attr: attribute "{name}" cannot be a function — attribute values are raw values or State
  • Attr: 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가 삭제된다

관련확장 가능한 디스패치 엔진

시그니처

NameMap: (self: Attr) -> { [string]: any }

반환 — 평탄화된 이름 맵(frozen). 값은 저장된 그대로Source 슬롯은 핸들 자체로, NoneNone으로 나옵니다.

동작 — 읽기용입니다. 합성 결과를 검사하거나 테스트에서 확인할 때 씁니다.

예제

local attr = q.Attr({ A = 1, B = q.Source("x"), C = q.None })
local map = attr:NameMap()
print(map.A) -- 1
print(map.C == q.None) -- true (:Get()을 부르지 않는다)

시그니처

Merged: (...Attr) -> Attr

반환 — 합쳐진 새 Attr.

동작Attr 받고, 이름이 겹치면 에러입니다. 두 출처가 같은 속성을 주장하는 사고를 조용히 넘기지 않으려는 자리입니다.

  • Attr.Merged: arguments must be Attr values
  • Attr.Merged: attribute name "{name}" appears more than once

시그니처

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) 였다면 겹침 에러

시그니처

AttrKey: (name: string) -> AttrKeyObject
type 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), -- 패밀리가 못 덮는 엔진 타입
}

시그니처

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 })).

동작타입드 스칼라 슈가입니다. 자기 핸들러를 갖지 않고 그룹 경로를 그대로 씁니다. 차이는 값 검증뿐입니다 — 패밀리는 자기 타입을 알기 때문에 원시 값의 타입을 여기서 확인합니다(Stateq.None은 그대로 통과합니다).

배열 부분에 놓으므로 strict 모드에서도 타입이 섭니다. AttrKey의 해시 키 형태를 대신하는 자리입니다.

에러

  • StringAttr: name must be a non-empty string
  • StringAttr: value of "{name}" must not be nil — use None to delete
  • StringAttr: 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: 그 속성을 지운다

시그니처

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)})

시그니처

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),
}