[Unity] 코루틴(Coroutine)의 작동 원리를 알아보자

ChangBeom·2025년 11월 21일

Unity

목록 보기
11/17
post-thumbnail

이전에 Task와 UniTask의 동작 방식을 정리하면서, 코루틴도 함께 정리해두면 좋겠다는 생각이 들었다.

그래서 이번 글에서는 코루틴이 실제로 어떤 구조로 동작하는지, 그 원리를 정리해보려고 한다.


[1. 코루틴은 스레드가 아니다]

많은 개발자가 처음 하는 오해는 이것이다.

코루틴 = 비동기 -> 스레드?

하지만 Unity의 코루틴은 스레드를 전혀 생성하지 않는다.
대신 메인 스레드에서 실행되는 프레임 기반 상태 머신이다.

즉, 코루틴은 멀티스레드가 아니라

Update처럼 매 프레임 실행되지만, 함수 전체를 한 번에 실행하는 것이 아닌 yield return을 기준으로 중단하고 다시 재개하는 구조이다.

그래서 코루틴은 Unity API를 안전하게 호출할 수 있고, 동시에 스레드에 비해 매우 가볍다.


[2. 왜 코루틴은 IEnumerator를 반환해야 할까?]

코루틴 함수는 아래와 같이 생겼다.

IEnumerator FadeOut()
{
	yield return new WaitForSeconds(1f);
    // ...
}

여기서 자연스럽게 궁금증이 생긴다.

IEnumerator가 무엇이고, 왜 IEnumerator를 반환할까?

그 이유는 Unity의 코루틴이 C#의 반복자(Iterator) 패턴을 기반으로 만들어졌기 때문이다.

<IEnumerator란?>
IEnumerator는 다음 요소로 이동할 수 있는 반복자를 의미하는 인터페이스이다.

public interface IEnumerator
{
	bool MoveNext();
    object Current { get; }
    void Reset();
}

여기서 중요한 것은 MoveNext()이다.

  • MoveNext()가 호출될 때마다 -> 코루틴의 다음 단계(state)로 이동
  • Current에는 yield return한 객체가 들어감
  • Unity는 멈춰야 할지 계속해야 할지 이 Current를 보고 판단함

즉, 코루틴은 스레드처럼 자동으로 실행되는 게 아니라, Unity가 매 프레임 MoveNext()를 호출해줘야만 조금씩 앞으로 진행되는 구조라는 뜻이다.


[3. 코루틴 함수는 "상태 머신(State Machine)으로 변환된다]

C# 컴파일러는 yield return이 포함된 메서드를 자동으로 상태 머신 형태의 클래스로 변환한다.

변환 과정은 다음과 같다.

  • 코루틴 함수 전체가 클래스로 재구성됨
  • yield return이 등장할 때마다 하나의 state가 생성됨
  • MoveNext()가 호출될 때마다 state가 변경되며 코드가 이어서 실행됨

즉, 코루틴은 중단되는 것이 아니라

매 프레임 MoveNext()가 실행되며, 이전 상태의 다음 줄부터 이어서 실행되는 구조이다.


[4. Unity PlayerLoop가 코루틴을 스케줄링한다]

코루틴은 다음 흐름으로 Unity에서 실행된다.

<1. StartCoroutine 호출>

  • IEnumerator 상태 머신 객체 생성
  • Unity의 코루틴 실행 목록에 등록

<2. PlayerLoop(Update 루프)에서 MoveNext 호출>
매 프레임 Unity 엔진은 다음을 반복한다.

코루틴 리스트 순회 -> 각 코루틴의 MoveNext() 실행

MoveNext()의 결과와 Current값에 따라 Unity는 스케줄링을 결정한다.

<3. yield return 결과로 재개 시점 결정>

yield 구문의미
yield return null다음 프레임까지 대기
yield return new WaitForSeconds(t)t초 대기
yield return new WaitUntil(cond)cond()가 true 될 때까지 대기
yield return StartCoroutine(sub)sub 코루틴 완료 후 재개
yield break코루틴 종료

Unity는 Current에 반환된 객체를 보고
다음 MoveNext를 언제 실행할지를 판단한다.


[5. Unity가 왜 이런 방식으로 코루틴을 설계했을까?]

<1. 스레드 없이 "비동기처럼 보이게" 만들기 위해>
실제로는 메인 스레드지만, yield return을 통해 비동기 흐름을 자연스럽게 표현할 수 있다.

<2. 대부분의 Unity API가 메인 스레드 전용이기 때문>
Transform, GameObject, Renderer 등 거의 모든 API는 멀티스레드에서 접근 불가하다. 따라서 스레드를 생성하는 방식은 적합하지 않다.

<3. 게임 연출·타이밍 제어에 최적화되어 있기 때문>
코루틴은 다음과 같은 작업에 매우 자연스럽다.

ex)

  • 3초 뒤에 실행
  • 조건 만족할 때까지 대기
  • 애니메이션 끝나면 다음 행동

즉, 프레임 기반 흐름 제어라는 게임 구조에 딱 맞는다.

<4. 매우 가벼운 구조>
코루틴은 상태 머신 객체 하나만 유지하면 되며, 스레드를 생성하는 비용에 비해 매우 싸다.


[6. 코루틴의 단점과 한계]

물론 코루틴이 만능은 아니다.

<1. CPU 작업에 취약함>
코루틴은 스레드가 아니므로

for(int i = 0; i< 99999999; i++) { }

이런 연산을 코루틴에서 실행하면
메인 스레드가 멈추고 게임이 프리즈된다.

<2. 복잡한 비동기 로직 표현이 어렵다>
중첩된 코루틴, 여러 대기 조건, 분기 처리 등이 많아지면 흐름 관리가 어려워지고 디버깅도 힘들다.

그래서 async/await 기반의 UniTask를 함께 쓰는 경우가 많다.


[7. 정리]

코루틴의 동작 방식은 한 문장으로 정리하면 다음과 같다.

코루틴은 스레드가 아니라, Unity PlayerLoop가 매 프레임 MoveNext()를 호출하는 프레임 기반 상태머신이다.

추가로 기억하면 좋은은 점
*IEnumerator기반이기 때문에 yield return을 사용할 수 있다.

  • Current 객체로 재개 시점을 Unity가 스케줄링한다.
  • 게임 연출과 타이밍 처리에 최적화되어 있다.
  • CPU 작업에는 절대 적합하지 않다.
  • 복잡한 비동기 흐름이 필요할 땐 UniTask를 사용하자.

[8. 마치며]

코루틴은 Unity 개발자에게 가장 친숙한 도구지만, 정작 내부 구조를 알고 쓰는 경우는 많지 않다.
막상 그 구조를 들여다보면 예상보다 훨씬 단순하면서도, 놀라울 만큼 효율적으로 설계되어 있다는 것을 알 수 있다.

프레임 기반 상태 머신이라는 구조를 이해하고 나면,
코루틴이 왜 게임 연출에 딱 맞는지,
왜 복잡한 비동기에는 적합하지 않은지 자연스럽게 느껴질 것이다.

결국 중요한 건 도구를 어떻게 쓰느냐이다.
코루틴을 제대로 알면, 게임의 흐름을 더 부드럽고 정확하게 다루는 데 많은 도움이 될 것이다.

0개의 댓글