[2025_10_31]Unity로딩(코루틴과 async/await방식)

2025. 10. 31. 21:08TIL

현재 코드 분석

현재는 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의 필요성

  1. 씬 전환 중 다른 씬으로 이동하는 경우
  2. 오브젝트가 파괴될 때 진행 중인 작업 정리
  3. 사용자가 로딩 취소 버튼을 누른 경우

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 사용을 권장, 특히 복잡한 비동기 로직, 예외 처리, 작업 취소가 필요한 경우 큰 장점