05. 이름표와 속성 — Tag와 Attr
대상 독자: 04. 반응하기를 끝낸 개발자 목표: 만든 인스턴스에 이름표와 값을 달고, 화면 밖의 코드가 그걸 기준으로 찾아 쓰게 하기
지금까지 숫자 키 자리에 넣은 것은 자식이었습니다. 다른 것도 올 수 있습니다 — 인스턴스
자신에 붙는 표시입니다. Roblox에는 그런 채널이 둘 있고(CollectionService의 태그와
인스턴스의 Attribute), quad에서는 둘 다 숫자 키 자리에 값으로 놓습니다.
지금까지의 코드
섹션 제목: “지금까지의 코드”-- (Main.client.luau 계속 — 배치 프로퍼티는 생략합니다)const count = q.Source(0)const countText: q.State<string> = count:Compute(function(c) return `카운트: {c:Get()}` end)
const card = q.Elem "Frame" { Size = UDim2.fromOffset(240, 160), BackgroundColor3 = Color3.fromRGB(35, 35, 42), UICorner = 12,
-- 라벨과 버튼(크기·색 같은 프로퍼티는 04장 그대로라 여기선 줄였습니다) q.Elem "TextLabel" { Text = countText }, q.Elem "TextButton" { Text = "+ 1", Activated = function() count:Set(count:Get() + 1) end },}1. 이름표 붙이기 — q.Tag
섹션 제목: “1. 이름표 붙이기 — q.Tag”card의 숫자 키 자리 맨 앞에 한 줄을 더합니다.
-- … 위쪽 코드에 이어집니다const card = q.Elem "Frame" { q.Tag("Card"), -- ← 숫자 키
Size = UDim2.fromOffset(240, 160), -- …나머지 그대로…}실행하면 이 Frame이 CollectionService에 태그 Card로 잡힙니다.
-- (확인용 — 화면 밖 아무 스크립트에서)const CollectionService = game:GetService("CollectionService")print(#CollectionService:GetTagged("Card")) --> 1quad-base가 태그를 어떻게 처리하나요?
quad-base는 태그 채널을 직접 건드리지 않습니다. 이름 집합을 계산해 백엔드가 심어 둔
addTag/removeTag에 넘기고, quad-roblox가 그것을 CollectionService로 잇습니다.
같은 이름의 Tag를 두 곳에서 붙이면요?
태그는 집합이라 두 자리의 요구가 합집합이 됩니다. 다만 quad는 자리마다 따로 세어 두었다가 마지막 자리가 물러날 때 비로소 엔진에서 뗍니다.
const inst = q.Elem "Frame" { q.Tag("Card"), q.Tag("Card", "Panel") }-- 태그 {Card, Panel} — 엔진 호출은 addTag:Card, addTag:Panel 둘뿐(Card는 한 번만)두 번째 자리가 빠져도(2절처럼 태그 자리를 State로 잡아 두었는데 그 파이프가 빈 q.Tag()를 내놓는 경우) 나가는 것은
removeTag:Panel 하나입니다 — Card는 첫 자리가 아직 잡고 있습니다. 그래서 컴포넌트 여럿이 같은 인스턴스에 같은 이름을 요구해도
서로를 지우지 않습니다. 같은 불변 Tag 객체를 두 자리에 놓아도 두 자리로 셉니다.
2. 이름표도 값에서 나올 수 있습니다
섹션 제목: “2. 이름표도 값에서 나올 수 있습니다”태그 자리에 State를 놓으면 그 파이프가 내놓는 Tag가 그대로 이름표가 됩니다. 여기서 짚어 둘
것 하나 — Tag는 불변이라 :Added/:Removed는 원본을 고치는 게 아니라 새 Tag를
돌려줍니다. 그래서 조건은 if문으로 감싸는 게 아니라 인자 안에 씁니다.
const card = q.Elem "Frame" { q.Tag("Card"), count:Compute(function(c: q.StateData<number>) -- 숫자 키 자리의 인라인 Compute도 파라미터 타입을 적어야 strict가 봅니다 return q.Tag("Card"):Added(if c:Get() % 2 == 0 then "Even" else nil) end),
-- …나머지 그대로…}nil은 “더할 것 없음”입니다 — 그래서 짝수일 땐 {Card, Even}, 홀수일 땐 {Card}가 나갑니다.
if even then base:Added("Even") end처럼 문장으로 쓰면 돌려준 Tag를 그 자리에서 버려
아무 일도 하지 않고, Tag가 불변인 걸 몰랐다는 신호를 타입 검사도 잡아 주지 않습니다 — 반환값을
버리는 문장이라 문법만 보면 멀쩡합니다. 그래서 조건은 늘 인자 자리에, 결과는 =으로 받는 걸
규칙으로 삼습니다.
이름표를 하나 더 갈아 끼우고 싶으면 :Added(...):Removed(...)처럼 이어 붙입니다 — 왼쪽부터
읽히는 체인입니다.
실행하면 버튼을 누를 때마다 이름표가 갈아 끼워집니다 — {Card, Even} → {Card} →
{Card, Even}. 정적으로 적은 Card는 교체 내내 남습니다(1절의 자리별 셈 그대로) — 파이프가
내놓는 Tag에도 매번 "Card"를 다시 적었지만, 그건 집합을 맞추기 위한 것일 뿐 엔진에
새로 붙는 게 아닙니다(자리 자체가 다르니까요). 엔진 호출도 진짜 바뀐 이름에만 나갑니다 —
짝수에서 홀수로 넘어가는 클릭 한 번이 만드는 것은 removeTag:Even 하나뿐입니다.
“진짜 바뀐 이름에만”을 한 번 더 짚어 둘 만합니다. 파이프가 새 Tag를 내놓을 때 quad가 하는 일은
옛 이름 집합과 새 이름 집합을 대 보는 것입니다 — 빠진 이름만 떼고, 생긴 이름만 붙입니다.
그래서 두 집합이 같으면 엔진으로 나가는 호출이 하나도 없습니다.
-- (확인용 — 카드가 막 만들어진 참이라 count는 0, 이름표는 {Card, Even}. 확인이 끝나면 아래 두 줄은 지웁니다)count:Set(2) -- 짝수 → 짝수: 집합이 {Card, Even} 그대로라 엔진 호출 0count:Set(3) -- 짝수 → 홀수: 집합이 {Card}가 되어 removeTag:Even 하나바뀔 때마다 태그를 전부 떼었다 다시 붙이는 건 아닌가요?
아닙니다. 여기에 1절의 자리별 셈이 겹칩니다 — 정적으로 적은 q.Tag("Card")는 자기 자리가
물러날 때까지 그대로이므로, 교체 도중에 Card가 잠깐 떨어지는 구간이 없습니다. 태그를 보고
반응하는 바깥 코드(CollectionService:GetInstanceRemovedSignal 같은 것)가 쓸데없이 깨어나지 않는다는
뜻이기도 합니다.
숫자 키 자리를 State로 잡아 두고 갈아 끼울 수 있는 것은 Tag만이 아닙니다. State<> 안에 들어갈
수 있는 것이 생각보다 넓어서, 08장에서는 관측 핸들을 이 방법으로 껐다
켜고, 11장에서는 자식 인스턴스를 이 방법으로 갈아 끼우게 됩니다. 세 장이 각각
“이렇게도 된다”고 말하지만 사실은 원리 하나이고, 그 하나는 20장에서 봅니다.
3. 값 심기 — q.Attr
섹션 제목: “3. 값 심기 — q.Attr”이름표가 “무엇인가”라면 Attribute는 “얼마인가”입니다. 여러 개는 그룹 하나로, 하나씩은 타입별 슈가로 놓습니다.
-- … 위쪽 코드에 이어집니다const active = q.Source<<boolean | typeof(q.None)>>(true) -- 뒤에서 q.None을 넣어 속성을 지울 것이라 타입에 None을 넣는다
const card = q.Elem "Frame" { q.Tag("Card"), -- …§2의 짝수/홀수 파이프 그대로…
q.Attr { Kind = "counter", Count = count }, -- 그룹 하나 — 값 자리에 State를 그대로 넣는다 q.BooleanAttr("Active", active), -- 값 하나(State도 된다)
-- …나머지 그대로…}그룹 안에 넣은 Count = count를 눈여겨보세요 — 항목 값은 원시 값뿐 아니라 State/Source도
그대로 받습니다(2절의 Tag처럼 항목 하나를 파이프로 감싸는 게 아니라, 그룹 자체는 리터럴로
두고 그 안의 한 값만 반응하게 둘 수 있다는 뜻입니다). count는 이 장 맨 위에서 만든 그 카운터
Source이므로, 여기서는 새로 만들 것이 없습니다.
실행하면 셋이 그 인스턴스의 Attribute로 심깁니다(Studio 속성 창 아래쪽 Attributes 칸).
값 타입도 그대로 보존됩니다 — 문자열은 문자열로, 숫자는 숫자로, 불리언은 불리언으로 — 그리고
Count는 count가 바뀔 때마다 따로 심을 필요 없이 따라갑니다.
-- (확인용 — 화살표 뒤는 실제 출력이 아니라 그 시점의 Attribute 상태입니다. 확인이 끝나면 active:Set 두 줄은 지웁니다)card:GetAttributes() --> {Active = true, Kind = "counter", Count = 0}
-- (+1 버튼을 두 번 누르면) card:GetAttributes() --> {..., Count = 2} — count를 따라간다
active:Set(false) -- 이제 {Active = false, Kind = "counter", Count = 2}active:Set(q.None) -- 이제 {Kind = "counter", Count = 2} — Active가 사라진다Attr 값을 지우려면요?
리터럴로 적는 자리에서 지우기의 유일한 표현은 **q.None**입니다. 넷을 구별하세요.
| 한 일 | 결과 |
|---|---|
값에 q.None을 둔다 |
엔진에서 그 속성이 삭제됩니다 |
바인딩된 State가 q.None이 된다 |
같습니다 — 삭제됩니다 |
바인딩된 State가 nil을 내놓는다 |
이것도 삭제됩니다 — State가 흘려보내는 nil은 거부되지 않습니다 |
그 Attr 값 객체를 다른 것으로 교체한다 |
옛 속성 값이 엔진에 그대로 남습니다 |
네 번째가 함정입니다(Attr을 내놓는 State로 자리를 잡아 두고 그 파이프가 다른 Attr을 내놓는 경우 — 2절의 Tag와 같은 모양이고, 원리는 20장에서 봅니다). 자리에서 빠지는 Attr은 구독을 끊고 이름을 반납할 뿐 엔진 쪽 값에는
손대지 않습니다. 리터럴 nil은 거부가 아니라 평범한 테이블이 nil을 담을 수 없어 항목이 조용히
사라지는 것이고(q.Attr({ A = nil })은 빈 그룹), State 안의 nil은 그 문제가 없어 그대로 삭제로 흐릅니다.
지우기 말고 하나가 더 있습니다 — 한 인스턴스의 두 자리가 같은 이름을 요구하는 경우입니다.
이름이 겹치는 Attr을 한 인스턴스의 두 자리에 놓으면요?
이기는 쪽이 없습니다 — 그 자리에서 에러가 납니다.
q.Elem "Frame" { q.Attr { Kind = "counter" }, q.Attr { Kind = "other" } }-- Quad0002 AttrKey: attribute "Kind" is already bound by another owner이름 하나의 주인은 하나여야 한다는 규칙이라, 겹칠 수 있으면 먼저 합쳐서 하나로 넘깁니다.
겹침을 사고로 볼 자리에는 q.Attr.Merged(a, b)(겹치면 에러), 의도된 덮어쓰기에는
q.Attr.Overridden(a, b)(뒤 인자가 조용히 이김)를 씁니다.
4. 붙인 것으로 찾기 — QueryDescendants
섹션 제목: “4. 붙인 것으로 찾기 — QueryDescendants”이름표와 속성이 붙었으니 화면 밖의 코드가 그걸 기준으로 인스턴스를 고를 수 있습니다.
Instance:QueryDescendants(selector)는 선택자에 맞는 자손 전부를 배열로 돌려줍니다.
01장에서 만든 screen(ScreenGui)에 대고 부릅니다.
print(#screen:QueryDescendants(".Card")) --> 1 태그가 Card인 것print(#screen:QueryDescendants("[$Kind=counter]")) --> 1 Attribute Kind가 counter인 것실행하면 둘 다 1이 찍힙니다 — 태그로 고른 것도 속성으로 고른 것도 방금 만든 그 카드 하나입니다.
선택자 문법은 뭐가 더 있나요?
| 선택자 | 뜻 | 예 |
|---|---|---|
ClassName |
클래스(IsA 기준이라 상위 클래스도 맞습니다) |
Frame, GuiObject |
.Tag |
CollectionService 태그 |
.Card |
#Name |
인스턴스의 Name |
#Card |
[$Attr] / [$Attr=값] |
Attribute가 있는가 / 값이 같은가 | [$Kind], [$Step=1] |
[속성=값] |
프로퍼티 값이 같은가 | [Visible=true] |
A > B / A >> B |
직계 자식 / 자손 | .Card >> TextLabel |
A, B |
합집합 | Frame, TextLabel |
선택자가 조용히 0개를 돌려주는데요?
에러 없이 빈 배열이 오는 경우가 둘 있고, 둘 다 CSS 습관에서 옵니다.
- 공백은 자손이 아니라 AND입니다.
.Card TextLabel은 “Card태그가 붙은 동시에TextLabel인 것”으로 읽혀 대개 0개가 됩니다. 자손은>>입니다. - Attribute에
$를 빠뜨리면 에러가 아니라 0개입니다.[Kind=counter]는Kind라는 프로퍼티를 찾으므로 아무것도 안 걸립니다.
값에 공백이 있으면 큰따옴표로 감쌉니다([$Label="two words"]). 작은따옴표는 에러이고, 값 없는
존재 검사는 Attribute에만 됩니다 — [Visible]은 '=' expected after property name이 납니다.
여기서 붙인 태그는 UI 스타일시트가 고르는 기준이기도 합니다. StyleRule의 선택자에서
.Card는 지금 붙인 그 태그를 가리키므로, quad로 선언한 태그 위에 스타일시트를 얹어 바깥에서
모양을 갈아 끼우는 구성이 가능합니다. 구성법 자체는 Roblox 쪽 기능이라 아래 공식 문서로 넘깁니다.
백엔드 없이도 되나요?
값을 만들고 합성하는 것까지는 됩니다. 실제로 인스턴스에 적용하는 것만 백엔드의 몫이라,
백엔드 없이 거기까지 가면 안내 에러가 납니다 — Quad0108 quad: addTag is not available - no backend has installed the tag ops …(뒤에 프로바이더를 설치하라는 안내가 붙습니다). 01장에서 Quad:UseProvider(QuadRoblox)를 이미 했다면 신경 쓸 일이 없습니다.
이해 점검
섹션 제목: “이해 점검”같은 이름표를 두 자리에서
한 인스턴스의 두 자리가 같은 이름의 q.Tag를 요구하다가 그중 한 자리가 빠지면 어떻게 되나요?
- 뒤에 온 자리가 앞의 것을 덮고 있었으므로, 빠지는 순간 그 이름이 엔진에서 떨어집니다
- quad가 자리마다 따로 세어 두므로, 마지막 자리가 물러날 때에야 엔진에서 뗍니다
- 같은 이름을 한 인스턴스의 두 자리에 놓는 것 자체가 에러라 그런 상황이 생기지 않습니다
태그는 집합이라 두 자리의 요구가 합집합이 되고, quad는 자리마다 따로 세어 두었다가 마지막 자리가 물러날 때 비로소 엔진에서 뗍니다. 그래서 컴포넌트 여럿이 같은 인스턴스에 같은 이름을 요구해도 서로를 지우지 않습니다.
`Attr` 값을 교체하면
자리에 놓은 Attr 값 객체를 다른 것으로 교체하면 엔진 쪽 속성은 어떻게 되나요?
- 교체되는 순간 옛 속성이 삭제되고 새 값만 남습니다
- 옛 속성 값이 엔진에 그대로 남습니다 — 빠지는
Attr은 구독을 끊고 이름을 반납할 뿐입니다 - 두 값이 합쳐져 옛 이름과 새 이름이 모두 심긴 채로 남습니다
지우기의 표현은 q.None이고, 바인딩된 State가 q.None이나 nil을 내놓을 때도 삭제됩니다. 값 객체를 교체하는 것만은 지우기가 아닙니다 — 자리에서 빠지는 Attr은 구독을 끊고 이름을 반납할 뿐 엔진 쪽 값에는 손대지 않습니다.
선택자가 0개를 돌려줄 때
screen:QueryDescendants(".Card TextLabel")이 에러 없이 0개를 돌려주는 이유는 무엇인가요?
- 태그 선택자 뒤에는 클래스 이름을 이어 쓸 수 없어 뒤쪽이 통째로 무시되기 때문입니다
- 공백은 자손이 아니라 AND라,
Card태그가 붙은 동시에TextLabel인 것을 찾기 때문입니다 -
QueryDescendants는 직계 자식만 보므로 한 단계 아래가 아닌 것은 걸리지 않기 때문입니다
공백은 AND로 읽히고 자손은 >>입니다. 같은 종류의 함정이 하나 더 있는데, Attribute 선택자에서 $를 빠뜨린 [Kind=counter]는 에러가 아니라 그 이름의 프로퍼티를 찾아 0개가 됩니다.
지금까지(03~05장) 배운 것은 전부 값을 상태에서 인스턴스로 내려보내는 흐름이었습니다. 현실의 화면에는 반대 방향도 있습니다 — 사용자가 입력창에 타이핑하는 값처럼, 인스턴스에서 상태로 올라오는 값입니다. 06장에서 그 반대 방향을 봅니다.
더 알고 싶다면
섹션 제목: “더 알고 싶다면”- 레퍼런스:
Tag/Attr— 전체 표면(tag:Added/:Contains/for name in tag,q.AttrKey,StringAttr/NumberAttr, 에러 문구) - Roblox:
CollectionService· 인스턴스 Attribute - Roblox:
Instance:QueryDescendants - Roblox: UI 스타일링 ·
StyleRule.Selector— 태그를 선택자로 삼는 스타일시트 구성
퀴즈 진행 상황
0 / 0 답변한 문제 (0%)
0 정답
퀴즈 완료!
0%
0 / 0 정답
답변함: 0 / 0 (0%)