Store
Store는 이름 붙은 Source들의 가방입니다. 값을 담는 새 반응형 노드가 아니라, 이미 만든 Source들을 한 레코드로 묶어 타입이 붙은 점 접근(store.hp)을 주는 것이 전부입니다.
이 페이지의 심볼: q.Store(defaults) · store.key · store:Of<<U>>(name) · store:Names() · 예약 키
-- 01장의 설정 모듈: quad_base에 quad_roblox를 설치하고 타입을 다시 내보낸다(시작하기 01 참고)local q = require("@game/ReplicatedStorage/Client/UI/Quad")q.Store(defaults)
섹션 제목: “q.Store(defaults)”시그니처
Store: (() -> Store<{}>) & (<T>(defaults: T) -> Store<T>)
export type Store<T> = T & { Of: <U>(self: any, name: string) -> Source<U>, Names: (self: any) -> { string }, __reservedCheck: CheckReservedKeys<keyof<T>>,}인자
| 이름 | 타입 | 설명 |
|---|---|---|
defaults |
T |
초기 필드. 넘긴 레코드 타입이 그대로 T가 됩니다. 런타임 계약은 메타테이블 없는 평범한 테이블이고 값은 **전부 Source**여야 한다는 것입니다 — 생략하면 빈 Store입니다. |
반환 — Store<T>. T는 넘긴 레코드 타입 그대로입니다(타입 함수 없이 평범한 레코드).
동작
-
초기화는 명시적입니다. 넘긴 테이블의 얕은 복사가 store가 되고(
table.clone— 넘긴 테이블 자체는 아니라 그 뒤 거기에 넣은 키는 store에 안 나타나고, 얼린 테이블을 넘겨도 store는 얼지 않습니다), 메소드는 메타테이블 쪽에 있습니다. 그래서store.hp는 넣은 그Source객체 자신이고,:Names()에 메소드 이름이 섞이지 않습니다. -
값 검증은 화이트리스트입니다 —
Source가 아닌 값은 그 자리에서 거부합니다. 통과시켰다면 첫:Get()에서야 죽었을 것이기 때문입니다. -
검증 에러 문구:
상황 문구 값이 Source가 아님Quad0198 Store: default for "{tostring(name)}" is not a Source (got {typeof(value)})테이블이 아님 Quad0195 Store: defaults must be a table of Sources (got {typeof(defaults)})메타테이블이 달림( Source하나를 그대로 넘긴 경우 등)Quad0196 Store: defaults must be a plain table without a metatable (a bare Source instead of { name = Source }?)브랜드 값(AttrKey·Mapper 디스크립터 등) Quad0197 Store: defaults must be a plain { name = Source } table (got an AttrKey/Mapper descriptor)키가 문자열이 아니거나 빈 문자열 Quad0193 Store: {what} must be a non-empty string (got {if name == "" then '""' else typeof(name)})({what}은 생성자에서key,:Of에서Of name)예약된 이름 Quad0194 Store: "{name}" is a reserved store key틀린 항목이 여럿이어도 에러는 하나만 납니다. 어느 항목의 어느 문구가 나는지는 테이블 순회(해시) 순서가 정해 정해져 있지 않습니다 — 같은 내용의 테이블로 여러 번 만들어도 매번 다를 수 있습니다([2026-09-27 mock 실측]). 하나를 고쳤더니 다른 문구가 새로 나면 남은 항목도 같이 확인하세요.
-
Source의 값 제약이 그대로 따라옵니다 — Modifier는 담을 수 없습니다. 이 검사는Source생성자가 하므로:Of로 만든 필드에도 적용됩니다. -
print(store)는 정렬된 키 목록으로Store{hp, mp}모양입니다. -
table.clone(store)로 만든 사본은 Store가 아닙니다 —q.isStore가 거짓이고,q.Attr에 넘기면Quad0010으로 거부됩니다([2026-09-27 mock 실측]). 같은 모양의 Store가 하나 더 필요하면q.Store로 새로 만드세요.
예제
local store = q.Store({ hp = q.Source(100), name = q.Source("player"),})
print(store.hp:Get()) --> 100store.hp:Set(80)관련 — How-To 02 폼 검증 · 02-source
store.key (선언된 필드)
섹션 제목: “store.key (선언된 필드)”동작
- 선언한 키는 평범한 레코드 필드입니다 — 넣은
Source가 그 자리에 그대로 있고, 타입도 넘긴 레코드 타입 그대로 붙습니다.store.hp는Source<number>이지 래퍼가 아닙니다. - 선언하지 않은 키를 점으로 읽으면
nil입니다. 점 접근에는 지연 생성이 없습니다 — 없는 이름을 그 자리에서 만들어주는 문은:Of하나뿐입니다. 예외는__로 시작하는 예약 이름입니다 —store.__index·store.__tostring처럼 점으로 읽으면 내부 값(공유 메소드 테이블, 함수)이 나오지만, 사용 범위 밖이니 읽거나 고치는 코드에 기대지 마세요.
local store = q.Store({ hp = q.Source(100) })print(store.hp:Get()) --> 100print((store :: any).mp) --> nil (선언 안 한 이름 — 만들어지지 않는다)store:Of<<U>>(name)
섹션 제목: “store:Of<<U>>(name)”시그니처
Of: <U>(self: any, name: string) -> Source<U>인자
| 이름 | 타입 | 설명 |
|---|---|---|
name |
string |
필드 이름. 빈 문자열이 아닌 문자열이어야 하고, 예약 이름이면 안 됩니다. |
반환 — 그 이름의 Source<U>. 없으면 그 자리에서 만들어 저장하고 돌려줍니다.
동작
- 계산된 이름을 쓰는 유일한 문이자 지연 생성이 남아 있는 유일한 자리입니다. 타입이 이름을 알 수 없으므로 결과 타입은 호출자가 타입 인자로 직접 줍니다 — 그만큼 타입 안전은 여기서 포기하는 것입니다.
- 새로 만들어진
Source의 값은 **nil**입니다.store:Of<<number>>("mp"):Get()은 타입이number여도 첫 호출에선nil입니다 — 필요하면 바로:Set하세요. - 같은 이름으로 다시 부르면 같은 객체가 옵니다.
- 타입 인자
U는 검사되지 않는 캐스트입니다 — 이미 있는 필드를:Of로 집어도 그 필드의 실제 타입과 대조하지 않고Source<U>로 내줍니다.Peek<<T>>/As<<T>>와 같은 부류이니, 이름과 타입이 맞는지는 호출자가 책임집니다. - 이름 검증:
Store: {what} must be a non-empty string(여기서{what}은Of name) /Store: "{name}" is a reserved store key
예제
local store = q.Store({ hp = q.Source(100) })
local mp = store:Of<<number>>("mp") -- 결과 타입은 호출자가 직접 준다mp:Set(50) -- 갓 만들어진 Source의 값은 nil이므로 먼저 채운다print(store:Of<<number>>("mp") == mp) --> true (같은 객체)store:Names()
섹션 제목: “store:Names()”시그니처
Names: (self: any) -> { string }반환 — 지금 이 store에 있는 필드 이름들.
동작
- 생성자에 선언한 키와
:Of가 만든 키가 둘 다 들어갑니다. - 메소드(
Of/Names)는 메타테이블 쪽에 있어 세지 않고, 타입에만 있는 팬텀 필드도 들어가지 않습니다. - 순서는 보장되지 않습니다 — 필요하면 직접 정렬하세요.
예제
local store = q.Store({ hp = q.Source(100) })store:Of("mp")
local names = store:Names()table.sort(names)print(table.concat(names, ",")) --> hp,mp예약 키
섹션 제목: “예약 키”Of, Names, 그리고 __(밑줄 둘)로 시작하는 모든 이름은 store 필드 이름으로 쓰지 마세요 — 생성자와 :Of가 거부합니다(그 두 문 밖에서 store.__x = v처럼 직접 대입하는 것까지 막지는 않습니다 — 그 결과도 보장하지 않습니다: store.x = 5처럼 Source가 아닌 값을 직접 넣으면 store:Of("x")가 그 값을 그대로 돌려주고, store.Of = …는 메소드를 가려 store:Of(...)가 죽고, store.hp = nil로 지운 뒤 :Of("hp")는 새 Source를 만들어 옛 Source를 구독하던 Compute와 조용히 갈라집니다. strict가 유일하게 통과시키는 직접 대입인 store.hp = q.Source(7)(같은 타입의 다른 Source로 교체)도 결과는 nil 삭제와 같아서, 옛 Source를 구독하던 Compute와 먼저 만든 q.Attr(store)는 옛 Source에 조용히 남습니다([2026-09-27 실측]). tostring(store)만은 어떤 키가 섞여도 던지지 않습니다). 앞의 둘은 메소드 이름이고, __ 접두는 타입 검사용 팬텀 필드(__reservedCheck 등) 자리입니다.
-
Store의 메소드는
Of/Names에서 늘어나지 않습니다. 사용자 필드와 메소드가 같은 테이블 이름 공간에 있어, 메소드를 하나 더하면 그 이름을 필드로 쓰던 코드가 깨지기 때문입니다. Store에 기능이 더 필요해지면 store를 인자로 받는 함수로 나옵니다. 그래서Of/Names와__접두만 피하면 앞으로도 어떤 필드 이름이든 안전합니다. -
런타임: 생성자와
:Of둘 다 같은 문으로 막습니다 —Store: "{name}" is a reserved store key -
타입 검사 시점:
Store<T>에 얹힌 타입 함수가 주석 자리에서 먼저 경고를 냅니다. 타입 함수는 에러를 던질 수 없어 출력으로만 알립니다 —quad.Store: "{v}" is a reserved key
답변함: 0 / 0 (0%)