---
title: "무엇이 다시 계산되는지가 네 가지로 나뉜다"
description: "uGUI 는 한 칸이 바뀌면 캔버스가 통째로 다시 계산됐습니다. UI Toolkit 은 스타일 해석·레이아웃·정점 버퍼·렌더 상태 넷으로 갈립니다. 바꾼 값마다 어디까지 번지는지, 숨기는 방법 셋의 비용, UsageHints, 바인딩의 리플렉션 비용까지 정리했습니다."
author: "유니얼"
pubDate: 2026-09-20
category: "game-dev/unity/ui"
tags: ["Unity", "UI", "UI Toolkit", "최적화"]
canonical: https://blog.unialgames.com/posts/uitk-update-cost/
---
[내가 만든 요소도 UXML 에 태그로 들어간다](/posts/uitk-custom-controls/)까지가 만드는 이야기였습니다. 이제 돌아가는 이야기입니다.

[한 칸이 바뀌면 그 캔버스는 통째로 다시 계산된다](/posts/unity-ui-rebuild-cost/)에서 uGUI 의 비용 구조를 봤습니다. 캔버스 안의 한 칸이 바뀌면 그 캔버스 전체가 다시 묶였습니다. 단위가 캔버스 하나였고, 그래서 캔버스를 어떻게 자르느냐가 최적화의 대부분이었습니다.

UI Toolkit 은 단위가 다릅니다. **무엇이 다시 도는지가 넷으로 갈려 있습니다.**

## 다시 도는 것이 넷입니다

문서가 추적되는 연산을 넷으로 적어 뒀습니다.

| 단계 | 하는 일 | 언제 |
| --- | --- | --- |
| 스타일 해석 | "Determines the final appearance of elements by applying USS selectors and styles" | 클래스나 스타일이 바뀔 때 |
| 레이아웃 재계산 | "Adjusts the size and position of elements to fit correctly within the UI hierarchy" | 크기, 위치, 정렬이 바뀔 때 |
| 정점 버퍼 갱신 | "Updates geometric shapes used to render UI elements" | 모서리 둥글리기처럼 기하가 바뀔 때 |
| 렌더 상태 변경 | "Changes rendering states like textures and blending modes" | 마스킹이나 고유 텍스처 |

넷이 늘 같이 돌지 않습니다. 무엇을 바꿨느냐에 따라 **어디까지 번지는지가 달라집니다.** 이게 uGUI 와 제일 크게 갈리는 지점입니다.

<figure class="diagram">
<svg viewBox="0 0 680 252" role="img" aria-label="uGUI 는 캔버스 전체가 다시 묶이고 UI Toolkit 은 네 단계 중 닿는 것만 도는 대비 그림">
<text x="24" y="24" font-size="13" fill="var(--text)">다시 도는 단위가 다릅니다</text>
<text x="24" y="50" font-size="11.5" fill="var(--accent2)">uGUI · 칸 하나를 바꿨을 때</text>
<rect x="24" y="62" width="290" height="150" rx="9" fill="var(--warn)" fill-opacity="0.08" stroke="var(--warn)" stroke-dasharray="4 3"></rect>
<rect x="40" y="76" width="130" height="38" rx="6" fill="var(--warn)" fill-opacity="0.22" stroke="var(--warn)" stroke-opacity="1"></rect>
<text x="105" y="99" font-size="9.5" fill="var(--text)" text-anchor="middle">바뀐 칸</text>
<rect x="180" y="76" width="130" height="38" rx="6" fill="var(--warn)" fill-opacity="0.1" stroke="var(--warn)" stroke-opacity="0.55"></rect>
<text x="245" y="99" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">안 바뀐 칸</text>
<rect x="40" y="122" width="130" height="38" rx="6" fill="var(--warn)" fill-opacity="0.1" stroke="var(--warn)" stroke-opacity="0.55"></rect>
<text x="105" y="145" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">안 바뀐 칸</text>
<rect x="180" y="122" width="130" height="38" rx="6" fill="var(--warn)" fill-opacity="0.1" stroke="var(--warn)" stroke-opacity="0.55"></rect>
<text x="245" y="145" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">안 바뀐 칸</text>
<rect x="40" y="168" width="130" height="38" rx="6" fill="var(--warn)" fill-opacity="0.1" stroke="var(--warn)" stroke-opacity="0.55"></rect>
<text x="105" y="191" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">안 바뀐 칸</text>
<rect x="180" y="168" width="130" height="38" rx="6" fill="var(--warn)" fill-opacity="0.1" stroke="var(--warn)" stroke-opacity="0.55"></rect>
<text x="245" y="191" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">안 바뀐 칸</text>
<text x="169" y="236" font-size="10.5" fill="var(--warn)" text-anchor="middle">캔버스가 통째로 다시 묶입니다</text>
<text x="366" y="50" font-size="11.5" fill="var(--accent)">UI Toolkit · width 를 바꿨을 때</text>
<rect x="366" y="62" width="290" height="32" rx="6" fill="var(--bg-subtle)" fill-opacity="1" stroke="var(--border)"></rect>
<text x="382" y="83" font-size="10.5" fill="var(--text-muted)">스타일 해석</text>
<text x="640" y="83" font-size="9.5" fill="var(--text-muted)" text-anchor="end">안 돕니다</text>
<rect x="366" y="102" width="290" height="32" rx="6" fill="var(--warn)" fill-opacity="0.18" stroke="var(--warn)"></rect>
<text x="382" y="123" font-size="10.5" fill="var(--text)">레이아웃 재계산</text>
<text x="640" y="123" font-size="9.5" fill="var(--warn)" text-anchor="end">돕니다</text>
<rect x="366" y="142" width="290" height="32" rx="6" fill="var(--warn)" fill-opacity="0.18" stroke="var(--warn)"></rect>
<text x="382" y="163" font-size="10.5" fill="var(--text)">정점 버퍼 갱신</text>
<text x="640" y="163" font-size="9.5" fill="var(--warn)" text-anchor="end">돕니다</text>
<rect x="366" y="182" width="290" height="32" rx="6" fill="var(--bg-subtle)" fill-opacity="1" stroke="var(--border)"></rect>
<text x="382" y="203" font-size="10.5" fill="var(--text-muted)">렌더 상태 변경</text>
<text x="640" y="203" font-size="9.5" fill="var(--text-muted)" text-anchor="end">안 돕니다</text>
<text x="511" y="236" font-size="10.5" fill="var(--accent)" text-anchor="middle">바꾼 값이 닿는 단계만 돕니다</text>
</svg>
<figcaption>uGUI 는 캔버스를 어떻게 자르냐가 문제였고, 여기서는 무엇을 바꾸냐가 문제입니다.</figcaption>
</figure>

<x-update>
<script type="application/json">
{"label":"값 하나를 바꾸면 어디까지 다시 도는가","note":"문서가 적어 둔 네 단계와, 각 변경이 어디에 걸리는지를 맞춰 본 것입니다.","default":{"change":0}}
</script>
</x-update>

표에 없는 것 둘만 더 적어 둡니다. **모서리를 둥글리는 `border-radius` 는 문서가 직접 든 정점 버퍼 갱신의 예시** 입니다. 그리고 마스크를 켜고 끄면 렌더 상태가 바뀝니다. 그쪽 비용은 다음 편에서 봅니다.

여기서 실무 규칙 하나가 바로 나옵니다. **크기를 건드리는 값만 형제와 부모까지 번집니다.** `translate` 나 색은 그 요소에서 끝나는데, `width` 나 `margin` 은 배치가 다시 잡히면서 옆 칸들도 같이 움직입니다. [움직임은 클립이 아니라 두 값 사이에서 만들어진다](/posts/uitk-transitions-binding/)에서 transform 계열에 전환을 걸라고 한 이유가 이것입니다.

## 숨기는 방법 셋을 비용으로 다시 봅니다

[칸은 남는 자리를 나눠 갖는 방식으로 정해진다](/posts/uitk-flex-layout/)에서 `display: none` 과 `visibility: hidden` 을 **자리 기준** 으로 갈랐습니다. 칸이 없어지느냐 남느냐였습니다. 비용 기준으로 보면 셋이 이렇게 갈립니다.

| 방법 | 문서 표현 | 비용 |
| --- | --- | --- |
| `opacity: 0` | "Still renders them as transparent" | **계속 그립니다.** 투명하게 그릴 뿐입니다 |
| `visibility: hidden` | "Prevents rendering but keeps it as part of the layout" | 안 그리지만 배치에는 남아 있습니다 |
| `display: none` | "Stops rendering and layout updates entirely" | 둘 다 멈춥니다 |

`display: none` 이 제일 싸 보이는데 따라오는 것이 있습니다. **다시 켤 때 레이아웃 재계산이 옵니다.** 칸이 없어졌다가 생기니 배치를 다시 잡아야 합니다.

그래서 고르는 기준이 이렇게 됩니다.

- 오래 안 보일 것, 목록에서 빠질 것이면 `display: none`
- 자주 껐다 켤 것이고 자리가 고정이면 `visibility: hidden` 쪽이 다시 켤 때 싸게 붙습니다
- `opacity: 0` 은 **숨기는 용도가 아닙니다.** 페이드처럼 값이 이어져야 할 때만 씁니다. 안 보이는데 계속 그리고 있습니다

마지막 줄이 14편에서 본 uGUI 쪽 결론과 정확히 반대라는 점이 재미있습니다. uGUI 에서는 `CanvasGroup.alpha = 0` 이 제일 싼 축이었습니다. 체계가 다르면 같은 방법의 값이 달라집니다.

## 자주 움직이는 것에는 표식을 답니다

위에서 `translate` 하나가 정점 버퍼를 다시 만들게 한다고 했습니다. 그걸 막는 장치가 따로 있습니다. `usageHints` 입니다.

```xml
<ui:VisualElement usage-hints="DynamicTransform" />
```

```csharp
visualElement.usageHints = UsageHints.DynamicTransform;
```

문서 표현으로 "pushes transform updates directly to the GPU, bypassing CPU mesh updates and improving performance" 입니다. **자리 갱신을 CPU 가 아니라 GPU 가 합니다.**

<figure class="diagram">
<svg viewBox="0 0 680 244" role="img" aria-label="자식마다 DynamicTransform 을 다는 것과 부모에 GroupTransform 하나를 다는 것의 비교">
<text x="24" y="24" font-size="13" fill="var(--text)">창 하나를 통째로 밀어 넣을 때</text>
<text x="24" y="50" font-size="11.5" fill="var(--text-muted)">자식마다 표식</text>
<rect x="24" y="62" width="290" height="142" rx="9" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="56" y="74" width="242" height="27" rx="5" fill="var(--bg)" stroke="var(--warn)"></rect>
<text x="68" y="92" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">DynamicTransform</text>
<line x1="40" y1="88" x2="52" y2="88" stroke="var(--warn)" stroke-width="1"></line>
<polygon points="56,88 46,84 46,92" fill="var(--warn)"></polygon>
<rect x="56" y="107" width="242" height="27" rx="5" fill="var(--bg)" stroke="var(--warn)"></rect>
<text x="68" y="125" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">DynamicTransform</text>
<line x1="40" y1="121" x2="52" y2="121" stroke="var(--warn)" stroke-width="1"></line>
<polygon points="56,121 46,117 46,125" fill="var(--warn)"></polygon>
<rect x="56" y="140" width="242" height="27" rx="5" fill="var(--bg)" stroke="var(--warn)"></rect>
<text x="68" y="158" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">DynamicTransform</text>
<line x1="40" y1="154" x2="52" y2="154" stroke="var(--warn)" stroke-width="1"></line>
<polygon points="56,154 46,150 46,158" fill="var(--warn)"></polygon>
<rect x="56" y="173" width="242" height="27" rx="5" fill="var(--bg)" stroke="var(--warn)"></rect>
<text x="68" y="191" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">DynamicTransform</text>
<line x1="40" y1="187" x2="52" y2="187" stroke="var(--warn)" stroke-width="1"></line>
<polygon points="56,187 46,183 46,191" fill="var(--warn)"></polygon>
<text x="169" y="228" font-size="10.5" fill="var(--warn)" text-anchor="middle">변환이 넷으로 따로 갑니다</text>
<text x="366" y="50" font-size="11.5" fill="var(--accent)">부모에 표식 하나</text>
<rect x="366" y="62" width="290" height="142" rx="9" fill="var(--accent)" fill-opacity="0.08" stroke="var(--accent)"></rect>
<text x="382" y="80" font-size="9.5" fill="var(--accent)" font-family="var(--font-mono)">GroupTransform</text>
<line x1="350" y1="76" x2="360" y2="76" stroke="var(--accent)" stroke-width="1.4"></line>
<polygon points="366,76 355,71 355,81" fill="var(--accent)"></polygon>
<rect x="396" y="92" width="242" height="22" rx="5" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="408" y="107" font-size="9.5" fill="var(--text-muted)">표식 없음</text>
<rect x="396" y="119" width="242" height="22" rx="5" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="408" y="134" font-size="9.5" fill="var(--text-muted)">표식 없음</text>
<rect x="396" y="146" width="242" height="22" rx="5" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="408" y="161" font-size="9.5" fill="var(--text-muted)">표식 없음</text>
<rect x="396" y="173" width="242" height="22" rx="5" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="408" y="188" font-size="9.5" fill="var(--text-muted)">표식 없음</text>
<text x="511" y="228" font-size="10.5" fill="var(--accent)" text-anchor="middle">한 번 걸면 GPU 가 아래로 퍼뜨립니다</text>
</svg>
<figcaption>"applies a single transform to the parent, which the GPU efficiently propagates to all child elements"</figcaption>
</figure>

값이 넷입니다.

| 값 | 문서 설명 |
| --- | --- |
| `DynamicTransform` | "Optimizes rendering of a VisualElement for frequent position and transformation changes" |
| `GroupTransform` | "Optimizes rendering of a VisualElement that changes its transformation and position frequently, and has many descendants that have their hints set to DynamicTransform" |
| `MaskContainer` | "Optimizes rendering of a VisualElement that has multiple descendants with nested masks" |
| `DynamicColor` | "Optimizes rendering of a VisualElement for frequent color changes, such as a built-in style color being animated" |

`GroupTransform` 의 쓰임이 특히 분명합니다. 창 하나를 통째로 밀어 넣는 연출이라면 창 안의 요소 하나하나에 `DynamicTransform` 을 달 게 아니라, **부모에 `GroupTransform` 을 답니다.** 문서 표현으로 "applies a single transform to the parent, which the GPU efficiently propagates to all child elements" 입니다.

붙이는 기준은 **자주 바뀌는가** 하나입니다. 한 번 놓고 안 움직이는 요소에 달아 두면 얻는 것 없이 GPU 쪽 자원만 잡아 둡니다.

## 바인딩은 리플렉션을 미리 없앱니다

[앞 편](/posts/uitk-transitions-binding/)에서 데이터 바인딩을 봤는데, 비용 이야기를 안 했습니다. 바인딩이 값을 읽으려면 그 타입의 속성 목록이 필요하고, 그걸 런타임에 리플렉션으로 뒤지면 느립니다.

문서가 말하는 **property bag** 이 그 목록입니다. "A property bag is a companion object that enables efficient traversal and manipulation of a type's data." 이걸 컴파일 때 만들어 두라는 것이 권고입니다.

```csharp
using Unity.Properties;

[GeneratePropertyBag]
public partial class PlayerData
{
    [CreateProperty]
    public float hp { get; set; }
}
```

`[CreateProperty]` 는 앞 편에서 이미 썼습니다. 바인딩하려면 필요한 표식이었습니다. 여기에 `[GeneratePropertyBag]` 을 더하면 그 타입의 속성 목록까지 컴파일 때 생성됩니다. **런타임에 뒤질 일이 없어집니다.**

갱신 시점을 줄이는 것도 같은 줄기입니다. `IDataSourceViewHashProvider` 와 `INotifyBindablePropertyChanged` 를 구현하면 "trigger updates only when specific property values change" 입니다. 안 구현하면 변화를 감지할 방법이 없어서 매 프레임 비교하는 쪽으로 떨어집니다.

값이 자주 안 바뀌는데 바인딩이 여럿이면 여기부터 봅니다.

## 무엇이 돌았는지 보는 법

지금까지가 무엇이 비싼지였고, 이건 무엇이 실제로 돌았는지 확인하는 쪽입니다. 문서가 네 가지를 듭니다.

- **Unity Profiler.** 프레임마다 어느 구간이 오래 걸렸는지
- **UI Toolkit Debugger.** 요소 트리와 걸린 스타일을 직접 보는 창
- **Frame Debugger.** 드로우 콜이 몇 개로 나갔고 무엇이 배치를 깼는지
- **`SetPanelChangeReceiver`.** Panel Settings 에 붙이는 것으로 "allows you to listen for changes to your UI and track their source" 입니다

마지막 것이 이 편의 내용과 제일 가깝습니다. **무엇이 UI 를 바꿨는지 그 출처를 알려 줍니다.** 에디터와 개발 빌드에서만 동작합니다.

유니티 6 에서 이 쪽이 꽤 손봐졌다는 것도 적어 둘 만합니다. 문서가 드는 것 중에 "Event dispatching rules have been simplified, making them easier to understand and twice as fast" 가 있고, 메시 생성이 잡으로 돌게 바뀌었고, 깊은 계층의 레이아웃 캐싱이 개선됐습니다.

## 정리

- uGUI 는 캔버스 하나가 단위였고, UI Toolkit 은 **스타일 해석 / 레이아웃 재계산 / 정점 버퍼 갱신 / 렌더 상태 변경** 넷으로 갈립니다
- 바꾼 값마다 어디까지 번지는지가 다릅니다. **크기를 건드리는 값만 형제와 부모까지 갑니다**
- `opacity: 0` 은 안 보일 뿐 계속 그립니다. **숨기는 용도가 아닙니다.** uGUI 에서의 결론과 반대입니다
- 자주 껐다 켤 것은 `visibility: hidden`, 오래 안 보일 것은 `display: none` 입니다
- 자주 움직이는 요소에는 `usageHints` 를 답니다. 묶음째 움직이면 부모에 **`GroupTransform`** 하나면 됩니다
- 바인딩은 `[GeneratePropertyBag]` 으로 속성 목록을 컴파일 때 만들어 두고,
  `INotifyBindablePropertyChanged` 로 바뀔 때만 갱신하게 합니다
- 확인은 Profiler, UI Toolkit Debugger, Frame Debugger, 그리고 **`SetPanelChangeReceiver`** 입니다