.NET Community Toolkit 기반 MVVM 실전 활용법

.NET Community Toolkit의 MVVM 도구들을 활용해 크로스플랫폼 애플리케이션을 구축하는 방법을 살펴본다. 이 글에서는 ObservableObject, RelayCommand, ObservableValidator 등 핵심 요소의 실제 활용 방식과 여러 .NET 플랫폼에서의 적용 사례를 다다.

MVVM 패턴의 구성 요소

MVVM은 사용자 인터페이스를 효과적으로 분리하는 아키텍처 패턴이다. Model은 데이터와 비즈니스 규칙을 담당하고, View는 시각적 표현을, ViewModel은 둘 사이의 상태 관리와 상호작용을 중재한다. 이러한 분리 덕분에 UI 로직을 단위 테스트하기가 수월해지며, 여러 플랫폼에서 재사용 가능한 코드베이스를 구성할 수 있다.

ObservableObject로 속성 변경 알림 구현

INotifyPropertyChanged를 직접 구현하는 대신,ObservableObject를 상속받아 보일러플레이트 코드를 줄일 수 있다. SetProperty 메서드가 속성 값의 실제 변경 여부를 감지하고, 변경된 경우에만 이벤트를 발동시킨다.

public sealed class ProfileEditorVm : ObservableObject
{
    private string _nickname = string.Empty;

    public string Nickname
    {
        get => _nickname;
        set => SetProperty(ref _nickname, value);
    }
}

명령 패턴의 동기 및 비동기 처리

사용자 액션을 ViewModel에서 처리하려면 ICommand 구현체가 필요하다. RelayCommand는 동기 작업에, AsyncRelayCommand는 비동기 작업에 각각 최적화되어 있으며, 실행 가능 여부를 판단하는 CanExecute 조건을 선택적으로 지정할 수 있다.

public sealed class DocumentLoaderVm : ObservableObject
{
    public IRelayCommand<string> FetchDocumentCmd { get; }
    public IAsyncRelayCommand<string> FetchDocumentAsyncCmd { get; }

    public DocumentLoaderVm()
    {
        FetchDocumentCmd = new RelayCommand<string>(OpenLocalFile);
        FetchDocumentAsyncCmd = new AsyncRelayCommand<string>(DownloadRemoteFileAsync);
    }

    private void OpenLocalFile(string filePath) { }
    private async Task DownloadRemoteFileAsync(string url) { }
}

ObservableValidator를 활용한 입력 검증

데이터 주석(Data Annotations)과 결합하여 폼 검증 로직을 선언적으로 기술할 수 있다. ValidateAllProperties 메서드로 전체 속성을 한 번에 검증하거나, 개별 속성 변경 시 자동 검증을 트리거할 수 있다.

public sealed class EnrollmentVm : ObservableValidator
{
    [Required(ErrorMessage = "이메일은 필수 입력값입니다")]
    [EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다")]
    public string SubscriberEmail
    {
        get => _subscriberEmail;
        set => SetProperty(ref _subscriberEmail, value, true);
    }

    public void SubmitEnrollment()
    {
        ValidateAllProperties();
        if (HasErrors) return;
        // 이후 제출 로직
    }
}

IMessenger 기반 느슨한 결합 통신

ViewModel 간 직접 참조 없이 메시지를 주고받을 수 있다. WeakReferenceMessenger는 수신자가 GC에 의해 수집될 경우 자동으로 등록을 해제하므로 메모리 누수 위험이 적다.

// 메시지 정의
public sealed record SessionExpiredMessage(Guid SessionId);

// 발신 측
WeakReferenceMessenger.Default.Send(new SessionExpiredMessage(currentSession));

// 수신 측 등록
WeakReferenceMessenger.Default.Register<DashboardVm, SessionExpiredMessage>(this, (vm, msg) =>
{
    vm.ForceLogout();
});

의존성 역전과 서비스 구성

MVVM-Samples는 Microsoft.Extensions.DependencyInjection을 기반으로 서비스 수명주기를 관리한다. 각 플랫폼의 진입점에서 호스트를 구성하고, ViewModel을 transient로 등록하여 상태 공유 문제를 방지한다.

// MauiProgram.cs
var builder = MauiApp.CreateBuilder();
builder.Services.AddSingleton<ISettingsRepository, JsonSettingsRepository>();
builder.Services.AddTransient<SettingsPanelVm>();

다중 플랫폼 프로젝트 구조

공유 코드는 .NET Standard 라이브러리로 분리하고, 각 플랫폼 프로젝트는 해당 라이브러리를 참조하는 방식으로 구성된다. 이를 통해 MAUI, UWP, Xamarin.Forms 각각의 네이티브 기능을 활용하면서도 비즈니스 로직을 재사용할 수 있다.

경로역할
samples/MvvmSample.Core/공통 모델, 서비스 인터페이스, ViewModel
samples/MvvmSampleMAUI/iOS/Android/Windows/macOS 공용 UI
samples/MvvmSampleUwp/Windows 특화 UI 및 API 활용
samples/MvvmSampleXF/레거시 Xamarin.Forms 구현체

실전 적용 시 고려사항

Command의 CanExecute 조건이 복잡해지면 ObservableProperty와 결합하여 상태를 명시적으로 추적하는 것이 유지보수에 유리하다. 또한 Messenger 사용 시 메시지 타입을 너무 세분화하면 오히려 가독성이 떨어지므로, 관심사별로 적절히 그룹화하는 것이 바람직하다.

대규모 애플리케이션에서는 ViewModel의 생성자 파라미터가 많아지는 현상을 방지하기 위해, 필요한 서비스만 주입받는 중간 계층을 도입하거나 팩토리 패턴을 고려할 수 있다. 또한 Source Generators를 활용하면 ObservableProperty 등의 반복적인 코드를 컴파일 시점에 자동 생성하여 런타임 성능과 개발 생산성을 동시에 확보할 수 있다.

태그: MVVM .NET Community Toolkit ObservableObject RelayCommand ObservableValidator

6월 11일 01:36에 게시됨