Unity Addressables 비동기 에셋 번들 다운로드와 프로그레스 바 구현 방법
Unity Addressables에서 원격 에셋 번들을 비동기로 다운로드하고 실제 다운로드 바이트 기준으로 UI 프로그레스 바와 상태 문구를 안정적으로 제어하는 C# 구현 방법을 정리합니다.

핵심 요약
Unity Addressables의 원격 에셋 번들은 GetDownloadSizeAsync로 다운로드 필요 용량을 먼저 확인한 뒤 DownloadDependenciesAsync로 내려받을 수 있습니다. 진행률은 AsyncOperationHandle.GetDownloadStatus()가 반환하는 DownloadedBytes와 TotalBytes를 이용해 계산하고 완료 후에는 핸들을 적절히 Addressables.Release해야 메모리와 참조 수를 안전하게 관리할 수 있습니다.
Addressables 다운로드 프로그레스 바가 필요한 이유
Addressables는 로컬에 없는 원격 AssetBundle을 필요한 시점에 내려받습니다. 하지만 다운로드 버튼을 누른 뒤 아무 피드백이 없으면 사용자는 앱이 멈췄다고 느끼기 쉽습니다. 특히 고해상도 텍스처, 음성, 컷신처럼 수백 MB 단위의 콘텐츠에서는 다음 정보를 UI에 노출하는 편이 좋습니다.
- 다운로드가 필요한지 여부
- 전체 다운로드 용량
- 현재 진행률과 받은 바이트 수
- 네트워크 오류 또는 다운로드 실패 여부
- 다운로드 완료 후 다음 로딩 단계

Addressables 원격 에셋 다운로드는 어떻게 동작할까?
Addressables는 카탈로그를 기준으로 키, 라벨, 주소에 연결된 의존성을 찾습니다. DownloadDependenciesAsync는 대상 에셋 자체뿐 아니라 로드에 필요한 의존 AssetBundle도 함께 다운로드합니다. 이미 캐시에 있는 번들은 다시 받지 않으므로 같은 라벨을 여러 번 요청해도 실제 전송량은 달라질 수 있습니다.
flowchart TD
A[콘텐츠 라벨 또는 주소 선택] --> B[GetDownloadSizeAsync]
B --> C{다운로드 크기 > 0?}
C -- 아니오 --> G[에셋 로드]
C -- 예 --> D[DownloadDependenciesAsync]
D --> E[GetDownloadStatus로 UI 갱신]
E --> F{다운로드 완료?}
F -- 아니오 --> E
F -- 예 --> G
| API | 역할 | 주의점 |
|---|---|---|
Addressables.GetDownloadSizeAsync(key) | 캐시되지 않은 의존성의 예상 다운로드 바이트를 반환 | 크기 확인 핸들도 완료 후 해제한다. |
Addressables.DownloadDependenciesAsync(key, false) | 키 또는 라벨에 연결된 의존 번들을 다운로드 | false는 완료 후 핸들을 직접 해제하겠다는 의미다. |
handle.GetDownloadStatus() | 현재 받은 바이트와 전체 바이트를 반환 | Percent만 쓰기보다 바이트 값도 함께 표시하는 편이 좋다. |
Addressables.LoadAssetAsync<T>(key) | 다운로드 또는 캐시된 번들에서 실제 에셋을 로드 | 사용 종료 시 로드 핸들을 해제한다. |
비동기 다운로드와 프로그레스 바를 어떻게 구현할까?
다음 예제는 Unity UGUI의 Slider와 TextMeshPro의 TMP_Text를 사용합니다. contentLabel에는 Addressables Groups에서 지정한 라벨을 입력합니다.
1. 다운로드 필요 용량을 확인한다
GetDownloadSizeAsync의 결과가 0이면 필요한 번들이 모두 캐시에 있거나 로컬 번들입니다. 이 경우 다운로드 UI를 잠깐 보여 주기보다 바로 다음 로딩 단계로 이동하는 편이 자연스럽습니다.
2. 다운로드 중 바이트 기준으로 UI를 갱신한다
진행률은 다음처럼 계산할 수 있습니다. TotalBytes가 0인 초기 구간에서는 0으로 처리해 0으로 나누는 문제를 막습니다.
3. 완료·실패 여부를 확인하고 핸들을 해제한다
AsyncOperationStatus.Succeeded를 확인하기 전에는 실제 콘텐츠 로드를 시작하지 않습니다. 실패한 핸들도 반드시 해제합니다.
using System;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.UI;
public sealed class AddressablesDownloadPanel : MonoBehaviour
{
[SerializeField] private string contentLabel = "chapter-02";
[SerializeField] private Slider progressBar;
[SerializeField] private TMP_Text progressText;
[SerializeField] private TMP_Text statusText;
public void StartDownload()
{
StartCoroutine(DownloadContent(contentLabel));
}
private IEnumerator DownloadContent(object key)
{
progressBar.value = 0f;
statusText.text = "다운로드 용량 확인 중";
AsyncOperationHandle<long> sizeHandle = Addressables.GetDownloadSizeAsync(key);
yield return sizeHandle;
if (sizeHandle.Status != AsyncOperationStatus.Succeeded)
{
ShowError("다운로드 용량을 확인하지 못했습니다.");
Addressables.Release(sizeHandle);
yield break;
}
long downloadBytes = sizeHandle.Result;
Addressables.Release(sizeHandle);
if (downloadBytes == 0)
{
progressBar.value = 1f;
progressText.text = "이미 다운로드됨";
statusText.text = "콘텐츠를 준비합니다.";
yield break;
}
statusText.text = "콘텐츠 다운로드 중";
AsyncOperationHandle downloadHandle = Addressables.DownloadDependenciesAsync(key, false);
while (!downloadHandle.IsDone)
{
var status = downloadHandle.GetDownloadStatus();
float progress = status.TotalBytes > 0
? (float)status.DownloadedBytes / status.TotalBytes
: 0f;
progressBar.value = progress;
progressText.text = $"{FormatBytes(status.DownloadedBytes)} / {FormatBytes(status.TotalBytes)} ({progress:P0})";
yield return null;
}
if (downloadHandle.Status == AsyncOperationStatus.Succeeded)
{
progressBar.value = 1f;
progressText.text = "다운로드 완료";
statusText.text = "콘텐츠를 준비합니다.";
}
else
{
ShowError($"다운로드에 실패했습니다.\n{downloadHandle.OperationException?.Message}");
}
Addressables.Release(downloadHandle);
}
private void ShowError(string message)
{
progressBar.value = 0f;
statusText.text = message;
}
private static string FormatBytes(long bytes)
{
const float megabyte = 1024f * 1024f;
return $"{bytes / megabyte:F1} MB";
}
}
PercentComplete 대신 GetDownloadStatus()를 써야 하는 이유는?
AsyncOperationHandle.PercentComplete은 Addressables 작업 전체의 일반적인 진행 상태를 나타냅니다. 반면 다운로드 화면에서 사용자가 기대하는 값은 보통 네트워크로 전송된 데이터의 비율입니다. 따라서 원격 콘텐츠 다운로드 UI에는 GetDownloadStatus()가 더 적합합니다.
| 값 | 적합한 용도 | 한계 |
|---|---|---|
PercentComplete | 다운로드, 초기화, 의존성 확인을 포함한 작업 흐름 표시 | 실제 네트워크 전송량과 체감 진행률이 다를 수 있다. |
DownloadStatus.Percent | 간단한 다운로드 비율 표시 | 총 바이트가 아직 확정되지 않은 구간을 고려해야 한다. |
DownloadedBytes / TotalBytes | 받은 용량과 남은 용량까지 표시하는 UI | TotalBytes == 0 방어 코드가 필요하다. |
다운로드 완료 뒤에도 LoadAssetAsync가 필요한가?
필요합니다. DownloadDependenciesAsync는 의존 번들을 로컬 캐시에 준비하는 작업이며 게임 오브젝트나 Sprite, AudioClip 같은 런타임 에셋을 메모리에 로드하지는 않습니다. 다운로드가 끝난 뒤 필요한 순간에 LoadAssetAsync<T>를 호출합니다.
AsyncOperationHandle<GameObject> prefabHandle =
Addressables.LoadAssetAsync<GameObject>("Chapter02BossPrefab");
yield return prefabHandle;
if (prefabHandle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(prefabHandle.Result);
}
Addressables.Release(prefabHandle);
단, Instantiate한 오브젝트를 Addressables 방식으로 관리해야 한다면 Addressables.InstantiateAsync와 Addressables.ReleaseInstance를 사용합니다. 일반 Instantiate와 ReleaseInstance를 섞으면 수명 관리가 복잡해질 수 있습니다.
실무에서 놓치기 쉬운 Addressables 다운로드 처리
다운로드 화면은 취소 가능하게 설계한다
Addressables 작업을 중간에 취소해야 한다면 화면만 닫지 말고 해당 작업의 핸들 수명도 설계해야 합니다. 이미 시작한 네트워크 요청의 즉시 중단 여부와 캐시에 남은 데이터는 플랫폼 및 내부 상태에 따라 다를 수 있으므로 최소한 씬 전환이나 UI 종료 시 중복 다운로드 코루틴이 남지 않도록 막아야 합니다.
라벨은 콘텐츠 단위로 구성한다
chapter-02, voice-ko, hd-texture처럼 사용자 선택이나 진입 시점에 대응하는 라벨을 구성하면 다운로드 크기 확인과 사전 다운로드 기능을 구현하기 쉽습니다. 반대로 하나의 거대한 공용 라벨에 모든 원격 에셋을 넣으면 필요한 범위를 좁히기 어렵습니다.
실패 메시지와 재시도 버튼을 분리한다
네트워크 단절, 원격 경로 오류, 카탈로그와 번들 버전 불일치는 모두 실패로 보이지만 대응 방법이 다릅니다. 사용자에게는 간단한 실패 문구와 재시도 버튼을 제공하고 개발 빌드에서는 OperationException을 로그에 남겨 원인을 추적합니다.
자주 묻는 질문 (FAQ)
Addressables 다운로드 크기가 0이면 오류인가요?
아닙니다. 필요한 번들이 이미 캐시에 있거나 해당 콘텐츠가 로컬 번들일 때 0이 반환될 수 있습니다. 바로 에셋 로드 단계로 진행하면 됩니다.
autoReleaseHandle을 true로 설정해도 되나요?
가능하지만 완료 후 상태나 예외를 UI에 반영하고 수명 관리를 명확히 하려면 예제처럼 false로 두고 완료 시 직접 Addressables.Release하는 방식이 이해하기 쉽습니다.
프로그레스 바가 잠시 멈춘 것처럼 보이는 이유는 무엇인가요?
의존성 계산, 캐시 검사, 디스크 쓰기, 번들 초기화 구간은 실제 네트워크 바이트 증가가 적거나 없을 수 있습니다. 상태 문구를 다운로드 준비 중, 캐시 확인 중, 콘텐츠 준비 중처럼 분리하면 사용자가 상황을 이해하기 쉽습니다.
정리
Addressables 다운로드 UI의 핵심은 다운로드 전 GetDownloadSizeAsync로 불필요한 대기 화면을 피하고 다운로드 중에는 GetDownloadStatus()의 바이트 정보를 사용해 실제 체감에 가까운 진행률을 보여 주는 것입니다. 작업이 끝난 뒤에는 성공 여부를 확인하고 핸들을 해제한 다음, 별도의 LoadAssetAsync로 필요한 에셋만 메모리에 올리면 안정적인 원격 콘텐츠 흐름을 만들 수 있습니다.



