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 창 지원
- 그 외 기타 등등
설치 방법
위 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();
결과 (둘 모두 동일)
종료/취소 처리
코루틴으로 구현한 버전
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
위와 같은 경로로 UniTaskTracker window를 열 수 있다.
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에 다음과 같이 추가해주자.
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 위치로 이동완료");
}
결과는 다음과 같다.