Skip to content

15. 층을 건너 값 넘기기 — q.Context

This content is not available in your language yet.

대상 독자: 14. 목록 만들기를 끝낸 개발자 목표: 진입점에서 만든 값을 중간 층이 모르는 채로 말단 컴포넌트까지 내려보내기

값은 props로 내리는 게 기본입니다. 다만 층이 깊어지면 중간 컴포넌트들이 자기가 쓰지도 않는 값을 계속 받아 넘겨야 합니다. 지금 우리 화면이 딱 그 모양입니다 — 진입점 → CounterBoard → Counter. 강조 색을 Counter가 쓰게 하려고 CounterBoard에 Accent를 받게 하고 싶지는 않습니다.

q.Context는 값 여러 개를 가방 하나로 묶어 넘기는 도구입니다. 중간 층은 가방만 넘기고, 안에 뭐가 들었는지는 몰라도 됩니다.


Provider는 가방 안에서 값을 찾을 열쇠입니다. 01장의 Quad:UseProvider(...)(백엔드를 설치하는 줄)와는 이름만 같은 다른 것입니다. 담는 쪽과 꺼내는 쪽이 같은 열쇠를 봐야 하니, 열쇠는 둘 다 require할 수 있는 모듈에 둡니다.

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

진입점에서 가방을 만들어 값을 담고, 화면에 같이 넘깁니다.

-- (Main.client.luau 계속 — 14장의 board 두 줄은 아래 두 줄로 갈아 끼웁니다)
const Theme = require("@game/ReplicatedStorage/Client/UI/Theme")
const ctx = q.Context():Set(Theme.Provider, {
Accent = Color3.fromRGB(0, 162, 255),
})
const board = CounterBoard { Rows = rows, Ctx = ctx } -- ← 가방을 같이 넘긴다
board.Parent = screen

CounterBoard가 바꿀 곳은 한 줄입니다. props.Ctx를 읽지도, 열지도 않고 Counter에 그대로 건네기만 합니다.

-- … (CounterBoard.luau) updateFn의 마지막 갈래를 이렇게 고칩니다
return Counter {
Label = ctx.Item.Label,
Start = ctx.Item.Start,
Ctx = props.Ctx, -- ← 이 한 줄만 추가. 안에 뭐가 들었는지는 모른다
}, ctx.UserData

CounterBoard는 Theme을 require하지도, 그 열쇠 이름을 알지도 못합니다. 강조 색이 하나 더 늘든 폰트가 들어오든 이 파일은 그대로입니다 — 그것이 이 도구가 줄여 주는 비용입니다.


Counter에서 색을 꺼내 씁니다. 여기서 바뀌는 것은 세 줄입니다 — 열쇠를 가져오는 require 한 줄, 가방을 여는 한 줄, 그리고 색을 쓰는 한 줄. 13장에서 얹은 강조 파이프(highlightColor)는 여기서 테마 색에 자리를 내줍니다.

-- … (Counter.luau) 파일 머리에 한 줄
const Theme = require("./Theme")
return function(props)
const theme = props.Ctx:Get(Theme.Provider) -- ← 내 열쇠로 내 값만 꺼낸다
const count = q.Source(props.Start or 0)
-- …중간 생략…
q.Elem "TextButton" {
BackgroundColor3 = theme.Accent, -- ← 13장의 highlightColor(count, 10) 대신 가방에서 온 색
Text = "+ 1",
-- …나머지 생략…
},
-- …생략…
end

실행하면 목록의 카운터 전부가 가방에 담아 둔 강조 색으로 칠해집니다. 진입점에서 Accent를 바꾸면 그 뒤에 만들어지는 카운터는 전부 새 색을 받고, 그 사이의 CounterBoard는 아무것도 하지 않았습니다. 가방의 값은 컴포넌트를 부를 때 한 번 읽는 평범한 값이라 이미 그려진 카운터는 그대로입니다 — 실행 중에 바꿔 따라오게 하려면 다음 장의 Store입니다.

여기서 중요한 것은 가방도 손으로 넘긴다는 점입니다. React의 Context처럼 트리를 거슬러 올라가 값을 찾아 주는 조회는 quad에 없습니다 — 줄어드는 것은 “중간 층이 알아야 할 이름의 수”이지, 넘기는 행위 자체가 아닙니다.

그래서 넘기는 걸 빠뜨리면 그 자리에서 드러납니다. props.Ctx가 아예 nil이면 nil:Get(...)이 되어 quad의 에러가 아니라 평범한 Lua 에러(attempt to index nil with 'Get')가 납니다.


  • q.Context.Provider(name?)가 돌려주는 것은 테이블 신원 키입니다 — 모듈 사이에 문자열 충돌이 없고, 같은 이름을 두 번 줘도 서로 다른 키입니다. 이름은 에러 메시지용 선택 인자입니다.
  • :Set은 가방 자신을 바꾸고 자기를 돌려줍니다(체이닝). 불변 복제 API도, 가방을 열거하는 API도 없으니, 서브트리용으로 값을 더하려면 만드는 쪽이 q.Context()로 새 가방을 만들어 필요한 열쇠를 다시 :Set 해 넘기세요.
  • :Get은 없는 키에 에러를 내고, :Peek은 nil을 돌려줍니다.
Quad0038 Context:Get: no value for Provider(Locale) - the creator of this Context did not :Set it (use :Peek to test)
왜 가방을 복제해서 넘기는 API는 없나요?

Tag나 Modifier처럼 “원본은 두고 몇 개만 바꾼 사본”을 돌려주는 방식을 일부러 두지 않았습니다. 가방이 서브트리마다 갈라지면 두 가지가 꼬입니다.

  • 디버깅할 표면이 늘어납니다. 가방이 하나면 어디서든 그걸 찍어 보면 전부 보이지만, 사본이 여럿이면 “이 자리는 어느 사본을 받았나”부터 따라가야 합니다.
  • 위로 넘기는 동작이 따로 필요해집니다. 자식이 받은 사본을 바꿨을 때 그걸 부모 쪽에 반영하려면 별도 장치를 만들어야 합니다.

그래서 가방은 하나의 진실로 두고, 서브트리에 값을 더해야 한다면 만드는 쪽이 새 가방을 만들어 필요한 열쇠를 다시 :Set 해 넘깁니다.

ctx:Get(…)의 타입은 어디서 오나요?

Provider<T>의 T는 값에 없는 팬텀이라, 키를 만드는 자리에서 한 번 적어 주면 그 뒤로는 :Get이 알아서 추론됩니다. 위 Theme 모듈에 타입까지 같이 두면 됩니다.

-- (§1의 Theme 모듈 — 타입까지 넣은 모양, --!strict)
const q = require("./Quad")
export type Theme = { Accent: Color3 }
return {
Provider = q.Context.Provider("Theme") :: q.Provider<Theme>,
-- 캐스트 대신 명시적 타입 인자도 된다
-- Provider = q.Context.Provider<<Theme>>("Theme"),
}

q.Provider<Theme>는 01장 설정 모듈의 export type Provider<T> = QuadTypes.Provider<T> 줄에서 옵니다. 이 열쇠의 T는 넣는 타입과 꺼내는 타입이 같아야 해서, 다른 타입의 값을 :Set 하거나 Provider<Frame>을 Provider<Instance> 자리에 넘기면 타입 에러가 납니다(레퍼런스).


중간 층이 하는 일

가방을 받아 아래로 넘기기만 하는 중간 컴포넌트에 대한 설명으로 옳은 것은 무엇인가요?

  • 중간 층도 열쇠 모듈을 require해서 가방을 한 번 열어 본 뒤 넘겨야 합니다
  • 말단이 트리를 거슬러 올라가 가방을 스스로 찾아오므로 중간 층은 아무것도 넘기지 않아도 됩니다
  • 가방을 그대로 건네기만 하면 되고, 열쇠 이름도 안에 든 것도 몰라도 됩니다

중간 층은 열쇠 모듈을 require하지도 그 이름을 알지도 못한 채 한 줄로 넘기기만 합니다. 다만 가방도 손으로 넘기는 것이라, 줄어드는 것은 중간 층이 알아야 할 이름의 수이지 넘기는 행위 자체가 아닙니다.

열쇠와 가방의 규칙

q.Context와 Provider에 대한 설명으로 옳은 것은 무엇인가요?

  • 같은 문자열 이름을 주면 같은 열쇠가 되므로 모듈끼리 이름이 겹치지 않게 관리해야 합니다
  • :Get도 없는 키에는 nil을 돌려주므로 :Peek과 결과가 같습니다
  • Provider(name?)가 돌려주는 것은 테이블 신원 키라, 같은 이름을 두 번 줘도 서로 다른 열쇠입니다

이름은 에러 메시지용 선택 인자일 뿐이라 문자열 충돌이 생기지 않습니다. 그리고 없는 키를 물었을 때 :Get은 에러를 내고 :Peek은 nil을 돌려줍니다.


Quiz Progress

0 / 0 questions answered (0%)

0 correct