---
title: "칸은 남는 자리를 나눠 갖는 방식으로 정해진다"
description: "UI Toolkit 에는 앵커도 피벗도 없습니다. 자리는 부모가 정하고 자식은 쌓입니다. 기본 방향이 column 인 것, flex-grow 로 남는 자리를 나누는 것, 주축과 교차축, absolute 를 언제만 쓰는지, 숨기는 방법 두 가지를 정리했습니다."
author: "유니얼"
pubDate: 2026-09-18
category: "game-dev/unity/ui"
tags: ["Unity", "UI", "UI Toolkit"]
canonical: https://blog.unialgames.com/posts/uitk-flex-layout/
---
[이름을 붙여야 스타일을 걸 수 있다](/posts/uitk-uss-selectors/)에서 값을 어디에 적고 누구에게 거는지까지 봤습니다. 이제 그 값 중에 제일 많이 쓰는 것, 자리와 크기입니다.

uGUI 에서 이 자리를 맡던 것은 RectTransform 이었습니다. 앵커, 피벗, 오프셋을 **각 요소가 스스로 들고** 있었고, 부모는 Layout Group 을 붙여야만 자식 배치에 끼어들 수 있었습니다. UI Toolkit 에는 앵커도 피벗도 없습니다. 반대로 되어 있습니다. **자리는 부모가 정하고 자식은 쌓입니다.**

## 앵커가 없어지고 쌓기가 남습니다

<figure class="diagram">
<svg viewBox="0 0 680 262" role="img" aria-label="uGUI 는 자식이 앵커와 오프셋을 들고 있고 UI Toolkit 은 부모가 방향을 정해 자식이 쌓이는 그림">
<text x="24" y="26" font-size="13" fill="var(--text)">자리를 누가 들고 있는가</text>
<text x="24" y="52" font-size="11.5" fill="var(--accent2)">uGUI</text>
<rect x="24" y="62" width="290" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<circle cx="36" cy="74" r="3.5" fill="var(--accent2)"></circle>
<circle cx="302" cy="74" r="3.5" fill="var(--accent2)"></circle>
<circle cx="36" cy="200" r="3.5" fill="var(--accent2)"></circle>
<circle cx="302" cy="200" r="3.5" fill="var(--accent2)"></circle>
<line x1="36" y1="74" x2="106" y2="112" stroke="var(--accent2)" stroke-width="1" stroke-dasharray="3 3"></line>
<line x1="302" y1="200" x2="232" y2="162" stroke="var(--accent2)" stroke-width="1" stroke-dasharray="3 3"></line>
<rect x="106" y="112" width="126" height="50" rx="7" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="169" y="142" font-size="11" fill="var(--text)" text-anchor="middle">Button</text>
<text x="169" y="236" font-size="10.5" fill="var(--text-muted)" text-anchor="middle">앵커와 오프셋을 요소가 직접 들고 있습니다</text>
<text x="366" y="52" font-size="11.5" fill="var(--accent)">UI Toolkit</text>
<text x="656" y="52" font-size="10.5" fill="var(--text-muted)" font-family="var(--font-mono)" text-anchor="end">flex-direction: column</text>
<rect x="366" y="62" width="290" height="150" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="380" y="76" width="262" height="36" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="394" y="99" font-size="11" fill="var(--text)">Label</text>
<rect x="380" y="120" width="262" height="36" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="394" y="143" font-size="11" fill="var(--text)">Button</text>
<rect x="380" y="164" width="262" height="36" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="394" y="187" font-size="11" fill="var(--text)">Button</text>
<text x="511" y="236" font-size="10.5" fill="var(--text-muted)" text-anchor="middle">부모가 방향을 정하고 자식은 차례로 쌓입니다</text>
</svg>
<figcaption>읽는 단위가 요소 하나에서 부모와 자식 묶음으로 올라갑니다.</figcaption>
</figure>

문서 표현으로 "The layout engine uses the layout principles of Yoga, which implements a subset of Flexbox" 입니다. 웹의 Flexbox 를 부분집합으로 가져온 것입니다.

그래서 요소 하나를 보고 "이게 어디 붙어 있지" 를 알 수 없습니다. 부모가 어느 방향으로 쌓는지, 남는 자리를 누가 가져가기로 했는지를 봐야 합니다. 값을 읽는 단위가 요소에서 **부모와 자식 묶음** 으로 올라갑니다.

Layout Group 을 아는 사람에게는 이렇게 말하는 게 빠릅니다. **모든 요소에 Vertical Layout Group 이 기본으로 붙어 있는 상태** 입니다. 붙일지 말지를 고르는 게 아니라, 방향을 무엇으로 할지만 고릅니다.

## 기본 방향이 세로입니다

여기서 웹을 해 본 사람이 제일 먼저 걸립니다. CSS 의 `flex-direction` 기본값은 `row` 인데, **UI Toolkit 은 `column` 입니다.** 문서가 "makes the main-axis run from top to bottom" 이라고 적어 뒀습니다.

`.uxml` 에 요소 셋을 넣고 아무 스타일도 안 걸면 세로로 쌓입니다. 가로로 놓고 싶으면 부모에 한 줄을 적습니다.

```css
.row {
    flex-direction: row;
}
```

쓸 수 있는 값은 `row`, `row-reverse`, `column`, `column-reverse` 넷입니다. `-reverse` 는 순서를 뒤집습니다. uxml 의 줄 순서를 그대로 두고 화면에서만 뒤집을 때 씁니다.

## 남는 자리를 누가 가져갑니까

<x-flex>
<script type="application/json">
{"label":"남는 자리를 누가 가져가는가","note":"브라우저의 실제 Flexbox 로 그린 것입니다. Yoga 가 구현한 부분집합이라 이 세 값의 동작은 같습니다.","default":{"dir":0,"grow":0,"cross":0}}
</script>
</x-flex>

칸 크기를 정하는 값이 셋입니다.

| 속성 | 하는 일 | 기본값 |
| --- | --- | --- |
| `flex-basis` | 나누기 전 시작 크기 | `auto` (내용 크기) |
| `flex-grow` | 남는 자리를 가져가는 비율 | `0` |
| `flex-shrink` | 모자랄 때 줄어드는 비율 | `1` |

기본값이 `flex-grow: 0` 이라서, 아무것도 안 적으면 각 칸은 내용만큼만 차지하고 남는 자리는 빈 채로 남습니다. 늘리고 싶은 칸에만 `flex-grow: 1` 을 겁니다.

주의할 조합이 하나 있습니다. 칸 셋에 전부 `flex-grow: 1` 을 줘도 **칸 크기가 같아지지 않습니다.** 시작 크기가 내용 크기라서, 남는 자리만 똑같이 나눠 가질 뿐입니다. 진짜로 삼등분하려면 시작 크기를 0 으로 놓아야 합니다.

```css
.item {
    flex-grow: 1;
    flex-basis: 0;
}
```

문서가 이 조합을 "ensures an even split regardless of content" 라고 적어 뒀습니다. 위 인터랙티브에서 `전부 1` 과 `전부 1, basis 0` 을 번갈아 보면 차이가 보입니다.

## 축이 둘이고, 각각 쓰는 속성이 다릅니다

쌓이는 방향을 주축, 그에 직각인 방향을 교차축이라고 부릅니다. 정하는 속성이 다릅니다.

<figure class="diagram">
<svg viewBox="0 0 680 284" role="img" aria-label="column 일 때와 row 일 때 주축과 교차축이 어느 방향인지 비교한 그림">
<text x="24" y="24" font-size="11.5" fill="var(--text)" font-family="var(--font-mono)">flex-direction: column</text>
<text x="356" y="24" font-size="11.5" fill="var(--text)" font-family="var(--font-mono)">flex-direction: row</text>
<text x="180" y="46" font-size="10" fill="var(--text-muted)" text-anchor="middle">교차축 · align-items</text>
<line x1="60" y1="54" x2="300" y2="54" stroke="var(--text-muted)" stroke-width="1.1"></line>
<polygon points="308,54 296,49 296,59" fill="var(--text-muted)"></polygon>
<text transform="rotate(-90 34 152)" x="34" y="152" font-size="10" fill="var(--accent2)" text-anchor="middle">주축 · justify-content</text>
<line x1="46" y1="66" x2="46" y2="230" stroke="var(--accent2)" stroke-width="1.1"></line>
<polygon points="46,238 41,226 51,226" fill="var(--accent2)"></polygon>
<rect x="60" y="62" width="250" height="180" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="76" y="76" width="218" height="40" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="76" y="124" width="218" height="40" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="76" y="172" width="218" height="40" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="185" y="230" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">남는 자리</text>
<text x="512" y="46" font-size="10" fill="var(--accent2)" text-anchor="middle">주축 · justify-content</text>
<line x1="392" y1="54" x2="632" y2="54" stroke="var(--accent2)" stroke-width="1.1"></line>
<polygon points="640,54 628,49 628,59" fill="var(--accent2)"></polygon>
<text transform="rotate(-90 366 152)" x="366" y="152" font-size="10" fill="var(--text-muted)" text-anchor="middle">교차축 · align-items</text>
<line x1="378" y1="66" x2="378" y2="230" stroke="var(--text-muted)" stroke-width="1.1"></line>
<polygon points="378,238 373,226 383,226" fill="var(--text-muted)"></polygon>
<rect x="392" y="62" width="250" height="180" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="408" y="76" width="62" height="152" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="478" y="76" width="62" height="152" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="548" y="76" width="62" height="152" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="626" y="158" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">남는</text>
<text x="626" y="172" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">자리</text>
<text x="24" y="272" font-size="11.5" fill="var(--text-muted)">칸이 쌓이는 쪽이 주축입니다. 기본 방향이 column 이라 주축이 세로입니다</text>
</svg>
<figcaption>두 칸 다 교차축은 stretch 라서 꽉 차 있습니다. 남는 자리는 주축 쪽에만 생깁니다.</figcaption>
</figure>

| 축 | 속성 | 값 |
| --- | --- | --- |
| 주축 | `justify-content` | `flex-start` `flex-end` `center` `space-between` `space-around` |
| 교차축 | `align-items` | `flex-start` `flex-end` `center` `stretch` |
| 교차축 (한 칸만) | `align-self` | 위와 같고 `auto` 가 추가 |

`align-items` 의 **기본값이 `stretch`** 입니다. 이것 때문에 처음에 헷갈리는 장면이 하나 나옵니다. 세로로 쌓은 칸에 가로 폭을 하나도 안 줬는데 부모 폭을 꽉 채우고 있습니다. 교차축이 늘어나기 때문입니다.

내용 크기만큼만 쓰고 싶으면 부모에 `align-items: flex-start` 를 주거나, 그 칸에만 `align-self: flex-start` 를 줍니다.

줄바꿈은 `flex-wrap` 입니다. `nowrap` 이 기본이고 `wrap`, `wrap-reverse` 가 있습니다. uGUI 의 Grid Layout Group 에 해당하는 것이 따로 없어서, 격자는 보통 `flex-wrap: wrap` 에 칸 폭을 정해서 만듭니다.

## 크기는 세 군데에서 옵니다

`width` 와 `height` 를 직접 적는 것이 첫째입니다. `px` 와 `%` 를 씁니다. `%` 는 부모 크기 기준입니다.

둘째가 위에서 본 `flex-basis` 와 `flex-grow` 입니다. 셋째가 아무것도 안 적었을 때의 내용 크기입니다.

경계는 `min-width` `max-width` `min-height` `max-height` 로 겁니다. `flex-shrink` 가 기본 `1` 이라 칸은 기본적으로 줄어드는데, 글자가 뭉개지면 안 되는 칸에는 `min-width` 를 걸어 두는 쪽이 낫습니다.

[짧은 글자일수록 번역에서 더 길어진다](/posts/unity-ui-localization/)에서 본 문제가 여기서도 똑같습니다. 다른 점은 uGUI 의 Content Size Fitter 에 해당하는 것이 따로 없다는 것입니다. 아무것도 안 적으면 그게 이미 내용 크기입니다.

## 겹쳐 놓을 때만 absolute 입니다

쌓기로 안 되는 배치가 있습니다. 배지를 아이콘 오른쪽 위에 얹는다든가, 팝업을 화면 위에 띄운다든가 하는 것입니다. 그때 쓰는 것이 `position` 입니다.

<figure class="diagram">
<svg viewBox="0 0 680 244" role="img" aria-label="relative 와 absolute 와 left right 를 둘 다 준 경우를 나란히 비교한 그림">
<text x="24" y="24" font-size="13" fill="var(--text)">흐름 안에 있는가, 빠져 있는가</text>
<text x="24" y="52" font-size="10" fill="var(--text-muted)" font-family="var(--font-mono)">position: relative</text>
<rect x="24" y="62" width="196" height="132" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="38" y="76" width="168" height="38" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="38" y="122" width="168" height="38" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<text x="122" y="184" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">차례로 쌓입니다</text>
<text x="242" y="52" font-size="10" fill="var(--text-muted)" font-family="var(--font-mono)">position: absolute</text>
<rect x="242" y="62" width="196" height="132" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="256" y="76" width="168" height="38" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="256" y="122" width="168" height="38" rx="6" fill="var(--accent)" fill-opacity="0.16" stroke="var(--accent)"></rect>
<rect x="330" y="98" width="94" height="44" rx="6" fill="var(--warn)" fill-opacity="0.2" stroke="var(--warn)"></rect>
<text x="377" y="125" font-size="10" fill="var(--text)" text-anchor="middle">배지</text>
<text x="340" y="184" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">흐름에서 빠져 위에 얹힙니다</text>
<text x="460" y="52" font-size="10" fill="var(--text-muted)" font-family="var(--font-mono)">left: 0; right: 0</text>
<rect x="460" y="62" width="196" height="132" rx="8" fill="var(--bg-subtle)" stroke="var(--border)" stroke-dasharray="4 3"></rect>
<rect x="460" y="102" width="196" height="44" rx="6" fill="var(--accent2)" fill-opacity="0.18" stroke="var(--accent2)"></rect>
<polygon points="466,124 478,119 478,129" fill="var(--accent2)"></polygon>
<polygon points="650,124 638,119 638,129" fill="var(--accent2)"></polygon>
<text x="558" y="128" font-size="10" fill="var(--text)" text-anchor="middle">양쪽에 붙어 늘어납니다</text>
<text x="558" y="184" font-size="9.5" fill="var(--text-muted)" text-anchor="middle">stretch 앵커가 하던 일</text>
</svg>
<figcaption>가운데 그림에서 아래 칸은 배지가 없는 것처럼 자리를 잡습니다.</figcaption>
</figure>

기본값은 `relative` 이고, 이때 요소는 쌓기 흐름 안에 있습니다. `left` 나 `top` 을 주면 **자기 자리를 그대로 차지한 채로** 그만큼 밀려납니다.

`absolute` 는 다릅니다. 문서 표현으로 "Absolute mode makes an element invisible to the default Flexbox-based layout engine" 입니다. 흐름에서 빠지기 때문에 형제들 배치가 그 요소를 없는 것으로 치고 다시 잡힙니다.

여기서 uGUI 의 앵커와 만나는 지점이 하나 있습니다. `left` 와 `right` 를 **둘 다** 주면 칸이 그 사이만큼 늘어납니다. `top` 과 `bottom` 도 마찬가지입니다. RectTransform 의 stretch 앵커가 하던 일이 이 두 줄입니다.

```css
.overlay {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
}
```

다만 문서가 남용을 막아 뒀습니다. "Avoid using Absolute position mode because it bypasses the built-in layout engine in UI Toolkit and can lead to high-maintenance UI." 오버레이와 팝업에는 맞고, 일반 배치를 이걸로 짜면 화면 크기가 바뀔 때마다 손이 갑니다.

## 숨기는 방법이 두 가지입니다

[한 칸이 바뀌면 그 캔버스는 통째로 다시 계산된다](/posts/unity-ui-rebuild-cost/)에서 uGUI 쪽 창 숨기기 세 가지를 봤습니다. 여기서 그 자리에 있는 것이 두 줄입니다.

| 적는 것 | 칸 | 언제 |
| --- | --- | --- |
| `display: none` | 칸까지 없어집니다 | 목록에서 항목을 빼듯 배치가 다시 잡혀야 할 때 |
| `visibility: hidden` | 칸은 그대로 남습니다 | 자리는 지키고 안 보이게만 할 때 |

문서 표현으로 "Set the `visibility` to `hidden` to hide the element, but the element still occupies space in the layout" 입니다. `display` 쪽은 값이 `flex` 와 `none` 둘뿐입니다. CSS 의 `display` 를 아는 사람에게는 이 부분이 좁아 보이는데, 문서가 "supports keywords that work with the Yoga layout engine" 이라고 이유를 적어 뒀습니다.

`opacity: 0` 도 있습니다. 칸도 그대로이고 값이 0 과 1 사이로 이어져서, 페이드처럼 전환이 필요한 자리에 씁니다.

내용이 넘칠 때 자르는 것은 `overflow` 입니다. 기본값이 `visible` 이라 **안 잘립니다.** 스크롤 영역이나 카드 안쪽처럼 잘려야 하는 곳에는 `overflow: hidden` 을 직접 적어야 합니다.

## 정리

- 앵커와 피벗이 없습니다. **부모가 방향을 정하고 자식이 쌓입니다.** 모든 요소에 Layout Group 이 붙어 있는 상태라고 보면 됩니다
- 기본 방향이 `column` 입니다. CSS 의 기본값과 다릅니다
- `flex-grow` 기본값이 `0` 이라 안 적으면 내용 크기입니다. 남는 자리를 나누려면 적어야 합니다
- 전부 `flex-grow: 1` 이어도 크기가 같아지지 않습니다. 똑같이 나누려면 `flex-basis: 0` 을 같이 줍니다
- 주축은 `justify-content`, 교차축은 `align-items` 입니다. `align-items` 기본값이 `stretch` 라서 폭을 안 줘도 꽉 찹니다
- `position: absolute` 는 흐름에서 빠집니다. `left` 와 `right` 를 둘 다 주면 stretch 앵커처럼 늘어납니다. 오버레이에만 씁니다
- `display: none` 은 칸까지 없애고 `visibility: hidden` 은 칸을 남깁니다. `overflow` 기본값은 `visible` 이라 안 잘립니다