본문 바로가기

[unity] 모듈 제작 : UniTask 활용한 Network system 제작

앤디가이 2022. 5. 26.

Unity Restful network manager 를 제작하여, 웹서버와 통신하는 모듈을 만들어 보자.

 

유니티에서 가장 흔하게 사용하는 방식은 UnityWebRequest와 코루틴을 통한 웹서버<->클라이언트 간 통신 방법이 있다.

코루틴을 사용한 방법들은 유니티 레퍼런스에 잘 설명되어 있으니 참고해보면 좋을 것 같다.

https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.html

 

Unity - Scripting API: UnityWebRequest

UnityWebRequest handles the flow of HTTP communication with web servers. To download and upload data, use DownloadHandler and UploadHandler respectively. UnityWebRequest includes static utility functions that return UnityWebRequest instances configured for

docs.unity3d.com

 

이번 모듈은 코루틴이 아닌 UniTask를 활용하여 제작해 보도록 하겠다.

UniTask는 C#의 Task를 유니티에 맞춰 래핑한 비동기 라이브러리라고 생각하면 좋다.

C#의 async/await를 유니티에 맞게 최적화하여, 기본 Task 보다 가볍다.

유니티의 메인 쓰레드 기반으로 동작하므로 다양한 플랫폼(WebGL,WASM,Android,IOS) 등에서도 활용이 가능하다.

그동안 코루틴과 콜백으로 처리했던 통신 구조를 비동기로 쉽게 바꿔줄 수 있다.(콜백 지옥 안녕~)

 

그럼 UniTask 라이브러리를 추가해 보도록 하겠다.

현재 깃허브에 공개 프로젝트로 올라와 있으니 패키지 파일을 다운받아 보자.

 

https://github.com/Cysharp/UniTask/releases

 

Releases · Cysharp/UniTask

Provides an efficient allocation free async/await integration for Unity. - Cysharp/UniTask

github.com

 

현재 시점에서는 UniTask.2.3.1 이 최신 버전이다.

 

유니티를 열고, 패키지를 압축풀어준다.

압축을 풀면, Plugins 안에 UniTask란 항목으로 생성이 된다. 

UniTask 폴더를 Library 폴더로 이동하여, 라이브러리들만 따로 관리를 하자. 

 

Modules 안에 Network 폴더와 자식 폴더인 Scripts 폴더를 만들어 준 후, NetworkManager, URI 클래스 2개를 만들어 준다.

URI.cs 클래스는 도메인 정보 및 필요한 URL 정보를 정의해준다.

public static class URI
{
    public static readonly string DOMAIN = "http://localhost:8080/";
}

 

NetworkManager.cs 클래스는 restful api 통신 스크립트이다.

using System;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

namespace Container.Network
{
    public enum SENDTYPE
    {
        GET,
        POST,
        PUT,
        DELETE
    }

    public class NetworkManager
    {
        protected static double timeout = 5; //5초 타임아웃.

        /// <summary>
        /// 서버 전송 함수.
        /// </summary>
        /// <typeparam name="T">Return Class</typeparam>
        /// <param name="url">api url 정보</param>
        /// <param name="sendType">Get,Post,Put,Delete</param>
        /// <param name="jsonBody">body 정보</param>
        /// <returns></returns>
        public static async UniTask<T> SendToServer<T>(string url, SENDTYPE sendType, string jsonBody = null)
        {
            //1. 네트워크 체크.
            await CheckNetwork();
            //2. API URL 생성.
            string requestURL = URI.DOMAIN + url;

            //3. Timeout 설정.
            var cts = new CancellationTokenSource();
            cts.CancelAfterSlim(TimeSpan.FromSeconds(timeout));

            //4. 웹 요청 생성(Get,Post,Delete,Update)
            UnityWebRequest request = new UnityWebRequest(requestURL, sendType.ToString());
            //5. Body 정보 입력
            request.downloadHandler = new DownloadHandlerBuffer();
            if (!string.IsNullOrEmpty(jsonBody))
            {
                byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
                request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            }
            //6. Header 정보 입력
            SetHeaders(request);
            try
            {
                var res = await request.SendWebRequest().WithCancellation(cts.Token);
                T result = JsonUtility.FromJson<T>(res.downloadHandler.text);
                return result;
            }
            catch (OperationCanceledException ex)
            {
                if (ex.CancellationToken == cts.Token)
                {
                    Debug.Log("Timeout");
                    //TODO: 네트워크 재시도 팝업 호출.

                    //재시도.
                    return await SendToServer<T>(url, sendType, jsonBody);
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                return default;
            }
            return default;
        }

        private static async UniTask CheckNetwork()
        {
            if (Application.internetReachability == NetworkReachability.NotReachable)
            {
                //TODO: 네트워크 오류 팝업 호출.
                Debug.LogError("The network is not connected.");
                await UniTask.WaitUntil(() => Application.internetReachability != NetworkReachability.NotReachable);
                Debug.Log("The network is connected.");
            }
        }

        private static void SetHeaders(UnityWebRequest request)
        {
            //필요한 Header 추가.
            request.SetRequestHeader("Content-Type", "application/json");
        }
    }
}

코드를 살펴보면, 상단에 timeout 값을 설정해준다. (서버에서 몇 초 동안 응답없을 경우 처리할지 정의 )

 

SendToServer UniTask를 사용한 비동기 함수는 웹 서버로 요청하는 함수이다.

기본 인자로 api url 과 전송방식, jsonBody 입력값을 받는다.

 

함수 플로우는 다음과 같다.

1. 네트워크 연결 여부 체크

2. URL 생성

3. Timeout 생성

4. 웹 요청 정보 생성

5. Json Body 정보 입력

6. Header 정보 입력

7. 요청

8. 응답 처리

9. 리턴 or 재시도(타임아웃 시)

 

해당 함수를 호출하는 예제 코드는 다음과 같다.

using System;
using Container.Network;
using Cysharp.Threading.Tasks;
using UnityEngine;

[Serializable]
public class UserInfo
{
    public string token;
    public string username;
    public string name;
}

[Serializable]
public class RequestSignInData
{
    public string username;
    public string password;

    public RequestSignInData(string newUsername, string newPassword)
    {
        username = newUsername;
        password = newPassword;
    }
}

public class NetworkTest : MonoBehaviour
{
    public async UniTask Login(RequestSignInData data)
    {
        string json = JsonUtility.ToJson(data);
        UserInfo info = await NetworkManager.SendToServer<UserInfo>("/login", SENDTYPE.POST, json);
        Debug.Log(info);
    }
}

username, password 를 입력해 로그인 한 후 UserInfo 데이터를 받는 샘플 코드이다.

실제 프로젝트 진행 시 호출부에 있는 데이터 클래스(UserInfo, RequestSignInData 등)는 따로 분리하는게 좋다.

 

댓글