📘 [UE5 C++] 인벤토리 학습 노트 - 5단계: UI 연동 뷰모델 [UFTInventoryViewModel]
인벤토리 데이터를 실제 화면(View)인 UMG 위젯과 연결해 주는 인벤토리 뷰모델의 구현 원리와 MVVM 패턴을 학습하고,뷰모델이 데이터의 상태 변화를 어떻게 위젯에 감지시켜 자동 갱신하는지, 카테고리 탭 필터링, 무게 게이지 바의 동적 색상 제어, 그리고 삭제 데이터의 인덱스 예외 처리를 자세히 공부한다.
1. MVVM 아키텍처 개요 및 필요성
왜 UI 위젯이 인벤토리 컴포넌트에 직접 접근하지 않을까?
전형적인 UMG 위젯 구현 방식은 다음과 같은 단점이 있다.
1. 디자이너와 개발자의 작업 간섭: 위젯 그래프나 블루프린트 내부에 비즈니스 로직이 녹아들어가 있으면, UI 레이아웃을 고치던 디자이너가 실수로 로직을 망가뜨리거나 머지충돌이 일어나기 쉽다.
2. UI 친화적 데이터 부재: C++ 모델은 아이템 무게 데이터를 float CurrentWeight = 35.5f 형태로만 준다. 하지만 UI 화면에는 이를 "35.5 / 100.0 kg"이라는 문자열 포맷으로 그려줘야 하고, 무게가 차오름에 따라 게이지 바의 색상을 노랑, 빨강으로 바꾸는 복잡한 UI 판단 로직이 필요하다. 이를 위젯 자체에 구현하면 위젯이 지나치게 복잡해집니다.
MVVM (Model-View-ViewModel) 패턴
· Model: 순수한 인벤토리 데이터와 코어 비즈니스 로직 [UFTInventoryComponent]
· View: 화면을 구성하는 시각적 껍데기 레이아웃 [UMG Widget]
· ViewModel: 데이터를 UI 친화적 형태로 재가공하여 뷰에 실시간으로 전해주는 통로 [UFTInventoryViewModel]
2. 뷰모델의 상태 동기화 및 갱신 흐름
1) 컴포넌트 바인딩 및 변경 알림
[FTInventoryViewModel.cpp]에서 컴포넌트의 데이터가 바뀔 때(OnInventoryChanged 브로드캐스트 시) 뷰모델이 이를 감지하여 자신의 상태를 가공하도록 연결한다.
void UFTInventoryViewModel::Initialize(UFTInventoryComponent* InInventoryComponent)
{
if (!InInventoryComponent) return;
// 이전에 다른 인벤토리에 바인딩돼 있었다면 정리(폰 교체/리스폰 시 스테일 콜백 방지)
if (UFTInventoryComponent* Prev = LinkedInventory.Get())
{
if (Prev != InInventoryComponent)
{
Prev->OnInventoryChanged.RemoveDynamic(this, &UFTInventoryViewModel::NotifyChanged);
}
}
LinkedInventory = InInventoryComponent;
// 인벤토리 변경 시 뷰모델이 감지하여 자동 동기화하도록 델리게이트 바인딩(중복 바인딩 방지)
InInventoryComponent->OnInventoryChanged.AddUniqueDynamic(this, &UFTInventoryViewModel::NotifyChanged);
// 초기 상태 갱신
NotifyChanged();
}
2) UI 친화적 데이터 가공 및 리프레시
데이터가 바뀌었을 때 실행되는 함수로, 데이터를 화면에 띄울 규격에 맞게 변환하고 UI 위젯에게 데이터가 바뀌었으니 바인딩된 변수들을 다시 읽어가라고 요청한다.
void UFTInventoryViewModel::NotifyChanged()
{
UFTInventoryComponent* Inventory = LinkedInventory.Get();
if (!Inventory) return;
// 1. 무게 데이터 동기화
CurrentWeight = Inventory->GetCurrentWeight();
MaxWeight = Inventory->GetMaxWeight();
// 2. 현재 선택된 상단 카테고리 탭에 따라 슬롯 아이템 ID 리스트 필터링
InventoryItems.Empty();
TArray<FFTInventoryItem> FilteredItems = Inventory->GetItemsByCategory(CurrentCategory);
for (const FFTInventoryItem& Item : FilteredItems)
{
InventoryItems.Add(Item.ItemId);
}
// 3. 우측 상세 패널 정보 동기화
if (SelectedItemIndex != INDEX_NONE)
{
TArray<FFTInventoryItem> CurrentCategoryItems = Inventory->GetItemsByCategory(CurrentCategory);
if (CurrentCategoryItems.IsValidIndex(SelectedItemIndex))
{
SelectedItemDetail = CurrentCategoryItems[SelectedItemIndex];
SelectedItem = SelectedItemDetail.ItemId;
}
else
{
// 소모 등으로 유효 인덱스가 무너졌을 경우 선택 비우기
SelectedItemDetail = FFTInventoryItem();
SelectedItem = NAME_None;
SelectedItemIndex = INDEX_NONE;
}
}
else
{
SelectedItemDetail = FFTInventoryItem();
SelectedItem = NAME_None;
}
// 4. 아이템 삭제 및 소진으로 인해 유효하지 않게 된 다중 체크 인덱스 소거
TArray<FFTInventoryItem> FullItems = Inventory->GetItems();
TArray<int32> InvalidIndices;
for (int32 Index : SelectedIndices)
{
if (!FullItems.IsValidIndex(Index))
{
InvalidIndices.Add(Index);
}
}
for (int32 Index : InvalidIndices)
{
SelectedIndices.Remove(Index);
}
// 5. UI 변경 공지 전송
OnViewModelChanged.Broadcast();
}
인벤토리 상태 변경에 따른 인덱스 예외 처리: 좀비 인덱스 소거 구현 ([ NotifyChanged]의 4번)
이렇게 진행하면 아이템 삭제 후에도 예전 인벤토리 인덱스를 가리키고 있어 엉뚱한 아이템이 다시 체크되거나 범위 초과로 크래시가 발생하는 것을 차단할 수 있다.
3. UI 맞춤형 실시간 데이터 가공 구현
1) 게이지 비율 및 색상 제어 (UI 로직의 뷰모델 내재화)
단순 수치를 전달하는 것이 아니라, 위젯에서 사용할 진행률 백분율(0.0~1.0)과 위험도를 나타낼 UI 전용 선형 색상을 계산해준다. 이런 식으로 진행하면 UI 디자인 시 위젯 내에서 "게이지 진행률"과 "색상"에 이 함수들을 단순 바인딩만 해놓으면, 수치가 늘어남에 따라 게이지바가 차오르며 자동으로 붉은색으로 바뀌는 UI 효과를 C++ 로직 처리만으로 무리 없이 구현할 수 있다.
FLinearColor UFTInventoryViewModel::GetWeightBarColor() const
{
const float Percent = GetWeightPercent();
if (Percent >= 0.9f)
{
return FLinearColor::Red; // 90% 이상 위험
}
else if (Percent >= 0.7f)
{
return FLinearColor::Yellow; // 70% 이상 경고
}
return FLinearColor::Green; // 안정 상태
}
float UFTInventoryViewModel::GetWeightPercent() const
{
if (MaxWeight <= 0.0f) return 0.0f;
return FMath::Clamp(CurrentWeight / MaxWeight, 0.0f, 1.0f);
}
블로그 정리용 Self-Q&A
Q1. UI 위젯이 C++ 클래스(Model)를 직접 가리켜 변경하는 기존 결합 방식에 비해 MVVM이 디버깅 측면에서 왜 강점인가요?
· A: UI 버그가 발생했을 때 기존 방식은 UMG 블루프린트 그래프, 위젯 바인딩 함수, 그리고 액터 코드를 모두 열어봐야 했다. 하지만 MVVM 패턴을 사용하면 UI는 뷰모델 변수를 단순히 그리기만 한다는 전제가 보장된다. 따라서 뷰모델의 NotifyChanged 함수 근처에 디버그 중단점을 잡아두고 뷰모델의 상태가 올바르게 재가공되는지만 추적하면 되므로, 데이터의 흐름 추적이 매우 정밀하고 빠르게 끝난다.
Q2. 뷰모델의 OnViewModelChanged 브로드캐스트가 성능상 렉을 유발할 수 있나요?
· A: 매 프레임 호출하는 Tick 방식이 아닌, 데이터에 실제 변경이 일어난 특정 사건(Event-Driven) 시점에만 제한적으로 델리게이트를 발행하기 때문에 CPU 소모율이 극도로 낮다. 데이터가 바뀌지 않는 대기 상태에서는 리프레시 연산이 전혀 발생하지 않아 매우 효율적인 방식이다.
'내배캠 > CH3 팀프로젝트(ProjectFT)' 카테고리의 다른 글
| [UE5 C++] WorldSubsystem과 GameplayMessage를 활용한 드롭 아이템 스폰 및 오브젝트 풀링(Pooling) 최적화 (0) | 2026.07.09 |
|---|---|
| [UE5 C++] 인터페이스를 활용한 필드 아이템 상호작용 및 물리 설정 (0) | 2026.07.08 |
| [UE5 C++] 컴포넌트 패턴으로 구현하는 인벤토리 핵심 기능과 데이터 동기화 (0) | 2026.07.07 |
| [UE5 C++] 데이터 기반 아이템 시스템 구축: UDataAsset과 AssetManager 활용법 (0) | 2026.07.06 |
| [TIL: 2026.06.17] CH3 팀프로젝트 ProjectFT의 아키텍처 분석 (0) | 2026.06.17 |