ProjectDDD/Assets/_DDD/_Scripts/GameUi/New/README.md
2025-08-20 15:22:08 +09:00

7.3 KiB

Unity MVVM 시스템 가이드

Unity에서 MVVM(Model-View-ViewModel) 패턴을 구현한 시스템입니다. Attribute 기반 데이터 바인딩과 nameof를 활용한 타입 안전한 구조를 제공합니다.

폴더 구조

GameUi/New/
├── ViewModels/           # ViewModel 클래스들
│   ├── Base/            # 기본 ViewModel 클래스
│   │   └── SimpleViewModel.cs
│   └── InventoryViewModel.cs    # 예시 ViewModel
├── Views/               # View 클래스들
│   └── Base/
│       └── AutoBindView.cs     # 자동 바인딩 View 기본 클래스
├── Services/            # 서비스 계층 클래스들
│   ├── IService.cs      # 서비스 인터페이스
│   └── InventoryService.cs     # 예시 서비스
├── Utils/               # 유틸리티 클래스들
│   ├── BindToAttribute.cs      # 바인딩 Attribute
│   └── BindingContext.cs       # 바인딩 컨텍스트
└── Converters/          # 값 변환기들
    ├── IValueConverter.cs      # 컨버터 인터페이스
    └── CommonConverters.cs     # 공통 컨버터들

핵심 클래스

1. SimpleViewModel

  • ViewModel의 기본 클래스
  • INotifyPropertyChanged 구현
  • 속성 변경 알림 자동 처리
  • 배치 업데이트 지원

2. IntegratedBaseUi

  • UI의 기본 클래스 (BaseUi + MVVM 기능 통합)
  • Attribute 기반 자동 바인딩
  • ViewModel과 UI 요소 자동 연결

3. BindToAttribute

  • UI 요소를 ViewModel 속성에 바인딩
  • nameof() 사용으로 타입 안전성 보장
  • 컨버터 지원

사용법

1. ViewModel 생성

namespace DDD.MVVM
{
    public class MyViewModel : SimpleViewModel
    {
        private string _title = "기본 제목";
        private int _count = 0;
        private bool _isVisible = true;

        public string Title
        {
            get => _title;
            set => SetField(ref _title, value);
        }

        public int Count
        {
            get => _count;
            set => SetField(ref _count, value);
        }

        public bool IsVisible
        {
            get => _isVisible;
            set => SetField(ref _isVisible, value);
        }

        public string CountText => $"개수: {Count}";

        public void IncrementCount()
        {
            Count++;
            OnPropertyChanged(nameof(CountText));
        }
    }
}

2. View 생성

namespace DDD
{
    public class MyView : IntegratedBaseUi<MyViewModel>
    {
        [SerializeField, BindTo(nameof(MyViewModel.Title))]
        private Text _titleText;

        [SerializeField, BindTo(nameof(MyViewModel.CountText))]
        private Text _countText;

        [SerializeField, BindTo(nameof(MyViewModel.IsVisible))]
        private GameObject _panel;

        // UI 이벤트 핸들러
        public void OnIncrementButtonClicked()
        {
            ViewModel.IncrementCount();
        }

        public void OnToggleVisibilityClicked()
        {
            ViewModel.IsVisible = !ViewModel.IsVisible;
        }
    }
}

3. 컨버터 사용

[SerializeField, BindTo(nameof(MyViewModel.IsVisible), typeof(InvertBoolConverter))]
private GameObject _hiddenPanel;

[SerializeField, BindTo(nameof(MyViewModel.Count), typeof(ItemCountConverter))]
private Text _formattedCountText;

기존 InventoryView 변환 예시

기존 코드 (InventoryView)

public class InventoryView : MonoBehaviour
{
    private InventoryCategoryType _currentCategory = InventoryCategoryType.Food;
    
    public void UpdateCategoryView(InventoryCategoryType category)
    {
        _currentCategory = category;
        // 복잡한 UI 업데이트 로직...
    }
}

MVVM 변환 후

InventoryViewModel.cs

namespace DDD.MVVM
{
    public class InventoryViewModel : SimpleViewModel
    {
        private InventoryCategoryType _currentCategory = InventoryCategoryType.Food;
        
        public InventoryCategoryType CurrentCategory
        {
            get => _currentCategory;
            set => SetField(ref _currentCategory, value);
        }

        public string CategoryDisplayText => CurrentCategory switch
        {
            InventoryCategoryType.Food => "음식",
            InventoryCategoryType.Drink => "음료",
            _ => "전체"
        };

        public void SetCategory(InventoryCategoryType category)
        {
            CurrentCategory = category;
            OnPropertyChanged(nameof(CategoryDisplayText));
        }
    }
}

InventoryView.cs

namespace DDD.MVVM
{
    public class InventoryView : AutoBindView<InventoryViewModel>
    {
        [SerializeField, BindTo(nameof(InventoryViewModel.CategoryDisplayText))]
        private Text _categoryLabel;

        public void OnCategoryButtonClicked(int categoryIndex)
        {
            ViewModel.SetCategory((InventoryCategoryType)categoryIndex);
        }
    }
}

장점

1. 타입 안전성

  • nameof() 사용으로 컴파일 타임 검증
  • 속성명 변경 시 자동 업데이트
  • IDE IntelliSense 지원

2. Unity 통합성

  • Inspector에서 바인딩 확인
  • MonoBehaviour 패턴 유지
  • 기존 Unity 워크플로우와 호환

3. 유지보수성

  • View와 비즈니스 로직 분리
  • 테스트 가능한 ViewModel
  • 재사용 가능한 컴포넌트

4. 개발 생산성

  • 자동 바인딩으로 보일러플레이트 코드 감소
  • 데이터 변경 시 UI 자동 업데이트
  • 일관된 개발 패턴

컨버터 목록

기본 컨버터들

  • InvertBoolConverter: 불린 값 반전
  • ItemCountConverter: 숫자를 "아이템 수: N" 형식으로 변환
  • InventoryCategoryConverter: 카테고리 열거형을 한국어로 변환
  • CurrencyConverter: 숫자를 통화 형식으로 변환
  • PercentageConverter: 소수를 백분율로 변환

커스텀 컨버터 생성

public class CustomConverter : IValueConverter
{
    public object Convert(object value)
    {
        // 변환 로직 구현
        return convertedValue;
    }

    public object ConvertBack(object value)
    {
        // 역변환 로직 구현 (선택사항)
        return originalValue;
    }
}

베스트 프랙티스

1. ViewModel 설계

  • 단일 책임 원칙 준수
  • UI 관련 Unity API 직접 사용 금지
  • 계산된 속성 적극 활용
  • 이벤트를 통한 느슨한 결합

2. 바인딩 설정

  • nameof() 사용 필수
  • 적절한 컨버터 활용
  • 복잡한 로직은 ViewModel에서 처리

3. 성능 고려사항

  • 배치 업데이트 활용
  • 불필요한 PropertyChanged 이벤트 방지
  • 컬렉션 변경 시 효율적인 업데이트

마이그레이션 가이드

단계별 적용 방법

1단계: 기존 View에서 ViewModel 분리

  1. View의 상태 변수들을 ViewModel로 이동
  2. 비즈니스 로직을 Service로 분리
  3. UI 업데이트 로직을 바인딩으로 대체

2단계: 자동 바인딩 적용

  1. AutoBindView<TViewModel> 상속
  2. UI 요소에 BindTo Attribute 추가
  3. 수동 UI 업데이트 코드 제거

3단계: 최적화 및 리팩토링

  1. 컨버터 활용으로 로직 단순화
  2. 계산된 속성으로 중복 제거
  3. 이벤트 시스템과 통합

이 MVVM 시스템을 통해 Unity UI 개발의 생산성과 유지보수성을 크게 향상시킬 수 있습니다.