02. 첫 화면 — 카드 하나 그리기
대상 독자: 01. 프레임워크 설정하기를 끝낸 개발자 목표: 화면에 카드 하나를 띄우고, 그 안에 글씨를 한 줄 넣기
이 장부터 17장까지는 하나의 예제를 조금씩 키웁니다. 마지막에는 클릭할 때마다 숫자와 색이 바뀌는 카운터 여러 개가 됩니다. 대체로 직전 장의 코드에 몇 줄을 더하지만, 개념 하나만 보여야 하는 장에서는 그 장에 필요한 최소한으로 줄여 놓고 시작하기도 합니다(그럴 땐 그 장이 먼저 알려 드립니다).
준비 코드
섹션 제목: “준비 코드”01장에서 만든 진입점 Main.client.luau 안에서 계속 짭니다. 01장의 screen·playerGui 줄은 그대로 두고 hello 라벨만 지웁니다. 이 장부터는 그 줄들과 아래 q 한 줄을 코드에서 반복해 적지 않습니다.
-- (Main.client.luau 계속 — 01장에서 만든 진입점입니다)const q = require("@game/ReplicatedStorage/Client/UI/Quad")1. 카드 만들기
섹션 제목: “1. 카드 만들기”01장의 hello 라벨을 지우고, 그 자리에 어두운 사각형 하나를 만듭니다.
-- … 위쪽 코드에 이어집니다const card = q.Elem "Frame" { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(240, 160), BackgroundColor3 = Color3.fromRGB(35, 35, 42), UICorner = 12,}card.Parent = screen실행하면 화면 한가운데에 모서리가 둥근 어두운 사각형이 하나 뜹니다.
q.Elem "Frame"은 Frame을 만드는 생성자를 돌려주고, 바로 뒤의 { … }가 그 생성자를 부릅니다(Luau는 인자가 문자열 하나나 테이블 하나면 괄호를 생략할 수 있습니다). 같은 클래스를 여러 번 만드는 파일이라면 const Frame = q.Elem "Frame"처럼 파일 머리에서 꺼내 두고 Frame { … }로 써도 됩니다 — 동작은 같고, 큰 모듈에서는 그쪽이 타입 검사도 가볍습니다. 이 튜토리얼은 무엇을 만드는지가 한눈에 보이도록 q.Elem "Frame" { … }을 그대로 씁니다.
q.Elem “Frame” { … }이 돌려주는 건 뭔가요?
부른 그 순간 만들어진 진짜 Roblox Instance입니다. 나중에 렌더러가 만들어 줄 “설계도”를 받은 게 아닙니다. 그래서 이렇게 바로 읽힙니다.
print(card.ClassName) --> "Frame"가상 DOM도, 이전 트리와 새 트리를 비교하는 단계도 없습니다. 넘긴 테이블은 그 자리에서 해석되어 인스턴스에 반영되고 끝납니다. 컴포넌트 함수가 반환되는 시점에 물리 인스턴스가 이미 존재하므로, 반환값을 그대로 쓰거나(card.Parent = screen) 나중에 배울 Ref로 잡아둘 수 있습니다.
중괄호 안의 이름 = 값은 그 인스턴스의 프로퍼티입니다. props 테이블에서 이 부분을 quad 문서에서는 문자 키(이름 = 값처럼 이름이 붙은 자리)라고 부릅니다 — 뒤에서 나올 숫자 키(이름 없이 순서대로 놓이는 자리)와 짝을 이루는 이름입니다. 어떤 이름을 쓸 수 있는지는 quad가 정하는 게 아니라 그 클래스가 정합니다 — Frame의 프로퍼티 목록은 Roblox 공식 레퍼런스가 소스입니다.
같이 끼워 넣은 UICorner = 12만은 프로퍼티가 아닙니다. UICorner/UIPadding/UIPaddingOffset/UIScale 네 키는 quad가 그 자리에 관리 자식(UICorner 인스턴스)을 만들어 붙여 주는 숏핸드입니다.
그리고 Parent만은 이 테이블 안에 못 넣습니다. 만들어진 뒤 밖에서 .Parent를 대입해 붙입니다 — 01장의 screen.Parent = playerGui가 그랬듯이.
Parent는 왜 안에 못 쓰나요?
Parent를 문자 키 자리에 적으면 그 키를 맡을 핸들러1가 없어 quad가 그 자리에서 에러를 냅니다.
Quad0076 Dispatch: no handler matched key Parent (value: Instance) - this key may not be a field of this element (a typo, a read-only field, or one from another class), or the provider that handles it (e.g. quad-roblox) is not initialized꼬리 힌트는 흔한 두 원인(키 이름이 틀렸거나, 프로바이더가 안 붙었거나)을 적지만 Parent는 어느 쪽도 아닙니다. 일부러 막아 두었습니다. q.Elem의 생성자가 돌려주는 것은 이미 실물 Instance이고, “부모에 붙이는 일”은 만든 쪽이 아니라 쓰는 쪽이 정하는 것이기 때문입니다. 그래야 같은 컴포넌트를 어디에 붙일지가 호출 지점에 그대로 보입니다.
2. 자식 하나 넣기
섹션 제목: “2. 자식 하나 넣기”라벨을 넣습니다. 자식 전용 키는 따로 없습니다 — 이름 없이 그냥 원소로 놓으면 그게 자식입니다.
card의 중괄호 안, 프로퍼티들 아래에 이 블록을 더하세요.
-- … 위쪽 코드에 이어집니다(방금 만든 card를 이렇게 고칩니다)const card = q.Elem "Frame" { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(240, 160), BackgroundColor3 = Color3.fromRGB(35, 35, 42), UICorner = 12,
-- 여기부터가 숫자 키 자리: 키 없는 원소는 자식이 된다 q.Elem "TextLabel" { Position = UDim2.fromOffset(16, 0), Size = UDim2.new(1, -32, 0, 48), BackgroundTransparency = 1, TextColor3 = Color3.fromRGB(255, 255, 255), TextScaled = true, Text = "카운트: 0", },}실행하면 사각형 위쪽에 흰 글씨로 카운트: 0이 보입니다.
한 테이블 안에 두 가지가 섞여 있습니다. 이름이 붙은 것(문자 키)은 프로퍼티, 이름 없는 원소(숫자 키)는 자식입니다. 숫자 키 자리에는 자식 말고도 여러 가지가 올 수 있는데, 그건 다음 장부터 하나씩 나옵니다.
숫자 키 자리는 1, 2, 3…으로 빈틈없이 이어지는 순서 자리입니다. 그래서 조심할 것이 하나 있습니다 — 중간이 nil로 비면 안 됩니다. 지금처럼 자식을 직접 적는 동안에는 생길 일이 없지만, 나중에 “있을 수도 없을 수도 있는 것”을 그 자리에 넣기 시작하면 바로 마주칩니다.
중간에 nil이 오면 무슨 일이 생기나요?
Lua 테이블 리터럴에서 { a, nil, b }처럼 중간 원소가 nil이면 그 자리는 구멍입니다. quad는 숫자 키 자리를 1번부터 순서대로 세다가 구멍을 만나면 그 자리에서 에러를 냅니다.
-- 새 예시: 별도 스크립트const maybe = nilconst card = q.Elem "Frame" { q.Elem "TextLabel" { Text = "a" }, maybe, -- ← 구멍 q.Elem "TextLabel" { Text = "b" },}Quad0023 Bookkeeping.recompute: sourceList[2] is nil - a nil hole in the numeric-key part of props ({ a, nil, b })? fill the optional slot with q.None (the children placed before this raise stay seated in the half-built Instance, reachable as child.Parent - q.dispose that Instance to clean up; it takes those children with it, so build the retry with new ones); if you are writing a handler, bookkeeping is broken (setLength without setOffsetSource? the contract says None)물음표까지가 여러분에게 하는 말입니다 — “2번 자리가 비었다, q.None으로 메꿔라”. 괄호 안은 이미 놓인 자식이 있을 때의 정리법(반쯤 지어진 인스턴스를 q.dispose)이고, 세미콜론 뒤는 확장을 만드는 사람을 위한 힌트라 지금은 몰라도 됩니다. 맨 끝이 nil인 것은 그냥 거기서 끝난 것으로 읽혀 넘어가지만, 뒤에 원소가 하나라도 더 붙는 순간 구멍이 되므로 자리와 무관하게 비우지 않는 습관이 안전합니다.
자리를 비워 둬야 할 때를 위한 값이 그래서 하나 있습니다 — q.None입니다. 구멍 대신 이것을 놓으면 자리는 차 있되 아무것도 만들지 않습니다. 지금은 숫자 키 자리에 자식만 직접 적고 있으니 쓸 일이 없고, 12장에서 컴포넌트가 바깥에서 뭔가를 넘겨받기 시작하면 실제로 필요해집니다.
이해 점검
섹션 제목: “이해 점검”`q.Elem "Frame"`이 돌려주는 것
q.Elem "Frame" { … }을 부른 직후 손에 들어오는 것은 무엇인가요?
- 그 자리에서 만들어진 진짜 Roblox Instance라
card.ClassName이 바로 읽힙니다 - 렌더러가 나중에 인스턴스로 바꿔 줄 설계도라 그때까지는 프로퍼티를 읽을 수 없습니다
- 이전 트리와 비교해 달라진 부분만 반영되는 가상 트리의 노드입니다
넘긴 테이블은 그 자리에서 해석되어 인스턴스에 반영되고 끝납니다 — 가상 DOM도, 이전 트리와 새 트리를 비교하는 단계도 없습니다. 그래서 반환값을 card.Parent = screen처럼 곧바로 쓸 수 있습니다.
문자 키와 숫자 키
props 테이블에서 이름 없이 순서대로 놓인 원소(숫자 키)는 무엇이 되나요?
- 그 인스턴스의 자식이 됩니다 — 자식 전용 키는 따로 없습니다
-
Children같은 자식 전용 키에 넣어야 자식이 되고, 이름 없는 원소는 무시됩니다 -
UICorner처럼 quad가 관리 자식을 만들어 붙여 주는 숏핸드가 됩니다
이름이 붙은 문자 키는 프로퍼티, 이름 없는 숫자 키는 자식입니다. UICorner·UIPadding·UIPaddingOffset·UIScale 넷은 문자 키 자리에 적는 숏핸드이고, Parent만은 이 테이블 안에 넣지 못해 만들어진 뒤 밖에서 대입합니다.
숫자 키 자리의 구멍
숫자 키 자리 중간에 nil이 오면 어떻게 되나요?
- 그 자리만 조용히 건너뛰고 나머지 원소가 순서대로 자식이 됩니다
- quad가 1번부터 세다가 그 자리에서 에러를 내므로
q.None으로 메꿔야 합니다 - 자리가 하나 줄어든 채로 만들어지고, 나중에 원소를 더할 때에야 에러가 납니다
숫자 키 자리는 1번부터 빈틈없이 이어지는 순서 자리라, 중간이 비면 그 자리에서 에러가 납니다. 자리는 차 있되 아무것도 만들지 않는 값이 q.None입니다. 맨 끝이 nil인 것은 거기서 끝난 것으로 읽혀 넘어가지만, 뒤에 원소가 하나라도 더 붙는 순간 구멍이 됩니다.
더 알고 싶다면
섹션 제목: “더 알고 싶다면”- 레퍼런스:
q.Elem— Instance 생성 — 문자 키·숫자 키에 올 수 있는 것 전부, 숏핸드 키 넷, 기본 클래스 맵 밖의 클래스(파트·모델 등)를 쓰는q.ForClasses - 00. 설치 §2 3단계 — 타입 검사 플래그(새 솔버는 필수, 한도 셋은 권장 — 편집기와 CI에 같은 값을 넣습니다)
퀴즈 진행 상황
0 / 0 답변한 문제 (0%)
0 정답
퀴즈 완료!
0%
0 / 0 정답
Footnotes
섹션 제목: “Footnotes”답변함: 0 / 0 (0%)