11. quad 위에 라이브러리 얹기
This content is not available in your language yet.
대상 독자: 컴포넌트 묶음(UI 킷)이나 위젯 라이브러리를 만들어 다른 프로젝트가 가져다 쓰게 하려는 개발자 다루는 개념: 게임의
q를 받는 팩토리로서의 라이브러리,q:GetOrCreate로 모듈마다 q당 한 벌 캐시하기,q:AssertProvider, 타입 모듈과typeof(build(nil :: any))반환 주석, 타입 내보내기,q.errorNamespace로 공개 함수 태깅
시작하기 01은 게임이 quad를 조립하는 설정 모듈을 다룹니다. 이 문서는 그 게임이 가져다 쓰는 라이브러리 쪽입니다. 본보기는 이 저장소의 소비자 킷 둘(kits/roblox-devui, kits/roblox-material)이 실제로 택한 모양이며, q:GetOrCreate·q:AssertProvider·q.Backend.provider 위에 서 있습니다.
1. 해결하려는 문제와 큰 그림
섹션 제목: “1. 해결하려는 문제와 큰 그림”라이브러리도 quad를 써야 하는데, 게임과 달리 세 가지를 모릅니다. 소비자의 q와 같은 것을 쓰고 있는지(소비자는 자기 Source를 라이브러리 컴포넌트에 넘기고, 라이브러리가 돌려준 Frame을 자기 Slot에 넣습니다), quad가 어디 설치될지, 잘못 설치됐을 때 소비자가 무엇을 보게 되는지입니다.
이 문서의 답은 하나입니다. 라이브러리는 quad를 찾지도 설치하지도 않고, 게임이 가진 q를 받아서 쓰는 팩토리입니다.
-- 게임 쪽: 설정 모듈이 q를 만들고, 그 q를 라이브러리에 넘긴다local q = require("@game/ReplicatedStorage/Client/UI/Quad") -- UseProvider까지 끝난 qlocal Devui = require("@game/ReplicatedStorage/kits/roblox-devui/src")(q)이렇게 하면 세 가지가 구조로 풀립니다.
- 같은
q를 쓰는가 - 라이브러리가 쓰는q는 게임이 넘긴 바로 그 값이라 다른 값일 수가 없습니다. 라이브러리가 quad를 직접 require하지 않으니 설치 사본이 둘로 갈라질 일도 없습니다. - quad가 어디 있는가 - 라이브러리의
src/는quad_base를 require하지 않습니다. 경로를 아는 쪽은 게임의 설정 모듈뿐입니다. - 라이브러리가 quad를 바꾸지 않는다 - 라이브러리는 quad의 소비자입니다. 자기 기능을
q에 심어 넣으면 소비자의q가 라이브러리에 따라 달라집니다. 그래서 라이브러리의 결과(Devui)는q와 따로 돌려줍니다. 타입 쪽 이유도 있습니다 -q의 타입에 라이브러리 타입을&로 겹치면 타입 검사 비용이 커지고 내보내기 하나가 거대해집니다.
옛 방식(라이브러리가 자기 설정 모듈에서 quad_base를 require하고 UseProvider를 걸어, 그 멱등성에 기대어 게임과 같은 인스턴스를 쓰기를 바라는 모양)은 쓰지 않습니다. 사본이 둘이면 조용히 갈라지고, 갈라졌는지 확인하는 코드를 따로 달아야 했기 때문입니다.
2. 라이브러리의 모든 모듈은 q당 한 번 만드는 팩토리
섹션 제목: “2. 라이브러리의 모든 모듈은 q당 한 번 만드는 팩토리”quad를 쓰는 라이브러리 모듈은 전부 같은 모양입니다. 모듈의 본문을 build(q) 함수에 담고, 모듈이 돌려주는 것은 “q를 받아 build의 결과를 캐시해 돌려주는 함수”입니다. Button이 Style을 쓰는 것처럼 형제 모듈도 StyleM(q)로 부르는데, 같은 q에서는 몇 번을 불러도 같은 결과입니다(q:GetOrCreate - 열쇠로 함수 자신을 넘기면 모듈마다 열쇠가 따로입니다).
--!strict-- MyKit/src/Button.luaulocal Quad = require("./Quad") -- 킷의 타입 모듈(§3) - 값은 없고 타입만 있다local StyleM = require("./Style") -- 형제 모듈은 팩토리로 받는다. StyleM.Style 같은 타입도 여기서 나온다
type Style = StyleM.Styleexport type ButtonProps = { read Text: (string | Quad.State<string>)?, read OnClick: (() -> ())?, read Style: Style?,}
-- stylua: ignore startlocal function build(q: Quad.Q)-- stylua: ignore endlocal Style = StyleM(q)local TextButton = q.Elem "TextButton"
local function Button(props: ButtonProps): TextButton local style = props.Style or Style.default return TextButton { Text = props.Text, BackgroundColor3 = style.Color.Button, -- ... }end
return { Button = Button }-- stylua: ignore startend
return function(q: Quad.Q): typeof(build(nil :: any)) return q:GetOrCreate(build, build)end-- stylua: ignore end위쪽(require와 export type)은 q 없이 모듈 수준에서 돌고, build 안은 q가 있어야 하는 모든 것입니다. 이 모양에서 낯선 부분이 둘 있습니다.
stylua: ignore로 본문을 들여쓰지 않는 이유. local function build(q)로 감싸면 포매터가 본문 전체를 한 칸 들여쓰려 합니다. 수천 줄짜리 킷에서는 이 한 칸이 파일 전체의 diff이자 읽기 부담입니다. 본문은 범위(scope)로는 모듈에서 q로 바뀌었지만 여전히 파일의 최상위 범위이므로, 머리와 꼬리를 -- stylua: ignore start / end로 감싸 본문을 모듈 들여쓰기 그대로 둡니다. 읽을 때는 “이 파일은 q가 있는 상태의 최상위”로 읽으면 됩니다.
반환 타입 주석 typeof(build(nil :: any))는 빼면 안 됩니다. [2026-10-22 실측] Quad & RobloxExtension처럼 교집합 타입인 q에서 q:GetOrCreate(...)를 메소드로 부르면 결과가 any로 나옵니다. 주석이 없으면 형제 모듈이 받는 값의 타입이 에러 하나 없이 조용히 사라지고, 에디터는 자동완성도 타입 검사도 못 합니다. 주석은 build가 돌려주는 타입을 그대로 쓰라는 뜻입니다. 파라미터 q는 자기 반환 타입 주석 안에서 아직 범위에 없으므로 build(nil :: any)로 씁니다 - typeof는 식을 실행하지 않고 타입만 읽습니다.
그 밖의 모양.
- 값에서 나오는 타입은 형 변환한
q로 식을 적어typeof로 뽑습니다. devui의Types.luau는export type GuiHook = typeof(((nil :: any) :: Quad.Q).PostRef(nil :: GuiObject?))이고, 형제 팩토리를nil :: any에 불러 뽑기도 합니다(materialInteraction.luau의typeof(UtilM(nil :: any).preHook(function() end))). - quad를 쓰지 않는 모듈(순수 함수, 상수 — devui
Input.luau)과 타입만 있는 모듈(devuiTypes.luau)은 팩토리로 만들지 않고 보통 모듈로 둡니다. 캐시할q가 없습니다. - 순환을 끊으려고 형제를 늦게 받을 때는 함수 안에서
require("../ColorPicker")(q)로 부릅니다(devuiInputs/InputColor3.luau- 색 선택기의 한 줄이InputColor3입니다). 빌드 중에 자기 열쇠를 다시 찾으면Quad0284로 던지므로, 서로를 빌드 시점에 부르는 순환은 늦은 호출로 풀어야 합니다. - 캐시가
q안에 있는 이유. 라이브러리가 약한 키 표(weak table)에q → 결과를 두면 되지 않느냐는 생각이 들 수 있습니다. 실측하니 결과(모듈 표)의 클로저가q를 쥐고 있어서, 약한 키 표의 값이 키를 붙잡는 꼴이 되어q가 영영 수거되지 않았습니다(Luau에는 에페머론이 없습니다).q:GetOrCreate의 캐시는q안에 있어서q를 놓으면 캐시도 같이 수거됩니다.
라이브러리의 진입점
섹션 제목: “라이브러리의 진입점”진입점(루트 모듈)은 백엔드를 확인하고, 문자열 열쇠로 한 벌을 만듭니다.
-- MyKit/src/init.luau 의 꼬리-- stylua: ignore startend
return function(q: Quad.Q): typeof(build(nil :: any)) q:AssertProvider("quad-roblox", "4.0^.0^") return q:GetOrCreate("quad_kit_my_kit_0", build)end-- stylua: ignore endq:AssertProvider(name, versionPattern)- 넘겨받은q에 필요한 백엔드가 깔려 있는지 확인만 합니다(설치하지 않습니다). 없으면 “게임의 설정 모듈에서UseProvider를 먼저 부르고 넘기라”는Quad0287을 던집니다. 비교를 프로바이더 함수가 아니라 프로바이더가 적어 둔 이름과 버전(q.Backend.provider)으로 하는 이유는, 라이브러리가 가진quad_roblox패키지 사본이 게임이 설치한 사본과 다른 사본일 수 있기 때문입니다 - 함수 비교는 그때 틀리고, 이름과 버전은 사본이 달라도 맞습니다.- 문자열 열쇠 - 형제 모듈은 함수 자신을 열쇠로 쓰지만(한 사본 안이라 충분합니다), 진입점은 문자열을 씁니다. 같은 라이브러리의 사본이 둘 설치돼도(서로 다른 패키지 경로) 같은 API 세대라면 한
q에서 한 벌을 나눠 쓰게 하려는 것입니다. 열쇠에 API 세대를 넣어(_0) API가 다른 사본끼리는 섞이지 않게 합니다. 두 킷의 버전은A.B.C이고A는 quad의 메이저,B는 그 킷 자신의 breaking 번호이므로 열쇠 끝은_<B>입니다.
게임이 라이브러리를 쓰는 쪽은 §1의 두 줄이 전부입니다. 라이브러리 자신의 갤러리·스토리·테스트도 소비자이므로 자기 설정 모듈을 두고 UseProvider를 걸어 그 q를 넘깁니다(kits/roblox-devui/gallery/Quad.luau, kits/roblox-devui/test/support/Quad.luau). 이 저장소는 scripts/check-kit-surface.py(각 킷의 test/run.sh가 돌립니다)로 이 경계를 지킵니다 - require는 킷 폴더 안에 머물고 quad는 킷 자신의 패키지 의존(roblox_packages/quad_roblox 같은 링커)으로만 닿으며, src/는 quad_base를 require하지 않으며, _로 시작하는 멤버에 접근하지 않습니다.
3. 타입 모듈과 타입 내보내기
섹션 제목: “3. 타입 모듈과 타입 내보내기”라이브러리는 src/Quad.luau 같은 타입 모듈 하나를 둡니다. 값이 없고(return table.freeze({})) q를 쥐지 않으며, 라이브러리의 모든 파일이 local Quad = require("./Quad")로 타입만 읽습니다. 이 모듈이 quad 패키지의 타입 위치를 아는 유일한 파일입니다. 라이브러리 자신의 매니페스트는 quad_roblox·quad_types를 의존으로(런타임엔 타입만 쓰지만 require가 풀려야 하므로), 갤러리·테스트용 quad_base를 개발 의존으로 적습니다 — 본보기는 kits/roblox-devui/pesde.toml입니다. 버전 범위는 ^(같은 메이저의 그 이상)로 두어 게임이 적은 quad 버전과 한 사본으로 풀리게 합니다. roblox 타깃 라이브러리는 roblox 타깃 패키지만 물 수 있으므로 quad_types도 roblox 타깃 사본을 받아 roblox_packages/에 놓입니다(이 레포의 두 킷은 워크스페이스에 그 사본이 없어 레포 안에서는 luau 타깃으로 받고, scripts/publish.py가 게시할 때 바꿉니다).
--!strict-- MyKit/src/Quad.luau - 킷의 타입 모듈-- 라이브러리 자신의 패키지 의존(pesde 매니페스트의 quad_roblox·quad_types)이 놓은 링커 — init이 아닌 모듈이라 ../가 패키지 루트local RobloxModule = require("../roblox_packages/quad_roblox")local QuadTypes = require("../roblox_packages/quad_types")
-- 라이브러리의 모듈이 받는 q의 타입: quad 코어 + Roblox 백엔드가 더한 표면export type Q = QuadTypes.Quad & RobloxModule.RobloxExtension
export type State<T> = QuadTypes.State<T>export type Source<T> = QuadTypes.Source<T>export type Slot<T> = QuadTypes.Slot<T>export type Modifier<C> = RobloxModule.Modifier<C>export type ModifierInto<C> = RobloxModule.ModifierInto<C>export type Props<C> = RobloxModule.Props<C>-- ...return table.freeze({})타입을 다시 내보내는 것은 라이브러리가 공개하는 props 타입(read Value: Quad.State<number>)이 이 이름을 쓰기 때문입니다. 소비자가 얻는 곳은 이렇게 갈립니다.
-
quad 타입(
State·Source·Modifier<C>등)은 게임의 설정 모듈에서 얻습니다. 게임은 이미q와 타입을 한 모듈에서 내보내고 있고(시작하기 01), 라이브러리는 자기 루트에서 quad 타입을 다시 내보내지 않습니다. 같은 타입이 두 문으로 나가면 어느 쪽을 써야 하는지 소비자가 매번 고민하게 됩니다. -
라이브러리 자기 타입(
Style같은 것)은 라이브러리를 부르기 전의 모듈에서 얻습니다. 팩토리를 부른 결과 값이 아니라require한 모듈 쪽에 타입이 있습니다.local DevuiM = require("@game/ReplicatedStorage/kits/roblox-devui/src")local Devui = DevuiM(q)local style: DevuiM.Style = Devui.Style.new() -
값의 타입은
quad_types에서, 클래스를 인자로 받는 타입(Modifier<C>·ModifierInto<C>·Spread<C>·SpreadInto<C>·Props<C>·ElemCtor<C>등)은quad_roblox패키지 루트에서 옵니다(전체 목록은 레퍼런스: 설치). -
제네릭 컴포넌트(
Combo<T>처럼 값의 타입을 제네릭으로 받는 것)를 만든다면 State props를 받는 법에 함정이 있습니다 - 실전 레시피 01의 “제네릭 컴포넌트의 타입” 절을 보세요. 거기서 타입 모듈에 한 줄(StateMarker<T>)을 더 내보내라고 합니다.
4. 게임 쪽의 확인: 설정 모듈은 하나
섹션 제목: “4. 게임 쪽의 확인: 설정 모듈은 하나”라이브러리 쪽에는 “사본이 둘인지” 확인하는 코드가 필요 없습니다. 라이브러리가 쓰는 q는 게임이 넘긴 값이고 라이브러리는 quad를 따로 require하지 않으니, 라이브러리와 게임이 서로 다른 quad 위에서 도는 일이 구조상 없습니다. 남는 확인은 하나, 넘긴 q에 백엔드가 깔려 있는가이고 이건 §2의 q:AssertProvider가 합니다.
게임이 자기 안에서 quad 사본을 둘 두면(예를 들어 설정 모듈이 두 개) 그 갈라짐은 게임의 문제입니다. 의심되면 두 설정 모듈이 돌려준 q의 q.moduleIdentity를 비교하세요 - 모듈 인스턴스마다 하나씩 있는 빈 테이블이고, 계약은 “인스턴스마다 하나, 서로 다름”입니다. 사본이 갈렸을 때 보이는 증상은 Quad 모듈 레퍼런스의 “인스턴스끼리 값을 섞지 마세요”에 있습니다.
5. 에러가 소비자의 줄을 짚게 — q.errorNamespace
섹션 제목: “5. 에러가 소비자의 줄을 짚게 — q.errorNamespace”quad 에러는 스택을 훑어 “quad 기계”와 “사용자 코드”의 경계를 찾고 사용자의 줄을 짚습니다. 경계는 공개 표면 함수에 붙여 둔 태그로 찾습니다(Quadnomicon 11). 라이브러리가 아무것도 하지 않으면 라이브러리의 함수는 “사용자 코드”로 보이므로, 라이브러리 안에서 부른 q.Elem이 던진 디스패치 에러 같은 것은 라이브러리 소스의 줄을 짚습니다. 소비자는 자기가 무엇을 잘못 넘겼는지 찾으러 라이브러리 소스를 열어야 합니다.
원칙은 둘입니다.
- quad의 네임스페이스를 받아 쓴다. 태그는
q.errorNamespace하나에 붙입니다 — 라이브러리가 자기quad_error사본으로 새 네임스페이스를 만들면 태그 맵이 갈려 경계 탐색이 끊깁니다. - 공개 함수를 표면으로 태그한다. 소비자가 부르는 함수들을
setFuncLevel로 한 번 등록합니다. 층 번호는quad_types의ERROR_LEVEL_SURFACE(값1)입니다 — quad-roblox도 자기 프로바이더 함수를 이렇게 등록합니다. 함수를 손으로 나열하지 말고 다 지은 내보내기 테이블을 한 번 순회하세요 — 새로 더한 함수가 빠지는 일이 없습니다. 내보내기 순회 뒤에 생기는 함수(생성자가 만들어 돌려주는 객체의 메소드)는 그 생성자가 만들 때 태깅합니다.
-- MyKit/src/init의 build 안 — 내보내기 테이블을 다 지은 뒤 한 번-- quad_types의 ERROR_LEVEL_SURFACE와 같은 값. 라이브러리의 src는 quad 패키지를 직접 부르지 않으므로(§2)-- 여기서는 값을 옮겨 적고, 라이브러리 테스트가 둘이 같은지 고정한다local SURFACE = 1-- MyKitError.fail: 라이브러리가 자기 에러를 던지는 헬퍼(아래 "자기 에러" 항목) — 이 순회에 걸리지 않게 뺀다local seen: { [any]: boolean } = {}local function tagSurface(t: { [any]: any }) seen[t] = true for _, v in t do if type(v) == "function" then if v ~= MyKitError.fail then -- 던지는 헬퍼는 빼기(아래 주의) q.errorNamespace.setFuncLevel(SURFACE, v) end elseif type(v) == "table" and getmetatable(v) == nil and not seen[v] then -- 모듈 테이블만, 한 번씩 tagSurface(v) end endendtagSurface(MyKit)태그를 붙이면 “가장 바깥 표면의 호출자”를 짚는 에러가 라이브러리 함수를 부른 소비자의 줄로 올라갑니다. 예를 들어 라이브러리 함수가 받은 자식을 q.Elem "Frame" { child }에 넣는데 그 자식이 이미 파괴된 Instance였다면, 태그 전에는 Quad0219가 라이브러리 안의 q.Elem 줄을, 태그 뒤에는 소비자가 MyKit.Wrap(…)을 부른 줄을 짚습니다 — [2026-10-20 mock 실측].
알아 둘 한계가 있습니다(Quadnomicon 11의 “보장되지 않는 것” 절).
- 모든 에러가 바깥으로 올라가지는 않습니다. “가장 가까운 표면의 호출자”를 짚는 에러(인자 검증, 콜백 안에서 부른 표면)는 여전히 라이브러리 안의 줄을 짚습니다 — 그 자리를 짚어야 맞는 에러들입니다.
- 테이블 필드로 불리는 함수만 태그할 수 있습니다. 로컬 함수를 직접 부르면 최적화 단계에서 인라인돼 태그가 스택에서 사라집니다. 공개 함수는
MyKit.Button(…)처럼 테이블 필드로 불리니 이 조건에 맞습니다. - 라이브러리가 자기 에러를 던질 때도 같은 네임스페이스의
errorBefore/errorBeforeNearest(층 번호는 같은ERROR_LEVEL_SURFACE)를 쓰면 소비자의 줄을 짚습니다 — 검사가 공개 함수 몇 단계 안쪽에 있어도errorBeforeNearest는 그 공개 함수를 부른 줄을 짚으므로error(msg, n)의 레벨을 손으로 셀 필요가 없습니다(공개 함수 자신이 바로 던지는 한 단계짜리 검사는error(msg, 2)로도 같습니다). 던지는 헬퍼를 따로 두면 그 헬퍼는 태깅하지 마세요 — 태깅하면 그것이 “가장 가까운 표면”이 되어 blame이 헬퍼를 부른 라이브러리 줄로 갑니다(레퍼런스: 백엔드 프로바이더 계약). 시그니처는q.errorNamespace, 넷의 차이는 Quadnomicon 11의 표에 있습니다. 메시지는 quad와 같은 모양(주어: 이유)으로 쓰면 소비자가 읽기 쉽습니다 — 10. 부록.
6. 라이브러리가 하지 말 것
섹션 제목: “6. 라이브러리가 하지 말 것”- 모듈을 require하는 순간 Instance를 만들어 모듈 변수에 쥐지 마세요. 기본 오버레이 레이어처럼 “아무것도 안 넘겨도 쓰이는” 값이 대표적인데, 처음 붙은 화면과 함께 죽습니다. 처음 달라고 할 때 만들고, 줄 때마다 살아 있는지 보는 모양은 실전 레시피 09 §6에 있습니다.
q.debug를 라이브러리 코드에서 켜지 마세요. 소비자와 같은 모듈 인스턴스를 쓰므로 그 필드는 소비자의 것이기도 합니다 — 라이브러리가 켜면 소비자 앱 전체의 진단 줄이 켜집니다. 라이브러리의 테스트에서만 켜세요(두 킷의 spec이 그렇게 합니다).- 라이브러리 안에서
NewModule()로 자기 인스턴스를 만들지 마세요. 그 인스턴스의 값은 소비자의q와 섞이지 않습니다. 격리된 인스턴스는 테스트 하네스의 몫입니다. - 라이브러리의
src/에서quad_base를 require하지 마세요, 그리고q에 무언가를 설치하지 마세요. 라이브러리는q의 소비자입니다 -q에 기능을 심으면 소비자의q가 라이브러리에 따라 달라집니다. 결과는q와 따로 돌려주세요(§1).
7. 체크리스트
섹션 제목: “7. 체크리스트”- 라이브러리의
src/가quad_base를 require하지 않고,local Kit = require(<라이브러리>/src)(q)꼴로 게임의q를 받는가? - quad를 쓰는 모든 모듈이
build(q)를q:GetOrCreate(build, build)로 캐시하는 팩토리이고, 반환 타입 주석typeof(build(nil :: any))가 있는가? - 진입점이
q:AssertProvider로 백엔드를 확인하고, 호환 세대를 넣은 문자열 열쇠로q:GetOrCreate를 부르는가? - 라이브러리의 결과를
q에 심지 않고 따로 돌려주는가? - 타입 모듈이 값 없이 타입만 내보내고, quad 타입은 게임의 설정 모듈에서, 라이브러리 타입은 팩토리를 부르기 전의 모듈에서 나가는가?
- 공개 함수를
q.errorNamespace로 표면 태깅했는가? - require 시점에 Instance를 만들지 않고,
q.debug를 켜지 않으며, 자기 모듈 인스턴스를 따로 만들지 않는가?
이해 점검
섹션 제목: “이해 점검”팩토리 모듈의 반환 타입 주석
라이브러리 모듈이 return function(q: Quad.Q): typeof(build(nil :: any))처럼 반환 타입을 typeof(build(nil :: any))로 적는 이유는 무엇인가요?
- 교집합 타입인
q에서q:GetOrCreate를 메소드로 부르면 결과가any로 나와, 주석이 없으면 형제 모듈이 받는 값의 타입이 에러 없이 사라지기 때문입니다 -
build가 한 번만 실행되도록 Luau가 강제하기 때문입니다 - 파라미터
q를 반환 타입 안에서 쓰려면nil :: any로 가려야 하기 때문입니다
nil :: any는 파라미터가 자기 반환 타입 주석 안에서 범위에 없기 때문에 쓰는 것이고, 주석 자체가 필요한 이유는 any로 새는 결과 타입을 되살리는 것입니다.
진입점이 AssertProvider로 확인하는 것
라이브러리의 진입점이 q:AssertProvider("quad-roblox", "4.0^.0^")로 이름과 버전을 비교하고, 프로바이더 함수를 비교하지 않는 이유는 무엇인가요?
- 라이브러리가 가진
quad_roblox패키지 사본이 게임이 설치한 사본과 다를 수 있어, 함수 비교는 틀리지만 이름과 버전은 맞기 때문입니다 - 이름과 버전 비교가 함수 비교보다 빨라서입니다
- 라이브러리가 직접
UseProvider를 부르기 때문입니다
라이브러리는 백엔드를 설치하지 않고 확인만 합니다. 설치는 게임의 설정 모듈이 합니다.
- 시작하기 01. 프레임워크 설정하기 — 게임의 설정 모듈, 문자열
require - 레퍼런스: Quad 모듈 —
UseProvider의 멱등성,moduleIdentity,errorNamespace, 인스턴스끼리 값을 섞을 때 - 레퍼런스: 설치 —
quad_roblox패키지 루트가 내보내는 타입 - 실전 레시피 01. 컴포넌트 경계 규약과 스타일 합성 — 라이브러리 컴포넌트의 props 규약, 제네릭 컴포넌트의 타입
- 실전 레시피 09. 떠 있는 것 열고 닫기 — 죽지 않는 기본 레이어, 바깥 클릭·Escape
- Quadnomicon 11 — 표면 태그와 blame의 원리
Quiz Progress
0 / 0 questions answered (0%)
0 correct
Your score
0%
0 / 0 correct
Answered: 0 / 0 (0%)