07. 인스턴스를 손에 쥐기 — Ref
This content is not available in your language yet.
대상 독자: 06. 다시 위로 올려보내기를 끝낸 개발자 목표: 만들어진 인스턴스를 변수로 잡아 두고, 그것이 언제 손에 들어오는지 알기
지금까지 화면에 무언가를 반영하는 길은 프로퍼티와 표시(태그·속성)였습니다.
그런데 만들어진 실물 인스턴스 자신이 필요할 때가 있습니다 — 크기를 재거나,
이벤트 안에서 그 인스턴스를 직접 만지거나, 밖으로 꺼내 다른 코드에 넘길 때입니다.
Ref는 그 인스턴스를 담아 두는 빈 상자입니다.
1. 상자 하나 놓기 — q.Ref
섹션 제목: “1. 상자 하나 놓기 — q.Ref”Ref를 props의 숫자 키 자리에 놓아 두면 quad가 만들어진 인스턴스를 그 상자에 넣어 줍니다.
카드의 버튼에 상자를 하나 놓습니다.
-- … 위쪽 코드에 이어집니다(card의 버튼을 이렇게 고칩니다)const buttonRef = q.Ref<<TextButton?>>(nil)
const card = q.Elem "Frame" { -- …생략… q.Elem "TextButton" { buttonRef, -- ← 숫자 키: 만들어진 버튼이 여기 담긴다 Text = "+ 1", Activated = function() count:Set(count:Get() + 1) end, },}
print(buttonRef.Value.ClassName) --> "TextButton"실행하면 출력 창에 TextButton이 찍힙니다.
q.Ref<<TextButton?>>(nil)의 꺾쇠 둘이 낯설 수 있습니다.
꺾쇠가 왜 둘인가요?
<<...>>는 Luau에서 타입 인자를 직접 지정하는 문법입니다. 홑화살괄호 하나(f<T>(x))는 비교 연산(f < T > (x))으로 파싱돼 버려서, Luau는 명시적 타입 인자를 두 겹으로 적습니다.
Ref에서 이 표기가 필요한 이유는 타입 파라미터가 초깃값에서 추론되기 때문입니다. q.Ref(nil)이라고만 쓰면 그 상자는 Ref<nil> — 영원히 nil만 담을 수 있는 상자가 됩니다. 그래서 담길 것을 직접 적어 넓힙니다.
const a = q.Ref(nil) -- Ref<nil> — 인스턴스를 담을 수 없다const b = q.Ref<<TextButton?>>(nil) -- Ref<TextButton?> — 비어 있다가 버튼이 담긴다Ref<nil>을 그대로 숫자 키 자리에 놓으면 타입 검사가 그 자리에서 막습니다(--!strict 기준). ?가 붙는 것은 처음엔 비어 있기 때문입니다. 숫자 키 자리에 놓은 Ref는 그 자리가 처리되기 전까지 nil이라, 사실상 항상 T? 모양입니다.
같은 표기가 뒤 장들에서도 계속 나옵니다 — 예를 들어 q.Slot<<Instance>>().
채워지는 시점은 “그 숫자 키 자리가 처리될 때”입니다. 위 예제에서는 q.Elem "TextButton" { ... }이 만들어지는 그 순간이고, 그래서 card가 완성된 뒤에는 이미 들어 있습니다. 반대로 말하면 그 앞줄에서는 아직 비어 있습니다 — 상자를 만든 직후부터 그 자리가 처리되기 전까지는 ref.Value가 nil입니다.
2. 먼저 채워지는 상자 — q.PreRef와 :Unwrap()
섹션 제목: “2. 먼저 채워지는 상자 — q.PreRef와 :Unwrap()”가장 자주 만나는 자리는 같은 props 안에서 자기 인스턴스를 만지는 경우입니다. 버튼의 Activated 안에서 그 버튼 자신의 색을 바꾸는 것처럼요.
사실 클릭은 버튼이 다 만들어진 뒤에 일어나므로, 이벤트 핸들러 안이라면 평범한 Ref도 그때는 이미 채워져 있습니다. 문제는 같은 props를 처리하는 도중에 읽는 자리입니다 — 평범한 Ref는 자기 숫자 키 자리가 처리될 때 채워지므로, 그보다 먼저 처리되는 자리에서는 아직 비어 있을 수 있습니다. q.PreRef는 그 인스턴스에 아무 일도 일어나기 전에 먼저 채워지므로 위치를 따질 필요가 없습니다. 아래는 가장 흔한 이벤트 핸들러 자리로 모양만 먼저 봅니다.
-- … 위쪽 코드에 이어집니다(1절의 buttonRef를 PreRef로 바꿉니다)const buttonRef = q.PreRef<<TextButton?>>(nil)
-- … 그리고 card의 숫자 키 자리, 버튼을 이렇게 고칩니다 q.Elem "TextButton" { buttonRef, Text = "+ 1", BackgroundTransparency = 0, Activated = function() count:Set(count:Get() + 1) buttonRef:Unwrap().BackgroundTransparency = 0.5 -- 가드 없이 end, },실행하면 버튼을 누를 때 숫자가 오르면서 버튼이 반투명해집니다 — BackgroundTransparency가 0에서 0.5로 바뀝니다.
같은 props 안에서 자기 인스턴스를 쓸 때는 PreRef를 권합니다. 숫자 키 자리의 위치와 무관하게 먼저 채워지므로, 이벤트 핸들러든 프로퍼티 계산이든 그 상자가 비어 있는 것을 볼 일이 없습니다.
:Unwrap()은 그 확신을 코드로 적는 방법입니다. 담긴 값을 그대로 돌려주되 타입에서 nil만 벗겨 주고, 정말 비어 있으면 부른 줄을 blame하며 던집니다.
Quad0135 Ref:Unwrap: the Ref is empty (Value is nil) - not filled yet, or never placed규약이지 강제가 아닙니다. :Unwrap()은 “여기서는 반드시 차 있다”를 아는 자리에서만 쓰고, 확신이 없으면 if ref.Value then으로 가드하세요.
PreRef는 일회용입니다. 한 번 채워진 PreRef를 다른 인스턴스에 다시 놓으면 그 자리에서 던집니다 — 인스턴스마다 새로 만드세요(3절의 접힘 “쓰던 Ref를 다른 인스턴스에 다시 써도 되나요?“가 그 이야기입니다).
3. 채워질 때 반응하기 — :Callback과 :Wait
섹션 제목: “3. 채워질 때 반응하기 — :Callback과 :Wait”상자가 언제 차는지를 신경 써야 할 때가 있습니다. 대개는 그 상자를 만든 쪽과 채우는 쪽이 다를 때입니다.
ref:Callback(fn)은 값이 담길 때 부를 함수를 등록합니다. 등록하는 그 자리에서 지금 값으로 한 번 즉시 불리고, 그 뒤로 채워질 때마다 다시 불립니다.
-- … 위쪽 코드에 이어집니다(여기서는 다시 평범한 Ref로 두고, 카드를 만들기 전에 등록합니다 — §2에서 넣은 BackgroundTransparency 두 줄은 그대로 둡니다)const buttonRef = q.Ref<<TextButton?>>(nil)
buttonRef:Callback(function(inst) print("상자:", if inst then inst.ClassName else "nil")end)
const card = q.Elem "Frame" { -- …생략… q.Elem "TextButton" { buttonRef, Text = "+ 1", --[[ …BackgroundTransparency 두 줄과 Activated는 §2 그대로… ]] },}실행하면 상자: nil이 먼저 찍히고(등록 시점엔 비어 있으니까), 카드가 만들어지면서 상자: TextButton이 이어서 찍힙니다.
기다렸다가 그 줄에서 이어서 쓰고 싶다면 ref:Wait()이 있습니다. 다만 성질이 하나 있습니다 — :Wait()은 언제나 다음번 채워짐을 기다립니다. 이미 차 있어도 기다립니다. 그래서 관용구는 둘을 합친 모양입니다.
-- … 위쪽 코드에 이어집니다task.spawn(function() const inst = if buttonRef.Value then buttonRef.Value else buttonRef:Wait().Value print("버튼 준비됨:", inst.ClassName)end)실행하면 상자가 이미 차 있으면 그 줄에서 곧바로 찍히고, 아직이면 채워지는 순간 찍힙니다. :Wait()을 인자 없이 부르려면 yield 할 수 있는 코루틴 안이어야 해서 task.spawn으로 감쌌습니다.
왜 이미 차 있어도 기다리나요?
Ref<T?>에서는 nil도 정당한 값이라 “차 있는가”를 :Wait 쪽에서 판정할 수 없기 때문입니다.
--!strict으로 올릴 때는 위 관용구의 마지막 .Value 대신 :Wait():Unwrap()을 쓰면 nil까지 벗겨집니다.
컴포넌트(12장)에서는 부모가 만든 Ref를 자식이 채우고, 부모가 이 두 방법으로 기다리는 모양이 흔합니다.
쓰던 Ref를 다른 인스턴스에 다시 써도 되나요?
하나의 Ref는 한 자리에만 놓습니다. 앞 인스턴스가 살아 있는 채로 같은 Ref를 둘째 자리에 놓으면 그 자리에서 던집니다.
Quad0232 bindLifetime: value is already bound to another Instance문제는 앞 인스턴스를 Destroy()한 뒤입니다. 그때는 에러 없이 다시 놓입니다. 그래서 “재사용해도 되나 보다” 하고 넘어가기 쉬운데, 그 사이가 함정입니다.
Ref는 담긴 인스턴스가 Destroy()돼도 스스로 비워지지 않습니다. Ref는 대상의 파괴를 감지하지도, 반응하지도 않습니다 — 마지막으로 담긴 것을 그대로 계속 가리킵니다. 그래서 새 인스턴스가 아직 안 만들어진 구간에서 if ref.Value then을 쓰면 죽은 옛 인스턴스를 잡습니다. 가드가 통과하는데 값은 쓰레기인, 가장 찾기 어려운 종류의 버그입니다.
PreRef/PostRef는 아예 일회용이라 그 시도 자체를 막습니다.
Quad0069 PreRef: already used - a PreRef is one-shot (used up once quad places it, fired or not); make a new one for each instance결론은 하나입니다 — Ref는 인스턴스마다 새로 만듭니다. 목록처럼 인스턴스가 계속 갈리는 자리라면 Ref를 돌려 쓰지 말고, 항목을 만드는 그 자리에서 하나씩 만드세요.
4. 전부 끝난 뒤에 받는 상자 — q.PostRef
섹션 제목: “4. 전부 끝난 뒤에 받는 상자 — q.PostRef”PreRef의 거울이 하나 더 있습니다. q.PostRef는 그 인스턴스의 자식과 프로퍼티가 전부 끝난 뒤 채워집니다 — 자기 아래의 완성된 서브트리를 재는 코드(레이아웃 측정 등)가 그 자리입니다. 채워지는 기준은 그 인스턴스의 숫자 키·문자 키 처리가 끝난 시점이지 부모에 붙는 시점이 아닙니다.
q.PostRef를 PreRef와 같은 자리(숫자 키)에 놓고 :Callback으로 받으면 됩니다.
-- 새 예시: 별도 스크립트const doneRef = q.PostRef<<Frame?>>(nil)
doneRef:Callback(function(inst) if inst then print("자식 수:", #inst:GetChildren()) endend)
const card = q.Elem "Frame" { doneRef, q.Elem "TextLabel" { Text = "A" }, q.Elem "TextLabel" { Text = "B" },}실행하면 자식 수: 2가 찍힙니다(등록 시점의 nil 한 번은 위 가드가 걸러 냅니다).
그때 Parent도 정해져 있나요?
계약은 “자식이 전부 붙었다”까지입니다 — 이 인스턴스가 부모에 붙었는지는 계약이 아닙니다. 불리는 그 순간 Parent는 아직 모르는 상태입니다: 위처럼 리터럴로 중첩해 만들었다면 대개 nil이지만, 이미 트리에 있던 인스턴스를 넘겨받았다면 처음부터 차 있습니다. 게다가 그 뒤로 Parent가 영영 바뀌지 않을 수도 있으므로, “나중에 Parent가 설정되면 그때 하자”고 기다리는 코드는 한 번도 안 돌 수 있습니다. 그러니 화면에 실제로 올라간 뒤라야 나오는 값(AbsoluteSize 등)을 여기서 읽지 마세요. 그건 Ref 쪽 이야기가 아니라 엔진 프로퍼티를 관측하는 쪽 이야기입니다.
PreRef와 마찬가지로 일회용이고, 숫자 키 자리에 리터럴로 놓아야 합니다.
이해 점검
섹션 제목: “이해 점검”상자가 채워지는 시점
평범한 q.Ref와 q.PreRef는 각각 언제 채워지나요?
- 둘 다 그 인스턴스가 부모에 붙는 순간 채워지고, 차이는 담기는 값의 타입뿐입니다
-
Ref는 그 숫자 키 자리가 처리될 때,PreRef는 그 인스턴스에 아무 일도 일어나기 전에 채워집니다 -
Ref는 만들어지는 즉시,PreRef는 자식과 프로퍼티가 전부 끝난 뒤에 채워집니다
Ref는 그 자리가 처리되기 전까지 비어 있어 앞줄에서는 nil이고, PreRef는 숫자 키 위치와 무관하게 먼저 채워지므로 같은 props 안에서 자기 인스턴스를 쓸 때 권합니다. 자식과 프로퍼티가 전부 끝난 뒤에 채워지는 것은 q.PostRef입니다.
`:Callback`과 `:Wait`
ref:Wait()의 성질로 옳은 것은 무엇인가요?
- 이미 차 있으면 그 줄에서 곧바로 담긴 값을 돌려줍니다
- 이미 차 있어도 언제나 다음번 채워짐을 기다립니다
- 부르는 그 자리에서 지금 값으로 한 번 즉시 불리고, 그 뒤로 채워질 때마다 다시 불립니다
Ref<T?>에서는 nil도 정당한 값이라 “차 있는가”를 :Wait 쪽에서 판정할 수 없어 언제나 다음 채워짐을 기다립니다. 그래서 관용구는 상자를 먼저 보고 비어 있을 때만 기다리는 모양이 됩니다. 등록 자리에서 한 번 즉시 불리는 것은 :Callback(fn) 쪽입니다.
`Ref`를 돌려 쓰면
담겼던 인스턴스를 Destroy()한 뒤 같은 Ref를 다른 인스턴스 자리에 다시 놓으면 어떻게 되나요?
- 앞 인스턴스가 파괴될 때 상자가 비워졌으므로 안전하게 재사용됩니다
- 에러 없이 다시 놓이지만, 새 인스턴스가 담기기 전까지
.Value는 죽은 옛 인스턴스를 가리킵니다 - 한 번 채워진 상자는 일회용이라 다시 놓는 그 자리에서 던집니다
Ref는 담긴 인스턴스가 Destroy()돼도 스스로 비워지지 않아, 그 구간에서 if ref.Value then은 통과하는데 값은 쓰레기입니다. 일회용이라 그 시도 자체를 막는 것은 PreRef/PostRef 쪽이고, 앞 인스턴스가 살아 있는 채로 같은 Ref를 둘째 자리에 놓으면 그때는 던집니다. 그래서 Ref는 인스턴스마다 새로 만듭니다.
더 알고 싶다면
섹션 제목: “더 알고 싶다면”- 레퍼런스:
Ref—Ref/PreRef/PostRef셋의 발화 시점,:Callback/:WeakCallback/:Wait/:Unwrap의 전체 계약과 에러 문구 - 01. 컴포넌트 경계 규약과 스타일 합성 — 바깥에서
Ref를 받는 컴포넌트의or None관용구
Quiz Progress
0 / 0 questions answered (0%)
0 correct
Your score
0%
0 / 0 correct
Answered: 0 / 0 (0%)