Unity에서 UI는 게임 로직과 별개의 영역처럼 보이지만 실제로는 CPU와 GPU 양쪽에 모두 영향을 준다.
특히 UGUI 기반 프로젝트에서는 UI의 구조가 잘못 설계되면 작은 UI 하나를 변경했을 뿐인데 Canvas 재구성 비용이 발생하거나, 수많은 UI 요소가 Raycast와 Layout 계산에 참여하면서 CPU 비용이 증가할 수 있다.
GPU 측에서도 문제가 있다.
투명한 UI가 여러 겹으로 쌓이면 Overdraw가 증가하고, 큰 텍스처와 복잡한 UI가 화면을 덮으면 Fill Rate 부담이 커진다.
따라서 UI 최적화는 단순히 Draw Call을 줄이는 작업이 아니다.
핵심은 다음과 같다.
UI 변경 -> Canvas / Layout 재구성 -> CPU 비용
UI 입력 -> Raycast 검사 -> CPU 비용
UI 렌더링 -> Overdraw / Fill Rate -> GPU 비용
대규모 UI -> Object / Layout / Geometry 증가 -> CPU / Memory 비용
결국 좋은 UI 구조는 변경되는 것과 변경되지 않는 것을 분리하고, 필요한 것만 계산하고, 필요한 것만 렌더링하는 구조다.
UI 최적화의 기본 원칙
Unity UI를 최적화할 때 가장 먼저 확인해야 하는 것은 UI의 개수 자체가 아니다.
어떤 UI가 얼마나 자주 변경되는지부터 봐야 한다.
정적 UI -> 거의 변경되지 않음
동적 UI -> 자주 변경됨
상호작용 UI -> 입력을 받아야 함
대규모 UI -> 많은 데이터를 표시함
이 네 가지 특성을 기준으로 Canvas, Layout, Raycast, Pooling 등을 설계해야 한다.
예를 들어 체력바가 매 프레임 변경된다고 해서 인벤토리 전체까지 같은 Canvas에서 관리할 이유는 없다.
HUD Canvas -> 체력 / 마나 / 스태미나
Static Canvas -> 배경 / 장식 / 고정 UI
Popup Canvas -> 팝업 / 알림
이런 식으로 변경 주기와 역할에 따라 UI를 분리하는 것이 기본적인 최적화 전략이다.
Canvas 최적화
UGUI에서 가장 먼저 이해해야 할 것이 Canvas다.
Canvas는 UI 요소의 Geometry를 구성하고 이를 Batch로 묶어 GPU에 렌더링한다. Canvas에 포함된 UI가 변경되면 해당 Canvas의 Geometry와 Batch를 다시 구성해야 할 수 있다. Unity 역시 Canvas를 분할하는 것을 UGUI 최적화의 핵심 전략으로 설명한다.
문제는 거대한 Canvas 하나에 모든 UI를 넣는 구조다.
거대한 Canvas -> UI 하나 변경 -> Canvas Dirty -> Canvas 재구성
예를 들어 다음과 같은 구조가 있다고 하자.
Main Canvas -> HUD / Inventory / Quest / Minimap / Popup / Notification
이 상태에서 체력바 하나가 계속 변경되면 다른 UI와 함께 Canvas 재구성에 영향을 줄 수 있다.
따라서 변경 주기가 다른 UI를 별도의 Canvas로 분리한다.
UI -> Static Canvas / Dynamic Canvas / Popup Canvas
이렇게 하면 한 영역의 변경이 다른 영역에 불필요하게 영향을 주는 것을 줄일 수 있다.
Canvas를 무조건 많이 만들면 되는가
아니다.
Canvas 분할에는 Trade-off가 존재한다.
Canvas를 분리하면 Canvas 간의 재구성 영향을 격리할 수 있지만, 지나치게 분리하면 Batch와 Draw Call 측면에서 불리해질 수 있다.
따라서 다음과 같이 생각해야 한다.
Canvas 분할 -> 재구성 비용 감소
Canvas 과도한 분할 -> 렌더링 관리 복잡도 / Draw Call 증가 가능
정답은 Canvas 개수를 특정 숫자로 맞추는 것이 아니다.
함께 변경되는 UI를 묶고, 서로 다른 주기로 변경되는 UI를 분리하는 것이 핵심이다.
Unity 역시 동적 요소와 정적 요소를 업데이트 빈도에 따라 분리하고, 필요하다면 Child Canvas를 사용해 계층을 격리하는 방식을 권장한다.
Canvas를 숨기는 방법
UI를 숨길 때 GameObject.SetActive(false)만 생각할 필요는 없다.
화면에 보이지 않게 만드는 것이 목적이라면 Canvas 컴포넌트 자체를 비활성화하는 방법도 있다.
Canvas 컴포넌트를 비활성화하면 GPU에 Draw Call을 보내지 않게 되지만 기존 Geometry를 버리지 않고 유지하기 때문에 다시 활성화할 때 재구성 비용을 피할 수 있다. 또한 GameObject 전체를 비활성화하는 것과 달리 계층 전체의 OnDisable / OnEnable 호출을 발생시키지 않는다.
따라서 다음과 같이 구분할 수 있다.
렌더링만 중지 -> Canvas.enabled = false
UI 로직까지 중지 -> GameObject 비활성화
단, Canvas를 껐다고 해서 자식 Script의 Update까지 자동으로 멈추는 것은 아니다.
렌더링과 로직 실행은 별개의 문제다.
Layout 최적화
Unity UI의 Layout System은 편리하다.
하지만 편리한 만큼 비용이 발생한다.
대표적인 컴포넌트는 다음과 같다.
HorizontalLayoutGroup
VerticalLayoutGroup
GridLayoutGroup
ContentSizeFitter
이러한 Layout 컴포넌트가 중첩되면 UI 변경에 따라 여러 계층의 Layout 계산이 연쇄적으로 발생할 수 있다.
Unity 공식 최적화 가이드 역시 가능하면 Layout Group을 피하고, Anchors 등을 활용하거나 동적인 UI에서는 필요한 시점에 직접 Layout을 계산하는 방식을 권장한다.
특히 다음 구조는 주의해야 한다.
VerticalLayoutGroup -> HorizontalLayoutGroup -> ContentSizeFitter -> LayoutElement
UI가 변경될 때마다 여러 계층이 다시 계산될 가능성이 있기 때문이다.
Layout Group을 사용하지 말아야 하는가
그것도 아니다.
Layout Group은 생산성과 유지보수성을 높여준다.
문제는 매 프레임 또는 빈번한 변경이 발생하는 UI에 복잡한 Layout 시스템을 사용하는 것이다.
예를 들어 고정된 HUD는 Anchor와 RectTransform으로 충분한 경우가 많다.
고정 UI -> Anchor / RectTransform
반면 실제로 동적으로 아이템이 추가되고 제거되는 UI라면 Layout Group이 개발 생산성을 크게 높여줄 수 있다.
따라서 기준은 다음이다.
정적 UI -> 직접 배치
단순 동적 UI -> 최소한의 Layout
복잡한 동적 UI -> Layout 비용 측정
대규모 동적 UI -> Custom Layout / Virtualization 검토
Raycast 최적화
UI는 렌더링만 하는 것이 아니다.
사용자의 입력을 받아야 한다.
UGUI에서는 Graphic Raycaster가 Canvas의 UI 요소를 대상으로 입력 이벤트 처리를 수행한다. 따라서 상호작용하지 않는 UI까지 Raycast 대상에 포함시키면 불필요한 검사 대상이 증가한다.
예를 들어 버튼 내부에 다음과 같은 구조가 있다고 하자.
Button -> Image + Text
실제로 클릭을 받아야 하는 것은 Button이다.
따라서 Text에 Raycast Target이 켜져 있을 필요가 없는 경우가 많다.
Button Image -> Raycast Target ON
Button Text -> Raycast Target OFF
배경이나 장식용 Image 역시 마찬가지다.
Background -> Raycast Target OFF
Decoration -> Raycast Target OFF
Interactive Element -> Raycast Target ON
Graphic Raycaster 자체도 관리해야 한다
입력을 받지 않는 Canvas라면 Graphic Raycaster가 필요하지 않을 수 있다.
Static UI Canvas -> Graphic Raycaster 제거
Interactive UI Canvas -> Graphic Raycaster 유지
Unity 역시 상호작용하지 않는 Canvas에는 Graphic Raycaster를 추가하지 말고, 비인터랙티브 UI의 Raycast Target을 끄는 것을 권장한다.
또한 Screen Space – Camera나 World Space Canvas에서 Blocking Mask를 사용해 2D/3D Physics Raycast까지 수행하도록 만들면 추가 비용이 발생할 수 있으므로 필요한 경우에만 사용하는 것이 좋다.
Camera.main 최적화
Unity 공식 UI 최적화 자료에서는 World Space Canvas와 Graphic Raycaster를 사용할 때 Camera.main의 반복적인 접근을 주의할 것을 설명한다. Event Camera를 비워두면 Main Camera를 찾는 경로가 사용될 수 있기 때문이다.
따라서 World Space Canvas에서는 가능한 한 Event Camera를 명시적으로 지정한다.
World Space Canvas -> Event Camera 지정
코드에서도 반복적인 탐색보다는 필요한 참조를 캐싱하는 방식이 기본이다.
Camera.main 반복 호출 -> 참조 탐색
Camera 참조 캐싱 -> 직접 접근
특히 매 프레임 실행되는 코드에서 불필요한 탐색을 반복하지 않는 것은 UI뿐 아니라 Unity 전체에서 적용되는 기본 원칙이다.
UI Pooling
대규모 UI에서는 Instantiate와 Destroy를 반복하는 구조를 피해야 한다.
예를 들어 인벤토리에 1,000개의 아이템이 존재한다고 하자.
아이템 하나당 UI GameObject 하나를 생성하면 다음과 같은 구조가 된다.
1000개 데이터 -> 1000개 UI GameObject
실제로 화면에 동시에 보이는 것이 20개뿐이라면 상당히 비효율적이다.
이 경우 UI Pooling을 사용할 수 있다.
1000개 데이터 -> 20개 UI 생성 -> 화면에 필요한 데이터 연결 -> 스크롤 -> UI 재사용
이 방식은 메모리뿐 아니라 Instantiate / Destroy 비용도 줄일 수 있다.
UI Pooling에서 중요한 순서
Pooling은 단순히 Object Pool을 사용하는 것으로 끝나지 않는다.
Unity 공식 최적화 자료에서는 UI Object를 Pool로 이동시킬 때 순서 역시 중요하다고 설명한다. 기존 계층을 불필요하게 여러 번 Dirty 상태로 만드는 것을 방지하기 위해 Pool로 이동할 때는 먼저 비활성화한 후 Reparent하는 방식이 권장된다. Pool에서 꺼낼 때는 먼저 Reparent하고 데이터를 갱신한 뒤 활성화하는 방식이다.
Pool 반환 -> Disable -> Reparent
Pool 사용 -> Reparent -> Data 갱신 -> Enable
이런 순서를 의식하면 불필요한 UI Dirtying을 줄일 수 있다.
Scroll View 최적화
대규모 Scroll View는 UI 최적화에서 자주 병목이 발생하는 영역이다.
단순히 ScrollRect 하나를 사용하는 것만으로 대량의 UI를 효율적으로 처리할 수 있는 것은 아니다.
예를 들어 다음 구조는 문제가 될 수 있다.
Scroll View -> 1000개 Item -> 각 Item마다 Image / Text / Button
이 구조에서는 UI Object 수 자체가 증가하고 Layout, Canvas, Raycast 등의 비용도 함께 증가할 수 있다.
따라서 데이터가 많다면 Virtualization을 고려한다.
전체 데이터 -> 화면에 필요한 UI만 생성 -> 스크롤 -> 기존 UI 재사용
이 구조를 사용하면 데이터의 개수와 실제 생성되는 UI의 개수를 분리할 수 있다.
특히 다음과 같은 UI에서 효과가 크다.
Inventory
Ranking
Quest List
Chat
Shop
Mail
Battle Log
UI Overdraw
CPU 최적화만 생각하면 안 된다.
UI는 GPU 측에서도 상당한 비용을 발생시킬 수 있다.
특히 UGUI는 투명한 Image가 많이 겹치는 구조에서 Overdraw가 발생하기 쉽다.
예를 들어 화면 전체를 덮는 투명 Image가 여러 개 있다면 동일한 픽셀이 여러 번 처리된다.
Background -> Panel -> Image -> Shadow -> Effect
이런 구조가 화면 전체에 걸쳐 반복되면 GPU가 처리해야 하는 픽셀 수가 증가한다.
따라서 UI 최적화에서는 Overdraw를 확인해야 한다.
Overdraw를 줄이는 방법
대표적인 방법은 다음과 같다.
불필요한 투명 영역 제거
겹쳐 있는 UI 최소화
과도한 Full Screen Image 제거
불필요한 UI Effect 제거
큰 반투명 영역 최소화
UI의 실제 이미지 영역보다 텍스처가 지나치게 큰 경우에도 불필요한 픽셀 처리가 증가할 수 있다.
따라서 아트 리소스 단계에서도 최적화가 필요하다.
UI Texture와 Atlas
여러 UI Sprite를 적절하게 Atlas로 구성하면 Texture 변경을 줄이고 Batch 효율을 높이는 데 도움이 된다.
Sprite A
Sprite B
Sprite C
를 각각 별도의 Texture로 사용하는 것보다 적절한 조건에서 하나의 Atlas로 관리하는 것이 효율적일 수 있다.
Sprite A + Sprite B + Sprite C -> UI Atlas
다만 Atlas 역시 무조건 하나로 합치는 것이 정답은 아니다.
UI가 사용하는 리소스와 Canvas의 구조를 함께 고려해야 한다.
Canvas 구성 -> Texture 구성 -> Material 구성 -> Batch 구성
이 관계를 함께 봐야 한다.
UI Animation 최적화
UI Animation에서도 불필요한 업데이트가 발생할 수 있다.
Unity 공식 가이드에서는 UI의 Animator 사용을 최적화하고, 지속적으로 변경되는 UI에만 Animator를 사용하며 이벤트성 UI 애니메이션은 코드나 Tween 방식으로 처리하는 방법을 권장한다.
예를 들어 다음과 같은 애니메이션은 짧게 실행되고 종료된다.
Popup 등장
Button Scale
Damage Text
Notification
Panel Slide
이런 UI는 Tween이나 직접적인 값 변경으로 구현하는 것이 더 단순할 수 있다.
반대로 지속적으로 상태를 평가해야 하는 복잡한 애니메이션이라면 Animator가 더 적합할 수 있다.
핵심은 도구 자체가 아니라 변경 빈도와 필요한 기능에 맞는 시스템을 선택하는 것이다.
전체 화면 UI 최적화
Pause 메뉴나 로딩 화면처럼 게임 화면 전체를 덮는 UI가 있다.
이때 뒤쪽 게임 월드가 전혀 보이지 않는다면 월드 렌더링을 계속할 이유가 없는 경우가 있다.
Game World -> Full Screen UI
구조라면 뒤쪽 월드가 실제 화면에 표시되지 않는다.
따라서 상황에 따라 게임 월드를 렌더링하는 Camera를 비활성화할 수 있다.
Full Screen UI 활성화 -> Game Camera 비활성화 -> UI만 렌더링
Unity 역시 전체 화면 UI가 씬을 완전히 덮는 경우 뒤쪽 3D 씬과 숨겨진 UI를 비활성화하는 방법을 제안한다.
단, 게임 로직까지 정지해야 하는지는 별개의 문제다.
UI Toolkit과 UGUI를 구분하자
Unity 6에서는 UI Toolkit의 성능도 상당히 개선됐다.
UI Toolkit은 UGUI의 Canvas 구조와 동일한 방식으로 이해하면 안 된다.
UI Toolkit은 Visual Tree, Style Resolution, Layout, Geometry, Vertex Buffer 등의 별도 업데이트 메커니즘을 사용한다. Unity 6에서는 이벤트 디스패치, Mesh 생성, Text 생성, 깊은 Hierarchy의 Layout 계산 등 여러 부분이 개선됐다.
또한 Runtime UI에서는 UsageHints를 사용해 요소의 사용 패턴을 알려주고 Geometry 재생성이나 Draw Call을 줄이는 방법을 제공한다.
따라서 Unity 6에서 UI 최적화를 이야기할 때는 다음처럼 구분해야 한다.
UGUI -> Canvas / Graphic / Layout / Graphic Raycaster / Batch
UI Toolkit -> Visual Tree / Style / Layout / Vertex Buffer / UsageHints
두 시스템을 같은 최적화 규칙으로 취급하면 안 된다.
Profiler 없이 최적화하지 않는다
UI 최적화에서 가장 위험한 것은 감으로 최적화하는 것이다.
“Canvas가 많아서 느릴 것이다.”
“Draw Call이 많아서 느릴 것이다.”
“UI Object가 많아서 느릴 것이다.”
이런 추측만으로 구조를 변경하면 오히려 성능을 악화시킬 수 있다.
먼저 병목을 확인해야 한다.
문제 발생 -> Profiler 측정 -> 병목 확인 -> 원인 분석 -> 수정 -> 재측정
UGUI라면 CPU 쪽에서 Canvas와 Layout 관련 비용을 확인하고, GPU 쪽에서는 Overdraw와 Draw Call을 확인한다.
Frame Debugger 역시 UI가 어떤 방식으로 Draw Call을 구성하는지 확인하는 데 유용하다.
UI Toolkit이라면 Unity 6의 Profiler와 관련 UI 성능 분석 도구를 함께 사용해 Style, Layout, Geometry, Rendering 등의 비용을 확인해야 한다. Unity 공식 문서 역시 복잡한 UI의 성능 균형을 잡을 때 Profiler와 Frame Debugger를 활용할 것을 권장한다.
실전 UI 최적화 체크리스트
구조
정적 UI와 동적 UI 분리
변경 주기가 다른 UI를 Canvas로 분리
Canvas를 무조건 많이 만들지 않음
불필요하게 깊은 UI Hierarchy 제거
Layout
불필요한 Layout Group 제거
중첩 Layout 최소화
정적 UI는 Anchor / RectTransform 활용
대규모 동적 UI는 Custom Layout 또는 Virtualization 검토
입력
비인터랙티브 UI의 Raycast Target OFF
입력이 필요 없는 Canvas의 Graphic Raycaster 제거
불필요한 Physics Blocking Raycast 사용 금지
World Space Canvas의 Event Camera 명시
Rendering
UI Overdraw 확인
불필요한 투명 영역 제거
Full Screen UI 중첩 최소화
Texture / Material / Atlas 구조 관리
불필요한 UI 렌더링 중지
대규모 UI
Instantiate / Destroy 반복 금지
UI Pooling
Scroll View Virtualization
화면에 필요한 UI만 생성
Animation
불필요한 Animator 사용 최소화
이벤트성 애니메이션은 Tween 또는 직접 제어 검토
매 프레임 변경되는 UI만 지속적인 업데이트 수행
측정
Unity Profiler
Frame Debugger
Rendering Statistics
Overdraw 확인
CPU / GPU 병목 분리
마무리
Unity UI 최적화의 본질은 UI를 적게 만드는 것이 아니다.
변경되는 UI를 적게 만들고, 계산해야 하는 UI를 적게 만들고, 입력을 받아야 하는 UI를 적게 만들고, 렌더링해야 하는 픽셀을 적게 만드는 것이다.
UGUI에서는 특히 Canvas가 핵심이다.
UI 변경 -> Canvas Dirty -> Geometry / Batch 재구성 -> CPU 비용
따라서 변경 빈도가 다른 UI를 적절하게 분리한다.
그 다음 Layout을 관리한다.
UI 변경 -> Layout Dirty -> Layout 계산 -> CPU 비용
불필요한 Layout Group과 중첩 구조를 줄인다.
입력도 관리한다.
Input -> Graphic Raycaster -> UI 검사 -> CPU 비용
상호작용하지 않는 UI는 Raycast 대상에서 제거한다.
GPU 측에서는 Overdraw를 본다.
UI Layer 증가 -> Overdraw 증가 -> 처리 픽셀 증가 -> GPU 비용 증가
그리고 대규모 UI에서는 Pooling과 Virtualization을 사용한다.
대량 데이터 -> 전체 UI 생성
이 아니라
대량 데이터 -> 필요한 UI만 생성 -> 재사용
으로 구조를 바꾼다.
결국 최적화의 방향은 하나다.
불필요한 변경 제거 -> 불필요한 계산 제거 -> 불필요한 입력 검사 제거 -> 불필요한 렌더링 제거 -> Profiler로 검증
이 원칙을 이해하면 특정 Unity 버전이나 특정 UI 컴포넌트에 종속되지 않고 UI 성능 문제를 구조적으로 분석할 수 있다.