09. 생성만 보고 싶다면 — 생명주기 훅
대상 독자: 08. 관측하기를 끝낸 개발자 목표: “만들어질 때 한 번 / 다 그려졌을 때 한 번 / 없어질 때 한 번”을 가장 짧게 적기
07장과 08장에서 Ref와 Effect를 봤습니다. 그런데 실제로 가장 자주 필요한 것은
그 둘의 아주 좁은 쓰임 하나입니다 — 만들어질 때 한 번, 다 그려졌을 때
한 번, 없어질 때 한 번. 값을 계속 지켜볼 것도, 상자를 나중에 꺼내 쓸 것도
아닌데 상자를 만들고 콜백을 걸고 nil 가드까지 쓰는 건 그 수요엔 번거롭습니다.
먼저 지금까지의 재료만으로 직접 짜 보고, 그 다음에 이름을 붙이겠습니다.
1. 먼저 손으로 짜 봅니다
섹션 제목: “1. 먼저 손으로 짜 봅니다”card에 세 자리를 더합니다. 전부 08장까지 나온 것뿐입니다.
-- … 위쪽 코드에 이어집니다(card를 이렇게 고칩니다)const created = q.PreRef<<Frame?>>(nil)created:Callback(function(inst) if inst then print("만들어짐:", inst.ClassName) endend)
const rendered = q.PostRef<<Frame?>>(nil)rendered:Callback(function(inst) if inst then const kids = inst:GetChildren() print("다 그려짐: 자식", #kids, "개") endend)
const card = q.Elem "Frame" { -- …프로퍼티 생략(UICorner = 12까지 그대로)… -- …05·08장에서 붙인 숫자 키(Tag·Attr·Effect)들도 그대로…
created, rendered, q.Effect(function() return function() -- ← 의존성 없이 cleanup만: 이게 곧 파괴 콜백 print("없어짐") end end),
q.Elem "TextLabel" { --[[ …생략… ]] }, q.Elem "TextButton" { --[[ …생략… ]] },}실행하면 카드가 만들어지는 동안 두 줄이 순서대로 찍힙니다(08장에서 붙인 관측·이펙트 줄은 여기 적지 않았습니다).
만들어짐: Frame다 그려짐: 자식 3 개그리고 나중에 card:Destroy()를 하면 그때 없어짐이 한 번 찍힙니다.
자식이 셋인 것은 라벨과 버튼에 더해, UICorner = 12가 만들어 붙인 관리 자식까지 세기 때문입니다.
셋이 각자 다른 시점을 잡는 이유는 07·08장에서 이미 봤습니다. PreRef는 그
인스턴스에 아무 일도 일어나기 전에 채워지고, PostRef는 자식과 프로퍼티가 전부
끝난 뒤에 채워지며, **의존성이 없는 Effect**는 만들어질 때 한 번 돌고 돌려준
cleanup만 남겨 두었다가 매달린 인스턴스가 죽을 때 그것을 한 번 실행합니다.
왜 nil 가드가 필요한가요?
ref:Callback(fn)은 07장 3절에서 본 대로 등록하는 그 자리에서 지금 값으로 한 번 즉시 불립니다. 위에서는 카드를 만들기 전에 등록했으니 그 첫 호출이 받는 값은 nil입니다.
그래서 if inst then이 없으면 “만들어짐”이 실제 생성보다 먼저, 그것도 빈 값으로 한 번 더 찍힙니다. 가드는 그 등록 시점의 한 번을 걸러 내는 것뿐입니다.
2. 이 셋에는 이름이 있습니다
섹션 제목: “2. 이 셋에는 이름이 있습니다”같은 것을 quad가 이미 이름 붙여 두었습니다. 위 코드를 이렇게 줄일 수 있습니다.
-- … 위쪽 코드에 이어집니다(1절에서 만든 상자 둘을 지우고 card를 이렇게 고칩니다)const card = q.Elem "Frame" { -- …프로퍼티 생략… -- …05·08장에서 붙인 숫자 키들도 그대로…
q.OnCreated<<Frame>>(function(inst) print("만들어짐:", inst.ClassName) end), q.OnRendered<<Frame>>(function(inst) const kids = inst:GetChildren() print("다 그려짐: 자식", #kids, "개") end), q.OnDestroyed(function() print("없어짐") end),
q.Elem "TextLabel" { --[[ …생략… ]] }, q.Elem "TextButton" { --[[ …생략… ]] },}실행하면 출력이 §1과 한 줄도 다르지 않습니다.
셋이 돌려주는 값은 각각 PreRef / PostRef / Effect 그 자체라서,
1절에서 상자를 놓았던 그 숫자 키 자리에 그대로 놓습니다. 콜백이 받는 첫 인자는
항상 인스턴스입니다 — nil 가드가 안에 들어 있습니다.
요소 타입은 q.OnCreated<<Frame>>(...)처럼 타입 인자로 줍니다(quad-base는
백엔드의 요소 타입을 모르므로 그 자리가 제네릭이고, 콜백 파라미터 주석으로는
채워지지 않습니다). OnDestroyed만 타입 인자가 없습니다 — 콜백이 인스턴스를
받지 않기 때문입니다.
3. 안이 이렇게 생겼습니다
섹션 제목: “3. 안이 이렇게 생겼습니다”이 셋은 새로 만들어진 물건이 아닙니다. quad-base의 LifecycleHooks 모듈에
들어 있는 것도 사실상 이 스무 줄 남짓이 전부입니다.
-- (문서용으로 다듬은 요지 — 인자 검사·에러 위치 지정 줄과 타입 인자는 뺐습니다)local function guard(fn) return function(value, ref) if value ~= nil then -- 등록 시 한 번 오는 nil 호출을 건너뛴다 fn(value, ref) end endend
local function OnCreated(fn) return q.PreRef(nil):Callback(guard(fn))end
local function OnRendered(fn) return q.PostRef(nil):Callback(guard(fn))end
local function OnDestroyed(fn) return q.Effect(function() return function(dying) -- 의존성 없는 Effect의 cleanup이 곧 파괴 콜백 — if dying then fn() end -- 인스턴스가 죽어서 불린 때만(자리를 떠날 때는 아님) end end)end새로운 종류의 값도, 새로운 처리 규칙도 없습니다. 1절에서 손으로 짠 것과 같은 모양이고,
그래서 두 절의 출력이 같았던 것입니다. OnDestroyed의 dying 한 줄만 다릅니다 — Effect의
cleanup은 인스턴스가 죽을 때만이 아니라 그 자리를 떠날 때도 불리는데(8장), 훅은 그중
죽음만 골라 받습니다. 자리에 직접 놓는 이 장의 쓰임에서는 둘이 같습니다. 훅을 여러 개
등록하는 것도 그저 숫자 키 자리를 여러 개 쓰는 일이라, OnCreated·OnRendered는 같은 종류끼리 숫자 키에
적은 순서대로 불립니다(OnDestroyed끼리의 순서는 정해져 있지 않습니다 — 파괴 시그널이 정합니다).
4. 알아 둘 것 둘
섹션 제목: “4. 알아 둘 것 둘”OnRendered가 보장하는 것은 자기 아래까지입니다. 얹혀 있는 PostRef와
같은 계약이라 불리는 시점의 Parent는 아직 모르는 상태이고(자세한 것은
07장 4절의 접힘), 화면 좌표나 AbsoluteSize처럼 조상에 따라
달라지는 값은 여기서 읽지 마세요(레퍼런스: 생명주기 훅).
OnDestroyed는 숫자 키 자리에 놓아야 삽니다. 인스턴스에 매달린 Effect의
cleanup이라, 어디에도 놓지 않으면 묶일 인스턴스가 없어 그 함수가 불릴 일도
없습니다. 08장의 Observer·Effect와 같은 규칙입니다.
이해 점검
섹션 제목: “이해 점검”훅 셋의 정체
q.OnCreated/q.OnRendered/q.OnDestroyed가 돌려주는 값은 무엇인가요?
- 훅 전용으로 새로 만든 종류의 값이라, 이름을 문자 키 자리에 적어야 합니다
- 각각
PreRef/PostRef/Effect그 자체라, 손으로 짤 때 상자를 놓던 숫자 키 자리에 그대로 놓습니다 - 값을 돌려주지 않고 등록만 하므로, 부르는 것만으로 인스턴스에 묶입니다
셋은 새로운 종류의 값도 새로운 처리 규칙도 아니고 PreRef·PostRef·의존성 없는 Effect 위에 얹은 것이라, §1에서 손으로 짠 것과 출력이 한 줄도 다르지 않습니다. 다른 점은 nil 가드가 안에 들어 있어 콜백의 첫 인자가 항상 인스턴스라는 것뿐입니다.
`OnRendered`가 보장하는 범위
q.OnRendered 콜백이 불리는 시점에 믿을 수 있는 것은 어디까지인가요?
- 그 인스턴스의 자식과 프로퍼티가 전부 끝났다는 것까지입니다
- 그 인스턴스가 부모에 붙어 화면에 올라갔다는 것까지입니다
- 조상까지 포함한 배치가 끝나
AbsoluteSize를 읽을 수 있다는 것까지입니다
OnRendered는 얹혀 있는 PostRef와 같은 계약이라 보장하는 것은 자기 아래까지이고, 불리는 시점의 Parent는 아직 모르는 상태입니다. 그래서 조상에 따라 달라지는 값은 여기서 읽지 않습니다.
더 알고 싶다면
섹션 제목: “더 알고 싶다면”- 레퍼런스: 생명주기 훅 — 셋의 시그니처와 불리는 시점,
OnRendered가 보장하지 않는 것, 여러 개 등록했을 때의 순서 - 레퍼런스:
Ref— 이 훅들이 얹혀 있는PreRef/PostRef프리미티브 - 레퍼런스:
Observer/Effect— cleanup이 도는 네 자리
퀴즈 진행 상황
0 / 0 답변한 문제 (0%)
0 정답
퀴즈 완료!
0%
0 / 0 정답
답변함: 0 / 0 (0%)