본문으로 건너뛰기
HY Devlog
뒤로 가기

await Awaitable은 변수에 담을 값이 없다

비동기를 어떻게든 쓰고 싶어서 async/await를 붙여봤다가, Unity가 권하는 형태가 따로 있다는 걸 알게 되어 찾던 중 스크랩한 글이다. Task로 먼저 짜놓고 돌리던 참이었는데 그 자리에 Awaitable이 있었다. 2025년 글이고 이 타입을 짧게 소개한다.

Task에서 넘어오는 쪽에 중요한 차이는 전에 한 번 다뤘다. Unity 코드 최적화 문서를 정리한 글에서 Awaitable이 Task와 다른 세 군데 — 풀링, 재사용 금지, 동기적 연속 실행 — 을 문서 문장으로 짚었다. 이 클리핑은 그 위에서 쓰는 쪽을 보여준다. 제약, 백그라운드 전환, 취소, 코루틴과의 비교까지 네 절이다.

지적이 대체로 정확하다. 특히 WhenAll·WhenAny가 없다는 불만은 지금도 유효하고, 뒤에서 현행 멤버 목록으로 확인했다.

걸리는 건 제약을 보여주는 그 예제다.

var awaitable = await Awaitable.EndOfFrameAsync();
await awaitable;
await awaitable;        // 안됨

글은 이 코드를 두고 “비록 컴파일 에러나 크래시가 나지는 않지만”이라고 적는다. 첫 줄이 컴파일되지 않는다.

목차

목차

await Awaitable은 변수에 담을 값이 없다

Unity가 타입을 둘로 나눠둔 게 이유다. 결과가 없는 쪽과 있는 쪽이다.

타입await한 결과
Awaitable없다
Awaitable<T>T

Awaitable.EndOfFrameAsync()가 돌려주는 것은 **제네릭이 아닌 Awaitable**이다. 그래서 await Awaitable.EndOfFrameAsync()는 값을 내놓지 않는다. 그걸 var에 대입하면 컴파일러가 막는다.

Cannot assign ‘expression’ to an implicitly-typed variable.

CS0815다. var는 오른쪽에서 타입을 받아와야 하는데 받아올 타입이 없다.

의도한 코드는 await가 없는 쪽이다.

// Awaitable 인스턴스를 변수에 담는다 — await를 붙이지 않는다.
Awaitable awaitable = Awaitable.EndOfFrameAsync();

await awaitable;
await awaitable;        // 여기가 '안 되는' 자리

이렇게 쓰면 글이 말하려던 상황이 실제로 만들어진다. 한 인스턴스를 두 번 기다리는 것이고, 그게 문서가 금지하는 동작이다 — 앞 글에서 인용한 “하나의 Awaitable 인스턴스를 두 번 이상 await하는 것은 결코 안전하지 않다”가 그 문장이다.

고친 코드가 중요한 이유는 진단이 바뀌기 때문이다. 원문대로 적으면 컴파일이 멈추니 런타임 동작을 볼 기회조차 없다. 글이 설명한 “두 번째 await이 즉시 반환된다”는 증상은 await를 뗀 형태에서만 나타난다.

GetAwaiter는 “못 하는” 게 아니라 문서에 없다

같은 절의 두 번째 제약이다.

awaitable의 GetAwaiter()를 하면 안됨

그리고 본문에서는 이렇게 쓴다.

GetAwaiter를 직접 호출할 수 없기 때문에 직접 WhenAll에 해당하는 확장 메서드를 만들기도 곤란한 문제점이 있다

“할 수 없다”와 “하면 안 된다”가 섞여 있다. 글 자신의 예제가 그 메서드를 호출하고 있으니 호출은 된다. await가 동작하려면 그 메서드가 있어야 하기도 하다.

현행 문서의 멤버 목록을 보면 사정이 보인다. Awaitable에 적혀 있는 것은 프로퍼티 IsCompleted, 메서드 Cancel, 그리고 정적 메서드 일곱 개다. GetAwaiter는 목록에 없다. 즉 공개되어 있지만 문서화된 표면이 아니다.

그래서 정확한 서술은 이렇게 된다. 호출은 되고, 결과가 보장되지 않는다. 그 이유는 글이 “정확히 확인할 수는 없지만 유사한 원인”이라고 추측한 그대로이고, 문서에 한 줄로 적혀 있다.

instances of the Awaitable class are pooled and therefore not safe to await multiple times in the same method.

풀링이다. 끝난 인스턴스는 풀로 돌아가고 다른 호출이 그걸 다시 꺼낸다. 내가 들고 있던 참조가 가리키는 대상이 더 이상 내 작업이 아니므로, 두 번째 await도 GetAwaiter().OnCompleted()도 기준이 사라진다. 추측이 아니라 문서에 적힌 설계다.

WhenAll과 WhenAny는 6.2에도 없다

글의 불만은 이것이다.

대표적인 예를 들면 복잡한 비동기 작업을 처리할 때 자주 사용하게 되는 WhenAll, WhenAny가 지원되지 않는 것이 있는데

지금도 그렇다. 현행 문서의 정적 메서드가 일곱 개다.

정적 메서드하는 일
NextFrameAsync다음 프레임에 이어진다
EndOfFrameAsync프레임 끝에 이어진다
FixedUpdateAsyncFixedUpdate 타이밍에 이어진다
WaitForSecondsAsync지정한 초 뒤에 이어진다
MainThreadAsync메인 스레드로 돌아간다
BackgroundThreadAsync스레드풀 백그라운드 스레드로 넘어간다
FromAsyncOperation기존 AsyncOperation에서 Awaitable을 만든다

조합 메서드가 하나도 없다. 그리고 2023.1, 6.2, 6.3, 6.4 네 판본의 목록이 모두 같다. 도입 이후 정적 멤버가 늘지 않았다는 뜻이다. Awaitable.WhenAll 페이지를 직접 열어보면 404이기도 하다. 글이 2025년에 적은 불만이 그대로 유지되고 있다.

그런데 문서는 이 공백에 대한 답을 따로 적어둔다. 매뉴얼의 Awaitable 코드 예제 절에 확장 메서드 하나가 실려 있다.

using System.Threading.Tasks;
using UnityEngine;

public static class AwaitableExtensions
{
    public static async Task AsTask(this Awaitable a)
    {
        await a;
    }

    public static async Task<T> AsTask<T>(this Awaitable<T> a)
    {
        return await a;
    }
}

Awaitable을 Task로 감싸는 길이다. 이러면 Task.WhenAll과 Task.WhenAny를 그대로 쓸 수 있다.

await Task.WhenAll(LoadStageAsync().AsTask(),
                   LoadAudioAsync().AsTask());

Task finished = await Task.WhenAny(TimeoutAsync().AsTask(),
                                   LoadStageAsync().AsTask());

이게 WhenAny까지 해결하는 유일한 문서화된 경로다. 글은 “UniTask나 고전적인 코루틴을 사용하는 방식으로 만들어야 한다”로 맺는데, 외부 라이브러리 없이 Task 쪽으로 건너가는 방법이 공식 예제에 있다. 대가는 분명하다 — Task로 감싸는 순간 Awaitable의 이점인 풀링과 동기적 연속 실행을 그 지점에서 포기한다.

글이 “현재 기능은 위의 간단한 예시 코드에 있는 것들이 거의 전부”라고 한 것도 거의 맞다. 다만 그 예시 코드에 없는 게 셋 있는데, 그중 둘이 글이 아쉬워한 자리를 메운다.

문서에 있고 글에 없는 멤버 셋

IsCompleted, Cancel, FromAsyncOperation이다.

취소 모델이 셋이다

글의 취소 절은 destroyCancellationToken 하나만 다룬다. 토큰을 메서드에 넘기는 방식이고, 정확한 설명이다. 그런데 Awaitable에는 인스턴스 메서드가 따로 있다.

Cancel — Cancels the awaitable. If the awaitable is being awaited, the awaiter receives a System.OperationCanceledException.

그리고 둘의 관계를 문서가 못 박는다.

some methods returning an Awaitable also accept a CancellationToken. Both cancelation models are equivalent.

두 모델이 동등하다. 토큰을 미리 넘길 수 없는 상황 — 이미 시작된 작업을 바깥에서 끊어야 할 때 — 에는 인스턴스를 들고 있다가 Cancel()을 부르면 된다. 받는 쪽의 예외는 같은 OperationCanceledException이라 catch 코드가 달라지지 않는다.

그리고 셋째가 있는데, 에디터에서 가장 자주 물리는 자리다. 매뉴얼의 연속 실행 절이 적는다.

Unity doesn’t automatically stop code running in the background when you exit Play mode. To cancel a background operation on exiting Play mode, use Application.exitCancellationToken.

이 토큰의 설명이 적용 범위를 적는다.

Cancellation token raised on exiting Play mode (Editor) or on quitting the application (Read Only).

에디터의 플레이 중지와 빌드된 앱의 종료를 둘 다 덮는다.

글의 destroyCancellationToken은 그 오브젝트가 파괴될 때 끊어주는 토큰이고, 플레이 중지는 그것과 다른 사건이다. 씬 오브젝트에 붙지 않은 작업 — 글이 Awaitable의 장점으로 든 바로 그 “GameObject에 묶이지 않은 작업” — 은 이쪽 토큰이 없으면 에디터를 멈춘 뒤에도 계속 돈다. 장점이 곧 책임이라는 이 글의 다른 자리와 같은 모양이다.

토큰·메서드끊는 사건
destroyCancellationToken그 MonoBehaviour가 파괴될 때
Application.exitCancellationToken플레이 모드를 나갈 때, 또는 앱이 종료될 때
Awaitable.Cancel()내가 부를 때
private Awaitable _running;

private async Awaitable StartLongWaitAsync()
{
    _running = Awaitable.WaitForSecondsAsync(10f);

    try
    {
        await _running;
        Debug.Log("정상 완료");
    }
    catch (OperationCanceledException)
    {
        // 토큰으로 끊어도, Cancel()로 끊어도 여기로 온다.
        Debug.Log("취소됨");
    }
    finally
    {
        _running = null;
    }
}

public void AbortNow()
{
    // 이미 시작된 작업을 바깥에서 끊는다.
    _running?.Cancel();
}

_running?.Cancel()에 ?.를 쓴 이유는 Awaitable이 Unity 오브젝트가 아니기 때문이다. Fake Null을 다룬 글의 주의는 UnityEngine.Object에만 해당한다.

AsyncOperation에서 건너오는 길이 있다

FromAsyncOperation의 선언이다.

public static Awaitable FromAsyncOperation(AsyncOperation op,
                                           CancellationToken cancellationToken);

Creates an Awaitable from an existing AsyncOperation object.

기존 AsyncOperation을 Awaitable로 감싼다. 다만 순서를 정확히 적어두면, AsyncOperation은 감싸지 않아도 그냥 await된다. AsyncOperation 문서가 그렇게 적는다.

Await its completion with the await key word as part of Unity’s Awaitable support.

매뉴얼의 코드 예제도 그 형태다.

public async Awaitable LoadResourcesAsync()
{
    var operation = Resources.LoadAsync("my-texture");
    await operation;                      // 감싸지 않는다
    var texture = operation.asset as Texture2D;
}

그러면 FromAsyncOperation은 왜 있나. 선언을 다시 보면 두 번째 인자가 CancellationToken이다. 직접 await하는 형태에는 토큰을 끼울 자리가 없다. 취소가 필요할 때 쓰는 형태가 이쪽이다.

// 토큰이 필요하면 감싼다
await Awaitable.FromAsyncOperation(Resources.LoadAsync("my-texture"), _destroyToken);

SceneManager.LoadSceneAsync, Resources.LoadAsync, 그리고 AsyncOperation을 돌려주는 Unity API 전반이 두 형태 모두에 들어온다.

이게 글의 다른 절과 맞물린다. 글은 백그라운드 스레드 절에서 이렇게 경고한다.

Addressables등의 유니티 메서드는 거의 전부 메인 스레드에서 실행하지 않으면 오류가 발생하기 때문에 리소스 로딩 등의 목적으로 사용할 때는 주의하여야 한다

맞는 경고다. 그리고 그 자리에 쓸 멤버가 같은 클래스에 있다. 리소스 로딩을 백그라운드로 밀어내는 게 아니라, 메인 스레드에서 AsyncOperation을 시작하고 그걸 await하는 쪽이다. Addressables를 다룬 글에서 본 로딩 핸들도 같은 모양으로 기다릴 수 있다.

IsCompleted는 폴링용 프로퍼티다. await 없이 완료 여부만 보고 싶을 때 쓰는 자리이고, 글의 범위에서는 쓸 일이 없어서 빠진 것으로 보인다.

백그라운드로 넘어가면 쓸 수 없는 것들이 있다

글의 병렬 처리 예제다.

await Awaitable.BackgroundThreadAsync();
Debug.Log("작업 스레드로 이동");
Thread.Sleep(5000);

await Awaitable.MainThreadAsync();
Debug.Log("메인 스레드로 복귀");

동작하는 코드이고 설명도 맞다. 문서가 적는 BackgroundThreadAsync의 성질이다.

Resumes execution on a ThreadPool background thread. Completes immediately when called from a background thread.

그런데 글이 적지 않은 제약이 나머지 메서드 쪽에 붙어 있다. NextFrameAsync의 설명이다.

This method can only be called from the main thread and always completes on main thread.

WaitForSecondsAsync도 같다 — 메인 스레드에서만 호출할 수 있고 완료도 메인 스레드다. 두 절을 이어 읽으면 섞으면 안 되는 조합이 보인다.

위치쓸 수 있는 것쓸 수 없는 것
메인 스레드일곱 개 전부—
백그라운드 스레드MainThreadAsync, BackgroundThreadAsyncNextFrameAsync, EndOfFrameAsync, FixedUpdateAsync, WaitForSecondsAsync

프레임과 시간에 걸린 넷은 메인 스레드 전용이다. 백그라운드로 넘어간 뒤에 “1초 쉬자”고 WaitForSecondsAsync를 부르면 제약을 넘는 것이고, 거기서는 Thread.Sleep이 맞다 — 글의 예제가 실제로 그렇게 쓰고 있다. 예제는 맞고 이유가 적혀 있지 않다.

일반 규칙도 매뉴얼에 한 쌍으로 적혀 있다.

If the method is called from the main thread, it resumes on the main thread. Otherwise it resumes on a .NET ThreadPool thread.

부른 곳으로 돌아온다는 것이고, MainThreadAsync·BackgroundThreadAsync가 그 예외다. 그래서 같은 절이 비용 하나를 덧붙인다.

It’s most efficient to call await Awaitable.MainThreadAsync() from the main thread and await Awaitable.BackgroundThreadAsync() from a background thread because in each case the code resumes immediately on completion.

반대로 백그라운드에서 메인으로 돌아오는 경우는 다음 프레임 업데이트까지 재개되지 않는다. 글의 예제가 하는 MainThreadAsync()가 그 경우이고, 한 프레임이 비용으로 붙는다. 무거운 작업 뒤의 한 프레임이라 문제는 안 되지만, 스레드를 자주 왕복하는 구조라면 그만큼 쌓인다.

스레드 자체에 대한 경고도 있다.

While Unity doesn’t prevent execution of multithreaded code in these contexts, crashes and other unpredictable errors are likely if you do use multiple threads.

그리고 “스레드풀 백그라운드 스레드”라는 점도 읽어둘 값이 있다. 글이 한 Thread.Sleep(5000)은 그 풀의 스레드 하나를 5초 동안 점유한다. 글이 “외부 I/O등 초 단위로 걸리는 무거운 작업”에 적합하다고 한 범위에서는 괜찮지만, 같은 패턴을 여러 개 동시에 돌리면 풀을 말리는 쪽이 된다.

destroyCancellationToken에는 캐시 조건이 붙어 있다

글의 취소 예제는 정확하다. 토큰을 넘기고, OperationCanceledException을 catch하고, 취소되면 그 아랫줄이 실행되지 않는다는 설명까지 맞는다.

문서에 한 줄이 더 있다.

You must cache the destroyCancellationToken before you destroy the MonoBehaviour object.

파괴 전에 캐시해두라는 것이다. 이 프로퍼티는 MonoBehaviour의 멤버이므로, 파괴된 뒤에 읽으려 하면 그 접근 자체가 문제가 된다. 글의 예제는 Awake에서 호출된 메서드가 시작 시점에 한 번 읽으므로 괜찮다.

걸리는 건 await 뒤에 읽는 형태다.

// 위험 — await 뒤에 다시 읽는다. 그 사이에 파괴됐을 수 있다.
private async Awaitable LoopRiskyAsync()
{
    while (true)
    {
        await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken);
    }
}

// 안전 — 시작할 때 한 번 캐시해둔다.
private async Awaitable LoopSafeAsync()
{
    CancellationToken token = destroyCancellationToken;

    while (true)
    {
        await Awaitable.WaitForSecondsAsync(1f, token);
    }
}

위쪽은 반복마다 프로퍼티를 다시 읽는다. 토큰이 취소되면 await가 예외를 던지고 빠져나가니 대개는 문제가 안 되지만, 문서가 캐시를 요구하는 쪽을 따르는 편이 싸다. 지역 변수 한 줄이다.

글의 예제에 한 가지 더 붙일 데가 있다.

public void Awake()
{
    AwaitLongtime();
}

반환값을 받지 않고 호출한다. 의도적인 fire-and-forget이고 흔한 형태인데, 이러면 그 메서드 안에서 catch하지 못한 예외를 아무도 보지 않는다. 글의 예제는 try/catch가 메서드 안에 있으니 취소는 잡힌다. 다만 다른 예외까지 덮으려면 catch 범위를 그에 맞춰 두어야 하고, 호출 쪽에서 _ =로 버리는 것임을 표시해두면 읽는 사람이 의도를 안다.

private void Awake()
{
    // 반환을 버린다는 것을 명시한다.
    _ = AwaitLongtimeAsync();
}

코루틴과의 비교는 맞는다

글의 마지막 절은 Awaitable이 코루틴보다 나은 네 경우를 든다. 넷 다 성립한다.

글의 주장확인
GameObject에 묶이지 않은 작업맞음. 코루틴은 MonoBehaviour에서 시작하고 그 수명을 탄다
반환값을 받아 연속으로 이어갈 때맞음. 코루틴은 IEnumerator라 값을 돌려줄 자리가 없다
취소를 명시적으로 하고 싶을 때맞음. CancellationToken과 Cancel() 두 모델이 있다
다른 async 메서드와의 호환성맞음. Awaitable은 C#의 awaitable 패턴을 따른다

첫째 줄에 한 겹 덧붙일 데가 있다. 글은 “코루틴의 경우 씬 어딘가에 최소한 한 개의 활성화된 GameObject가 필요하다”고 적는데, 활성화 조건이 시작 시점만의 문제가 아니다.

오브젝트 풀링을 다룬 글에서 확인한 문서 문장이 그 자리다 — SetActive(false)는 그 오브젝트에 붙은 코루틴을 전부 멈춘다. 풀로 돌려보낸 오브젝트의 코루틴은 거기서 끝나고, 다시 꺼내도 이어지지 않는다. Awaitable은 그 규칙을 타지 않는다. 비활성화와 무관하게 계속 진행되고, 끊고 싶으면 내가 끊어야 한다.

장점과 책임이 같은 문장에서 나온다. GameObject에 묶이지 않는다는 것은, GameObject가 사라져도 알아서 멈추지 않는다는 뜻이기도 하다. 그래서 글이 다룬 destroyCancellationToken이 선택이 아니라 기본값이 된다.

이 블로그에서 Awaitable을 실제로 쓴 코드는 Sentis 워크플로 글에 있다. async Awaitable로 추론 결과를 기다리는 형태이고, 연속 실행이 같은 프레임에서 이어진다는 성질에 기대고 있다.

어디에 왜 쓰나

코루틴의 yield를 그대로 옮긴다

대기 종류별 대응이 거의 일대일이다.

코루틴Awaitable
yield return nullawait Awaitable.NextFrameAsync()
yield return new WaitForEndOfFrame()await Awaitable.EndOfFrameAsync()
yield return new WaitForFixedUpdate()await Awaitable.FixedUpdateAsync()
yield return new WaitForSeconds(1f)await Awaitable.WaitForSecondsAsync(1f)
yield return asyncOperationawait operation (토큰이 필요하면 FromAsyncOperation)
yield return new WaitUntil(cond)직접 만든다 — 아래 참고
yield breakreturn
StopCoroutine토큰 취소 또는 Cancel()

WaitForSecondsRealtime에 해당하는 것이 목록에 없다는 게 눈에 띈다. 시간 배율을 무시하는 대기가 필요하면 그건 직접 만들어야 한다.

WaitUntil 쪽은 매뉴얼이 패턴을 적어둔다.

public static async Awaitable AwaitableUntil(Func<bool> condition,
    CancellationToken cancellationToken)
{
    while (!condition())
    {
        cancellationToken.ThrowIfCancellationRequested();
        await Awaitable.NextFrameAsync();
    }
}

매 프레임 조건을 보는 것이라 코루틴의 WaitUntil과 같은 구조다. 다른 건 토큰 자리가 있는 쪽이다.

섞어 쓸 때의 제약도 한 줄 적혀 있다.

You can safely yield return an Awaitable from a traditional iterator-based coroutine, but you can’t yield return an Awaitable<T0>.

코루틴 안에서 Awaitable은 기다릴 수 있고 Awaitable<T>는 안 된다. 점진적 이행 중에 둘이 섞이는 자리에서 걸린다.

옮긴 모양은 이렇게 된다. 함정이 일정 시간 뒤에 재무장하는 코드다.

using System;
using System.Threading;
using UnityEngine;

public class TrapRearm : MonoBehaviour
{
    private const float DEFAULT_COOLDOWN = 3f;

    [Header("Cooldown")]
    [SerializeField, Range(0.1f, 30f), Tooltip("발동 후 재무장까지의 시간")]
    private float _cooldown = DEFAULT_COOLDOWN;

    [SerializeField, Tooltip("재무장 상태")]
    private bool _isArmed = true;

    // 파괴 전에 캐시해둔다. 문서의 요구다.
    private CancellationToken _destroyToken;

    private void Awake()
    {
        _destroyToken = destroyCancellationToken;
    }

    public void Trigger()
    {
        if (!_isArmed)
        {
            return;
        }

        _isArmed = false;

        // 반환을 버린다는 것을 명시한다.
        _ = RearmAsync();
    }

    private async Awaitable RearmAsync()
    {
        try
        {
            await Awaitable.WaitForSecondsAsync(_cooldown, _destroyToken);

            _isArmed = true;
            Debug.Log("재무장", this);
        }
        catch (OperationCanceledException)
        {
            // 오브젝트가 파괴되어 취소됐다. 되돌릴 상태가 없으면 비워둔다.
        }
    }
}

코루틴으로 짰다면 StartCoroutine이 필요하고, 오브젝트가 비활성화되면 쿨다운이 멈춰서 영원히 재무장되지 않는다. 풀에서 꺼내 쓰는 함정이라면 그게 바로 버그가 된다. Awaitable은 비활성화와 무관하게 흐르므로 그 증상이 없다.

WhenAll이 없을 때

조합 메서드가 없어도 “동시에 시작하고 모두 기다리는” 모양은 만들 수 있다. C#의 async 메서드는 첫 await까지 동기적으로 실행되므로, 호출만 해두면 그 자리에서 시작된다.

private async Awaitable LoadAllAsync()
{
    // await를 붙이지 않는다. 호출하는 순간 셋 다 시작된다.
    Awaitable a = LoadStageAsync();
    Awaitable b = LoadAudioAsync();
    Awaitable c = WarmUpShadersAsync();

    // 하나씩 기다린다. 각 인스턴스를 '한 번만' await하므로 규칙을 지킨다.
    await a;
    await b;
    await c;

    Debug.Log("셋 다 끝났다");
}

각 인스턴스를 한 번씩만 await한다. 앞 절의 재사용 금지에 걸리지 않는 형태다. 가장 오래 걸리는 쪽이 전체 시간을 정하니 결과도 WhenAll과 같다.

다만 WhenAll과 다른 데가 하나 있다. a가 예외를 던지면 그 자리에서 빠져나가고, b와 c는 아무도 기다리지 않는 상태로 계속 돈다. Task.WhenAll은 전부 끝난 뒤에 예외를 모아 돌려주는데, 이 형태는 그렇지 않다. 실패를 묶어 처리해야 하면 각 메서드 안에서 catch하고 결과를 Awaitable<bool> 같은 형태로 돌려받는 쪽이 낫다.

WhenAny에 해당하는 것은 이 방식으로 만들 수 없다. 먼저 끝난 쪽을 알아야 하는데 await가 순서를 고정하기 때문이다. 그때는 앞에서 본 문서의 AsTask() 로 건너간다.

private async Awaitable LoadWithTimeoutAsync()
{
    Task load = LoadStageAsync().AsTask();
    Task timeout = Awaitable.WaitForSecondsAsync(10f).AsTask();

    Task finished = await Task.WhenAny(load, timeout);

    if (finished == timeout)
    {
        Debug.LogWarning("로딩이 10초를 넘었다");
    }
}

Task로 감싸는 순간 풀링과 동기적 연속 실행을 포기하는 것이므로, 매 프레임 도는 코드가 아니라 로딩처럼 드물게 한 번 도는 자리에 쓴다. Task와 토큰을 쓰는 코드는 TcpListener를 다룬 글에 있다.

쓰지 말아야 할 자리

정리


참고

이 글의 출발점이 된 자료는 leffe. — 유니티 6 Awaitable 소개 (2025-03-20)이다. Awaitable이 Task와 다른 세 가지는 Unity 코드 최적화 문서를 정리한 글에서 먼저 다뤘고, 이 글에서는 쓰는 쪽을 대조했다. 정적 멤버 목록은 2023.1·6.2·6.3· 6.4 네 판본에서 같았고, Awaitable.WhenAll 페이지는 404였다. AsTask(), Application.exitCancellationToken, yield return 제약은 매뉴얼의 Awaitable 절 셋에서 확인했다. 확인 시점은 2026-10-07이다.


이 글 공유하기:

이전 글
1 << -1은 에러가 아니라 31번 레이어다
다음 글
스냅샷을 권하고, 코드가 스냅샷에서 빼낸다