---
title: "그리는 순서는 하이어라키가 아니라 숫자가 정한다"
description: "NGUI 는 위젯마다 depth 정수를 들고 그 숫자로 앞뒤를 정합니다. 오브젝트를 옮겨도 순서가 안 바뀌는 대신 숫자를 관리해야 합니다. 판과 위젯의 두 단계 정렬, 같은 숫자를 줬을 때의 재질 묶기, 그 숫자 배치가 드로우 콜을 가르는 과정까지 소스에서 확인했습니다."
author: "유니얼"
pubDate: 2026-09-20
category: "game-dev/unity/ui"
tags: ["Unity", "UI", "NGUI", "라이브러리"]
canonical: https://blog.unialgames.com/posts/ngui-depth-order/
---
[uGUI 는 NGUI 에서 시작했다](/posts/ngui-history/)에서 갈리는 지점 셋을 짚었습니다. 그중 첫째를 봅니다.

[하이어라키 맨 아래가 화면 맨 앞이다](/posts/unity-ui-canvas-draw-order/)에서 uGUI 의 규칙을 봤습니다. 형제 중에 아래에 있는 오브젝트가 앞에 그려지고, 순서를 바꾸려면 하이어라키에서 끌어 올리거나 내립니다.

NGUI 에는 그 규칙이 없습니다. **오브젝트를 맨 아래로 내려도 아무 일도 안 일어납니다.**

## 숫자 한 칸이 순서입니다

`UIWidget` 에 `depth` 라는 정수가 있습니다. 소스 주석이 한 줄입니다.

> "Depth controls the rendering order -- lowest to highest."

작은 것이 뒤에 깔리고 큰 것이 앞에 그려집니다. 인스펙터의 Depth 줄에는 **Back** 과 **Forward** 버튼이 붙어 있고, 누르면 숫자가 1 씩 내려가고 올라갑니다. 여러 개를 같이 선택하고 눌러도 됩니다.

숫자를 바꾸면 그 자리에서 정리가 돌아갑니다. 세터가 하는 일이 이렇습니다. 위젯을 판에서 뺐다가 새 숫자로 다시 넣고, 에디터에서는 곧바로 위젯을 다시 정렬하고 드로우 콜을 전부 다시 만듭니다. **숫자 하나가 그 판의 묶음 전체를 다시 잡습니다.**

재미있는 흔적이 하나 남아 있습니다. `depth` 게터 안에 주석 처리된 코드가 있는데, 앞에 이렇게 적혀 있습니다.

> "Experiment with a transform-based depth, uGUI style"

하이어라키의 형제 번호를 깊이로 쓰는 방식을 만든 사람도 실험해 봤다는 뜻입니다. 남겨 두기만 하고 켜지 않았습니다.

## 오브젝트를 옮겨도 순서가 안 바뀝니다

여기서 갈립니다. 같은 화면을 만들어 놓고 오브젝트를 하이어라키에서 끌어 올리면, uGUI 는 화면이 바뀌고 NGUI 는 안 바뀝니다.

**얻는 것이 있습니다.** 하이어라키를 정리 목적으로 쓸 수 있습니다. 관련 있는 것끼리 묶어 두고, 폴더 역할의 빈 오브젝트를 넣고, 순서를 알파벳으로 맞춰도 화면은 그대로입니다. 프리팹으로 떼어다 다른 부모 밑에 붙여도 앞뒤가 유지됩니다. uGUI 에서는 이 셋 다 화면을 건드립니다.

**잃는 것도 있습니다.** 숫자를 관리해야 합니다. 화면이 커지면 어느 숫자가 비어 있는지 알기 어렵고, 팝업을 새로 얹을 때 "이건 몇으로 줘야 하지" 가 매번 나옵니다. 규칙을 안 정해 두면 100, 101, 105, 999 같은 숫자가 쌓입니다.

이어받은 프로젝트라면 **먼저 숫자대를 파악하는 게 순서입니다.** 배경이 0번대, 내용이 10번대, 팝업이 100번대 하는 식으로 이미 약속이 있는 경우가 많습니다.

## 세 체계가 순서를 각각 다른 곳에 둡니다

지금까지 본 셋을 한자리에 놓으면 차이가 분명해집니다.

<figure class="diagram">
<svg viewBox="0 0 680 252" role="img" aria-label="uGUI 와 NGUI 와 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="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--accent2)"></rect>
<text x="36" y="84" font-size="10.5" fill="var(--text)">Background</text>
<rect x="24" y="102" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--accent2)"></rect>
<text x="36" y="124" font-size="10.5" fill="var(--text)">Card</text>
<rect x="24" y="142" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--accent2)"></rect>
<text x="36" y="164" font-size="10.5" fill="var(--text)">Popup</text>
<text x="24" y="200" font-size="10.5" fill="var(--accent2)">아래에 있는 것이 앞</text>
<text x="24" y="218" font-size="9.5" fill="var(--text-muted)">순서를 바꾸려면 끌어 옮깁니다</text>
<text x="240" y="50" font-size="11.5" fill="var(--accent)">NGUI</text>
<rect x="240" y="62" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--accent)"></rect>
<text x="252" y="84" font-size="10.5" fill="var(--text)">Background</text>
<text x="428" y="84" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">depth 0</text>
<rect x="240" y="102" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--accent)"></rect>
<text x="252" y="124" font-size="10.5" fill="var(--text)">Card</text>
<text x="428" y="124" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">depth 10</text>
<rect x="240" y="142" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--accent)"></rect>
<text x="252" y="164" font-size="10.5" fill="var(--text)">Popup</text>
<text x="428" y="164" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">depth 100</text>
<text x="240" y="200" font-size="10.5" fill="var(--accent)">숫자가 큰 것이 앞</text>
<text x="240" y="218" font-size="9.5" fill="var(--text-muted)">하이어라키는 상관없습니다</text>
<text x="456" y="50" font-size="11.5" fill="var(--warn)">UI Toolkit</text>
<rect x="456" y="62" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--warn)"></rect>
<text x="468" y="84" font-size="10.5" fill="var(--text)">HUD 패널</text>
<text x="644" y="84" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">Sort 0</text>
<rect x="456" y="102" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--warn)"></rect>
<text x="468" y="124" font-size="10.5" fill="var(--text)">팝업 패널</text>
<text x="644" y="124" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">Sort 10</text>
<rect x="456" y="142" width="200" height="34" rx="7" fill="var(--bg-subtle)" stroke="var(--warn)" stroke-dasharray="4 3"></rect>
<text x="468" y="164" font-size="10.5" fill="var(--text)">판 안쪽은 쌓인 순서</text>
<text x="456" y="200" font-size="10.5" fill="var(--warn)">판끼리는 숫자</text>
<text x="456" y="218" font-size="9.5" fill="var(--text-muted)">판 안쪽은 순서. 둘이 섞여 있습니다</text>
<text x="24" y="244" font-size="11" fill="var(--text-muted)">판 단위로 숫자를 주는 방식은 셋 중 둘이 씁니다</text>
</svg>
<figcaption>다른 것은 판 안쪽을 무엇으로 정하느냐입니다.</figcaption>
</figure>

| 체계 | 순서를 정하는 것 | 바꾸는 방법 |
| --- | --- | --- |
| uGUI | 하이어라키의 형제 순서 | 오브젝트를 끌어 올리고 내립니다 |
| NGUI | `UIWidget.depth` 정수 | 숫자를 바꿉니다 |
| UI Toolkit | 한 판 안에서는 쌓인 순서, 판끼리는 `Sort Order` | 둘이 섞여 있습니다 |

[해상도 대응은 씬이 아니라 에셋 한 장에 적힌다](/posts/uitk-panel-settings/)에서 본 UI Toolkit 의 `Sort Order` 가 NGUI 의 `UIPanel.depth` 와 같은 자리입니다. **판 단위로 숫자를 주는 방식은 셋 중 둘이 쓰고 있습니다.** 다른 것은 판 안쪽을 무엇으로 정하느냐입니다.

## 판이 먼저이고 위젯이 그다음입니다

`UIPanel` 에도 `depth` 가 있습니다. 정렬은 두 단계입니다. 소스의 `UIWidget.FullCompareFunc` 가 그대로 두 줄입니다. 먼저 두 위젯이 속한 판을 비교하고, 판이 같을 때만 위젯끼리 비교합니다.

<figure class="diagram">
<svg viewBox="0 0 680 266" role="img" aria-label="판 depth 가 위젯 depth 보다 먼저 비교되어 판 B 의 낮은 숫자가 판 A 의 높은 숫자보다 앞에 그려지는 그림">
<text x="24" y="24" font-size="13" fill="var(--text)">판이 먼저, 위젯이 그다음</text>
<text x="24" y="50" font-size="11" fill="var(--accent2)" font-family="var(--font-mono)">판 A · UIPanel.depth = 0</text>
<rect x="24" y="58" width="290" height="104" rx="9" fill="var(--bg-subtle)" stroke="var(--accent2)" stroke-dasharray="4 3"></rect>
<rect x="38" y="72" width="262" height="32" rx="6" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="52" y="93" font-size="10.5" fill="var(--text)">배경</text>
<text x="286" y="93" font-size="10" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">depth 900</text>
<rect x="38" y="112" width="262" height="32" rx="6" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="52" y="133" font-size="10.5" fill="var(--text)">아주 앞에 두고 싶은 것</text>
<text x="286" y="133" font-size="10" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">depth 950</text>
<text x="366" y="50" font-size="11" fill="var(--accent)" font-family="var(--font-mono)">판 B · UIPanel.depth = 1</text>
<rect x="366" y="58" width="290" height="104" rx="9" fill="var(--bg-subtle)" stroke="var(--accent)" stroke-dasharray="4 3"></rect>
<rect x="380" y="72" width="262" height="32" rx="6" fill="var(--bg)" stroke="var(--border)"></rect>
<text x="394" y="93" font-size="10.5" fill="var(--text)">작은 아이콘 하나</text>
<text x="628" y="93" font-size="10" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">depth 0</text>
<text x="394" y="133" font-size="10" fill="var(--text-muted)">숫자는 제일 작은데</text>
<rect x="40" y="182" width="176" height="34" rx="7" fill="var(--accent2)" fill-opacity="0.16" stroke="var(--accent2)"></rect>
<text x="128" y="204" font-size="10.5" fill="var(--text)" text-anchor="middle" font-family="var(--font-mono)">판 A · 900</text>
<rect x="232" y="182" width="176" height="34" rx="7" fill="var(--accent2)" fill-opacity="0.16" stroke="var(--accent2)"></rect>
<text x="320" y="204" font-size="10.5" fill="var(--text)" text-anchor="middle" font-family="var(--font-mono)">판 A · 950</text>
<rect x="424" y="182" width="176" height="34" rx="7" fill="var(--accent)" fill-opacity="0.2" stroke="var(--accent)"></rect>
<text x="512" y="204" font-size="10.5" fill="var(--text)" text-anchor="middle" font-family="var(--font-mono)">판 B · 0</text>
<line x1="40" y1="230" x2="612" y2="230" stroke="var(--text-muted)" stroke-width="1"></line>
<polygon points="620,230 608,225 608,235" fill="var(--text-muted)"></polygon>
<text x="34" y="234" font-size="9.5" fill="var(--text-muted)" text-anchor="end">뒤</text>
<text x="628" y="234" font-size="9.5" fill="var(--text-muted)">앞</text>
<text x="24" y="258" font-size="11" fill="var(--accent)">판 B 의 0 이 판 A 의 950 보다 앞입니다</text>
</svg>
<figcaption>팝업이 뒤에 깔린다면 위젯 숫자가 아니라 판 숫자를 봐야 합니다.</figcaption>
</figure>

그래서 **다른 판에 있는 위젯끼리는 위젯 숫자가 아무리 커도 판 순서를 못 이깁니다.** 팝업이 뒤에 깔린다면 위젯 depth 를 올릴 게 아니라 판 depth 를 봐야 합니다.

입력 쪽에도 같은 규칙이 그대로 들어가 있습니다. `UIWidget.raycastDepth` 가 이렇게 계산됩니다.

```csharp
return (panel != null) ? mDepth + panel.depth * 1000 : mDepth;
```

판 하나가 위젯 1000칸 값어치입니다. 한 판 안의 위젯 숫자를 999 까지 쓰는 동안은 판 순서가 항상 이깁니다. **그리는 순서와 클릭 받는 순서를 같은 규칙으로 맞춰 둔 것입니다.**

## 같은 숫자를 주면 재질로 묶습니다

위젯 둘의 depth 가 같으면 어떻게 될까요. 소스의 `UIWidget.PanelCompareFunc` 가 답입니다. 숫자를 먼저 비교하고, 같으면 **재질의 인스턴스 ID** 를 비교합니다.

숫자가 같은 위젯들은 재질끼리 알아서 붙는다는 뜻입니다. 둘 사이의 앞뒤는 정해 주지 않은 셈이라 겹치면 안 되지만, 안 겹치는 것들이라면 문제가 없습니다.

NGUI 인스펙터가 이 상황을 알려 줍니다. 같은 숫자를 쓰는 위젯이 둘 이상이면 이런 줄이 뜹니다.

> "{n} widgets are sharing the depth value of {depth}"

**경고가 아니라 안내입니다.** 겹치는 것들이면 숫자를 갈라 줘야 하고, 안 겹치는 아이콘 여러 개라면 그대로 두는 편이 오히려 낫습니다. 다음 절이 그 이유입니다.

## 그래서 숫자 배치가 곧 드로우 콜입니다

`UIPanel.FillAllDrawCalls` 가 하는 일은 단순합니다.

1. 위젯을 `PanelCompareFunc` 로 정렬합니다
2. 앞에서부터 차례로 돌면서 정점을 한 버퍼에 쌓습니다
3. **재질이나 텍스처나 셰이더가 바뀌는 자리에서 드로우 콜을 끊고 새로 시작합니다**

정렬된 순서가 곧 쌓는 순서이고, 재질이 바뀌는 지점이 곧 끊는 지점입니다. 그러니 같은 아틀라스를 쓰는 것들 사이에 글자가 하나 끼면 그 자리에서 갈라집니다.

<x-depth>
<script type="application/json">
{"label":"depth 숫자가 그리는 순서와 묶음을 가른다","note":"하이어라키는 그대로 두고 숫자만 바꾼 것입니다. 드로우 콜 묶음이 달라집니다.","default":{"plan":0}}
</script>
</x-depth>

[한 칸이 바뀌면 그 캔버스는 통째로 다시 계산된다](/posts/unity-ui-rebuild-cost/)에서 본 uGUI 의 배치 깨짐과 같은 이야기입니다. 다른 점은 **NGUI 에서는 그 순서를 숫자로 직접 정한다** 는 것입니다. uGUI 에서는 하이어라키를 옮겨야 해서 화면 구조와 성능이 얽히는데, NGUI 에서는 화면 구조를 안 건드리고 숫자만 바꿔서 묶을 수 있습니다.

실무 규칙이 여기서 나옵니다.

- **같은 아틀라스를 쓰는 것들의 숫자를 붙여 둡니다.** 사이에 글자를 끼우지 않습니다
- 글자는 글자끼리 한 구간에 몰아 둡니다
- 안 겹치는 아이콘 여러 개라면 **같은 숫자를 줘도 됩니다.** 재질로 알아서 붙습니다

## 마지막에는 렌더 큐 번호가 됩니다

정렬과 묶기가 끝나면 그 결과가 유니티의 렌더 큐 번호로 내려갑니다. `UIPanel` 이 판을 순서대로 돌면서 **3000 부터 시작해서 드로우 콜마다 번호를 하나씩 올립니다.** 판 하나가 드로우 콜 셋을 만들었으면 다음 판은 3003 부터입니다.

그래서 화면에 보이는 앞뒤는 결국 숫자 하나입니다. 하이어라키가 끼어들 자리가 처음부터 없었던 셈입니다.

무엇이 몇 개로 묶였는지는 Draw Call Viewer 창에서 봅니다. 소스 설명이 이렇습니다. "Draw Call Viewer shows a list of draw calls created by NGUI and lets you hide them selectively." 하나씩 꺼 보면서 어느 묶음에 무엇이 들어갔는지 확인할 수 있습니다. 숫자를 바꾸기 전에 여기부터 보는 게 빠릅니다.

## 정리

- `UIWidget.depth` 정수가 순서입니다. 소스 주석으로 "lowest to highest" 입니다
- **오브젝트를 하이어라키에서 옮겨도 화면이 안 바뀝니다.** 하이어라키를 정리 목적으로 쓸 수 있는 대신 숫자를 관리해야 합니다
- 정렬은 두 단계입니다. **판 depth 가 먼저이고 위젯 depth 가 그다음** 입니다
- 입력도 같은 규칙입니다. `raycastDepth = mDepth + panel.depth * 1000` 이라 판 하나가 위젯 1000칸입니다
- 숫자가 같으면 **재질 인스턴스 ID** 로 정렬합니다. 같은 재질끼리 알아서 붙습니다
- 드로우 콜은 정렬된 순서로 돌다가 **재질이 바뀌는 자리에서 끊깁니다.** 같은 아틀라스끼리 숫자를 붙여 두는 것이 그대로 최적화입니다
- 결과는 3000 부터 매겨지는 렌더 큐 번호입니다. Draw Call Viewer 로 묶음을 확인합니다