포스트

UniTask에 대해서

UniTask는 Unity에서 비동기 작업을 보다 효율적으로 처리하기 위해 설계된 라이브러리. Unity는 기본적으로 Coroutine을 통해 비동기 작업을 처리하는데, UniTask는 이보다 더 직관적이고 효율적인 방법을 제공한다. C#의 async/await 패턴을 Unity 환경에서 사용할 수 있도록 해주는 것이 특징.


필요성

비동기 프로그래밍을 지원하기 위한 기존의 코루틴과 비교해보자

기존의 Coroutine

  • 서버로부터 받은 값을 IEnumerator Function()에서 리턴할 수 없다.
  • StartCoroutine 자체는 큰 GC 할당을 발생시키지 않지만, yield return new에서 발생하는 할당이 주요 원인이다. yield return new SomeYieldInstruction()에 사용되는 new로 인한 할당이 지속적으로 발생한다.

UniTask의 장점

  • Struct 기반으로 제작되어 있어서 Zero Allocation이 특징이다.
  • 서버로부터 받은 값을 Return할 수 있다.
  • UniTask.Yield, UniTask.Delay, UniTask.DelayFrame처럼 Coroutine과 유사한 기능을 제공한다.
  • 메모리 누수를 방지하기 위한 TaskTracker 창 지원
  • 그 외 기타 등등

설치 방법

UniTask Github

위 GitHub Repository를 통해 패키지를 다운로드할 수 있다.

사용 방법

동일한 기능에 대해 코루틴으로 구현한 버전과, UniTask로 구현한 버전을 비교해보자.

1초를 기다리는 예제

  • 코루틴으로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    
      IEnumerator Wait1Second()
      {
          yield return new WaitForSeconds(1f);
            
          Debug.Log("1초가 지남");
      }
        
      // 실행 : StartCoroutine(Wait1Second());
    
  • UniTask로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    
      private async UniTaskVoid Wait1SecondAsync()
      {
          await UniTask.Delay(TimeSpan.FromSeconds(1));
            
          Debug.Log("1초가 지남");
      }
        
      // 실행 : Wait1SecondAsync().Forget();
    

TimeScale과 무관하게 1초를 기다리는 예제

  • 코루틴으로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    
      IEnumerator Wait1Second()
      {
          yield return new WaitForSecondsRealtime(1f);
            
          Debug.Log("1초가 지남");
      }
        
      // 실행 : StartCoroutine(Wait1Second());
    
  • UniTask로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    
      private async UniTaskVoid Wait1SecondAsync()
      {
          await UniTask.Delay(TimeSpan.FromSeconds(1), DelayType.UnscaledDeltaTime);
            
          Debug.Log("1초가 지남");
      }
        
      // 실행 : Wait1SecondAsync().Forget();          
    

특정 조건이 만족되었을 때 실행하는 예제

  • 코루틴으로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      public int count = 0;   
                       
      // 코루틴으로 구현한 버전
      IEnumerator Wait3Count()
      {
          yield return new WaitUntil(() => count == 3);
          Debug.Log("카운트가 3이 되었다.");
      }
        
      // 실행 : StartCoroutine(Wait3Count());
    
  • UniTask로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    
      // UniTask로 구현한 버전
      private async UniTaskVoid Wait3CountAsync()
      {
          await UniTask.WaitUntil(() => count == 3);
          Debug.Log("카운트가 3이 되었다.");
      }
        
      // 실행 : Wait3CountAsync().Forget();          
    

웹에 있는 이미지를 가져오는 예제

  • 코루틴으로 구현한 버전

    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
    
      private const string imagePath = "https://github.com/user-attachments/assets/00793767-5c22-41da-a7df-baa913f56548";
      public RawImage profileImage;
        
      IEnumerator WaitGetWebTexture(UnityAction<Texture2D> action)
      {
          // 웹으로부터 받아온 텍스쳐를 반환
          UnityWebRequest request = UnityWebRequestTexture.GetTexture(imagePath);
          yield return request.SendWebRequest();
        
          // 요청 결과로 연결에러 혹은 프로토콜에러가 나면 
          if (request.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError)
          {
              // 에러처리
              Debug.LogError(request.error);
          }
          // 정상적인 경우에는
          else
          {
              Texture2D texture = ((DownloadHandlerTexture)request.downloadHandler).texture;
              action.Invoke(texture);
          }
      }
        
      // 실행
      // StartCoroutine(WaitGetWebTexture(texture =>
      // {
      //     profileImage.texture = texture;
      // }));
    
  • UniTask로 구현한 버전

    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
    31
    
      private const string imagePath = "https://github.com/user-attachments/assets/00793767-5c22-41da-a7df-baa913f56548";
      public RawImage profileImage;
        
      async UniTask<Texture2D> WaitGetWebTextureAsync()
      {
          // 웹으로부터 받아온 텍스쳐를 반환
          UnityWebRequest request = UnityWebRequestTexture.GetTexture(imagePath);
          await request.SendWebRequest();
            
          // 요청 결과로 연결에러 혹은 프로토콜에러가 나면 
          if (request.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError)
          {
              // 에러처리
              Debug.LogError(request.error);
          }
          // 정상적인 경우에는
          else
          {
              Texture2D texture = ((DownloadHandlerTexture)request.downloadHandler).texture;
              return texture;
          }
          return null;
      }
        
      async UniTaskVoid GetImageAsync()
      {
          Texture2D texture2D = await WaitGetWebTextureAsync();
          profileImage.texture = texture2D;
      }
        
      // 실행 : GetImageAsync().Forget();        
    
  • 결과 (둘 모두 동일)

    image (1)

종료/취소 처리

  • 코루틴으로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      private Coroutine _coroutine;       // 코루틴 캐싱 필요
        
      private void Start()
      {
          _coroutine = StartCoroutine(Wait3Second());
      }
        
      IEnumerator Wait3Second()
      {
          yield return new WaitForSeconds(3);
          Debug.Log("3초가 지났습니다.");
      }
        
      private void Update()
      {
          // 스페이스 바를 누르면 코루틴을 멈추도록 한다.
          if (Input.GetKeyDown(KeyCode.Space))
          {
              StopCoroutine(_coroutine);
          }
      }
    
  • UniTask로 구현한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      private CancellationTokenSource source = new CancellationTokenSource();
        
      async UniTaskVoid Wait3Seconds()
      {
          await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: source.Token);
            
          Debug.Log("3초가 지났습니다.");
      }
        
      private void Start()
      {
          Wait3Seconds().Forget();
      }
        
      private void Update()
      {
          if(Input.GetKeyDown(KeyCode.Space))
          {
              source.Cancel();
          }
      }       
    

    추가적으로 Cancel과 Dispose 처리까지 한 버전

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
      private CancellationTokenSource source = new CancellationTokenSource();
        
      async UniTaskVoid Wait3Seconds()
      {
          await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: source.Token);
            
          Debug.Log("3초가 지났습니다.");
      }
        
      private void Start()
      {
          Wait3Seconds().Forget();
      }
        
      private void Update()
      {
          if(Input.GetKeyDown(KeyCode.Space))
          {
              source.Cancel();
          }
      }
        
      private void OnDestroy()
      {
          source.Cancel();
          source.Dispose();
      }
        
      private void OnEnable()
      {
          if(source != null) 
              source.Dispose();
        
          source = new CancellationTokenSource();
      }
        
      private void OnDisable()
      {
          source.Dispose();
      }
    

    이를 간단하게 한 버전

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
      private void Start()
      {
          Wait3Seconds().Forget();
      }
        
      async UniTaskVoid Wait3Seconds()
      {
          await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: this.GetCancellationTokenOnDestroy());
            
          Debug.Log("3초가 지났습니다.");
      }
        
      private void OnEnable()
      {
          if(source != null) 
              source.Dispose();
        
          source = new CancellationTokenSource();
      }
        
      private void OnDisable()
      {
          source.Dispose();
      }
    

    이 패턴에서는 source.Dispose()를 사용하여 CancellationTokenSource를 재활용하기 위한 처리가 필요할 수 있지만, 일반적으로는 새로운 CancellationTokenSource를 생성할 때 이전 소스를 Dispose()하는 방식으로 충분하다.

실행중인 Task 추적 - TaskTracker

image (20)

위와 같은 경로로 UniTaskTracker window를 열 수 있다.

image (21)

UniTask Tracker를 연 화면

간단한 UniTask를 하나 동작시킨 후, UniTask Tracker window를 통해 현재 Scene에서 동작중인 UniTask를 실시간으로 확인할 수 있다.

이렇게 UniTask Tracker 창은 주로 디버깅 도구로 사용되며, 메모리 누수의 방지를 위해 사용할 수 있다. 현재 Scene에서 동작 중인 UniTask를 실시간으로 추적할 수 있어 디버깅에 유용하다.

DOTween과 호환

UniTask는 공식적으로 DOTween과의 호환을 지원한다. DOTween의 Tween 객체를 UniTask로 변환하여 비동기적으로 사용할 수 있도록한다. 이를 간단히 사용해보자.

Setup

DOTween과 함께 사용해볼려면, 우선 DOTween을 설치한 후 별도의 셋업과정이 필요하다.

간단하다, Project Settings > Player > 대상 플랫폼의 Other Settings > Script Compilation에 다음과 같이 추가해주자.

스크린샷 2024-08-30 130456

UNITASK_DOTWEEN_SUPPORT 이라는 Define 심볼을 추가해주면 된다.

간단한 사용 예제

1
2
3
4
5
6
7
8
9
10
11
12
[SerializeField] private Transform targetPosition;

public void MoveAsync()
{
    WaitMove().Forget();
}

async UniTaskVoid WaitMove()
{
    await transform.DOMove(targetPosition.position, 2);
    Debug.Log("Target Position 위치로 이동완료");
}

결과는 다음과 같다.

2024-08-30131231-ezgif com-video-to-gif-converter (1)

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.