| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- dots
- 정렬
- URP
- 유니티 최적화
- 에셋번들
- 리눅스
- GPU 병목
- 디자인패턴
- AssetBundle
- SQLD
- 프로세스
- 렌더링파이프라인
- SQL시험
- 유니티
- ubuntu
- 쉐이더
- HDRP
- 리눅스 명령어
- 성능 최적화
- c#
- 버텍스 쉐이더
- 배칭
- 메모리
- 스레드
- SRP
- rocky linux
- 픽셀 쉐이더
- unity
- ecs
- 멀티스레드
- Today
- Total
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- dots
- 정렬
- URP
- 유니티 최적화
- 에셋번들
- 리눅스
- GPU 병목
- 디자인패턴
- AssetBundle
- SQLD
- 프로세스
- 렌더링파이프라인
- SQL시험
- 유니티
- ubuntu
- 쉐이더
- HDRP
- 리눅스 명령어
- 성능 최적화
- c#
- 버텍스 쉐이더
- 배칭
- 메모리
- 스레드
- SRP
- rocky linux
- 픽셀 쉐이더
- unity
- ecs
- 멀티스레드
- Today
- Total
Let's Girin!
[Unity] 성능 최적화 및 프로파일링 정리 본문
※ 유니티 프로젝트를 진행하면서 겪었던 최적화 이슈들과 프로파일링 과정에서 알게 된 핵심 개념들을 정리해서 작성한 글입니다.
측정 및 근거를 찾는 최적화를 목표로, CPU/GPU 병목, 텍스처 압축 등의 내용 위주로 정리했습니다.
1. 프로파일링의 기본 원칙

★ 유니티 프로젝트의 성능 최적화는 추측이 아니라,
정확한 측정과 데이터 분석에서 시작해야 한다.
- 타겟 플랫폼 선행 조사 & 프로토타이핑
개발 초기 단계에서 목표 하드웨어의 한계를 명확히 파악하고, 프로토타입 단계부터 구현 가능성을 검증한다.
기능 구현에만 몰두하다가 출시 직전에 몰아서 최적화하는 방식은 위험하다. 아키텍처 자체를 갈아엎어야 하는 상황이 올 수도 있기 때문이다.
- 프로파일링을 통한 병목 탐지
무작정 코드를 고치는 것이 아니라 프로파일러를 통해 정확한 병목(Bottleneck) 지점을 찾아내고 이를 제거하는 과정이다.
병목을 특정하지 않은 채로 여기저기 손대는 최적화는 시간 대비 효과가 없다.
- 지속적인 QA 프로세스 정립
개발 막바지에 한 번에 몰아서 최적화하는게 아니라, 빌드 프로세스 및 정기적인 QA 파이프라인에 자동화된 벤치마크 테스트 환경을 구축한다.
- 환경 분리
유니티 에디터에서의 프로파일링 결과는 참고용이다. 정확한 하드웨어 특성(모바일 기기의 발열, 스로틀링..)은 에디터에서 재현되지 않기 때문에 타겟 디바이스에서 직접 프로파일링해야 정확한 수치를 얻을 수 있다.
※ 스로틀링(Throttling)
모바일 기기는 PC와 달리 냉각 팬이 없기 때문에 열(발열)에 매우 취약하다. 기기의 과열을 막기 위해 시스템이 강제
로 CPU 및 GPU의 클럭 속도와 자원 사용량을 제한하는 제어 기술을 스로틀링(Throttling)이라고 한다.
스로틀링에 걸리면 게임의 FPS가 급격히 떨어지며 유저 경험을 해치게 되므로, 모바일 게임 개발 시에는 발열 및 트래
픽 제어가 필수적이다.
대응 방법
- 적절한 targetFrameRate 설정: 무조건 높은 프레임을 유지하는 것보다, 게임 장르나 현재 상황(메뉴 화면, 대기
상태 등)에 맞추어 타겟 프레임을 가변적으로 조절하는 Adaptive Performance를 적용하는 것이 좋다.
// 상황에 따라 타겟 프레임을 동적으로 제한해 발열 최소화.
Application.targetFrameRate = 40; // 전투 중엔 60, 메뉴에선 40 등으로 조절.
- VSyncCount 활용 : QualitySettings.vSyncCount 설정을 통해 모니터나 디바이스 화면의 주사율에 맞춰FPS를 강제로 고정함으로써 불필요한 리소스 낭비를 방지한다.
public class Example : MonoBehaviour
{
void Start()
{
// 디스플레이 새로 고침률의 절반으로 렌더링한다.
QualitySettings.vSyncCount = 2;
}
}
2. CPU 병목 vs GPU 병목
성능 이슈를 분석할 때 가장 먼저 해야 할 일은 CPU와 GPU 중 어느 쪽이 병목을 유발하는지 판단하는 것이다.
CPU와 GPU는 기본적으로 서로 독립적이며 병렬로 로직을 진행한다.
- CPU 병목
게임 로직/물리/애니메이션 등 CPU 연산이 오래 걸려 GPU가 명령을 받지 못하고 대기하는 상태이다.

- GPU 병목
CPU는 Draw Call을 모두 전달하고 프레임 시작 후 대기 상태에 들어가지만, GPU 내부 연산(RenderLoopJob, 복잡한 셰이더, Fillrate 등..)이 끝나지 않아 프레임 종료가 지연되는 상태이다.

유니티 프로파일러의 Graphics.PresentAndSync 지표를 보면 병목의 주체를 가늠할 수 있다. 특히 Graphics.Blit 이나 Device.Present 구간의 점유율이 비정상적으로 크다면 GPU 병목을 강하게 의심해야 한다.

🔹프레임 타임과 FPS
프로파일링에서는 FPS보다 ms(밀리초) 단위 프레임 타임을 기준으로 이야기하는 게 훨씬 직관적이다.
ms = 1000 / FPS
FPS = 1000 / ms
60 FPS를 안정적으로 유지하려면 프레임 타임이 16.66ms 이하로 제어되어야 한다는 뜻이다.
3. CPU 최적화
🔹물리 엔진 최적화
프로파일러에서 Physics2D.SyncTransformChanges 항목이 과도한 시간을 차지한다면, Transform 변경 사항이 물리 엔진 세계로 동기화되면서 오버헤드가 발생하고 있다는 것이다. 게임 엔진 내부의 논리적 세계와 물리 엔진 세계 간의 불필요한 실시간 동기화를 줄이기 위해 다음 설정을 변경하는 것이 권장된다.
※ Project Settings ▷ Physics 2D ▷ Auto Sync Transforms 항목 체크 해제(비활성화)
일반적으로 성능 향상을 위해 꺼두고, 정말 필요한 순간에만 스크립트에서 수동으로 동기화한다.
🔹프로파일링 도구 확장 활용
- Custom Profiler Tags
특정 코드 블록의 비용을 정밀하게 측정하고 싶다면 Profiler.BeginSample() 와 EndSample() 를 사용하여 프로파일러에 커스텀 마커(MarkerSample)를 표시할 수 있다. 다만 요즘은 중간에 return이 되어도 자동으로 마커가 닫혀 안전하고, 문자열 오버헤드도 없는 ProfilerMarker와 using 구문을 사용하는 것이 더 권장됩니다.
public class ProfilerExample : MonoBehaviour
{
void Update()
{
// 1. 샘플 시작.
Profiler.BeginSample("MyLegacyHeavyTask");
if (SomeCondition())
{
// 주의) 여기서 return 해버리면 EndSample이 호출되지 않아 프로파일러 세션 전체가 왜곡된다!
return;
}
DoHeavyWork();
// 2. 샘플 종료.
Profiler.EndSample();
}
bool SomeCondition() => true;
void DoHeavyWork() { }
}
using Unity.Profiling;
public class ProfilerExample : MonoBehaviour
{
// 정적 필드로 마커를 미리 정의.
private static readonly ProfilerMarker s_HeavyTaskMarker = new ProfilerMarker("MyModernHeavyTask");
void Update()
{
// using 구문과 Auto() 메서드 활용.
using (s_HeavyTaskMarker.Auto())
{
if (SomeCondition())
{
// 중간에 return 되어도 using 구문 덕분에 내부적으로 스코프를 벗어나며 안전하게 마커가 닫힌다!
return;
}
DoHeavyWork();
} // 이 지점에서 자동으로 마커 종료.
}
bool SomeCondition() => true;
void DoHeavyWork() { }
}
- Profile Analyzer
유니티 패키지 매니저에서 제공하는 확장 기능으로, 여러 프로파일러 결과물의 스냅샷(Snapshot)을 찍어 이상 현상이 발생한 프레임과 정상 프레임을 비교 분석할 수 있다.

- Xcode Instruments
iOS 디바이스의 경우 유니티 프로파일러뿐만 아니라 Xcode에서 제공하는 Instruments의 Time Profiler를 활용하는 것이 CPU 성능을 정밀 분석하는 데 가장 효과적이다.

🔹코드/아키텍처 작성
- 플레이어 루프 타이밍 이해
Awake → OnEnable → Start → FixedUpdate → Update → LateUpdate → OnDisable → OnDestroy 의 호출 순서와 생명주기를 정확히 알고 로직을 배치해야 불필요한 순서 문제를 피할 수 있다.
- 빈 MonoBehaviour 메서드 제거
내부에 아무런 코드가 없는 공백 상태의 Start(), Update() 등의 이벤트 함수는 유니티 내부(C++)에서 C# 스크립트를 호출할 때 불필요한 가비지 및 메시지 오버헤드를 발생시키므로 반드시 삭제한다.
- 무거운 API 호출 캐싱
GameObject.Find() 나 GetComponent() 는 매우 무거운 연산이므로 빈번하게(특히 Update 내에서) 호
출해서는 안 되며, 전역 변수에 참조를 캐싱하여 사용해야 한다.
- 문자열 연산 주의
문자열 덧셈·조작은 GC를 자주 유발하므로 StringBuilder를 사용하거나 조작 자체를 최소화해야 한다.
- Debug.Log() 비용
Debug.Log() 호출은 상용 빌드에서도 성능을 소모하므로, [Conditional("ENABLE_LOGS")] 같은 조
건부 컴파일 속성을 사용하여 릴리즈 빌드에서 제외되도록 처리한다.
- Non-Allocating API 사용
가비지를 반환하는 Component.GetComponents() 나 Animator.parameters 등의 프로퍼티 접근을 주의한다.
- 대용량 정적 데이터는 ScriptableObject 사용
대용량의 XML이나 JSON 데이터를 로딩하고 파싱하는 작업은 느리고 메모리를 많이 소모한다. 이러한 데이터는 유니티 고유의 스크립터블 오브젝트(ScriptableObject)로 변환하여 에셋화하는 것이 로딩 속도와 메모리 측면에서 훨씬 유리하다.
- 비동기 에셋 로딩
AssetBundle.LoadFromFileAsync(), LoadAssetAsync() 등으로 로딩 시 프레임 드랍을 방지한다.
4. GPU 최적화
GPU 병목의 주된 원인 중 하나는 화면에 픽셀을 그리는 연산 량인 필레이트(Fillrate) 과부하입니다. 필레이트는 다음
과 같은 공식으로 결정된다.
Fillrate = 화면의 픽셀 수 × 프래그먼트 셰이더 복잡도 × 오버드로우(Overdraw)
🔹오버드로우(Overdraw) 병목 제거
오버드로우는 동일한 픽셀 위치에 투명하거나 반투명한 오브젝트가 겹쳐서 여러 번 그려질 때 발생한다.
특히 모바일 환경에서는 이펙트에 사용되는 수많은 파티클과 UI의 반투명 에셋이 오버드로우의 주범이 되므로 연산 낭
비를 철저히 방지해야 한다.

🔹해상도 스케일링(Resolution Scaling)
화면의 전체 픽셀 수를 강제로 조절하여 필레이트 부하를 직접적으로 줄이는 방법이다.
3D 씬의 해상도는 낮추되, 텍스트나 UI의 해상도는 그대로 유지 하여 가시성을 확보하는 기법이 주로 사용된다.
URP나 HDRP에서는 내장된 RenderScale 옵션을 통해 손쉽게 구현할 수 있다. 이를 통해 해상도(Target DPI)를 적절히 낮추면 GPU 연산량이 대폭 줄어들어 게임 속도가 눈에 띄게 빨라진다.

5. 에셋 최적화
🔹텍스처 압축
게임 내에서 가장 많은 메모리를 차지하는 에셋은 텍스처이다.
여기서 헷갈리기 쉬운 게 디스크 저장용 포맷과 GPU가 실제로 읽는 포맷이 다르다는 점이다.
디스크 저장 용량을 줄이기 위한 포맷(PNG, JPG)과 GPU가 메모리(VRAM)상에서 직접 읽어 들이는 전용 압축 포맷의 차이를 이해해야 한다.
- PNG / JPG
단순히 디스크 저장 공간을 아끼기 위한 포맷일 뿐이다.
1MB짜리 PNG라도 별도 GPU 압축이 없으면 텍스처 메모리에는 비압축 용량(예: 4MB 이상)으로 그대로 올라간다.
- GPU 전용 압축 포맷
GPU가 텍스처를 압축된 상태 그대로 메모리에 올려두고, 필요한 부분만 블록 단위로 즉시 디코딩하여 사용하는 포맷이다.
메모리 대역폭과 점유율을 획기적으로 줄여줍니다.
🔹주요 GPU 텍스처 압축 포맷 비교
| 포맷 | 주요 특징 | 최적화 고려사항 |
| ETC1 | 구형 안드로이드(OpenGL ES 2.0)용 기본 포맷. 알파 채널(투명도) 미지원 |
투명도가 필요하면 Split Alpha Channel + Packing Tag 필수 |
| ECT2 | OpenGL ES 3.0부터 표준. 알파 채널 지원. |
ES 2.0 구형 기기에서 강제 로드 시 비압축(32 bit)으로 적재되어 메모리 폭증 위험 |
| PVRTC | 구형 아이폰(PowerVR GPU) 전용. 4bpp/2bpp 고정 비트 블록 방식. |
가로세로가 반드시 2의 거듭제곱(POT)이어야하고, 색상 경계면에 블록 현상이 도드라져 픽셀 아트류엔 부적합 |
| ASTC | 블록 크기(4x4 ~ 12x12)를 직접 지정 가능한 최신 포맷. 화질 대비 크기를 자유롭게 조절(숫자가 클수록 압축률 높음, 정밀도 낮음). |
모바일 디바이스의 표준 포맷 |


🔹에셋 관리 자동화
텍스처 압축 설정을 수동으로 변경하다 보면 누락이 생기기 쉽다.
유니티에서 제공하는 AssetPostprocessor 스크립트 코드를 작성하면 프로젝트에 에셋이 임포트될 때 플랫폼별 텍스처 포맷, 메시 임포트 옵션, 오디오 압축 등을 명시적이고 일괄적으로 자동 세팅하도록 강제할 수 있어 매우 안전하다.
using UnityEditor;
using UnityEngine;
// Editor 폴더 안에 위치해야 한다.
public class TextureAutoImporter : AssetPostprocessor
{
// 텍스처가 임포트되기 직전에 호출되는 함수.
void OnPreprocessTexture()
{
// 현재 임포트 중인 에셋의 설정 객체 가져오기.
TextureImporter textureImporter = (TextureImporter)assetImporter;
// 에셋이 저장된 폴더 경로를 파악 (대소문자 구분 없이).
string assetPathLower = assetPath.ToLower();
// 경로에 따라 텍스처 타입 자동 지정.
if (assetPathLower.Contains("/ui/"))
{
textureImporter.textureType = TextureImporterType.Sprite; // UI용 스프라이트.
textureImporter.mipmapEnabled = false; // UI는 밉맵이 필요 없음.
}
else if (assetPathLower.Contains("/normals/"))
{
textureImporter.textureType = TextureImporterType.NormalMap; // 노멀맵.
textureImporter.mipmapEnabled = true;
}
else
{
textureImporter.textureType = TextureImporterType.Default; // 일반 3D 텍스처.
textureImporter.mipmapEnabled = true;
}
// 모바일 플랫폼별 압축 포맷 자동 세팅.
SetMobilePlatformSettings(textureImporter, "Android");
SetMobilePlatformSettings(textureImporter, "iPhone");
}
private void SetMobilePlatformSettings(TextureImporter importer, string platform)
{
TextureImporterPlatformSettings settings = importer.GetPlatformTextureSettings(platform);
settings.overridden = true; // 플랫폼 오버라이드 체크 활성화.
settings.maxTextureSize = 2048; // 최대 해상도 제한.
// 앞서 설정한 텍스처 타입에 따라 ASTC 블록 크기 최적화.
if (importer.textureType == TextureImporterType.Sprite)
{
// UI는 화질이 중요하므로 4x4.
settings.format = TextureImporterFormat.ASTC_4x4;
}
else
{
// 일반 텍스처는 용량 균형을 위해 6x6.
settings.format = TextureImporterFormat.ASTC_6x6;
}
importer.SetPlatformTextureSettings(settings);
}
}
6. 그 외 알아두면 좋은 프로파일링 도구들
🔹오디오 Load Type
오디오 파일의 Load Type 설정을 무조건 Decompress On Load 로 두면 로딩 시 압축이 모두 풀린 상태로 메모리에 적재되므로 과도한 메모리 낭비가 생긴다. 배경음악(BGM) 등 용량이 큰 사운드는 스트리밍 방식으로 변경하고 중복 리소스가 없는지 체크해야 한다.

🔹Frame Debugger
드로우 콜(Draw Call)이 비정상적으로 많이 발생할 때, 프레임이 그려지는 단계를 한 단계씩 추적해가며 배칭(Batching)이 어떤 원인으로 인해 깨지는지 시각적으로 원인을 정확하게 분석할 수 있는 강력한 도구이다. Sprite Packing(아틀라스화)을 진행하고 UI Layer 순서를 잘 정리하는 것만으로도 드로우 콜을 크게 낮출 수 있다.

🔹Memory Profiler
어떤 오브젝트와 에셋이 메모리를 얼마나 점유하고 있는지 세부 항목별로 분석할 수 있는 툴이다.
패키지 매니저(Package Manager)를 통해 설치 가능하며 가용 메모리가 제한적인 모바일 환경 최적화 시 필
수적으로 사용된다.

'Unity' 카테고리의 다른 글
| [Unity] VAT로 애니메이션 구현하기 (0) | 2026.07.15 |
|---|---|
| [Unity] 버텍스 애니메이션 텍스처(VAT) (0) | 2026.07.14 |
| [Unity] 최적화 - Draw Call 횟수보다 SetPass Call 횟수를 줄이는 것이 성능에 더 영향을 미치는 이유 (0) | 2026.07.01 |
| [Unity] DOTS - Netcode for Entities (0) | 2026.06.27 |
| [Unity] DOTS - Entities (0) | 2026.06.27 |