[기타팁] 뒤끝 이용 시 만날 수 있는, 유니티 자체 에러 모음

안녕하세요, 개발자님!
함께 뛰는 게임 서버, 뒤끝입니다😊

오늘은 유니티를 처음 접하면서, 그리고 뒤끝을 이용하면서 자주 만날 수 있는 유니티 에러들을 간단히 모아보았습니다. 

지금부터 함께 만나 보실까요?

🚫 에러가 발생하는 순간들

1. 참조할 오브젝트를 연결하지 않을 경우

				
					UnassignedReferenceException: The variable popupUI of NewBehaviourScript has not been assigned.
You probably need to assign the popupUI variable of the NewBehaviourScript script in the inspector.
				
			

아래 간단한 코드가 있습니다. 이렇게 간단한 코드에서도 에러가 발생할 수 있을까요?

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    [SerializeField] private GameObject popupUI;

    public void ShowUI() {
        popupUI.gameObject.SetActive(true);
    }
}
				
			

발생할 수 있습니다! 실제로 실행해 보면, 너무나 간단한 코드인데도 에러가 발생합니다.
왜 그런 걸까요?

바로 오브젝트를 제대로 설정하지 않았기 때문입니다.

게임이 완성되어 가면서, 설정한 그리고 설정해야 할 오브젝트가 점점 많아지는데요. 

디버그 모드 중에 오브젝트를 참조할 경우 또는 public GameObject로 설정했는데 public Transform으로 타입을 변환하면서, 참조가 초기화될 수 있습니다. 결과적으로 오브젝트가 제대로 설정되지 않아 에러가 발생할 수 있으니 주의하세요!🚨

2. 오브젝트 내에서, 접근할 컴포넌트 혹은 오브젝트를 찾지 못한 경우

				
					System.NullReferenceException
System.NullReferenceException: Object reference not set to an instance of an object
				
			

이번에 이야기할 에러는, 유니티에서 가장 많이 보게 될 에러입니다!

이 에러의 정체는 무엇일까요?
바로 변수를 선언하고, 대상이 되는 객체를 제대로 설정하지 않았을 때 발생하는 에러입니다.

마찬가지로, 해당 에러가 발생할 수 있는 간단한 예시를 소개해 드립니다.

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    [SerializeField] private GameObject popupUI;

    public void ShowUI() {
        popupUI.GetComponent<MyCustomUI>().ShowUI();
    }
}
				
			
NewBehaviourScript에서 PopupUI 게임 오브젝트를 참조하는 화면

해당 예시에는 NewBehaviourScript에서 PopupUI 게임 오브젝트를 참조하고, 해당 오브젝트가 가진 MyCustomUI스크립트의 ShowUI를 호출하는 로직이 있습니다. 

그런데 PopupUI 오브젝트에는 MyCustomUI 스크립트(컴포넌트)가 존재하지 않는 것을 이미지를 통해 확인할 수 있습니다. 따라서, 접근할 컴포넌트가 없어 NullReferenceException이 발생하게 됩니다.

이 에러가 발생할 경우, 오브젝트에서 호출하는 참조나 해당 오브젝트에 접근할 컴포넌트들이 모두 잘 있는지 확인해야 합니다! 

또한, 오브젝트가 삭제되었음에도 계속해서 접근을 시도하거나, 오브젝트를 선언만 하고 생성하지 않은 경우에도 해당 에러가 발생할 수 있으니, 여러 상황을 고려해 주의하시기를 바랍니다!🚨 

3. Dictionary 혹은 JsonData에서 없는 키값을 이용하려고 할 경우

Dictionary 혹은 JsonData에서 없는 키값을 이용하려고 할 경우 발생하는 에러
				
					KeyNotFoundException: The given key was not present in the dictionary.
				
			

이 오류는 주로 뒤끝에서 JsonData나 Dictionary를 사용할 때 발생합니다아래 해당 에러가 발생할 수 있는 간단한 예시를 공유해 드립니다.

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					Dictionary<string, string> dic = new Dictionary<string, string>();
dic.Add("key1","안녕하세요");

Debug.Log(dic["없는키값"].ToString());
				
			

이 에러는 존재하지 않는 키값에 접근을 시도하려고 했기 때문에 발생합니다.

대부분의 사전(Dictionary)은 for문이나 파싱을 통해 데이터가 자동으로 입력되므로, 예상치 못하게 원하는 데이터가 들어가지 않아 이런 에러가 발생하기 쉽습니다.

뒤끝에서도 데이터를 불러와 파싱할 때, 이와 같은 에러를 자주 볼 수 있습니다.

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					var bro = Backend.GameData.Get("score", new Where());

Debug.Log(bro);
        
if (bro.FlattenRows()[0].ContainsKey("gold")) {
    Debug.Log("gold 값이 존재합니다!");
} else {
   Debug.LogError("gold 값이 존재하지 않습니다.");
}
        
bro.FlattenRows()[0]["gold"].ToString();
				
			
뒤끝에서 데이터를 불러와 파싱할 때, 존재하지 않는 키 값에 접근을 시도해 발생하는 에러

JsonData에는 ‘gold’라는 값이 없지만 계속해서 접근하려 하므로 문제가 발생하는 것인데요! 이 경우, ContainsKey()를 사용하여 해당 키값이 존재하는지 확인할 수 있습니다.

 

또한, 뒤끝 함수 호출의 반환 값인 BackendReturnObject를 Debug.Log를 확인해 보세요. 반환 값이 모두 표시되므로 이를 분석하는 것도 좋은 방법입니다.

4. List에서 가지고 있는 아이템의 최대치 이상을 지정하려고 할 경우

				
					IndexOutOfRangeException: Index was outside the bounds of the array
				
			

아직 List에 대해 익숙해지지 않으면 가끔 볼 수 있는 에러입니다. 배열의 범위를 초과할 때 볼 수 있는 에러인데요.

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					List<int> list = new List<int> { 1, 2, 3, 4, 5 };
Debug.Log(list[5].ToString());
				
			

간단히 설명하면, 리스트에는 0부터 시작하여 1, 2, 3, 4, 5가 들어가므로, 최대로 접근할 수 있는 값은 4가 됩니다. (4번째 위치에는 값 5가 들어가 있습니다) 그렇기 때문에 리스트에서는 list[5] 이후의 값은 존재하지 않게 되는데요! 

이렇게 존재하지 않는 부분을 참조하려고 시도하면 에러가 발생합니다.

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					List<int> list = new List<int> { 1, 2, 3, 4, 5 };
int max = list.Count;
        
for (int i = 0; i < max; i++) {
    list.RemoveAt(i);
}
				
			

같은 논리로, 위 같은 삭제 로직에서도 오류가 발생하는 것을 확인할 수 있습니다. 

“0번째 데이터부터 순차적으로 제거하면 될 것”이라고 생각하여, 위처럼 로직을 구성하는 경우가 있는데요. RemoveAt을 수행하면 해당 목록의 항목이 제거되기 때문에, 5개였던 항목은 4개가 됩니다. 

따라서 [0, 1, 2, 3, 4]가 [1, 2, 3, 4]가 되고, for문이 실행된 후에는 두 번째 항목을 가리키기 때문에 ‘1’을 남겨두고 ‘2’가 제거됩니다. 이런 방식으로 최댓값이 점점 줄어들지만, 우리는 기존의 최댓값 만큼 삭제하도록 구성했으므로 범위를 초과하는 값을 지정하게 됩니다.

⭕ 아래는 잘못된 부분을 수정한 올바른  예시입니다.

				
					List<int> list = new List<int> { 1, 2, 3, 4, 5 };
int max = list.Count;
        
for (int i = 0; i < list.Count; ) {
    list.RemoveAt(0);
}
				
			

따라서 위와 같이, 0번째 위치에서 계속해서 데이터를 제거하며 list.Count가 0이 될 때까지 제거하도록 만드는 것이 올바른 방법입니다!

5. 유니티 함수 혹은 유니티 UI 함수를 외부 스레드에서 실행할 경우

유니티 함수 혹은 유니티 UI 함수를 외부 쓰레드에서 실행할 경우 발생하는 에러
				
					UnityEngine.UnityException: {오브젝트 이름} can only be called from the main thread.
				
			

이해하기 어려운 이 오류는 유니티의 스레드 관리로 인해 발생합니다.
뒤끝과 같은 서버 SDK를 사용하신다면, 이 문제를 자주 직면하게 될 것인데요. 

유니티는 메인 스레드에서만 작동한다는 사실, 알고 계실까요? 유니티 함수와 유니티 UI 함수를 사용하기 위해서는, 반드시 메인 스레드에서 실행해 주셔야 합니다.

그렇기 때문에, 아래와 같이 비동기를 처리하기 위해 외부 스레드를 생성하고, 그곳에서 유니티 함수를 호출하면 해당 에러가 발생하게 됩니다.

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					public void Test() {
    Debug.Log("함수 호출 시작");
    Test2Asnyc();
    Debug.Log("함수 호출 종료");
}

async void Test2Asnyc() {
    await System.Threading.Tasks.Task.Run(() => {
        for (int i = 0;  i < 3; i++) {
            Debug.Log("카운트 시작 " + i);
        }
        Instantiate(popupUI);
    });
}
				
			

그렇다면 뒤끝을 이용할 경우, 어떤 상황에서 이 에러가 발생할까요?

🚫 아래는 의도적으로 작성한 잘못된 예시입니다.

				
					Backend.Initialize(false);
Backend.BMember.CustomLogin("a0", "a0");
Backend.GameData.Get("score", new Where(), callback => {
    Instantiate(popupUI);
});
				
			

뒤끝을 이용하시면서 이렇게👇 생각하셨다면, 위 같은 에러가 발생합니다.

‘서버에 요청한 순간 게임이 멈추면 안 되니까, 비동기로 함수를 작성하고 응답이 오면 게임 오브젝트를 생성해서 UI 혹은 GameObject를 띄워주자!’

뒤끝은 이러한 에러를 방지하기 위해 Backend.Initialize(true)를 사용하는데요. 

간단히 말해 비동기 함수가 끝난 이후, 외부 스레드에서 처리하는 것이 아니라, callback을 Queue 형식으로 저장하는 것입니다. 이후 Backend.AsyncPoll()을 Update에서 지속적으로 호출해 Queue에 값이 들어왔는지 확인하고, 호출합니다. 그렇기 때문에 메인 스레드에서 문제없이 잘 작동하게 됩니다.

그러나 뒤끝에서 Backend.Initialize(asyncpoll = false)를 사용할 경우, 비동기 함수에 대한 응답이 외부 스레드에서 처리되기 때문에 위와 같은 에러가 발생합니다. 따라서 뒤끝을 이용하실 때, 위 에러가 발생했다면 (1)Backend.Initialize(true)를 사용하거나, (2)별도의 로직을 제작해 주셔야 합니다!

아래 두 가지 로직을 공유해 드리면서 마칩니다!

Backend.Initialize(true)

⭕ 아래는 올바른  예시입니다.

				
					public void Test() {
    Backend.Initialize(true);
    Backend.BMember.CustomLogin("a0", "a0");
    Backend.GameData.Get("score", new Where(), callback => {
        // 바로 실행하는 것이 아닌 queue에 저장해서 Update문에서 실행시켜준다.
        queue.Enqueue( () => {
            Instantiate(popupUI);
        });
    });
}

void Update() {
    if (Backend.IsInitialized) {
        Backend.AsyncPoll();
    }
}
				
			

별도의 로직

⭕ 아래는 올바른  예시입니다.

				
					private Queue<Action> queue = new Queue<Action>();
public void Test() {
    Backend.Initialize(false);
    Backend.BMember.CustomLogin("a0", "a0");
    Backend.GameData.Get("score", new Where(), callback => {
        // 바로 실행하는 것이 아닌 queue에 저장해서 Update문에서 실행시켜준다.
        queue.Enqueue( () => {
            Instantiate(popupUI);
        });
    });
}

void Update() {
    if (queue.Count > 0) {
        queue.Dequeue().Invoke();
    }
}
				
			
1

댓글