[2025_10_31]Unity로딩(코루틴과 async/await방식)
2025. 10. 31. 21:08ㆍTIL
현재 코드 분석
현재는 Coroutine 방식을 사용하고 있습니다:
csharp
private IEnumerator LoadSceneAsyncCoroutine(string _sceneName)
{
// Coroutine 기반 비동기 처리
yield return new WaitForSeconds(0.5F);
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(_sceneName);
while (!asyncLoad.isDone)
{
yield return null;
}
}
Coroutine vs async/await 비교
Coroutine의 특징
- Unity의 전통적인 비동기 처리 방식
- Unity 생명주기와 잘 통합됨
- 중첩 시 콜백 지옥 가능
- 예외 처리가 복잡함
- 반환값 받기 어려움
async/await의 특징
- 현대적이고 직관적인 문법
- 예외 처리가 간단 (try-catch)
- 반환값 처리 용이
- 코드 가독성 향상
- Unity 2023+ 에서 권장
async/await로 변환하기
1. 기본 변환
csharp
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneController : MonoBehaviour
{
// Coroutine 방식 (기존)
public void LoadSceneAsync(string _sceneName)
{
StartCoroutine(LoadSceneAsyncCoroutine(_sceneName));
}
// async/await 방식 (개선)
public async void LoadSceneAsync(string _sceneName)
{
await LoadSceneAsyncTask(_sceneName);
}
}
2. 전체 구현 코드
csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneController : MonoBehaviour
{
public static int CurrentLoadingStage { get; private set; }
// Cancellation을 위한 토큰
private CancellationTokenSource _cts;
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// 진행 중인 비동기 작업 취소
_cts?.Cancel();
_cts?.Dispose();
}
private void OnSceneLoaded(Scene _scene, LoadSceneMode _mode)
{
CurrentLoadingStage = -1;
if (_scene.name == "StageSelect")
{
ManagerRoot.UIManager.ShowPanel<StageSelectUI>();
}
else if (_scene.name == "Main_Title")
{
ManagerRoot.UIManager.ShowPanel<MainTitleUI>();
}
else if (_scene.name == "Intro")
{
ManagerRoot.UIManager.ShowPanel<IntroUI>();
}
else if (_scene.name.StartsWith("Stage "))
{
ManagerRoot.UIManager.ShowPanel<StageOptionUI>();
ManagerRoot.GameManager.currentStageStars = 0;
ManagerRoot.GameManager.IsDie = false;
}
}
#region 동기/비동기 로딩
// 동기 방식 씬 로딩 (즉시 전환)
public void LoadScene(string _sceneName)
{
SceneManager.LoadScene(_sceneName);
}
// 비동기 방식 씬 로딩 (로딩 UI 표시)
public async void LoadSceneAsync(string _sceneName)
{
// 이전 작업이 있다면 취소
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
try
{
await LoadSceneAsyncTask(_sceneName, _cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("씬 로딩이 취소되었습니다.");
}
catch (Exception ex)
{
Debug.LogError($"씬 로딩 중 오류 발생: {ex.Message}");
ManagerRoot.UIManager.ClosePanel<LoadingUI>();
}
}
// 실제 비동기 로딩 작업을 수행하는 Task 메서드
private async Task LoadSceneAsyncTask(string _sceneName, CancellationToken cancellationToken = default)
{
// 1. 로딩 UI 표시
ManagerRoot.UIManager.ShowPanel<LoadingUI>();
LoadingUI loadingUI = ManagerRoot.UIManager.GetPanel<LoadingUI>();
// 2. 최소 로딩 시간 (UX 개선용)
await Task.Delay(TimeSpan.FromSeconds(0.5f), cancellationToken);
// 3. 씬 비동기 로딩 시작
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(_sceneName);
if (asyncLoad == null)
{
Debug.LogError($"씬을 찾을 수 없습니다: {_sceneName}");
ManagerRoot.UIManager.ClosePanel<LoadingUI>();
return;
}
// 자동 전환 방지 (로딩 완료 후 수동으로 전환)
asyncLoad.allowSceneActivation = false;
// 4. 로딩 진행률 업데이트
while (!asyncLoad.isDone)
{
// 취소 요청 확인
cancellationToken.ThrowIfCancellationRequested();
// 진행률 계산 (0.9까지만 올라가므로 정규화)
float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f);
// UI 업데이트
if (loadingUI != null)
{
loadingUI.UpdateProgress(progress);
}
// 로딩이 90% 완료되면 씬 활성화
if (asyncLoad.progress >= 0.9f)
{
// 추가 대기 시간 (선택사항)
await Task.Delay(TimeSpan.FromSeconds(0.3f), cancellationToken);
asyncLoad.allowSceneActivation = true;
}
// 다음 프레임까지 대기
await Task.Yield();
}
// 5. 로딩 UI 닫기
ManagerRoot.UIManager.ClosePanel<LoadingUI>();
}
#endregion
#region 씬 이동 메서드
public void LoadStageSelectScene()
{
LoadScene("StageSelect");
}
public void RestartScene()
{
Scene curScene = SceneManager.GetActiveScene();
LoadScene(curScene.name);
}
public void LoadStageScene(int _stageNumber)
{
CurrentLoadingStage = _stageNumber;
string sceneName = $"Stage {_stageNumber:D2}";
LoadSceneAsync(sceneName);
}
public void LoadMainScene()
{
LoadScene("Main_Title");
}
#endregion
}
핵심 개념 상세 설명
1. async/await 키워드
csharp
// async: 이 메서드가 비동기 메서드임을 선언
public async void LoadSceneAsync(string _sceneName)
{
// await: 비동기 작업이 완료될 때까지 기다림 (메인 스레드는 블로킹되지 않음)
await LoadSceneAsyncTask(_sceneName);
}
async의 역할
- 메서드 내에서 await 키워드 사용 가능하게 만듦
- 메서드를 **상태 머신(State Machine)**으로 변환
- 반환 타입: void, Task, Task<T>
await의 역할
- 비동기 작업이 완료될 때까지 기다림
- 하지만 메인 스레드를 막지 않음
- await 지점에서 제어권을 Unity 엔진에 반환
- 작업 완료 후 다음 줄부터 실행 재개
2. Task vs void 반환
csharp
// void 반환 - 예외 처리 불가, 완료 대기 불가
public async void LoadSceneAsync(string sceneName)
{
await LoadSceneAsyncTask(sceneName);
}
// Task 반환 - 예외 처리 가능, 완료 대기 가능
public async Task LoadSceneAsyncTask(string sceneName)
{
// ...
}
void를 사용하는 경우
- Unity 이벤트 핸들러 (Button onClick 등)
- 최상위 진입점
- Fire-and-forget 상황
- Fire-and-forget->지시를 내려놓고 끝낫는지 확인 안함
Task를 사용하는 경우
- 다른 async 메서드에서 await할 수 있어야 할 때
- 예외를 상위로 전파해야 할 때
- 반환값이 필요한 경우 Task<T> 사용
3. CancellationToken - 작업 취소
csharp
private CancellationTokenSource _cts;
public async void LoadSceneAsync(string _sceneName)
{
// 이전 작업 취소
_cts?.Cancel();
_cts?.Dispose();
// 새 토큰 생성
_cts = new CancellationTokenSource();
try
{
await LoadSceneAsyncTask(_sceneName, _cts.Token);
}
catch (OperationCanceledException)
{
// 취소 처리
}
}
private async Task LoadSceneAsyncTask(string sceneName, CancellationToken token)
{
// 작업 중 취소 확인
token.ThrowIfCancellationRequested();
await Task.Delay(500, token); // 취소 가능한 대기
}
CancellationToken의 필요성
- 씬 전환 중 다른 씬으로 이동하는 경우
- 오브젝트가 파괴될 때 진행 중인 작업 정리
- 사용자가 로딩 취소 버튼을 누른 경우
4. Task.Yield() - 프레임 대기
csharp
while (!asyncLoad.isDone)
{
// UI 업데이트 등의 작업
loadingUI.UpdateProgress(progress);
// 다음 프레임까지 대기 (Unity 방식)
await Task.Yield();
// 이렇게 하면 안됨 - CPU 과다 사용
// while (!asyncLoad.isDone) { }
}
Task.Yield()의 역할
- Coroutine의 yield return null과 유사
- Unity 메인 스레드에 제어권 반환
- 다음 프레임에 실행 재개
- UI 업데이트, 물리 계산 등이 정상 동작
5. Task.Delay() - 시간 대기
csharp
// async/await 방식
await Task.Delay(TimeSpan.FromSeconds(0.5f), cancellationToken);
// 기존 Coroutine 방식
yield return new WaitForSeconds(0.5f);
Task.Delay의 특징
- 지정된 시간만큼 대기
- CancellationToken과 함께 사용 가능
- TimeSpan 또는 밀리초(int) 지정 가능
실행 흐름 비교
Coroutine 방식
```
LoadSceneAsync() 호출
↓
StartCoroutine() → Unity가 Coroutine 스케줄링
↓
yield return null → 다음 프레임
↓
yield return null → 다음 프레임
↓
반복
async/await 방식
LoadSceneAsync() 호출
↓
await LoadSceneAsyncTask() → Task 시작
↓
await Task.Yield() → 제어권 반환
↓
(Unity가 다른 작업 수행)
↓
다음 프레임에 실행 재개
↓
반복
6. async/await의 장점
1. 예외 처리가 명확함
csharp
// async/await - try-catch로 깔끔하게
public async void LoadSceneAsync(string sceneName)
{
try
{
await LoadSceneAsyncTask(sceneName);
}
catch (OperationCanceledException)
{
Debug.Log("취소됨");
}
catch (Exception ex)
{
Debug.LogError($"오류: {ex.Message}");
// 로딩 UI 정리 등
}
}
// Coroutine - 예외 처리 복잡
private IEnumerator LoadSceneCoroutine(string sceneName)
{
// try-catch가 제대로 작동하지 않을 수 있음
// yield 키워드와 예외 처리가 복잡하게 얽힘
}
2. 순차 작업이 직관적
csharp
// async/await - 마치 동기 코드처럼 읽힘
public async Task LoadWithFadeAsync(string sceneName)
{
await FadeOutAsync(); // 1. 화면 어둡게
await LoadSceneAsyncTask(sceneName); // 2. 씬 로딩
await FadeInAsync(); // 3. 화면 밝게
}
// Coroutine - 중첩되면 복잡해짐
private IEnumerator LoadWithFadeCoroutine(string sceneName)
{
yield return StartCoroutine(FadeOutCoroutine());
yield return StartCoroutine(LoadSceneCoroutine(sceneName));
yield return StartCoroutine(FadeInCoroutine());
}
3. 반환값 처리 용이
csharp
// async/await
public async Task<bool> LoadSceneWithValidation(string sceneName)
{
bool isValid = await ValidateSceneAsync(sceneName);
if (!isValid) return false;
await LoadSceneAsyncTask(sceneName);
return true;
}
// 사용
if (await LoadSceneWithValidation("Stage01"))
{
Debug.Log("로딩 성공");
}
7.주의사항
1. Unity 생명주기와의 조화
csharp
private void OnDisable()
{
// 필수: 진행 중인 비동기 작업 취소
_cts?.Cancel();
_cts?.Dispose();
}
2. async void는 최소화
csharp
// 외부 진입점만 async void
public async void OnButtonClick() // Unity 이벤트용
{
await LoadSceneAsyncTask("Stage01");
}
// 나머지는 Task 반환
private async Task LoadSceneAsyncTask(string sceneName)
{
// ...
}
3. ConfigureAwait(false) 불필요
csharp
// Unity에서는 항상 메인 스레드로 돌아와야 하므로
// ConfigureAwait(false)를 사용하지 말 것!
// X
await Task.Delay(1000).ConfigureAwait(false);
// O
await Task.Delay(1000);
성능 비교
항목Coroutine async/await
| 메모리 할로케이션 | 적음 | 약간 더 많음 (상태 머신) |
| 가독성 | 보통 | 우수 |
| 예외 처리 | 복잡 | 간단 |
| 취소 처리 | 수동 구현 | CancellationToken |
| Unity 통합 | 완벽 | 양호 (2023+) |
async/await는 현대적이고 유지보수하기 쉬운 비동기 코드를 작성할 수 있게 해줌. async/await 사용을 권장, 특히 복잡한 비동기 로직, 예외 처리, 작업 취소가 필요한 경우 큰 장점
'TIL' 카테고리의 다른 글
| [2025_11_04]프로젝트 구현 마무리 및 회상 (0) | 2025.11.04 |
|---|---|
| [2025_11_03]EventBus (0) | 2025.11.03 |
| [2025_10_30]Unity UI관리 기법 (0) | 2025.10.30 |
| [2025_10_29]오늘 배운것.. 회의... 새프로젝트 기획 (0) | 2025.10.29 |
| [2025_10_28]메타버스_미니게임 마무리 (1) | 2025.10.28 |