-
[Unreal] Urneal MVVM plugin, UMG ViewModel 사용 및 후기쾌락없는 책임 (공부)/Unreal 2025. 8. 31. 10:35반응형
개요
MVVM 패턴 자체는 게임뿐 아니라 다른 곳에서도 많이 사용하는 패턴입니다. 때문에 여러 곳에서도 자료를 찾아볼 수 있을 정도라 이 글에서 해당 패턴에 대해서 설명하는 글을 기재하지는 않겠습니다. 이번에 소개할 건 에픽의 자체적인 MVVM 패턴용 플러그인인 "UMG ViewModel"입니다.
기존 개발자 입장에서도 프레임워크를 제작해 MVVM 패턴을 충분히 개발할 수 있지만 해당 플러그인은 블루프린트 지원을 많이 준비해 두었습니다. 이로 인해 가능하다면 개발자는 View Model 만을 바라보면서 개발이 가능하고 UI 디자이너는 만들어진 ViewModel로 바인딩이 가능하게 되었습니다. 물론 디자이너에게 ViewModel 관련된 지식이 요구되지만 개발자 입장에서는 일부 업무를 전가 가능해 협업에 있어 타이밍을 맞추지 않아도 View Model을 우선 개발해도 된다는 장점이 있습니다.
기본 사용법
공식 독스에서는 cpp 코드로 작업하는 부분들에 대해 설명이 부족하기에 이번 블로그 글에서는 해당 부분들에 대해서 다루려고 합니다.
Module 추가
PublicDependencyModuleNames.AddRange(new string[] { //... "ModelViewViewModel", });일단 기본적으로 Module에 이렇게 추가를 해 두어야 합니다. 이후 'UMVVMViewModelBase'가 기본 View Model 클래스이므로 이를 상속받아 사용할 수 있습니다.
ViewModel 용 매크로들을 통해 이벤트를 발생할 수 있도록 소스코드 작성
/** After a field value changed. Broadcast the event. */ #define UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(MemberName) \ BroadcastFieldValueChanged(ThisClass::FFieldNotificationClassDescriptor::MemberName) /** If the property value changed then set the new value and notify. */ #define UE_MVVM_SET_PROPERTY_VALUE(MemberName, NewValue) \ SetPropertyValue(MemberName, NewValue, ThisClass::FFieldNotificationClassDescriptor::MemberName) /** Use this version to set property values that can't be captured as a function arguments (i.e. bitfields). */ #define UE_MVVM_SET_PROPERTY_VALUE_INLINE(MemberName, NewValue) \ [this, InNewValue = (NewValue)]() { if (MemberName == InNewValue) { return false; } MemberName = InNewValue; BroadcastFieldValueChanged(ThisClass::FFieldNotificationClassDescriptor::MemberName); return true; }()UMVVMViewModelBase 클래스를 상속받으면 위와 같은 매크로들을 사용할 수 있습니다.
매크로 설명 UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED ViewModel 의 Field 가 변경되었다는 이벤트를 발생합니다 UE_MVVM_SET_PROPERTY_VALUE ViewModel 의 Field 의 Setter 함수입니다. 만일 값이 달라진 경우 이벤트를 발생합니다
(UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED 호출과 동일)UE_MVVM_SET_PROPERTY_VALUE_INLINE 위 매크로의 인라인 버전입니다. 이후 아래 예시처럼 매크로를 사용하면 됩니다.
UPROPERTY(BlueprintReadWrite, FieldNotify, Setter, Getter, meta = (AllowPrivateAccess = true)) float PlayerInventoryWeight; void SetPlayerInventoryWeight(const float NewWeight) { UE_MVVM_SET_PROPERTY_VALUE(PlayerInventoryWeight, NewWeight); } float GetPlayerInventoryWeight() const { return PlayerInventoryWeight; }기본적으로는 FieldNotify, Setter, Getter 속성을 추가한 뒤 Set {필드 이름}, Get {필드 이름}을 위와 같이 선언하면 완료됩니다. 이를 통해 Blueprint에서도 볼 수 있게 됩니다.
UPROPERTY(BlueprintReadWrite, FieldNotify, Getter, meta = (AllowPrivateAccess = true)) FItemEncodedInfo HoveredItemInfo; // Getter 함수로 인해 Get{필드 이름}() const 함수를 선언해야 합니다. FItemEncodedInfo GetHoveredItemInfo() const { return HoveredItemInfo; } // cpp 에서 이런 방식으로 Broadcast 를 호출할 수 있습니다 void UItemListViewModel::HandleItemHover(const FItemEncodedInfo& ItemInfo, bool bIsPlayerSide) { HoveredItemInfo = ItemInfo; UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(HoveredItemInfo); } // Getter 만 선언하게 되면 GetbIsContextMenuVisible 이라는 이상한 이름을 만들어야 합니다 UPROPERTY(BlueprintReadWrite, FieldNotify, Getter = "GetIsContextMenuVisible", meta = (AllowPrivateAccess = true)) bool bIsContextMenuVisible;그 외 Setter를 두기 애매한 경우 위와 같은 방법으로 해당 아이템이 변경되는 위치에서 BROADCAST 매크로를 호출해 주면 됩니다. 또한 Getter, Setter는 함수 이름을 직접 지정할 수도 있으니 함수 이름을 마음대로 지정하여도 됩니다.
Blueprint에서 사용
위 소스코드를 통해 View Model을 제작하였다면 UMG의 View Model 을 설정하여야 합니다.

해당하는 UMG 의 Blueprint에서 View Bindings, Viewmodels를 위 사진의 메뉴에서 활성화하여야 설정이 가능합니다.


이후 Viewmodels 창에서 [+Viewmodel] 메뉴를 누르면 UMG에 View Model 추가가 가능합니다. 이는 여러 개도 가능하니 구조를 짜실 때 해당 부분을 참고하여도 좋습니다.

이후 View Bindings에서 해당하는 위젯 요소들, 함수에 대해서 바인딩을 추가할 수 있습니다. 여기서 볼 건 중간의 화살표와 Auto로 되어 있는 드롭박스이며 이는 아래 표에서 설명하도록 하겠습니다.
[ 바인딩 방향 ]
바인딩 방향 설명 위젯에 한 번(One Time to Widget) 바인딩이 뷰모델에서 위젯에 한 번만 적용됩니다. 이로 인해 선택된 위젯 프로퍼티가 업데이트됩니다. 위젯으로 단방향(One Way to Widget) 바인딩은 뷰 모델에서 위젯으로만 적용됩니다. 뷰 모델에서 관련 변수를 업데이트할 때마다 위젯에 변수가 변경되었음을 알리고 선택된 위젯 프로퍼티를 업데이트합니다. 또는 함수를 선택한 경우 해당 함수를 호출하면 선택된 위젯 프로퍼티가 업데이트됩니다. 뷰 모델로 단방향(One Way to Viewmodel) 바인딩은 위젯에서 뷰 모델로만 적용됩니다. 위젯에서 사용자 또는 코드가 선택된 프로퍼티를 변경할 때마다 해당 변경사항을 뷰 모델 프로퍼티에 적용합니다. 일반적인 예시로는 사용자가 편집한 텍스트 필드 또는 그래픽 옵션이 있습니다. 양방향 바인딩이 두 방향 모두에 적용됩니다. 
[ Trigger Option ]
옵션 설명 Immediate 변수가 변경되는 즉시 이벤트를 활성화 합니다. Delayed 변수가 변경된 후 해당 프레임의 마지막에서 이벤트를 활성화 합니다. Tick 프레임의 마지막 시점에 Tick 단위로 항상 이벤트를 활성화 합니다. Auto
(DelayedWhenSharedElseImmediate)여러곳에서 트리거 된 순간 Delayed, 그 외의 경우 Immediate 처럼 동작합니다. View Model을 할당하기
이후의 문제는 UMG에서 열심히 바인딩 한 View Model 은 어디서 오느냐입니다.

Viewmodels에서 설정한 ViewModel을 클릭하면 Detail 창에서 이를 확인할 수 있으며 여기서 Creation Type에서 설정이 가능합니다.
Creation Type 설명 Manual 시작 시 View Model 은 null 로 비어 있으며 이후 할당을 해야 합니다. Create Instance 인스턴스를 자동으로 생성합니다. Global Viewmodel UMVVMGameSubsystem 을 활용해 찾아냅니다. Property Path 위젯 초기화 시 함수를 실행해 뷰 모델을 찾습니다. 애니메이션 BP 에서와 유사하게 마침표로 구분된 경로를 활용합니다. (GetPlayerController.Vechicle.ViewModel)
본인일 경우 Self 는 따로 지정하지 않습니다.Resolver Viewmodel을 제공하는 기능을 가진 객체 저의 경우 기존의 Subsystem 은 후술할 이유로 사용하지 않고 있습니다. 때문에 Manual을 통해 BP에서 설정을 해주고 있습니다. 그 외 Property Path의 경우 View Model을 할당하는게 초기화 시점이라 기본적으로 View Model 을 View(위젯)에서 할당한 게 아니라면, 또는 초기화 이후 시점에 ViewModel을 생성한다면 고려하지 않는 게 좋아 보입니다.
Create Instance는 해당 방법을 했을 때 C++에서 가져오기 위해서는 BP 함수를 거쳐야 한다는 문제 등이 있어 거의 사용하고 있지 않습니다. 저의 개인적인 견해입니다.
프로젝트의 업무 진행 절차에 따라 Creation Type을 지정하면 좋을 것입니다.
View Binding에 내 함수를 보여주고 싶어요
만일 View Binding 창에서 기본으로 정의된 함수가 아닌 내가 cpp에서 만든 위젯 함수를 보여주고 싶다면 아래와 같은 지정자를 추가하면 됩니다.
UFUNCTION(BlueprintCallable, BlueprintCosmetic) void HandleToolTipWidgetChanged(const FItemEncodedInfo& ItemInfo);
이후 View Binding에서 위젯 자체를 Add Widget 한 뒤 함수를 검색해 사용할 수 있습니다.
자체 제작한 Subsystem
기존 서브 시스템이 준비되어 있습니다만, 제가 원하는 기능은 어디서든 View Model을 제작하고 언제 어디서든 View Model 을 View(위젯) 이 할당받을 수 있는 걸 원했습니다. 때문에 기존 서브시스템의 경우 사용할 수가 없었고 이를 위해 아래 서브시스템을 추가해 사용하게 되었습니다.
// All copyrights for this code are owned by Aster. #pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "PAViewModelSubsystem.generated.h" class UPABaseViewModel; /** ViewModel이 준비되었을 때 브로드캐스트될 멀티캐스트 델리게이트입니다. */ DECLARE_MULTICAST_DELEGATE_OneParam(FOnViewModelReady, UPABaseViewModel* /*ViewModel*/); /** ViewModel을 식별하기 위한 복합 키 구조체입니다. */ USTRUCT() struct FViewModelKey { GENERATED_BODY() UPROPERTY() FName Name; UPROPERTY() TSubclassOf<UPABaseViewModel> Class; bool operator==(const FViewModelKey& Other) const; bool IsValid() const; friend uint32 GetTypeHash(const FViewModelKey& Key) { return HashCombine(GetTypeHash(Key.Name), GetTypeHash(Key.Class)); } }; template<typename T> concept CViewModel = TIsDerivedFrom<T, UPABaseViewModel>::IsDerived; /** * 프로젝트의 ViewModel을 중앙에서 관리하고, 등록을 구독하는 기능을 제공하는 독립적인 서브시스템입니다. */ UCLASS() class PROJECTPA_API UPAViewModelSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Deinitialize() override; /** 지정된 키로 ViewModel을 등록하고, 이 키를 기다리던 모든 콜백을 실행(브로드캐스트)합니다. */ void AddViewModel(const FViewModelKey& ViewModelKey, UPABaseViewModel* ViewModel); /** 특정 ViewModel이 준비되면 호출될 콜백을 등록(구독)합니다. */ void BindCallbackOnViewModelAdded(const FViewModelKey& ViewModelKey, const FOnViewModelReady::FDelegate& Callback); template<CViewModel TViewModel> TViewModel* GetViewModel(const FViewModelKey& ViewModelKey) { return (ViewModelCollection.Contains(ViewModelKey)) ? ViewModelCollection[ViewModelKey] : nullptr; } private: UPROPERTY(Transient) TMap<FViewModelKey, TObjectPtr<UPABaseViewModel>> ViewModelCollection; TMap<FViewModelKey, FOnViewModelReady> PendingCallbacks; };선언한 헤더 파일로 큰 아이디어는 다음과 같습니다.
- 언제 어디서든 View Model을 만들고 등록할 수 있기에 해당 View Model 이 생성되면 Callback 을 받아야 합니다
- 이를 구현하기 위해 View Model 을 구분할 2가지 요소 '클래스 타입, FName)'을 준비해 구분할 수 있는 요소를 제작
- FName 이 추가로 지정된 이유는 같은 타입의 View Model 이 여러 개 존재할 가능성도 있어서입니다.
- 프로젝트에서 사용 중인 View Model 은 기본적으로 DefaultName을 가지게 해 1개만 있을 수 있는 View Model의 등록을 편하게 하였습니다.
- View Model 이 언제 생성될지 알 수 없으므로 View에서 이 알림 받기 위해 Callback 들을 준비해 둡니다.
- 만일 이미 View Model 이 있다면 해당 Callback을 저장하지 않고 바로 호출해 줍니다.
void UPAViewModelSubsystem::BindCallbackOnViewModelAdded(const FViewModelKey& ViewModelKey, const FOnViewModelReady::FDelegate& Callback) { if (ViewModelKey.IsValid() == false || Callback.IsBound() == false) { return; } if (TObjectPtr<UPABaseViewModel>* FoundViewModel = ViewModelCollection.Find(ViewModelKey)) { Callback.Execute(*FoundViewModel); } else { PendingCallbacks.FindOrAdd(ViewModelKey).Add(Callback); } }위 함수가 핵심으로 View 역할을 하는 각 위젯들에서 해당 함수를 호출해 View Model을 받을 수 있도록 합니다.
마주했던 오류 사항들
아직까지는 해당 플러그인이 베타 버전이라 기존 UMG 시스템을 리팩토링 하면서 겪은 오류 사항들을 기입해 봅니다.
Property Path 가 잘 동작하지 않을 때
- 해당 옵션으로 정할 때는 디폴트에서 이미 있어야 합니다.The field for source '[위젯의 요소 이름]' exists but is not accessible at runtime.
- 위젯 요소가 BlueprintReadWrite 가 없어서 생기는 문제입니다.
- 관련 프로퍼티가 없거나 BlueprintReadOnly 인 경우에도 동일한 오류가 발생하므로 해당하는 요소는 꼭 BlueprintReadWtire로 지정해야 합니다.
View Model을 Widget BP에서 설정했는데 다시 시작하면 해당 정보가 보이지 않습니다.
- 새로운 WBP를 제작한 뒤 View Model을 할당합니다
- 그래도 사라진다면 DefaultEngine.ini의 [CoreRedirects] 부분들을 제거해 보세요.
- 왜인지는 모르나 ConstructorHelpers를 통해 해당 BP를 로드하면 바인딩이 사라지는 문제가 있습니다
사용 후기
일단 자체적으로 MVVM 프레임 워크를 만들 필요가 없이 공식의 플러그인들을 쓸 수 있는 건 큰 장점입니다. 또한 가장 큰 장점으로는 블루프린트 바인딩 시 GUI를 통해 바인딩이 가능합니다. 이 덕분에 UI 디자이너와 병행적인 업무 진행이 가능하며 더 이상 UI 코드는 UI 적인 부분만 신경을 쓰면 됩니다. 기능적이나 이벤트 바인딩 등은 View Model에서 진행하니깐요.
다만 위에서 기술한 여러 오류들이 있으며 View Model들이 UObject 들이라 모든 위젯에 대해서 (특히 ListView 안의 각 요소들이 걱정됩니다) View Model을 제작하게 되면 해당 객체들의 성능과 생명주기 관리가 걱정요소로 떠오르기도 합니다. 또한 디자이너들에게도 Binding 작업이 전가될 수 있어 프로젝트 내부 정책에 따라 개발자가 바인딩을 진행하게 될 가능성이 큽니다. 이 경우에는 더 가볍게 직접 MVVM 패턴을 적용해도 될 정도입니다. 개발자만 사용하는데 플러그인을 쓰기에는 블루프린트 지원용 함수가 많아 과한 느낌이 있거든요.
반응형'쾌락없는 책임 (공부) > Unreal' 카테고리의 다른 글
[Unreal] Unreal Engine5 Unreal Lightmass executable is outdated 오류 (1) 2026.01.18 [Unreal] Game Ability System, GAS를 써본 후기 (0) 2025.09.02 [Unreal] Unreal Engine 에서 RAII 패턴을 통해 자동으로 함수에서 EndAbility 하기 (1) 2025.08.20 [Unreal] Unreal Engine 에서 아이템 만들기 일지 (0) 2025.08.18 [Unreal] 언리얼 애니메이션 본 위치 옮기기 (0) 2025.07.20