Skip to content

16. 설정값 여럿을 한 곳에 담기 — q.Store

This content is not available in your language yet.

대상 독자: 15. 층을 건너 값 넘기기를 끝낸 개발자 목표: 카운터의 보폭(+1)을 흩어진 지역 변수 대신 이름 붙은 묶음 하나로 두고, 진입점에서 바꾸면 모든 카운터가 따라가게 만들기

지금까지 값이 하나 필요할 때마다 q.Source를 하나씩 만들었습니다. 값이 여러 개로 늘면 그것들을 낱개 변수로 흩어 두기보다 이름 붙은 묶음 하나로 갖고 다니는 게 낫습니다. q.Store가 그 묶음입니다.


1. Store는 새 노드가 아니라 이름 붙은 가방입니다

섹션 제목: “1. Store는 새 노드가 아니라 이름 붙은 가방입니다”

Store는 반응형 그래프에 사는 새 노드가 아닙니다. 이미 있는 Source들을 한 테이블에 담아 store.Step처럼 점으로 꺼내 쓰게 해 주는 것이 전부입니다. 진입점에 카운터 설정을 담을 Store를 하나 만듭니다.

-- (Main.client.luau 계속)
const settings = q.Store({
Step = q.Source(1),
})

settings.Step은 넣은 그 Source 객체 자신입니다 — 래퍼가 아니라서 지금까지 배운 :Get/:Set/:Compute가 그대로 통합니다.

print(settings.Step:Get()) --> 1
settings.Step:Set(5) -- 확인용 — 확인했으면 이 줄은 지웁니다(남겨 두면 §3의 보폭 버튼이 5부터 돕니다)

2. 15장의 가방에 그대로 실어 보내기

섹션 제목: “2. 15장의 가방에 그대로 실어 보내기”

Store 하나를 위해 새 전달 경로를 만들 필요는 없습니다. 15장에서 이미 만든 ctx에 열쇠를 하나 더 :Set 하면 됩니다 — :Set이 가방 자신을 바꾸고 자기를 돌려주는 체이닝이라는 것은 15장 4절에서 봤습니다.

먼저 이 설정의 열쇠 모듈을 하나 만듭니다. Theme 모듈과 모양이 똑같습니다.

-- 새 파일: ReplicatedStorage/Client/UI/Settings
const q = require("./Quad")
return {
Provider = q.Context.Provider("Settings"),
}

진입점에서 ctx를 만들던 줄에 :Set을 하나 이어 붙입니다.

-- (Main.client.luau 계속 — 15장에서 만든 ctx 줄을 이렇게 고칩니다)
const Settings = require("@game/ReplicatedStorage/Client/UI/Settings")
const ctx = q.Context()
:Set(Theme.Provider, { Accent = Color3.fromRGB(0, 162, 255) })
:Set(Settings.Provider, settings)

CounterBoard.luau는 한 글자도 바뀌지 않습니다. 14장에서 만든 그 컴포넌트는 애초에 props.Ctx를 열어 보지 않고 Counter에 그대로 넘기기만 했으므로(15장 2절), 가방 안의 열쇠가 하나 늘어도 중간 층이 손댈 일이 없습니다. Store가 “값 여럿을 몰아 넣는” 쪽에 잘 맞는 이유가 이것입니다 — 값이 늘 때마다 새 Provider와 새 전달 경로를 만드는 대신, 이미 뚫려 있는 가방 하나에 이름만 늘립니다.


Counter에서 Theme을 꺼내던 것과 똑같은 방식으로 Settings도 꺼냅니다 — 같은 ctx, 다른 열쇠입니다.

-- … (Counter.luau) 파일 머리에 한 줄 더
const Settings = require("./Settings")
return function(props)
const theme = props.Ctx:Get(Theme.Provider)
const settings = props.Ctx:Get(Settings.Provider) -- ← 같은 가방에서 다른 열쇠로 하나 더
const count = q.Source(props.Start or 0)
-- …중간 생략…

버튼의 Activated에서 고정돼 있던 + 1을 settings.Step으로 바꿉니다.

-- … (Counter.luau) 버튼의 Activated를 이렇게 고칩니다
Activated = function()
count:Set(count:Get() + settings.Step:Get())
end,

마지막으로 그 보폭을 바꿀 수 있는 버튼을 진입점에 하나 답니다. settings는 진입점이 직접 만든 값이니 Context 없이 바로 씁니다.

-- (Main.client.luau 계속 — board 뒤에)
const stepButton = q.Elem "TextButton" {
Size = UDim2.fromOffset(120, 32),
Text = settings.Step:Compute(function(s: q.StateData<number>) -- 테이블 안 인라인 Compute는 파라미터 타입을 적습니다
return `보폭: {s:Get()}`
end),
Activated = function()
settings.Step:Set(settings.Step:Get() % 5 + 1) -- 1→2→…→5→1
end,
}
stepButton.Parent = screen

실행하면 “보폭: 1” 버튼이 하나 뜨고, 누를 때마다 2, 3, 4, 5, 1로 돌아갑니다. 그 뒤로 아무 카운터나 눌러 보면 오르는 폭이 방금 정한 숫자를 따라갑니다 — Counter.luau도 CounterBoard.luau도 다시 건드리지 않았는데, 진입점에서 바꾼 값 하나가 목록의 카운터 전부에 퍼집니다. 15장의 Accent는 만들 때 한 번 읽는 평범한 값이라 이미 있는 카운터엔 닿지 않았지만, 이번엔 가방에 담긴 것이 Source라 실행 중에 바꿔도 이미 있는 카운터까지 따라옵니다 — 그게 Store를 쓰는 이유입니다.

계산된 이름으로 필드를 만들고 싶다면요?

지금까지처럼 필드 이름을 코드에 직접 적는다면 생성자에 넣는 것으로 충분합니다. 이름이 런타임에만 정해지는 경우(서버가 준 키 목록 등)를 위한 문이 하나 더 있습니다 — store:Of<<T>>(name)입니다. 없는 이름이면 그 자리에서 Source<T>를 만들어 저장하고, 같은 이름으로 다시 부르면 같은 객체가 돌아옵니다. 갓 만들어진 값은 nil이라 바로 :Set해야 하고, 타입 인자 T는 검사되지 않는 캐스트입니다. Of/Names와 밑줄 둘(__)로 시작하는 이름은 필드로 못 씁니다 — 앞의 둘은 메소드 이름이고 뒤는 팬텀 필드 자리라 생성자와 :Of 둘 다 거부합니다. 전체 표면은 레퍼런스: Store에 있습니다.

컴포넌트 안에서 만든 Source/Store는 누가 정리하나요?

딱히 부를 것이 없습니다. q.Source도 q.Store도 평범한 Luau 테이블이라, 더 이상 아무도 참조하지 않으면 다른 테이블과 똑같이 가비지 컬렉트됩니다. 진입점이 계속 들고 있는 settings는 계속 살아 있고, Counter 안에서 그때그때 만드는 count는 그 카운터가 파괴되고 아무도 안 붙잡으면 같이 사라집니다.

인스턴스에 매달린 값만 다릅니다 — 프로퍼티에 꽂힌 값이나 자리에 놓인 State처럼 인스턴스가 사는 동안 붙들려 있어야 하는 것들은, q.Backend.bindLifetime이라는 별도 프리미티브로 그 인스턴스에 강하게 묶입니다(레퍼런스: 생명주기와 센티널). 지금까지 써 온 자리는 전부 quad가 알아서 이 프리미티브를 불러 준 경우였습니다 — Store 자신은 이 부류가 아닙니다.


Store가 하는 일

q.Store({ Step = q.Source(1) })를 만들 때 실제로 일어나는 일로 옳은 것은 무엇인가요?

  • Step이라는 이름의 새 반응형 노드가 하나 생기고, 그 안에 넘긴 Source가 감싸입니다
  • 넘긴 Source가 store.Step 자리에 그대로 들어앉고, Store는 그것들을 이름으로 묶는 가방일 뿐입니다
  • Store가 필드를 전부 하나로 합쳐 :Compute로 파생시킨 새 State를 돌려줍니다

Store는 반응형 그래프에 사는 새 노드가 아닙니다. 이미 만든 Source들을 한 테이블에 담아 점 접근을 주는 것이 전부라, store.Step은 넣은 그 Source 객체 자신입니다.

가방에 열쇠를 하나 더

같은 ctx에 Settings.Provider를 하나 더 :Set 했을 때 CounterBoard.luau를 그대로 둬도 되는 이유는 무엇인가요?

  • CounterBoard가 q.Context를 구독하고 있어서 새 열쇠가 생기면 알아서 반영하기 때문입니다
  • CounterBoard는 props.Ctx를 열어 보지 않고 그대로 Counter에 넘기기만 해서, 가방 안의 열쇠 수와 무관하기 때문입니다
  • Settings.Provider와 Theme.Provider가 같은 이름을 공유해 CounterBoard가 이미 알고 있었기 때문입니다

15장에서 본 것과 같습니다 — 중간 층은 가방을 그대로 건네기만 하고 안에 뭐가 들었는지 모릅니다. 그래서 가방에 열쇠가 늘어도 중간 층의 코드는 바뀔 이유가 없습니다.

선언 안 한 키를 점으로 읽으면

settings.Label처럼 생성자에 넣지 않은 키를 점으로 읽으면 어떻게 되나요?

  • 그 자리에서 새 Source(nil)이 만들어져 저장되고 그것이 돌아옵니다
  • nil입니다 — 점 접근에는 지연 생성이 없고, 없는 이름을 만들어 주는 문은 :Of 하나뿐입니다
  • Quad0194 Store: "Label" is a reserved store key 에러가 그 자리에서 납니다

점 접근으로 없는 이름을 읽으면 그냥 nil입니다. 계산된 이름으로 필드를 지연 생성하는 유일한 문은 store:Of<<T>>(name)입니다.


Quiz Progress

0 / 0 questions answered (0%)

0 correct