안녕하세요. 

 

지난 두 번의 포스팅으로 인터페이스(Interface)와 IEnumerator 그리고 IEnumerable에 대해 살펴보았습니다.

 

코루틴을 이해하는데 인터페이스 부터 시작하는건 좀 너무한것 아니냐는 분도 계실지 모르겠지만, 단순히 코루틴의 사용법을 익힌다기보다 코루틴까지 가는 과정을 이해하면서 C#문법과 유니티 함수에 대해서 조금 더 깊이 이해하고 응용할 수 있게 되는 것이 이 글의 목표라고 생각해주시면 되겠습니다. 

 

아직 앞의 글들을 읽지 않으셨다면 읽고 돌아오시는 것을 추천드립니다. 

 

2024.05.03 - [유니티 기초 스킬] - 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(1) - Interface

 

코루틴(Coroutine)과 IEnumerator을 이해하기 위해(1) - Interface

안녕하세요. 유니티에서 게임을 개발할 때 많이 사용하는 함수 중 코루틴이 있습니다. 그리고 코루틴 함수를 사용할 때는 항상 IEnumerator를 사용해야한다고만 배웠죠. 그런데 C#문법을 공부하

developer-coffee.tistory.com

 

 

2024.05.03 - [유니티 기초 스킬] - 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(2) - IEnumerator와 IEnumerable

 

코루틴(Coroutine)과 IEnumerator을 이해하기 위해(2) - IEnumerator와 IEnumerable

안녕하세요. 지난번 포스팅에서는 IEnumerator와 IEnumerable를 이해하기 위해서 먼저 인터페이스에 대해서 살펴보았습니다.  혹시 아직 보시지 못하신 분이 있으시다면 이 전 포스팅을 읽고 오시는

developer-coffee.tistory.com

 

아무튼 이제 긴 시간을 거쳐 드디어 코루틴에 대해 말할 수 있게 되었냐고 한다면 아직은 아닙니다. 

 

코루틴을 가기전에 'yield return'에 대해 이해하고 나면 좋을 것 같습니다. 

 

 

yield return

 

yield return은 C#에서 제공하는 키워드로 주로 IEnumerator, IEnumerable 타입을 반환하는 함수에서 사용됩니다. 

 

yield return은 함수 안에 블록을 지정하고 그 블록안에 있는 요소를  차례로 반환해주는 역할을 합니다. 

 

예를 들어보겠습니다. 

 

지역변수 int 타입의 배열 numbers = {1 ,2,3} 이 있습니다. 내가 원하는 것은 GetNumers라는 함수를 하나 만들어 GetNumbers를 호출할때 첫번째 호출에서는 numbers의 첫번째 요소를 두번째 호출 할때는 두번째 요소를 반환하는 함수를 만들고 싶습니다. 

 

그럼 어떻게 만들면 될까요?

단순하게 생각한다면 이렇게 만들면 됩니다.

    int GetNumbers(int i)
    {
        private int[] number = { 1, 2, 3 };
        return number[i];
    }

    private void Start()
    {
        Debug.Log(GetNumbers(0));
        Debug.Log(GetNumbers(1));
        Debug.Log(GetNumbers(2));
        
    }

 

 

그런데 만일 한꺼번에가 아니라 각각 다른 함수에서 호출할때마다 다음 요소가 나오게 하고 싶다면 어떻게 해야할까요? 

 

int GetNumbers(int i)
    {
        int[] number = { 1, 2, 3 };
        return number[i];
    }

    void funtion1()
    {
        Debug.Log(GetNumbers(0));
    }
    void funtion2()
    {
        Debug.Log(GetNumbers(1));
    }
    void funtion3()
    {
        Debug.Log(GetNumbers(2));
    }

 

이런 식으로 해야할 텐데.. 만일 number의 요소가 100까지 있다면 저런식으로 100줄을 만들어 각 요소를 불러줘야 할겁니다. 게다가 다른 곳에서 GetNumbers의 매개변수를 0부터 각각 순서대로 집어넣어 준다면 방금 전까지 어느요소를 호출했는지 그 인덱스를 기억해야하는 일까지 생깁니다. 너무 피곤하겠죠?

 

하지만 이런 일을 하기 위해서 만들어 진 것이 바로 yield return입니다. 

public IEnumerator GetNumbers()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

 

IEnumerator을 반환하는 GetNumbers() 함수를 만들었습니다. 그런데 보통 void가 아닌 함수 형태에서는 return을 사용해서 반환타입의 요소를 리턴해줘야 하는데, 이 함수에는 그냥 return이 아닌  yield return입니다. 

 

그리고 이 yield return이 해주는 일은 이렇습니다.

 

GetNumbers()함수를 한번 호출 하면 첫번째 yield return 1; 을 실행하여 1을 반환합니다. 그리고 함수의 나머지 부분을 실행하지 않고 멈춥니다. 

 

그리고 GetNumbers()함수를 그 다음 다시 한번 호출 한다면 yield return2;를 실행하여 2를 반환합니다. 그리고 함수의 뒷부분을 실행하지 하고 멈춰 있습니다. 

 

GetNumbers()를 세번째로 호출하면 yield return3;를 실행하여 3을 반환하고 멈춥니다. 

 

민일 4번째 호출 하면 어떻게 될까요? null값을 반환하게 됩니다. 

 

이로써 알 수 있는 것은 yield return은 어디까지 반환했는지를 기억했다가 다음 호출할때에는 그 다음요소를 넘겨주는 역할을 하는 키워드 입니다.

 

yield return을 사용하는 함수의 동작원리를 좀 더 잘 이해하기 위해 위의 코드를 다음과 같이 바꿔봤습니다.

 IEnumerator GetNumbers()
    {
        int num = 1; // 1번 연산 부분
        
        yield return num; //1번 연산 부분에서 연산한 값을 리턴 한후 멈춤

        num++;        //2번 연산 부분

        yield return num; // 2번 연산을 실행한 후 그 값을 리턴

        num++;            // 3번 연산부분

        yield return num;   // 3번 연산을 실행한 후 그 값을 리턴
    }

 

이 함수는 바로 위에 있는 함수와 완전히 같은 기능을 하는 함수입니다. 차이는 yield return 사이에 연산하는 코드들이 있다는 거겠죠?

 

다시 말해 yield return은 함수 안에서 실행되는 구역을 나눠주는 역할을 하는겁니다.  그리고 함수가 호출 될 때마다 첫번째 구역 다음 호출될때 두번째 구역 이런식으로 실행해 그 값을 반환하는 겁니다. 

 

그럼 yield return을 int 형 함수나 void 형 함수처럼 일반 적인 형태의 함수에서 사용할 수 있을까요? 

 

아쉽지만 일반 함수에서는 사용할 수 없고 반환타입이 반드시 IEnumerable 혹은 IEnumerator인 함수에만 사용할 수 있습니다. 

 

정확히는 IEnumerator타입 함수를 사용해야하는 것이지만 IEnumerable 타입은 그안에 IEnumerator를 불러오는 기능을 포함하고 있기 때문에 사용할 수 있게 되는 겁니다. 

 

IEnumerator가 구현하는 기능은 무엇이었을까요? 

 

IEnumerator 는 어떤 대상의 순차적으로 되어 있는각 요소에 접근하고(Next.Move()) , 접근한 그 순번째의 요소를 반환하고(object Currunt), 혹시 마지막 요소까지 모두 접근이 끝났지만 다시 처음부터 반복하고 싶을때 순번을 되돌리는 기능(Reset())을 가지고 있습니다. 

 

따라서 함수 안에서 yield return을 사용하면 yield return을 기점으로 함수를 실행하는 요소가 순차적으로 생성된 것입니다. 위의 코드로 설명해보자면 yield return을 기준으로 1번 연산 부분, 2번 연산 부분, 3번 연산 부분으로 3개의 순차적인 요소가 생성 되는 겁니다. 

 

그럼 이 함수안에 나눠진 구역을 요소로 생각한다면 IEnumerator 가 하는 일은 MoveNext()를 실행해서 그 1번 연산 부분에 접근하고, Currnt를 이용해서 1번연산의 결과값을 리턴하는 요소를 가져올 수 있습니다. 그리고 만일 요소가 다 끝났다면 Reset()를 실행하여 처음부터 다시 이과정을 실행할 수 있는 겁니다. 

 

자 그럼 이제 위에서 구현한 IEnumerator 함수를 어떻게 사용하는지 살펴보겠습니다. 

 

GetNumbers()함수를 호출 될때 마다  각각 1,2,3 이렇게 출력이 되게 만들고 싶습니다. 

 

그런데 만일 처음 int 배열을 사용했던 스크립트 처럼 그냥 함수 자체를 출력하려고 한다면 원하는 값이 나오지는 않습니다. 

 

 IEnumerator GetNumbers()
    {
        int num = 1;
        yield return num;

        num++;

        yield return num;

        num++;

        yield return num;
    }

    private void Start()
    {
        Debug.Log(GetNumbers());
        Debug.Log(GetNumbers());
        Debug.Log(GetNumbers());
    }

 

Start 함수에서 GetNumbers()를 호출해 출력하는 것으로는 1,2,3 이런식으로 출력이 되지 않습니다. 

 

왜냐하면 GetNumbers()의 반환타입은 int가 아닌 IEnumerator 타입이기 때문입니다. 

 

그렇기 때문에 Debug.Log(GetNumbers()) 라고 실행하면 AA+<GetNumbers>d__0이런식의 결과값이 출력됩니다. 

 

IEnumerator 타입을 통해 접근하고 있는 인덱스의 요소를 반환받고 싶다면 반드시 IEnumerator.Current를 사용해야합니다. 

 

그렇기 떄문에 1,2,3의 결과를 출력하고 싶다면 다음과 같이 사용해야합니다.

 

  private void Start()
    {
        IEnumerator ienumerator = GetNumbers();

        ienumerator.MoveNext();
        Debug.Log(ienumerator.Current);
        ienumerator.MoveNext();
        Debug.Log(ienumerator.Current);
        ienumerator.MoveNext();
        Debug.Log(ienumerator.Current);

    }

 

먼저 GetNumbers(); 함수로 반환되는 IEnumerator의 객체를 생성해 IEnumerator 타입 변수 ienumerator 에 할당합니다. 

 

(곧바로 Debug.Log( GetNumbers()); 이런 형식으로 사용하지 않는 이유는 GetNumbers(); 를 호출할 때 마다  IEnumerator의 객체가 새로 생성되기 때문입니다. 그렇다면 Debug.Log( GetNumbers()); 를 사용할 때마다 첫번째이전 요소만 출력하게 되지 순차적으로 다음 요소를 출력할 수 없게 됩니다. 그래서 먼저 GetNumbers() 명령을 통해  IEnumerator의 객체를 하나 생성해주고 그 객체를 사용함으로서 연속된 인덱스에 순차적으로 접근할 수 있게 됩니다.)

 

그리고 먼저 ienumerator의 첫번째 요소에 접근(MoveNext)하는 명령을 내려주고, 그 요소를 출력하라는 명령 (Current)  을 내려려서 출력해줍니다. 이런식으로 다음요소에 접근해 출력하고 또 다음 요소에 접근해 출력하면 우리가 원하는 결과인 1,2,3이 순차적으로 출력되게 되는 겁니다. 

 

게다가 이렇게 순차적인 출력을 start함수에서 한번에 하는 것이 아니라 각각 다른 함수에서 다른 시점에서 내가 몇번째 요소까지 호출했는지 기억할 필요 없이

 

ienumerator.MoveNext();        

Debug.Log(ienumerator.Current)

 

이 두 줄을 사용하면 각 요소를 순차적으로 출력할 수 있게 된다는 말입니다. 

 

그렇다면 IEnumerable 과 yield return은 어떻게 함께 사용하는 것일까요? 

 

 IEnumerable GetNumbers()
    {
        int num = 1;
        yield return num;

        num++;

        yield return num;

        num++;

        yield return num;
    }

    private void Start()
    {
        IEnumerator ienum = GetNumbers().GetEnumerator();
        
        ienum.MoveNext();
        Debug.Log(ienum.Current);
        ienum.MoveNext();
        Debug.Log(ienum.Current);
        ienum.MoveNext();
        Debug.Log(ienum.Current);
    }

 

이번에는 GetNumbers()를 IEnumerable 반환타입 함수로 만들었습니다. 함수 내용은 이전과 같습니다.

 

IEnumerable은   IEnumerator의 기능을 사용할 수 있는 환경 자체를 인식하고 기능을 전달하는 기능을 가지고 있습니다. 어떤 대상이 IEnumerator를 사용할 수 있도록 일정한 인덱스 요소를 가지고 있다면 IEnumerable 타입으로 변환이 가능하다는 의미입니다. 

 

여기서는 IEnumerable GetNumbers() 함수 내부에서 yield return를 사용하는 순간 함수 내부의 1,2,3 각 연산부를 인덱스로 보고 그 인덱스 구조 자체를 IEnumerable 타입에 담아서 반환할 수 있다는 이야기입니다. 

 

그리고 인덱스 구조가 있다면 IEnumerator의 기능을 사용할 수 있게 됩니다.

 

따라서 IEnumerable 타입의 객체가 반환되었다면 우리는 그 객체에 IEnumerator기능을 사용할 수 있게 되었다는 것을 의미하며 그 기능을 사용하기 위해서 다음과 같은 작업이 필요합니다. 

 

   IEnumerator ienum = GetNumbers().GetEnumerator();

 

 

 Debug.Log( GetNumbers().GetEnumerator() ) 이렇게 사용해도 될 것 같지만  위의 그전 예제에서 IEnumerator 객체를 따로 할당해서 사용했던 것과 동일한 이유로 ienum 이라는  IEnumerator 타입 변수에 할당해서 인덱스의 값이 연속적으로 출력될 수 있도록 합니다. 

 

그리고 Move.Next와 Current를 사용해서 요소에 접근하고 출력할 수 있게 되는 겁니다.

 

IEnumerable를 사용할때에는 IEnumerable안에 들어가는 객체의 요소에 반드시 인덱스 적인 요소가 필요합니다. 

 

인덱스적라는 말을 해석하자면 순차적으로 접근할 수 있는 구분된 요소들이 존재해야한다는 겁니다.

 

(사실 이해를 돕기 위해 인덱스라는 말을 사용했지만 보통 '시퀀스'라는 말을 사용합니다. )

 

위의 예제에서는 yield return이 함수의 부분을 인덱스적으로 만들어주는 역할을 하는 겁니다. 다시말해 순차적인 요소를 구분하는 일종의 구분선의 역할을 해준다고 보면 되는 겁니다. 

 

만일 yield return를 사용하지 않는 IEnumerable 타입 반환 타입의 함수를 만든다면 반환되는 값은 반드시 인덱스적인 요소를 가지고 있는 것이어야 합니다. 

 

그렇기 때문에 IEnumerable안에 배열이나 컬렉션을 반환 요소로 삼아도 되는 겁니다. 

 

 IEnumerable GetNumbers()
    {
        int[] num = { 1, 2, 3};

        return num;
    }

    private void Start()
    {
        IEnumerator ienum = GetNumbers().GetEnumerator();
        ienum.MoveNext();
        Debug.Log(ienum.Current);
        ienum.MoveNext();
        Debug.Log(ienum.Current);
        ienum.MoveNext();
        Debug.Log(ienum.Current);
    }

 

그래서 IEnumerable 함수 안에 배열을 넣어 반환했다면 똑같이 IEnumerator기능을 불러와 사용할 수 있게 되는 겁니다. 

 

자 오늘 이야기를 정리해 보겠습니다. 

 

1. IEnumerator 는 어떤 인덱스 적인 요소에 순차적으로 접근해서 그 요소를 반환해주는 기능을 가지고 있습니다. 

 

2. IEnumerable은 어떤 대상 혹은 객체가 인덱스적인 요소를 가지고 있는지를 인식해주는 역할을 합니다.

 따라서 어떤 대상이나 객체가 IEnumerable 타입에 할당될 수 있다면  그 대상은 반드시 IEnumerator의 기능들을 사용할 수 있는 구조를 가지고 있다는 의미입니다. 

 

3. yield return은 바로 함수 안에 인덱스적인 구조를 나눠주는 역할을 하는 키워드입니다. yield return 가 들어가는 함수는 '실행하는 요소가 순차적으로 구분되어 있는 구조'로 바뀌게 됩니다. 따라서 이러한 구조를 이해하고 사용할 수 있는 IEnumerable나 IEnumerable가 아니면 yield return가 있는 함수의 구조를 이해하거나 해석할 수 없게 되는 겁니다. 

 

4. 그래서 yield return은 반드시 IEnumerable나 IEnumerable가 반환되는 함수에만 사용할 수 있게 되는 겁니다. 

 

자, 이제 비로소 유니티의 코루틴을 이해할 수 있는 준비가 된 것 같습니다. 

 

다음시간에는 드디어 유니티의 코루틴을 함께 이해해보는 시간을 갖겠습니다. 

 

감사합니다.

+ Recent posts