콘텐츠로 이동

09. 생성만 보고 싶다면 — 생명주기 훅

대상 독자: 08. 관측하기를 끝낸 개발자 목표: “만들어질 때 한 번 / 다 그려졌을 때 한 번 / 없어질 때 한 번”을 가장 짧게 적기

07장과 08장에서 Ref와 Effect를 봤습니다. 그런데 실제로 가장 자주 필요한 것은 그 둘의 아주 좁은 쓰임 하나입니다 — 만들어질 때 한 번, 다 그려졌을 때 한 번, 없어질 때 한 번. 값을 계속 지켜볼 것도, 상자를 나중에 꺼내 쓸 것도 아닌데 상자를 만들고 콜백을 걸고 nil 가드까지 쓰는 건 그 수요엔 번거롭습니다.

먼저 지금까지의 재료만으로 직접 짜 보고, 그 다음에 이름을 붙이겠습니다.


card에 세 자리를 더합니다. 전부 08장까지 나온 것뿐입니다.

-- … 위쪽 코드에 이어집니다(card를 이렇게 고칩니다)
const created = q.PreRef<<Frame?>>(nil)
created:Callback(function(inst)
if inst then
print("만들어짐:", inst.ClassName)
end
end)
const rendered = q.PostRef<<Frame?>>(nil)
rendered:Callback(function(inst)
if inst then
const kids = inst:GetChildren()
print("다 그려짐: 자식", #kids, "개")
end
end)
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이 없으면 “만들어짐”이 실제 생성보다 먼저, 그것도 빈 값으로 한 번 더 찍힙니다. 가드는 그 등록 시점의 한 번을 걸러 내는 것뿐입니다.


같은 것을 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만 타입 인자가 없습니다 — 콜백이 인스턴스를 받지 않기 때문입니다.


이 셋은 새로 만들어진 물건이 아닙니다. quad-base의 LifecycleHooks 모듈에 들어 있는 것도 사실상 이 스무 줄 남짓이 전부입니다.

-- (문서용으로 다듬은 요지 — 인자 검사·에러 위치 지정 줄과 타입 인자는 뺐습니다)
local function guard(fn)
return function(value, ref)
if value ~= nil then -- 등록 시 한 번 오는 nil 호출을 건너뛴다
fn(value, ref)
end
end
end
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끼리의 순서는 정해져 있지 않습니다 — 파괴 시그널이 정합니다).


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는 아직 모르는 상태입니다. 그래서 조상에 따라 달라지는 값은 여기서 읽지 않습니다.


퀴즈 진행 상황

0 / 0 답변한 문제 (0%)

0 정답