---
title: "화면 하나가 파일 두 개와 오브젝트 하나가 된다"
description: "UI Toolkit 으로 화면을 만들면 무엇이 어디에 생기는지부터 봅니다. uxml 과 uss 가 각각 무엇을 적는 파일인지, VisualElement 가 uGUI 의 무엇 자리인지, UI Document 와 Panel Settings 가 왜 따로 있는지를 정리했습니다."
author: "유니얼"
pubDate: 2026-09-18
category: "game-dev/unity/ui"
tags: ["Unity", "UI", "UI Toolkit"]
canonical: https://blog.unialgames.com/posts/uitk-uxml-uss-basics/
---
[UI Toolkit 은 화면에 오브젝트를 놓지 않는다](/posts/unity-ui-toolkit-vs-ugui/)에서 두 체계가 다르다는 것까지 봤습니다. 이제 실제로 만들어 봅니다.

시작은 **무엇이 어디에 생기는지**입니다. uGUI 에 익숙하면 이 부분이 제일 낯섭니다. 버튼을 만들었는데 하이어라키에 아무것도 안 생기거든요.

## 화면 하나가 파일 두 개와 오브젝트 하나가 됩니다

<figure class="diagram">
<svg viewBox="0 0 680 290" role="img" aria-label="uxml 과 uss 파일이 UIDocument 오브젝트를 거쳐 화면에 뜨는 경로">
<text x="24" y="26" font-size="13" fill="var(--text)">화면 하나를 띄우는 데 필요한 것</text>
<text x="118" y="54" font-size="11.5" fill="var(--accent2)" text-anchor="middle">프로젝트 창 (파일)</text>
<rect x="24" y="64" width="188" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)"></rect>
<rect x="38" y="78" width="160" height="38" rx="6" fill="var(--accent2)" fill-opacity="0.14" stroke="var(--accent2)"></rect>
<text x="48" y="95" font-size="11" fill="var(--text)" font-family="var(--font-mono)">Popup.uxml</text>
<text x="48" y="109" font-size="9.5" fill="var(--text-muted)">무엇이 있는지</text>
<rect x="38" y="126" width="160" height="38" rx="6" fill="var(--accent2)" fill-opacity="0.14" stroke="var(--accent2)"></rect>
<text x="48" y="143" font-size="11" fill="var(--text)" font-family="var(--font-mono)">Popup.uss</text>
<text x="48" y="157" font-size="9.5" fill="var(--text-muted)">어떻게 보일지</text>
<rect x="38" y="174" width="160" height="38" rx="6" fill="var(--accent2)" fill-opacity="0.14" stroke="var(--accent2)"></rect>
<text x="48" y="191" font-size="11" fill="var(--text)" font-family="var(--font-mono)">PanelSettings</text>
<text x="48" y="205" font-size="9.5" fill="var(--text-muted)">해상도와 순서</text>
<text x="370" y="54" font-size="11.5" fill="var(--accent)" text-anchor="middle">씬 (오브젝트 하나)</text>
<rect x="256" y="64" width="228" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)"></rect>
<rect x="272" y="100" width="196" height="78" rx="7" fill="var(--accent)" fill-opacity="0.18" stroke="var(--accent)"></rect>
<text x="370" y="126" font-size="12" fill="var(--text)" text-anchor="middle" font-family="var(--font-mono)">UI Document</text>
<text x="370" y="146" font-size="10" fill="var(--text-muted)" text-anchor="middle">Source Asset ← uxml</text>
<text x="370" y="162" font-size="10" fill="var(--text-muted)" text-anchor="middle">Panel Settings ← 에셋</text>
<text x="600" y="54" font-size="11.5" fill="var(--text)" text-anchor="middle">화면</text>
<rect x="528" y="64" width="128" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)"></rect>
<rect x="552" y="118" width="80" height="26" rx="6" fill="var(--accent)" fill-opacity="0.22" stroke="var(--accent)"></rect>
<text x="592" y="136" font-size="11" fill="var(--text)" text-anchor="middle">확인</text>
<line x1="216" y1="139" x2="252" y2="139" stroke="var(--text-muted)"></line>
<path d="M252 139 l-7 -4 l0 8 z" fill="var(--text-muted)"></path>
<line x1="488" y1="139" x2="524" y2="139" stroke="var(--text-muted)"></line>
<path d="M524 139 l-7 -4 l0 8 z" fill="var(--text-muted)"></path>
<text x="24" y="244" font-size="12.5" fill="var(--text)">하이어라키에 남는 것은 UI Document 를 붙인 오브젝트 하나뿐입니다.</text>
<text x="24" y="266" font-size="11.5" fill="var(--text-muted)">버튼도 글자도 씬에 없습니다. uxml 안에 있습니다.</text>
<text x="24" y="286" font-size="11.5" fill="var(--text-muted)">Panel Settings 는 여러 화면이 같이 씁니다. 해상도 규칙을 한곳에서 정하는 자리입니다.</text>
</svg>
<figcaption>파일 셋과 오브젝트 하나. 이게 전부입니다.</figcaption>
</figure>

- **`.uxml`**: 무엇이 어떤 순서로 있는지
- **`.uss`**: 그것들이 어떻게 보일지
- **Panel Settings**: 해상도를 어떻게 맞출지, 여러 화면 중 무엇이 위인지
- **UI Document**: 씬 오브젝트에 붙는 컴포넌트. 위의 것들을 물고 화면에 띄웁니다

앞의 셋은 **프로젝트 창의 에셋**이고 마지막 하나만 씬에 있습니다. 그래서 하이어라키를 열면 오브젝트 하나뿐입니다.

Panel Settings 가 왜 따로 떨어져 있는지는 이유가 있습니다. **여러 화면이 같은 것을 씁니다.** 해상도 규칙을 화면마다 다시 정하지 않으려고 한곳에 모아 둔 것입니다. 그 안에 무엇이 있는지는 따로 한 편에서 봅니다.

## UXML 은 무엇이 어떤 순서로 있는지 적습니다

XML 입니다. 확장자만 다릅니다.

```xml
<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
>
    <engine:VisualElement class="popup">
        <engine:Label text="보상 획득" />
        <engine:VisualElement class="row">
            <engine:Button name="ok" text="확인" />
            <engine:Button name="cancel" text="취소" />
        </engine:VisualElement>
    </engine:VisualElement>
</engine:UXML>
```

읽을 때 필요한 것은 셋입니다.

- **루트는 `<engine:UXML>`.** 문서가 "includes attributes for the namespace prefix definitions and the location of schema definition files" 라고 적어 뒀습니다. 맨 위의 `xmlns:` 들이 그 접두사 정의입니다. `xmlns:engine="UnityEngine.UIElements"` 가 `engine` 이라는 접두사를 그 네임스페이스로 묶습니다
- **태그 이름이 곧 클래스 이름입니다.** `Button`, `Label`, `Toggle`, `VisualElement` 는 실제 C# 타입 이름입니다. 없는 태그를 적으면 그런 타입이 없다고 합니다
- **`name` 과 `class` 는 쓰임이 다릅니다.** `name` 은 나중에 코드에서 그 하나를 찾을 때, `class` 는 스타일을 걸 때 씁니다

<figure class="diagram">
<svg viewBox="0 0 680 254" role="img" aria-label="uxml 에 적은 순서가 그대로 화면에 쌓이는 모습">
<text x="24" y="26" font-size="13" fill="var(--text)">uxml 에 적은 순서가 그대로 쌓입니다</text>
<text x="170" y="54" font-size="11.5" fill="var(--accent2)" text-anchor="middle" font-family="var(--font-mono)">Popup.uxml</text>
<rect x="24" y="64" width="292" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)"></rect>
<text x="40" y="90" font-size="11" fill="var(--text)" font-family="var(--font-mono)">VisualElement</text>
<text x="133.8" y="90" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">class="popup"</text>
<text x="56" y="115" font-size="11" fill="var(--text)" font-family="var(--font-mono)">Label</text>
<text x="97.0" y="115" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">text="보상 획득"</text>
<text x="56" y="140" font-size="11" fill="var(--text)" font-family="var(--font-mono)">VisualElement</text>
<text x="149.8" y="140" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">class="row"</text>
<text x="72" y="165" font-size="11" fill="var(--text)" font-family="var(--font-mono)">Button</text>
<text x="119.6" y="165" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">name="ok"</text>
<text x="72" y="190" font-size="11" fill="var(--text)" font-family="var(--font-mono)">Button</text>
<text x="119.6" y="190" font-size="9.5" fill="var(--text-muted)" font-family="var(--font-mono)">name="cancel"</text>
<text x="520" y="54" font-size="11.5" fill="var(--accent)" text-anchor="middle">화면</text>
<rect x="400" y="64" width="240" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)"></rect>
<rect x="416" y="80" width="208" height="118" rx="7" fill="var(--accent)" fill-opacity="0.1" stroke="var(--accent)"></rect>
<text x="520" y="112" font-size="12" fill="var(--text)" text-anchor="middle">보상 획득</text>
<rect x="432" y="136" width="86" height="28" rx="6" fill="var(--accent)" fill-opacity="0.22" stroke="var(--accent)"></rect>
<text x="475" y="155" font-size="10.5" fill="var(--text)" text-anchor="middle">ok</text>
<rect x="524" y="136" width="86" height="28" rx="6" fill="var(--border)" fill-opacity="0.4" stroke="var(--border)"></rect>
<text x="567" y="155" font-size="10.5" fill="var(--text)" text-anchor="middle">cancel</text>
<line x1="322" y1="139" x2="394" y2="139" stroke="var(--text-muted)"></line>
<path d="M396 139 l-7 -4 l0 8 z" fill="var(--text-muted)"></path>
<text x="24" y="240" font-size="12" fill="var(--text)">들여쓴 만큼 자식이 되고, 위에 적은 것이 위에 놓입니다. 기본 방향이 세로라서 그렇습니다.</text>
</svg>
<figcaption>구조를 글로 적으면 그 순서대로 쌓입니다.</figcaption>
</figure>

들여쓴 만큼 자식이 되고, 위에 적은 것이 위에 놓입니다. uGUI 의 하이어라키에서 순서가 그리는 순서였던 것과 비슷하지만, 여기서는 **쌓이는 방향**이 세로라서 그렇습니다. 그 얘기는 레이아웃 편에서 합니다.

## USS 는 어떻게 보일지 적습니다

CSS 처럼 생겼습니다. 앞 UXML 에 붙일 스타일이라면 이렇습니다.

```css
.popup {
    background-color: rgb(28, 28, 34);
    padding: 16px;
}

.row {
    flex-direction: row;
    justify-content: flex-end;
}

#ok {
    background-color: rgb(80, 120, 240);
}
```

- **`.popup`** 처럼 점으로 시작하면 클래스 선택자입니다. UXML 의 `class="popup"` 에 걸립니다
- **`#ok`** 처럼 샵으로 시작하면 이름 선택자입니다. `name="ok"` 에 걸립니다
- 점도 샵도 없이 **`Button`** 이라고만 적으면 타입 선택자입니다. 그 타입 전부에 걸립니다

생김새가 CSS 라서 그대로 될 것 같지만 아닙니다. 문서가 못을 박아 뒀습니다.

> USS supports a set of simple selectors that are analogous, but not identical, to simple selectors in CSS.

무엇이 되고 무엇이 안 되는지는 선택자 편에서 따로 봅니다. 여기서는 **파일이 나뉘어 있다는 것**만 알면 됩니다. 구조를 고치는 것과 모양을 고치는 것이 서로 다른 파일을 건드리는 일이 됩니다.

## VisualElement 는 uGUI 의 오브젝트 자리입니다

uxml 에 적은 태그 하나가 실행 중에는 `VisualElement` 하나가 됩니다. uGUI 로 치면 오브젝트 하나 자리입니다.

**다만 오브젝트가 아닙니다.** 게임오브젝트도 아니고 컴포넌트도 아니고, 그냥 C# 객체입니다. 그래서 하이어라키에 안 보이고, `GetComponent` 로 못 찾고, 프리팹으로 못 만듭니다.

찾는 방법이 따로 있습니다.

```csharp
var root = GetComponent<UIDocument>().rootVisualElement;
var ok = root.Q<Button>("ok");
ok.clicked += () => Debug.Log("눌렀다");
```

- `rootVisualElement` 가 그 문서의 뿌리입니다
- `Q<T>(name)` 으로 이름을 걸어 하나를 찾습니다. uGUI 의 `transform.Find` 와 `GetComponent` 를 합친 자리입니다
- 여러 개를 찾을 때는 `Query<T>()` 를 씁니다

이 트리를 문서에서는 **비주얼 트리**라고 부릅니다. 하이어라키가 아니라 이쪽이 실제 부모 자식 관계입니다.

## UI Document 가 화면에 올려 줍니다

씬에 오브젝트를 하나 만들고 **UI Document** 컴포넌트를 붙입니다. 그리고 두 칸을 채웁니다.

- **Source Asset**: 띄울 `.uxml`
- **Panel Settings**: 해상도 규칙이 들어 있는 에셋

띄우는 시점은 정해져 있습니다. 문서 표현으로 UI Document 는 "loads a UI Document component's source UXML when `OnEnable` is called" 입니다. **오브젝트가 켜질 때 읽습니다.**

그래서 uGUI 에서 하던 것 중 몇 가지가 여기서는 다르게 됩니다.

- 화면을 끄고 켜는 것은 그 오브젝트를 끄고 켜는 것입니다. 안쪽 요소를 하나씩 끄는 것이 아닙니다
- 화면 여러 개를 동시에 띄우려면 **UI Document 를 여러 개** 둡니다. 겹치는 순서는 Panel Settings 의 Sort Order 로 정합니다
- uxml 을 바꿔 끼우면 화면이 통째로 바뀝니다. 팝업을 여는 방법 중 하나가 이것입니다

## UI Builder 는 그 파일들을 저장하는 창입니다

여기까지 읽으면 전부 손으로 적어야 할 것 같지만 아닙니다. **UI Builder** 라는 창이 있습니다. 문서 표현으로 "a visual authoring tool to create and edit UI Toolkit assets such as UXML and USS files" 입니다.

끌어다 놓고 값을 고치면 그게 `.uxml` 과 `.uss` 로 저장됩니다. **만드는 방법이 두 가지가 아니라, 같은 파일을 두 가지 방법으로 고치는 것**입니다. 창에서 고치든 텍스트로 고치든 결과는 같은 파일입니다.

이 구조에서 따라오는 것이 하나 있습니다. **UI 변경이 텍스트 차이로 남습니다.**

uGUI 에서 화면을 고치면 프리팹이나 씬 파일이 바뀝니다. 그 파일들은 사람이 읽기 어렵고, 두 사람이 같은 화면을 고치면 합치기가 까다롭습니다. uxml 과 uss 는 그냥 텍스트라서 무엇이 바뀌었는지 줄 단위로 보입니다. 화면을 여럿이 만지는 프로젝트에서는 이게 꽤 큽니다.

## 정리

- 화면 하나는 **`.uxml`(구조) + `.uss`(스타일) + Panel Settings(해상도 규칙)** 에셋 셋과, 씬의 **UI Document** 오브젝트 하나로 만들어집니다
- 하이어라키에 남는 것은 그 오브젝트 하나뿐입니다. 버튼도 글자도 파일 안에 있습니다
- uxml 의 루트는 `<engine:UXML>` 이고 맨 위 `xmlns:` 들이 접두사를 정의합니다. 태그 이름은 실제 C# 타입 이름입니다
- `name` 은 코드에서 찾을 때, `class` 는 스타일을 걸 때 씁니다
- uss 는 CSS 처럼 생겼지만 "analogous, but not identical" 입니다
- `VisualElement` 는 오브젝트가 아니라 C# 객체입니다. `rootVisualElement` 에서 `Q<T>()` 로 찾습니다
- UI Document 는 오브젝트가 켜질 때 uxml 을 읽습니다. 화면 여러 개는 UI Document 여러 개입니다
- UI Builder 는 그 파일을 고치는 창일 뿐입니다. 결과가 텍스트라서 변경이 줄 단위로 남습니다