10. 스타일을 값으로 들고 다니기 — Modifier
대상 독자: 09. 생성만 보고 싶다면을 끝낸 개발자 목표: 반복되는 프로퍼티 묶음을 값 하나로 빼서 여러 곳에 놓기
카드와 버튼의 색·테두리를 다른 화면에서도 그대로 쓰고 싶습니다. Modifier는
“이 프로퍼티들을 이렇게 설정한다”를 값 하나로 만든 것입니다. 컴포넌트와는
무관합니다 — 지금처럼 인스턴스(카드 Frame, 버튼) 하나하나에 그대로 놓습니다.
1. 값으로 빼기
섹션 제목: “1. 값으로 빼기”-- … 위쪽 코드에 이어집니다(card 위에 한 덩이를 둡니다)const CardStyle = q.Modifier "Frame" { BackgroundColor3 = Color3.fromRGB(35, 35, 42), BorderSizePixel = 0,}card의 문자 키에서 BackgroundColor3 한 줄을 지우고, 숫자 키 맨 앞에 CardStyle을 놓습니다.
-- … 위쪽 코드에 이어집니다const card = q.Elem "Frame" { CardStyle, -- ← 숫자 키
Size = UDim2.fromOffset(240, 160), UICorner = 12,
-- …05·07~09장에서 붙인 숫자 키와 자식들 생략…}실행하면 겉모습은 그대로입니다(BorderSizePixel = 0이 새로 들어가 검은 1px 테두리가 빠지는 것 말고는) — 다만 이제 그 스타일은 값이라, 다른 인스턴스에도 같은 것을 놓을 수 있습니다.
q.Modifier "Class"는 클래스 태그가 붙은 생성자입니다. TextButton 전용 Modifier를 Frame의 숫자 키 자리에 넣으면 타입 검사에서 걸립니다. 테이블을 넘기는 위 형태와 빌더 체인 형태(q.Modifier "TextButton" ():TextSize(16)) 둘 다 됩니다. 필드 이름·값 타입까지 검사받는 건 체인 형태뿐입니다(테이블 형태는 클래스 태그만 붙고 필드는 런타임에서 잡힙니다).
2. 필드에도 값이 흐릅니다
섹션 제목: “2. 필드에도 값이 흐릅니다”Modifier의 필드 값 자리에는 리터럴뿐 아니라 State/Source도 올 수 있습니다. 그러면 그 Modifier를 놓은 인스턴스의 프로퍼티가 그 값을 따라갑니다.
-- … 위쪽 코드에 이어집니다(CardStyle을 이렇게 고칩니다)const hot = q.Source(false)
const cardColor: q.State<Color3> = hot:Compute(function(h) return if h:Get() then Color3.fromRGB(240, 60, 60) else Color3.fromRGB(35, 35, 42)end)
const CardStyle = q.Modifier "Frame" { BackgroundColor3 = cardColor, -- Compute를 테이블 안에 바로 쓰면 strict가 콜백 타입을 못 잡습니다 — 지역 변수로 빼세요 BorderSizePixel = 0,}
const card = q.Elem "Frame" { CardStyle, Size = UDim2.fromOffset(240, 160), UICorner = 12 } -- …05·07~09장에서 붙인 숫자 키와 자식들은 그대로…
hot:Set(true) -- 카드 배경이 빨강으로 바뀐다(확인이 끝나면 이 줄은 지웁니다)실행하면 hot:Set(true) 시점에 카드 배경이 바뀝니다. 03장에서 프로퍼티에 직접 꽂았던 그 흐름이, Modifier를 한 단계 거쳐 같은 자리에 도착한 것입니다.
우선순위는 이 장에서 하나만 알면 충분합니다. 직접 적은 문자 키가 Modifier보다 우선합니다.
그리고 하나 더 알아 두면 좋은 성질이 있습니다. Modifier는 불변입니다. setter를 하나 부를 때마다 새 값이 생기고 원본은 그대로라, 하나의 기본 스타일에서 갈라 나온 형제들이 서로를 오염시키지 않습니다.
-- … 위쪽 코드에 이어집니다const base = q.Modifier "TextButton" { TextSize = 16 }const big = base:TextSize(24) -- base는 그대로 16Modifier가 여럿이면 누가 이기나요?
무엇이 무엇을 덮는지는 규칙 셋뿐입니다.
- 직접 적은 문자 키가 이깁니다.
q.Elem "TextButton" { SomeMod, Text = "확정" }에서Text는 어떤Modifier가 무엇을 갖고 오든 그대로 유지됩니다. - 숫자 키 자리에서는 뒤에 온
Modifier가 앞의 것을 필드 단위로 덮습니다. Modifier.Overridden(A, B)도 같은 방향입니다 — 뒤 인자가 앞 인자를 덮습니다. 닷 형태와 콜론 형태(a:Overridden(b)) 둘 다 됩니다.
빈 자리를 명시적으로 비우고 싶으면 q.None을 넣습니다. nil은 “이 Modifier는 그 필드에 관심 없음”이고, q.None은 “그 필드를 비워라”입니다 — 뜻이 다릅니다.
컴포넌트가 바깥에서 Modifier를 받아 자기 안쪽에 꽂을 때의 경계 규약(props.Modifier or q.None)과 상위 클래스 Modifier를 받는 법은 01. 컴포넌트 경계 규약과 스타일 합성 §2·§3에 있습니다.
Modifier는 프로퍼티만 담습니다. 이벤트 핸들러·Ref·자식까지 한 묶음으로 넘기고 싶다면 요소 테이블 조각인 q.Spread "Class" { … }가 있습니다 — 같은 키에서는 Modifier와 반대로 먼저 놓인 쪽이 이깁니다. 그것도 같은 문서의 §3에 있습니다.
3. 팩토리로 뽑고, 모듈 하나에 모으기
섹션 제목: “3. 팩토리로 뽑고, 모듈 하나에 모으기”스타일을 함수로 만들면 인자를 받는 스타일이 됩니다.
-- 새 예시: 별도 스크립트local function accent(color) return q.Modifier "TextButton" { BackgroundColor3 = color, TextColor3 = Color3.fromRGB(255, 255, 255), BorderSizePixel = 0, }end
const button = q.Elem "TextButton" { accent(Color3.fromRGB(0, 162, 255)), Text = "+ 1" }이 버튼을 화면에 붙이면 넘긴 색으로 칠해집니다 — 흰 글자와 BorderSizePixel = 0은 팩토리 안에 그대로 있어서, 색만 다른 버튼을 얼마든지 찍어낼 수 있습니다.
권장하는 배치는 스타일을 모듈 하나에 모아 내보내는 것입니다. 화면마다 색 리터럴이 흩어지지 않고, 나중에 테마를 바꿀 때 고칠 자리가 한 곳이 됩니다.
-- 새 파일: ReplicatedStorage/Client/UI/Stylesconst q = require("@game/ReplicatedStorage/Client/UI/Quad")
local function accent(color) return q.Modifier "TextButton" { BackgroundColor3 = color, TextColor3 = Color3.fromRGB(255, 255, 255), BorderSizePixel = 0, }end
return { Card = q.Modifier "Frame" { BackgroundColor3 = Color3.fromRGB(35, 35, 42), BorderSizePixel = 0, }, Accent = accent,}쓰는 쪽은 const Styles = require("@game/ReplicatedStorage/Client/UI/Styles") 뒤에 Styles.Card, Styles.Accent(색)을 숫자 키 자리에 놓습니다 — 모양은 §1의 CardStyle을 그런 모듈로 옮긴 것입니다(§2의 hot 파이프가 든 CardStyle을 이것으로 갈아 끼우면 배경은 정적 색으로 돌아갑니다). 이 튜토리얼에서는 만들어만 두고 쓰지 않습니다 — 실전 배치는 05. 디자인 토큰과 테마 전환에 있습니다.
4. 테마를 바꿀 때 — Modifier를 갈아 끼우지 말고 값을 흘립니다
섹션 제목: “4. 테마를 바꿀 때 — Modifier를 갈아 끼우지 말고 값을 흘립니다”§2와 §3을 합치면 “테마 전환”의 모양이 나옵니다. 팩토리에 리터럴 대신 State를 넘기면, 그 팩토리가 만든 Modifier는 그 State를 따라갑니다.
-- … 위쪽 코드에 이어집니다(§3 Styles 모듈의 accent 함수를 이 스크립트에도 같은 모양으로 정의해 둔 것으로 칩니다 — 모듈로 만들었다면 Styles.Accent)const accentColor = q.Source(Color3.fromRGB(0, 162, 255))const button = q.Elem "TextButton" { accent(accentColor), Text = "+ 1" }
accentColor:Set(Color3.fromRGB(240, 60, 60)) -- 버튼 색만 바뀐다(확인이 끝나면 이 줄은 지웁니다)흔한 시도 하나는 안 됩니다. 테마마다 Modifier를 통째로 만들어 두고 q.Source(darkStyle)처럼 Modifier 자체를 State에 담아 갈아 끼우려 하면 Quad0181 Source: cannot hold a Modifier as a Source value로 막힙니다. 바뀌는 것은 스타일의 필드이므로, 필드마다 State를 두고 Modifier가 그것을 참조하게 합니다 — 위 모양 그대로입니다. 왜 그렇게 막아 두었는지는 10. 에러 읽는 법 함정 5에, 토큰 여러 개를 테마 하나로 묶는 실전 배치는 05. 디자인 토큰과 테마 전환에 있습니다.
한 걸음 더 가면 Modifier 위에 얹는 조합이 있습니다. mod:Apply(fn)은 fn(mod)를 돌려주는 호출 슈거라, 인자를 받아 함수를 돌려주는 팩토리와 짝지으면 “스타일 조각”을 체인 중간에 끼울 수 있습니다.
-- 새 예시: 별도 스크립트local function emphasis(size) return function(mod) return mod:TextSize(size):TextColor3(Color3.fromRGB(255, 230, 120)) endend
const title = q.Modifier "TextLabel" { BackgroundTransparency = 1 }:Apply(emphasis(24))const label = q.Elem "TextLabel" { title, Text = "카운터" }emphasis(24)가 돌려준 것은 값이 아니라 함수이고, 그 함수는 24를 기억합니다. :Apply는 넘긴 것이 함수인지만 검사하는 순수한 호출이라 emphasis(24)(base)와 같지만, 체인 안에서 읽히는 순서가 코드 순서와 같아집니다. 이 “함수를 돌려주는 함수” 모양에는 13장에서 이름(커링)을 붙입니다.
이해 점검
섹션 제목: “이해 점검”누가 누구를 덮나
q.Elem "TextButton" { SomeMod, Text = "확정" }에서 SomeMod도 Text를 갖고 있다면 어느 값이 남나요?
- 직접 적은 문자 키가 이기므로
"확정"이 그대로 남습니다 - 숫자 키가 먼저 처리되므로
Modifier가 갖고 온 값이 남습니다 - 한 프로퍼티를 둘이 요구한 것이라 그 자리에서 에러가 납니다
우선순위 규칙 셋 중 첫째가 “직접 적은 문자 키가 이긴다”입니다. 숫자 키 자리에 Modifier가 여럿일 때 그들끼리는 뒤에 온 것이 앞의 것을 필드 단위로 덮고, Modifier.Overridden(A, B)도 같은 방향입니다.
`Modifier`는 불변입니다
const big = base:TextSize(24)를 부른 뒤 base는 어떻게 되나요?
-
base의 값도 24로 바뀌고,big은 같은 것을 가리키는 다른 이름입니다 - setter가 새 값을 만들어 돌려주므로
base는 그대로 16입니다 -
base는 갈라져 나간 뒤로 더 쓸 수 없어big만 자리에 놓을 수 있습니다
Modifier는 불변이라 setter를 하나 부를 때마다 새 값이 생기고 원본은 그대로입니다. 그래서 하나의 기본 스타일에서 갈라 나온 형제들이 서로를 오염시키지 않습니다.
더 알고 싶다면
섹션 제목: “더 알고 싶다면”- 레퍼런스:
Modifier— setter 체인,Peek,Overridden, 클래스 태그와 다운캐스트 - 레퍼런스:
q.Modifier— 클래스별 생성자와 setter 타입, 요소 테이블 조각을 넘기는q.Spread - 05. 디자인 토큰과 테마 전환 — 토큰을 모아 두고 테마를 갈아 끼우는 실전 배치
퀴즈 진행 상황
0 / 0 답변한 문제 (0%)
0 정답
퀴즈 완료!
0%
0 / 0 정답
답변함: 0 / 0 (0%)