Skip to content

18. 흐름을 잠시 막기 — Blocker

This content is not available in your language yet.

대상 독자: 17. 움직이게 하기를 끝낸 개발자 목표: 값 여럿을 한 번에 바꿀 때 중간 상태가 화면에 새지 않게 막기

지금까지는 값을 하나씩 바꿨습니다. 둘을 같이 바꿔야 할 때 무슨 일이 생기는지부터 봅니다.

미리 밝혀 두면, 이 장의 q.Blocker는 코어 메커니즘이 아니라 state:Gate 위에 얹힌 순수 슈거입니다 — 같은 것을 라이브러리 밖에서 그대로 짤 수 있고, 그래서 레퍼런스에서도 sugar 그룹에 있습니다. 붙이는 자리도 새 문법이 아닙니다 — 13장에서 :Apply에 넘긴 것은 함수였고, Blocker는 같은 자리에 __apply를 가진 객체로 들어갑니다.


1. 값 둘을 같이 바꾸면 화면이 두 번 바뀝니다

섹션 제목: “1. 값 둘을 같이 바꾸면 화면이 두 번 바뀝니다”

카운터에 단위를 붙입니다. 03장에서 suffix를 의존으로 넘겼던 그 모양 그대로입니다.

이 장의 예제는 Blocker 하나를 보기 위해 props 없이 값을 직접 박은 단독 카운터로 되돌립니다 — 12장의 props.Start/props.Label도, 15장의 props.Ctx도 여기서는 쓰지 않습니다. 15장에서 테마 색을 꽂았던 + 1 버튼의 BackgroundColor3도 리터럴 Color3.fromRGB(0, 162, 255)로 되돌리고, props.Ctx를 읽던 줄과 Theme require는 지웁니다. 16장에서 넣은 셋 — Settings require, props.Ctx:Get(Settings.Provider), Activated의 settings.Step:Get() — 도 지우고 count:Set(count:Get() + 1)로 되돌립니다(남겨 두면 settings가 없어 + 1 클릭이 던집니다)(14장의 보드는 그대로 둬도 됩니다 — 그러면 같은 카운터가 여럿 뜹니다).

-- … (Counter.luau) 상태 두 개와 파이프 하나
const count = q.Source(10)
const unit = q.Source("점")
const countText = count:Compute(function(c, prev, u)
return `{c:Get()} {u:Get()}`
end, unit)

단위를 바꾸는 버튼을 답니다. 10점은 100포인트라 값과 단위를 같이 바꿔야 합니다.

-- … (Counter.luau) 카드의 숫자 키 자리에 이어집니다
q.Elem "TextButton" {
Text = "단위 바꾸기",
Activated = function()
const toPoint = unit:Get() == "점"
count:Set(if toPoint then count:Get() * 10 else count:Get() // 10)
unit:Set(if toPoint then "포인트" else "점")
end,
},

실행하면 버튼 한 번에 라벨이 10 점 → 100 포인트로 갑니다. 그런데 :Set 사이를 끊어 보면 이렇습니다.

마운트 직후 Text = "10 점"
count:Set(100) 뒤 Text = "100 점" ← 어느 시점에도 진실이 아닌 중간 상태
unit:Set 뒤 Text = "100 포인트"

두 :Set이 각각 통지를 냈기 때문입니다. 파이프 함수가 돈 횟수도 마운트 1회 + 버튼 2회 = 3회입니다.


2. 통지를 붙잡아 뒀다가 한 번에 — q.Blocker()

섹션 제목: “2. 통지를 붙잡아 뒀다가 한 번에 — q.Blocker()”

Blocker는 전파를 잠시 붙잡아 두는 스위치입니다. 파이프 끝에 게이트를 하나 끼워 두고 그 스위치로 여닫습니다.

-- … (Counter.luau) 위 §1의 파이프 끝에 한 토막을 덧붙입니다
const blocker = q.Blocker()
const countText = count:Compute(function(c, prev, u)
return `{c:Get()} {u:Get()}`
end, unit):Apply(blocker) -- ← 파이프 끝에 게이트 하나

§1에서 바뀐 것은 마지막 :Apply(blocker) 한 토막뿐입니다 — 원천 둘도, 파이프의 콜백도 그대로입니다.

그 한 토막이 돌려주는 것이 새 노드라는 게 핵심입니다. 라벨이 보는 countText는 게이트를 지난 쪽이어야 합니다 — Blocker를 만들어 두기만 하거나, :Apply를 부르고 그 반환을 받지 않은 채 게이트 없는 예전 노드를 계속 꽂아 두면 아무것도 막히지 않습니다.

03장의 그림에 게이트 한 단이 끼어든 모양입니다 — 원천 둘이 파이프에서 하나로 합쳐진 뒤에 게이트가 섭니다.

flowchart LR
    C["<b>원천</b><br/><code>count</code><br/>값을 넣는다"] --> P["<b>파이프</b><br/><code>:Compute(fn)</code><br/>값을 처리한다"]
    U["<b>원천</b><br/><code>unit</code><br/>값을 넣는다"] --> P
    P --> G["<b>게이트</b><br/><code>:Apply(blocker)</code><br/>통지를 붙잡아 둔다"]
    G --> T["<b>프로퍼티</b><br/><code>Text = …</code><br/>화면에 그린다"]

게이트를 파이프 뒤에 두는 이유는 게이트가 모으는 것이 값이 아니라 통지이기 때문입니다. 붙잡아 둔 통지는 푸는 순간 전부 하류로 나갑니다. 그러니 통지가 아직 여러 갈래로 갈라져 있는 자리에 게이트를 여러 개 세우면, 푸는 순간 그 갈래 수만큼 통지가 나갑니다. 통지까지 한 번으로 접으려면 두 원천이 합쳐진 뒤 한 자리에서 막아야 합니다. 1차선 톨게이트를 세우는 자리라고 보면 됩니다 — 길이 합쳐진 다음에 세워야 차가 한 줄로 지나갑니다.

원천마다 게이트를 두면 안 되나요?

됩니다. 중간 상태는 그쪽으로도 막힙니다 — count:Apply(blocker):Compute(fn, unit:Apply(blocker))로 짜도 100 점은 화면에 나타나지 않습니다. 첫 통지를 받고 라벨이 값을 당겨 올 때 count와 unit이 이미 둘 다 최신이기 때문입니다.

갈리는 것은 그다음입니다. :Off() 한 번에 통지가 둘 나가고 라벨의 Text 쓰기도 두 번입니다(파이프 뒤에 하나면 각각 한 번). 반면 파이프 함수가 돈 횟수는 두 모양이 똑같이 한 번인데, 그건 게이트 덕이 아니라 19장의 게으름 덕입니다 — 둘째 통지가 왔을 때는 새로 계산할 것이 남아 있지 않습니다.

하나의 Blocker를 여러 노드에 붙이는 것 자체는 유효한 쓰임입니다(§4).

Blocker 아래에는 뭐가 있나요?

state:Gate(setup)입니다. 게이트는 상류에서 온 통지를 자기 안에 모아 두고, 언제 한 번에 내려보낼지를 setup이 돌려준 정책 함수에 맡기는 노드입니다.

Blocker는 그 위에 얹힌 정책 하나입니다 — “켜져 있으면 모으고, 끄면 흘려보낸다”. state:Apply(blocker)는 state:Gate(function(emit) return blocker:Policy(emit) end)와 정확히 같고, 게이트를 직접 배선하면 다른 정책도 만들 수 있습니다(레퍼런스: state:Gate(setup)).

버튼은 바꾸는 구간을 :On()과 :Off()로 감쌉니다.

-- … (Counter.luau) 단위 바꾸기 버튼의 Activated
Activated = function()
blocker:On()
-- …§1의 본문 세 줄 그대로…
blocker:Off() -- 여기서 한 번만 통지된다
end,

실행하면 라벨은 10 점에서 100 포인트로 곧바로 넘어가고 100 점은 나타나지 않습니다. 파이프 함수가 돈 횟수는 마운트 1회 + 버튼 1회 = 2회이고(§1은 3회였습니다), :Off()가 낸 통지는 1회입니다.


3. 화면만 멈추는 “일시정지” 버튼

섹션 제목: “3. 화면만 멈추는 “일시정지” 버튼”

같은 스위치를 켜 둔 채로 둘 수도 있습니다. §2의 상태와 파이프는 그대로 두고, 버튼 하나만 더해 그 버튼이 스위치를 토글하게 합니다.

-- … (Counter.luau) 카드의 숫자 키 자리에 버튼 하나 더
q.Elem "TextButton" {
Text = "일시정지",
Activated = function()
if blocker.Blocking then blocker:Off() else blocker:On() end
end,
},

실행하면 일시정지를 누른 뒤로는 + 1을 아무리 눌러도 라벨이 10 점에서 멈춰 있습니다. 다시 누르면 밀린 것이 한 번에 최신값으로 반영됩니다 — 세 번 눌렀다면 11 점, 12 점을 거치지 않고 곧장 13 점입니다.

막는 것은 통지지 값이 아닙니다. 멈춰 있는 동안에도 값은 최신입니다.

-- (확인용 — 일시정지를 켜 두고 + 1을 세 번 누른 뒤. 화면의 라벨은 아직 "10 점")
print(countText:Get()) --> "13 점" (값은 이미 최신이다)
시간으로 막고 싶으면요?

“신호가 멎고 0.2초 뒤에 한 번만 통과” 같은 시간 정책은 손으로 여닫는 대신 q.Debounce / q.Throttle을 쓰면 됩니다. 같은 게이트 위에 얹힌 시간 버전이라 붙이는 방법도 state:Apply(...)로 같고, 옵션과 제어 핸들은 레퍼런스: Debounce / Throttle에 있습니다.


  • :OffWithoutEmit()은 쌓인 것을 버리고 풉니다 — 통지 없이 조용히 끝내고 싶을 때. 버려도 그래프는 망가지지 않습니다(값은 어차피 읽는 시점에 최신입니다).
  • 하나의 Blocker를 여러 노드에 붙일 수 있습니다. :Off() 한 번이 붙어 있는 게이트를 차례로 풉니다 — 다만 통지는 게이트마다 하나씩 나갑니다. 그래서 한 화면을 한 번에 갱신하려는 목적이라면 게이트는 통지가 합쳐지는 자리에 하나만 두고(§2), 서로 다른 화면 여러 곳을 같은 스위치로 묶고 싶을 때 여러 노드에 붙입니다.
  • Blocking은 카운터가 아니라 평범한 불리언입니다. :On()을 두 번 해도 :Off() 한 번이면 풀리니, 겹치는 구간이 필요하면 구간마다 Blocker를 따로 만드세요.

스위치를 어디에 꽂아야 하나

q.Blocker()를 만들어 두기만 하고 라벨에는 예전 노드를 그대로 꽂아 두면 어떻게 되나요?

  • Blocker가 그 파이프를 찾아 붙으므로 :On()을 부르면 그대로 막힙니다
  • 아무것도 막히지 않습니다 — 라벨이 보는 것은 :Apply(blocker)가 돌려준 새 노드여야 합니다
  • 만들어 둔 Blocker는 그 스크립트의 모든 원천을 한꺼번에 막습니다

:Apply(blocker)가 돌려주는 것이 게이트를 지난 새 노드라는 게 핵심입니다. 반환을 받지 않은 채 게이트 없는 예전 노드를 계속 꽂아 두면 통지는 그대로 흘러갑니다.

게이트를 파이프 뒤에 두는 이유

원천 둘이 합쳐진 뒤에 게이트 하나를 두는 이유는 무엇인가요?

  • 원천마다 게이트를 두면 중간 상태가 화면에 새기 때문입니다
  • 원천마다 게이트를 두면 파이프 함수가 원천 수만큼 다시 돌기 때문입니다
  • 게이트가 모으는 것이 값이 아니라 통지라서, 갈래마다 게이트를 두면 푸는 순간 그 갈래 수만큼 통지가 나가기 때문입니다

원천마다 게이트를 두어도 중간 상태는 막히고 파이프 함수가 도는 횟수도 같습니다. 갈리는 것은 통지의 수이고, 통지까지 한 번으로 접으려면 길이 합쳐진 다음에 1차선 톨게이트를 세워야 합니다.

막혀 있는 동안의 값

일시정지로 스위치를 켜 둔 채 + 1을 세 번 누르면 어떻게 되나요?

  • 막힌 동안의 :Set은 버려지므로 다시 풀면 화면이 멈췄던 값 그대로입니다
  • 다시 풀면 밀린 값이 차례로 재생되어 화면이 한 칸씩 올라갑니다
  • 화면은 멈춰 있지만 값은 이미 최신이라, 다시 풀면 중간 값을 거치지 않고 곧장 최신값으로 갑니다

막는 것은 통지지 값이 아닙니다 — 멈춰 있는 동안에도 게이트된 노드의 :Get()은 최신을 돌려줍니다. 그래서 다시 열면 밀린 것이 한 번에 최신값으로 반영됩니다.


Quiz Progress

0 / 0 questions answered (0%)

0 correct