08. 생성만 보고 싶다면 — 생명주기 훅
대상 독자: 07. 관측하기를 끝낸 개발자 목표: “만들어질 때 한 번 / 다 그려졌을 때 한 번 / 없어질 때 한 번”을 가장 짧게 적기
06장과 07장에서 Ref와 Effect를 봤습니다. 그런데 실제로 가장 자주 필요한 것은
그 둘의 아주 좁은 쓰임 하나입니다 — 만들어질 때 한 번, 다 그려졌을 때
한 번, 없어질 때 한 번. 값을 계속 지켜볼 것도, 상자를 나중에 꺼내 쓸 것도
아닌데 상자를 만들고 콜백을 걸고 nil 가드까지 쓰는 건 그 수요엔 번거롭습니다.
먼저 지금까지의 재료만으로 직접 짜 보고, 그 다음에 이름을 붙이겠습니다.
1. 먼저 손으로 짜 봅니다
섹션 제목: “1. 먼저 손으로 짜 봅니다”card에 세 자리를 더합니다. 전부 07장까지 나온 것뿐입니다.
-- … 위쪽 코드에 이어집니다(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 = D.Frame { -- …프로퍼티 생략(UICorner = 12까지 그대로)… -- …05·07장에서 붙인 숫자 키(Tag·Attr·Effect)들도 그대로…
created, rendered, q.Effect(function() return function() -- ← 의존성 없이 cleanup만: 이게 곧 파괴 콜백 print("없어짐") end end),
D.TextLabel { --[[ …생략… ]] }, D.TextButton { --[[ …생략… ]] },}실행하면 카드가 만들어지는 동안 두 줄이 순서대로 찍힙니다(07장에서 붙인 관측·이펙트 줄은 여기 적지 않았습니다).
만들어짐: Frame다 그려짐: 자식 3 개그리고 나중에 card:Destroy()를 하면 그때 없어짐이 한 번 찍힙니다.
자식이 셋인 것은 라벨과 버튼에 더해, UICorner = 12가 만들어 붙인 관리 자식까지 세기 때문입니다.
셋이 각자 다른 시점을 잡는 이유는 06·07장에서 이미 봤습니다. PreRef는 그
인스턴스에 아무 일도 일어나기 전에 채워지고, PostRef는 자식과 프로퍼티가 전부
끝난 뒤에 채워지며, **의존성이 없는 Effect**는 만들어질 때 한 번 돌고 돌려준
cleanup만 남겨 두었다가 매달린 인스턴스가 죽을 때 그것을 한 번 실행합니다.
왜 nil 가드가 필요한가요?
ref:Callback(fn)은 06장 3절에서 본 대로 등록하는 그 자리에서 지금 값으로 한 번 즉시 불립니다. 위에서는 카드를 만들기 전에 등록했으니 그 첫 호출이 받는 값은 nil입니다.
그래서 if inst then이 없으면 “만들어짐”이 실제 생성보다 먼저, 그것도 빈 값으로 한 번 더 찍힙니다. 가드는 그 등록 시점의 한 번을 걸러 내는 것뿐입니다.
2. 이 셋에는 이름이 있습니다
섹션 제목: “2. 이 셋에는 이름이 있습니다”같은 것을 quad가 이미 이름 붙여 두었습니다. 위 코드를 이렇게 줄일 수 있습니다.
-- … 위쪽 코드에 이어집니다(1절에서 만든 상자 둘을 지우고 card를 이렇게 고칩니다)const card = D.Frame { -- …프로퍼티 생략… -- …05·07장에서 붙인 숫자 키들도 그대로…
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),
D.TextLabel { --[[ …생략… ]] }, D.TextButton { --[[ …생략… ]] },}실행하면 출력이 §1과 한 줄도 다르지 않습니다.
셋이 돌려주는 값은 각각 PreRef / PostRef / EffectHandle 그 자체라서,
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 fn -- 의존성 없는 Effect의 cleanup이 곧 파괴 콜백 end)end새 브랜드도, 새 디스패치 개념도 없습니다. 1절에서 당신이 손으로 짠 것과 글자까지 같은 모양이고, 그래서 두 절의 출력이 같았던 것입니다. 훅을 여러 개 등록하는 것도 그저 숫자 키 자리를 여러 개 쓰는 일이라, 같은 종류끼리는 숫자 키에 적은 순서대로 불립니다.
4. 알아 둘 것 둘
섹션 제목: “4. 알아 둘 것 둘”OnRendered가 보장하는 것은 자기 아래까지입니다. 얹혀 있는 PostRef와
같은 계약이라 불리는 시점의 Parent는 아직 모르는 상태이고(자세한 것은
06장 4절의 접힘), 화면 좌표나 AbsoluteSize처럼 조상에 따라
달라지는 값은 여기서 읽지 마세요(레퍼런스: 생명주기 훅).
OnDestroyed는 숫자 키 자리에 놓아야 삽니다. 인스턴스에 매달린 Effect의
cleanup이라, 어디에도 놓지 않으면 묶일 인스턴스가 없어 그 함수가 불릴 일도
없습니다. 07장의 Observer·Effect와 같은 규칙입니다.
더 알고 싶다면
섹션 제목: “더 알고 싶다면”- 레퍼런스: 생명주기 훅 — 셋의 시그니처와 불리는 시점,
OnRendered가 보장하지 않는 것, 여러 개 등록했을 때의 순서 - 레퍼런스:
Ref— 이 훅들이 얹혀 있는PreRef/PostRef프리미티브 - 레퍼런스:
Observer/Effect— cleanup이 도는 네 자리