[Unity] 느슨한 결합의 UI System 만들기

Running boy·2023년 8월 28일
0

유니티

목록 보기
5/9

대부분의 경우 UI 매니저를 만든다면 캔버스의 하위에 UI View를 만든 다음에 해당 오브젝트를 매니저 오브젝트의 필드로 할당하는 방식을 사용한다.

강한 결합

지금까지 나는 작업을 혼자 해왔기 때문에 이 부분에 큰 불편함을 느끼지 않았다.
새로운 UI View를 추가한다면 매니저에 새로운 필드를 할당하고 프리팹을 수정하면 그만이었다.
하지만 Unity Korea의 어느 영상을 보고 이 방식으로 인해 협업 과정에서 생길 불편함에 대해 생각해보게 되었다.

요약하면 UI 작업자가 따로 있을 경우 각 UI View는 컨트롤러와 느슨하게 결합돼야 한다는 것이다.
그렇지 않다면 빈번한 UI 수정에 프로그래머가 전부 대응해야 한다.

아무튼 그래서 내 프로젝트에 맞는 UI System을 갖출 겸 리펙토링을 하기로 했다.

UI View 구현

using System.Collections.Generic;
using UnityEngine;

namespace Runningboy.UI
{
    public abstract class UIView : MonoBehaviour
    {
        static Dictionary<string, UIView> _uiViewDictionary = new Dictionary<string, UIView>();

        [SerializeField]
        string _viewID;

        #region UnityMessage

        protected virtual void Awake()
        {
            AddUIView(_viewID, this);
        }

        #endregion

        #region Virtual

        public virtual void Show()
        {
            gameObject.SetActive(true);
        }

        public virtual void Hide()
        {
            gameObject.SetActive(false);
        }

        #endregion

        #region Static

        public static bool TryGetView(string viewID, out UIView view)
        {
            return _uiViewDictionary.TryGetValue(viewID, out view);
        }

        #endregion

        #region Private

        private void AddUIView(string viewID, UIView view)
        {
            if (string.IsNullOrEmpty(viewID))
            {
                Debug.LogWarningFormat("Invalid view ID. {0}", gameObject.name);
                viewID = gameObject.name;
            }

            _uiViewDictionary.Add(viewID, view);
        }

        #endregion
    }
}

당장에 복잡한 기능이 필요한건 아니라서 간단하게 구현을 해봤다.

UIView를 상속받은 UI 요소는 자동으로 전역 dictionary에 등록된다.
이제 TryGetView 메서드로 언제든 dictionary에 등록된 UI 요소를 ID로 접근할 수 있다.

느슨한 결합으로 구현했을 때의 장단점

가장 큰 장점은 UI 작업이 완전히 분리되었다는 것이다.
UI 작업자가 새로운 UI를 추가하거나 기존의 UI를 제거하더라도 프로그래머는 신경쓰지 않아도 된다.
그저 viewID에 대한 메뉴얼만 잘 지켜주면 된다.

하지만 viewID는 string 형식이기 때문에 언제든 실수할 수 있는데 이는 곧 단점이 된다.
가령 스크립트 내부에서 UIView를 호출하는데 viewID에 오타가 있다거나 UIView의 viewID가 빠져서 dictionary에 등록이 안된다거나 등 런타임에 발생할 수 있는 수많을 오류의 위험성을 감수해야 된다.
게다가 느슨한 결합의 경우 런타임 도중 오류의 발생을 대비해 여러 예외처리를 하는 편이기 때문에 디버깅도 어렵다.
그렇기 때문에 나는 최소한의 안전장치로 viewID가 빠졌을 경우 게임오브젝트명으로 대신 등록하도록 했다.

추가적인 개선점

Awake 이벤트 메서드는 오브젝트가 활성 상태여야 호출이 되므로 초기화에 문제가 발생할 수 있다.
그래서 초기화의 시점을 최초로 UIView를 호출하는 시점으로 수정하였다.

public static bool TryGetView(string viewID, out UIView view)
{
    if (_uiViewDictionary == null)
    {
        InitUIViewDictionary();
    }

    return _uiViewDictionary.TryGetValue(viewID, out view);
}

private static void InitUIViewDictionary()
{
    _uiViewDictionary = new Dictionary<string, UIView>();
    var canvases = FindObjectsOfType<Canvas>();
    foreach (var canvas in canvases)
    {
        var views = canvas.transform.GetComponentsInChildren<UIView>(true);
        foreach (var newView in views)
        {
            newView.AddUIView();
        }
    }
}

UI가 비활성 상태인 경우에도 dictionary에 등록돼야 한다.
따라서 FindObjectsOfType 메서드는 사용할 수 없다.
대신 씬에서 Canvas 컴포넌트를 갖는 오브젝트를 찾고 canvas의 하위 오브젝트를 대상으로 탐색을 진행했다.
GetComponentsInChildren 메서드는 includeInactive 파라미터를 true로 설정하면 비활성 상태의 오브젝트도 탐색이 가능하다.

이후 추가할 부분

상술했듯이 내 프로젝트는 당장에 복잡한 기능이 필요하지 않기 때문에 단순히 UIView를 등록하고 가져오는 기능만 추가했다.
하지만 UIView가 스택이나 큐 구조로 쌓여야 된다거나 하는 등의 추가적인 기능이 필요할 경우 별도의 컨트롤러를 만들 필요가 있을 것이다.

Data binding을 적용하여 각 Text 데이터를 로직에서 분리하는 작업은 다음 포스트에서 다루겠다.


참고 자료

Unity Korea - Dev Weeks: 작업 효율을 높이기 위한 유니티 UI 제작 프로그래밍 패턴들

profile
Runner's high를 목표로

0개의 댓글