321 lines
8.8 KiB
Markdown
321 lines
8.8 KiB
Markdown
![]() |
# 기존 UI 시스템을 MVVM으로 마이그레이션 가이드
|
||
|
|
||
|
Unity 프로젝트에서 기존 UI 시스템(BaseUi, BasePopupUi, PopupUi)을 MVVM 패턴으로 점진적으로 전환하는 방법을 설명합니다.
|
||
|
|
||
|
## 호환성 보장
|
||
|
|
||
|
### 1. 기존 UI 시스템은 그대로 유지됩니다
|
||
|
- `BaseUi`, `BasePopupUi`, `PopupUi<T>` 클래스들은 변경되지 않음
|
||
|
- 기존 UI들은 계속해서 정상 동작
|
||
|
- `UiManager`와 `PopupUiState`도 기존 방식 그대로 지원
|
||
|
|
||
|
### 2. 새로운 Integrated 클래스들 사용
|
||
|
- `IntegratedBaseUi<TViewModel>` : BaseUi + MVVM 기능 통합
|
||
|
- `IntegratedBasePopupUi<TViewModel>` : BasePopupUi + MVVM 기능 통합
|
||
|
- `IntegratedPopupUi<TInputEnum, TViewModel>` : PopupUi<T> + MVVM 기능 통합
|
||
|
|
||
|
## 마이그레이션 전략
|
||
|
|
||
|
### 단계 1: 점진적 전환 계획 수립
|
||
|
|
||
|
1. **우선순위 결정**
|
||
|
```
|
||
|
높음: 복잡한 상태 관리가 필요한 UI (InventoryView, ShopView 등)
|
||
|
중간: 중간 규모의 팝업 UI
|
||
|
낮음: 간단한 확인/알림 다이얼로그
|
||
|
```
|
||
|
|
||
|
2. **전환 범위 설정**
|
||
|
- 한 번에 하나의 UI씩 전환
|
||
|
- 관련된 ViewModel과 Service 함께 구현
|
||
|
- 철저한 테스트 후 다음 UI 진행
|
||
|
|
||
|
### 단계 2: ViewModel 및 Service 구현
|
||
|
|
||
|
#### 기존 View 분석
|
||
|
```csharp
|
||
|
// 기존 InventoryView
|
||
|
public class InventoryView : MonoBehaviour
|
||
|
{
|
||
|
private InventoryCategoryType _currentCategory;
|
||
|
private List<ItemViewModel> _items;
|
||
|
|
||
|
public void UpdateCategoryView(InventoryCategoryType category)
|
||
|
{
|
||
|
_currentCategory = category;
|
||
|
// 복잡한 UI 업데이트 로직
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
#### ViewModel로 분리
|
||
|
```csharp
|
||
|
// 새로운 InventoryViewModel
|
||
|
public class InventoryViewModel : SimpleViewModel
|
||
|
{
|
||
|
private InventoryCategoryType _currentCategory;
|
||
|
private List<ItemViewModel> _visibleItems;
|
||
|
|
||
|
public InventoryCategoryType CurrentCategory
|
||
|
{
|
||
|
get => _currentCategory;
|
||
|
set => SetField(ref _currentCategory, value);
|
||
|
}
|
||
|
|
||
|
public void SetCategory(InventoryCategoryType category)
|
||
|
{
|
||
|
CurrentCategory = category;
|
||
|
UpdateVisibleItems(); // 비즈니스 로직
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
#### Service 계층 분리
|
||
|
```csharp
|
||
|
// 비즈니스 로직을 Service로 분리
|
||
|
public class InventoryService : IUiService
|
||
|
{
|
||
|
public IEnumerable<ItemViewModel> FilterItems(InventoryCategoryType category)
|
||
|
{
|
||
|
// 복잡한 필터링 로직
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### 단계 3: View를 MVVM으로 전환
|
||
|
|
||
|
#### 기존 View 코드
|
||
|
```csharp
|
||
|
public class InventoryView : MonoBehaviour, IEventHandler<InventoryChangedEvent>
|
||
|
{
|
||
|
[SerializeField] private Transform _slotParent;
|
||
|
[SerializeField] private Text _categoryLabel;
|
||
|
|
||
|
// 많은 상태 변수들과 복잡한 로직들...
|
||
|
}
|
||
|
```
|
||
|
|
||
|
#### MVVM View로 전환
|
||
|
```csharp
|
||
|
public class MvvmInventoryView : MvvmBasePopupUi<InventoryViewModel>
|
||
|
{
|
||
|
[SerializeField] private Transform _slotParent;
|
||
|
[SerializeField] private Text _categoryLabel;
|
||
|
|
||
|
protected override void SetupBindings()
|
||
|
{
|
||
|
BindText(_categoryLabel, nameof(InventoryViewModel.CategoryDisplayText));
|
||
|
// 간단한 바인딩 설정만
|
||
|
}
|
||
|
|
||
|
public void OnCategoryButtonClicked(int categoryIndex)
|
||
|
{
|
||
|
ViewModel.SetCategory((InventoryCategoryType)categoryIndex);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### 단계 4: 마이그레이션 체크리스트
|
||
|
|
||
|
#### ViewModel 체크리스트
|
||
|
- [ ] SimpleViewModel 상속
|
||
|
- [ ] 모든 상태를 속성으로 변환
|
||
|
- [ ] SetField 사용한 PropertyChanged 알림
|
||
|
- [ ] 계산된 속성 구현
|
||
|
- [ ] 비즈니스 로직을 Service로 위임
|
||
|
|
||
|
#### View 체크리스트
|
||
|
- [ ] 적절한 MVVM Base 클래스 상속
|
||
|
- [ ] SetupBindings() 구현
|
||
|
- [ ] UI 이벤트를 ViewModel 메서드 호출로 변경
|
||
|
- [ ] 직접적인 UI 업데이트 코드 제거
|
||
|
- [ ] HandleCustomPropertyChanged에서 복잡한 UI 처리
|
||
|
|
||
|
#### Service 체크리스트
|
||
|
- [ ] IService 인터페이스 구현
|
||
|
- [ ] 복잡한 비즈니스 로직 포함
|
||
|
- [ ] 데이터 접근 로직 캡슐화
|
||
|
- [ ] 테스트 가능한 구조
|
||
|
|
||
|
## 구체적인 마이그레이션 예시
|
||
|
|
||
|
### 예시 1: 간단한 팝업 UI
|
||
|
|
||
|
#### Before (기존)
|
||
|
```csharp
|
||
|
public class SimpleDialogUi : BasePopupUi
|
||
|
{
|
||
|
[SerializeField] private Text _messageText;
|
||
|
[SerializeField] private Button _confirmButton;
|
||
|
|
||
|
public void ShowMessage(string message)
|
||
|
{
|
||
|
_messageText.text = message;
|
||
|
_confirmButton.onClick.AddListener(Close);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
#### After (MVVM)
|
||
|
```csharp
|
||
|
// ViewModel
|
||
|
public class DialogViewModel : SimpleViewModel
|
||
|
{
|
||
|
private string _message;
|
||
|
|
||
|
public string Message
|
||
|
{
|
||
|
get => _message;
|
||
|
set => SetField(ref _message, value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// View
|
||
|
public class MvvmSimpleDialogUi : MvvmBasePopupUi<DialogViewModel>
|
||
|
{
|
||
|
[SerializeField] private Text _messageText;
|
||
|
[SerializeField] private Button _confirmButton;
|
||
|
|
||
|
protected override void SetupBindings()
|
||
|
{
|
||
|
BindText(_messageText, nameof(DialogViewModel.Message));
|
||
|
}
|
||
|
|
||
|
protected override void Awake()
|
||
|
{
|
||
|
base.Awake();
|
||
|
_confirmButton.onClick.AddListener(() => Close());
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### 예시 2: 복잡한 목록 UI
|
||
|
|
||
|
#### Before (기존 InventoryView)
|
||
|
```csharp
|
||
|
public class InventoryView : MonoBehaviour
|
||
|
{
|
||
|
private InventoryCategoryType _currentCategory;
|
||
|
private Dictionary<string, ItemSlotUi> _slotLookup = new();
|
||
|
|
||
|
public void UpdateCategoryView(InventoryCategoryType category)
|
||
|
{
|
||
|
_currentCategory = category;
|
||
|
|
||
|
// 복잡한 필터링 로직
|
||
|
foreach (var slot in _slotLookup.Values)
|
||
|
{
|
||
|
bool shouldShow = MatchesCategory(slot.Model, category);
|
||
|
slot.SetActive(shouldShow);
|
||
|
}
|
||
|
|
||
|
// 정렬 로직
|
||
|
// UI 업데이트 로직
|
||
|
}
|
||
|
|
||
|
private bool MatchesCategory(ItemViewModel model, InventoryCategoryType category)
|
||
|
{
|
||
|
// 복잡한 카테고리 매칭 로직
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
#### After (MVVM)
|
||
|
```csharp
|
||
|
// ViewModel (상태 관리)
|
||
|
public class InventoryViewModel : SimpleViewModel
|
||
|
{
|
||
|
private InventoryCategoryType _currentCategory;
|
||
|
private List<ItemViewModel> _visibleItems;
|
||
|
|
||
|
public InventoryCategoryType CurrentCategory
|
||
|
{
|
||
|
get => _currentCategory;
|
||
|
set => SetField(ref _currentCategory, value);
|
||
|
}
|
||
|
|
||
|
public List<ItemViewModel> VisibleItems
|
||
|
{
|
||
|
get => _visibleItems;
|
||
|
private set => SetField(ref _visibleItems, value);
|
||
|
}
|
||
|
|
||
|
public string CategoryDisplayText => GetCategoryDisplayText();
|
||
|
|
||
|
public void SetCategory(InventoryCategoryType category)
|
||
|
{
|
||
|
CurrentCategory = category;
|
||
|
UpdateVisibleItems();
|
||
|
OnPropertyChanged(nameof(CategoryDisplayText));
|
||
|
}
|
||
|
|
||
|
private void UpdateVisibleItems()
|
||
|
{
|
||
|
var filtered = _inventoryService.FilterItems(CurrentCategory, CurrentSortType);
|
||
|
VisibleItems = filtered.ToList();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Service (비즈니스 로직)
|
||
|
public class InventoryService : IUiService
|
||
|
{
|
||
|
public IEnumerable<ItemViewModel> FilterItems(InventoryCategoryType category, InventorySortType sortType)
|
||
|
{
|
||
|
// 복잡한 필터링과 정렬 로직을 Service로 이동
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// View (UI 바인딩만)
|
||
|
public class MvvmInventoryView : MvvmBasePopupUi<InventoryViewModel>
|
||
|
{
|
||
|
[SerializeField] private Text _categoryLabel;
|
||
|
[SerializeField] private Transform _slotParent;
|
||
|
|
||
|
protected override void SetupBindings()
|
||
|
{
|
||
|
BindText(_categoryLabel, nameof(InventoryViewModel.CategoryDisplayText));
|
||
|
}
|
||
|
|
||
|
protected override void HandleCustomPropertyChanged(string propertyName)
|
||
|
{
|
||
|
if (propertyName == nameof(InventoryViewModel.VisibleItems))
|
||
|
{
|
||
|
UpdateItemSlots(); // 복잡한 UI 업데이트는 여전히 필요
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## 마이그레이션 주의사항
|
||
|
|
||
|
### 1. 기존 코드와의 의존성
|
||
|
- 기존 UI를 참조하는 다른 코드들 확인
|
||
|
- 이벤트 시스템과의 연동 유지
|
||
|
- ScriptableObject 설정 파일들과의 호환성
|
||
|
|
||
|
### 2. 성능 고려사항
|
||
|
- PropertyChanged 이벤트의 과도한 발생 방지
|
||
|
- UI 업데이트 배칭 활용
|
||
|
- 메모리 누수 방지 (이벤트 핸들러 정리)
|
||
|
|
||
|
### 3. 테스트 전략
|
||
|
- ViewModel 단위 테스트 작성
|
||
|
- 기존 UI와 새 UI 동시 테스트
|
||
|
- 사용자 시나리오 기반 통합 테스트
|
||
|
|
||
|
## 마이그레이션 우선순위 추천
|
||
|
|
||
|
### 1차 마이그레이션 (높은 우선순위)
|
||
|
- **InventoryView**: 복잡한 상태 관리, 필터링, 정렬
|
||
|
- **ShopView**: 상품 목록, 카테고리, 구매 로직
|
||
|
- **MenuView**: 메뉴 관리, 레시피 선택
|
||
|
|
||
|
### 2차 마이그레이션 (중간 우선순위)
|
||
|
- **SettingsView**: 다양한 설정 옵션들
|
||
|
- **StatisticsView**: 데이터 표시와 계산
|
||
|
|
||
|
### 3차 마이그레이션 (낮은 우선순위)
|
||
|
- **SimpleDialogUi**: 간단한 확인 다이얼로그들
|
||
|
- **NotificationUi**: 알림 팝업들
|
||
|
|
||
|
이 가이드를 따라 점진적으로 UI를 MVVM으로 전환하면, 기존 시스템의 안정성을 유지하면서도 새로운 아키텍처의 이점을 얻을 수 있습니다.
|