- 이재웅, 구양 MVP 저
- WPF 프로젝트 아키텍처
- 유튜브 실습 영상
- WPF INSIDE OUT Book (1/4)
- WPF INSIDE OUT Book (2/4)
- WPF INSIDE OUT Book (3/4)
- WPF INSIDE OUT Book (4/4)
- XAML-Based 기반의 플랫폼과 크로스 플랫폼
- 크로스플랫폼을 고려한 닷넷 선택
- 뷰/뷰모델 연결 전략
- 프레임워크 설계를 위한 필수 기능
- 분산된 프로젝트 관리를 위한 Bootstrapper 설계
XAML(eXtensible Application Markup Language)은 다양한 플랫폼에서 사용되는 UI 마크업 언어로, 여러 프레임워크의 기반이 되었습니다.
- WPF (Windows Presentation Foundation)
- Silverlight
- Xamarin
- UWP (Universal Windows Platform)
- WinUI3
- MAUI
- UnoPlatform
- AvaloniaUI
- OpenSilver
WPF는 XAML 기반의 다양한 플랫폼 기술에 많은 영향을 주었고, 특히 데스크톱 환경에서 매우 밀접한 관계를 형성하고 있습니다.
- UnoPlatform: WinUI3와 UWP와 동일한 Microsoft.Xaml.dll 라이브러리 사용
- AvaloniaUI: WPF의 핵심 기능을 계승하면서 독자적인 스타일링 방식 발전
- OpenSilver: WPF 방식을 고수하면서 웹 플랫폼으로 확장
이러한 크로스 플랫폼 프레임워크들은 기존의 WPF 프로젝트를 전환하기에도 매우 효과적입니다.
크로스 플랫폼 개발을 위해서는 적절한 .NET 버전 선택이 중요합니다.
- .NET
- .NET Standard 2.0
- .NET Standard 2.1
- 크로스 플랫폼 지원: macOS, Linux 지원을 위해 .NET Framework 제외
- 호환성: .NET Standard 2.0 사용 시 .NET Framework 기반 WPF와도 호환 가능
- 최신 기능: .NET 또는 .NET Standard 2.1 사용 시 최신 기능 활용 가능
.NET Standard 2.0을 기반으로 하면 다양한 플랫폼에서의 호환성을 확보하면서도 .NET Framework 기반 WPF 프로젝트와의 호환성도 유지할 수 있습니다.
WPF에서는 다양한 뷰와 뷰모델 연결 전략이 사용됩니다. 각 방식의 장단점을 이해하고 프로젝트에 적합한 방식을 선택하는 것이 중요합니다.
이 방식은 코드 비하인드에서 직접 뷰모델을 인스턴스화하고 DataContext에 할당합니다.
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
장점:
- 간단하고 직관적인 구현
- 뷰모델 생성 시점을 명확하게 제어 가능
- 필요한 인스턴스를 아규먼트로 전달 가능
단점:
- 뷰와 뷰모델 간의 강한 결합 발생
- 단위 테스트 시 뷰모델 모킹이 어려움
- 아규먼트에 대한 제약이 없어 관리가 어려울 수 있음
XAML에서 직접 뷰모델을 인스턴스화하여 DataContext로 설정합니다.
<Window x:Class="MyApp.MainWindow"
xmlns:local="clr-namespace:MyApp.ViewModels">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<!-- Window content -->
</Window>
장점:
- XAML에서 인텔리센스 지원으로 바인딩 오류 감소
- 디자이너에서 실제 데이터 바인딩 확인 가능
- 뷰와 뷰모델의 관계를 명확하게 표현
단점:
- 뷰모델 생성 시 외부 의존성 주입의 어려움
- 복잡한 초기화 로직 구현이 제한적
이 방식은 코드 비하인드에서 뷰모델을 생성하면서 필요한 의존성을 직접 주입합니다.
public MainWindow()
{
InitializeComponent();
var dataService = new DataService();
var loggingService = new LoggingService();
DataContext = new MainViewModel(dataService, loggingService);
}
장점:
- 뷰모델 생성 시 필요한 의존성을 명시적으로 전달 가능
- 복잡한 초기화 로직 구현 가능
- 런타임에 다양한 설정으로 뷰모델 생성 가능
단점:
- 뷰가 뷰모델의 의존성을 알아야 함
- 의존성이 늘어날수록 생성자가 복잡해짐
DI 컨테이너를 사용하여 뷰모델과 그 의존성을 관리합니다.
public MainWindow()
{
InitializeComponent();
DataContext = ServiceProvider.GetService<MainViewModel>();
}
장점:
- 뷰모델 및 의존성의 생명주기를 일관되게 관리
- 단위 테스트 용이성 크게 향상
- 결합도 감소 및 유연성 증가
- 의존성 관리의 중앙화로 유지보수성 향상
단점:
- 초기 설정이 다소 복잡할 수 있음
- DI 컨테이너에 대한 이해가 필요
각 전략은 프로젝트의 규모, 복잡도, 팀의 숙련도 등에 따라 적합성이 달라질 수 있습니다. 소규모 프로젝트에서는 직접 할당 방식이 간단하고 효과적일 수 있지만, 대규모 또는 장기 프로젝트에서는 DI를 활용한 방식이 유지보수성과 확장성 측면에서 이점을 제공할 수 있습니다.
의존성 주입은 현대적인 애플리케이션 아키텍처의 핵심 요소입니다. .NET에서는 다양한 방식으로 DI를 구현할 수 있습니다.
Microsoft.Extensions.DependencyInjection은 .NET의 공식 DI 컨테이너로, 다양한 서비스 등록 방식과 생명주기 관리 기능을 제공합니다.
public partial class App : Application
{
public App()
{
var services = new ServiceCollection();
services.AddTransient<JamesManager>();
var provider = services.BuildServiceProvider();
}
}
public class JamesManager
{
public string Name { get; set; }
}
AddTransient<TService, TImplementation>()
: 서비스가 요청될 때마다 새 인스턴스 생성AddScoped<TService, TImplementation>()
: 요청(일반적으로 웹 요청) 범위 내에서 동일한 인스턴스 사용AddSingleton<TService, TImplementation>()
: 애플리케이션 수명 동안 단일 인스턴스 사용
이 방식의 장점은 .NET 생태계와의 높은 통합성, 다양한 생명주기 옵션, 그리고 확장성입니다.
Microsoft.Extensions.DependencyInjection은 다음과 같은 많은 .NET 프레임워크 및 라이브러리에서 기본 DI 컨테이너로 사용됩니다:
- ASP.NET Core (웹 애플리케이션 및 API)
- Entity Framework Core (ORM)
- .NET MAUI (크로스 플랫폼 모바일/데스크톱 앱)
- Blazor (웹 UI 프레임워크)
- gRPC (원격 프로시저 호출)
- SignalR (실시간 웹 기능)
- Azure Functions
- Worker Service
- WPF/Windows Forms (.NET Core 3.0 이상)
이 광범위한 사용은 Microsoft.Extensions.DependencyInjection이 .NET 생태계에서 표준으로 자리 잡았음을 보여줍니다.
CommunityToolkit.Mvvm은 기본적으로 의존성 주입을 제공하지 않지만, 유연한 DI 설정을 지원합니다. Ioc.Default
를 사용하여 선택한 DI 컨테이너를 구성할 수 있습니다.
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
// 서비스 컬렉션 생성 및 구성
var services = new ServiceCollection();
services.AddTransient<IDataService, DataService>();
services.AddTransient<MainViewModel>();
// 서비스 프로바이더 생성
var serviceProvider = services.BuildServiceProvider();
// Ioc.Default 구성
Ioc.Default.ConfigureServices(serviceProvider);
// 사용 예
var viewModel = Ioc.Default.GetService<MainViewModel>();
이 방식의 장점은 WPF와 같은 데스크톱 애플리케이션에 특화된 방식으로 DI를 구성할 수 있다는 점입니다. CommunityToolkit.Mvvm은 XAML 기반 애플리케이션의 특성을 고려하여 설계되었으며, 뷰모델 생성, 속성 변경 통지, 커맨드 처리 등 MVVM 패턴의 핵심 요소들을 효율적으로 구현할 수 있는 도구를 제공합니다. 이를 통해 WPF 애플리케이션의 아키텍처를 더욱 깔끔하고 유지보수가 용이하게 구성할 수 있습니다.
간단한 DI 컨테이너를 직접 구현할 수도 있습니다. 이를 두 단계로 나누어 설명하겠습니다.
먼저, IServiceProvider 인터페이스의 핵심인 GetService 메서드를 구현합니다:
public class JamesContainer : IServiceProvider
{
private readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
public object? GetService(Type serviceType)
{
if (_services.TryGetValue(serviceType, out var service))
{
return service;
}
var constructor = serviceType.GetConstructors().FirstOrDefault();
if (constructor == null)
{
return null;
}
var parameters = constructor.GetParameters()
.Select(p => GetService(p.ParameterType))
.ToArray();
var instance = Activator.CreateInstance(serviceType, parameters);
_services[serviceType] = instance;
return instance;
}
public T GetService<T>() where T : class
{
return (T)GetService(typeof(T));
}
}
이 GetService 구현의 주요 특징:
- 등록된 서비스를 Dictionary에서 검색
- 서비스가 없으면 해당 타입의 인스턴스를 생성
- 생성자 매개변수에 대해 재귀적으로 의존성 해결
- 제네릭 버전의 GetService도 제공하여 사용 편의성 향상
다음으로, 서비스를 등록하기 위한 AddSingleton 메서드를 추가합니다:
public class JamesContainer : IServiceProvider
{
// 기존 코드 유지
public void AddSingleton<T>(T implementation) where T : class
{
_services[typeof(T)] = implementation;
}
public void AddSingleton<TInterface, TImplementation>()
where TInterface : class
where TImplementation : class, TInterface
{
_services[typeof(TInterface)] = GetService(typeof(TImplementation));
}
}
AddSingleton 구현의 주요 특징:
- 구체적인 인스턴스를 직접 등록하는 버전
- 인터페이스와 구현 타입을 분리하여 등록하는 버전
- 싱글톤 패턴을 구현하여 항상 동일한 인스턴스 반환
이러한 DI 컨테이너 구현은 기본적인 의존성 주입 기능을 제공하며, 프로젝트의 요구사항에 따라 더 확장할 수 있습니다. 예를 들어, 생명주기 관리(Transient, Scoped 등)나 팩토리 패턴 지원 등의 기능을 추가할 수 있습니다.
이 방식을 사용하면 뷰와 뷰모델 간의 결합도를 낮추고, 테스트 용이성을 높일 수 있습니다.
프로젝트를 분산시키게 되면 이것들을 관리할 체계가 필요합니다. Application을 확장하여 관리하는 대신, Application에 의존하지 않는 Bootstrapper를 설계하는 것이 효과적입니다.
- 애플리케이션 초기화 프로세스 통합
- 모듈 및 서비스 등록 중앙화
- 플랫폼 독립적인 초기화 로직 구현
public class XamlBootstrapper
{
public void Initialize()
{
// 공통 초기화 로직
}
public void RegisterServices(IServiceCollection services)
{
// 서비스 등록
}
public void ConfigureApplication()
{
// 애플리케이션 설정
}
}
WPF 기술을 다양한 XAML 기반 플랫폼에서 효과적으로 활용하기 위한 전략은 크로스 플랫폼 개발의 핵심입니다. 이를 통해 코드 재사용성을 높이고 개발 효율성을 극대화할 수 있습니다.
이 프로젝트들은 XAML-Based 기반의 세 가지 플랫폼(WPF, Uno Platform, WinUI3)에서 WPF 기술을 기준으로 동일한 기술과 프레임워크 아키텍처를 구현한 사례입니다. 이 프로젝트들은 앞서 다룬 DI를 비롯한 WPF 프레임워크의 핵심 기술을 기반으로, .NET Standard 2.0 기반의 단일 프레임워크를 모든 플랫폼에서 구현했습니다.
이 연구 성과는 .NET Foundation에서 인정받아 소개되기도 했습니다. 특히, 리그 오브 레전드의 뛰어난 UX를 CustomControl로 표현하여 다양하고 풍부한 CustomControl 예제를 제공합니다.
리그 오브 레전드 프로젝트에서 WPF, Uno Platform, WinUI3를 선택한 이유는 다음과 같습니다:
- VSM 지원: 세 플랫폼 모두 VisualStateManager를 제공하여 일관된 상태 관리 로직 구현 가능
- Microsoft.Xaml의 WPF 핵심 계승: WinUI3와 Uno Platform은 Microsoft.Xaml.dll을 사용하여 WPF의 핵심 기능을 거의 동일하게 제공
- 높은 호환성: UWP, WinUI3, Uno Platform 간의 호환성이 높아 코드 재사용성 극대화
- 크로스 플랫폼 지원: Uno Platform을 통해 iOS, Android, WebAssembly 등 다양한 플랫폼 지원 가능
이러한 플랫폼 선택은 WPF 기술을 기반으로 한 크로스 플랫폼 개발 전략의 핵심입니다. Microsoft.Xaml을 공유함으로써 WPF의 강력한 기능을 다른 플랫폼에서도 최대한 활용할 수 있으며, VSM을 통해 일관된 UI 상태 관리가 가능해집니다.
IValueConverter는 WPF 외의 프로젝트에서 Trigger를 제공하지 않는 상황에서 특히 중요한 대체 전략입니다. WPF의 강력한 기능을 다른 플랫폼에서 그대로 활용하기 위해 IValueConverter의 활용을 극대화하면, 크로스 플랫폼 확장에 있어 큰 이점을 가질 수 있습니다.
IValueConverter를 통해 다음과 같은 이점을 얻을 수 있습니다:
- 플랫폼 간 데이터 표현 차이 추상화
- 복잡한 로직을 뷰에서 분리하여 관리 용이성 증대
- 재사용 가능한 변환 로직 구현으로 코드 중복 감소
- Trigger 부재 시 조건부 UI 변경을 위한 대안 제공
VisualStateManager(VSM)는 WPF 외의 프로젝트에서 Trigger를 대체하는 핵심 기술입니다. VSM을 활용함으로써 WPF의 상태 기반 UI 변경 로직을 다른 XAML 기반 플랫폼에서도 효과적으로 구현할 수 있습니다.
VSM의 장점:
- 플랫폼 독립적인 상태 관리 로직 구현 가능
- 성능 최적화된 상태 전환 메커니즘 제공
- 디자이너와 개발자 간 협업 용이성 증대
- 복잡한 UI 상태 관리의 단순화
VSM의 역사는 흥미롭습니다. Silverlight에서 처음 도입되어 웹에서의 성능 최적화를 위해 개발되었고, 이후 .NET 4.0에서 WPF에 도입되었습니다. 이로 인해 WPF와 Silverlight 간의 코드 재사용성이 크게 향상되었으며, 현재는 대부분의 XAML 기반 플랫폼에서 핵심 기술로 자리 잡았습니다.
지금까지 설명한 전략은 주로 WPF와 동일한 데스크톱 환경의 플랫폼을 고려한 것입니다. 그러나 모바일과 웹까지 확장하려면 더 많은 연구와 고민이 필요합니다.
-
MAUI: Xamarin을 계승한 MAUI는 .NET 기반의 크로스 플랫폼 프레임워크로, WPF의 많은 개념을 공유합니다. MAUI를 통해 데스크톱과 모바일 플랫폼을 동시에 타겟팅할 수 있습니다.
-
Uno Platform: WinUI와 UWP의 API를 다양한 플랫폼에서 사용 가능하게 해주는 Uno Platform은 모바일, 웹(WebAssembly)을 포함한 광범위한 플랫폼 지원을 제공합니다.
-
AvaloniaUI: WPF 스타일의 XAML을 크로스 플랫폼에서 구현할 수 있게 해주는 AvaloniaUI는 독자적인 렌더링 엔진을 사용하여 Uno Platform과 마찬가지로 광범위한 플랫폼을 지원합니다.
-
OpenSilver: Silverlight 애플리케이션을 웹으로 마이그레이션할 수 있게 해주는 OpenSilver는 웹 환경에서 WPF와 유사한 개발 경험을 제공합니다.
이러한 다양한 프레임워크들은 각각의 장단점을 가지고 있으며, 프로젝트의 요구사항에 따라 적절한 선택이 필요합니다. WPF 기술을 최대로 활용하기 위한 연구는 앞으로도 활발하게 이어질 것으로 예상되며, 이는 XAML 기반 기술의 진화와 확장을 이끌 것입니다.
WPF 기술을 기반으로 한 크로스 플랫폼 개발 전략은 XAML 기반 애플리케이션의 미래를 보여줍니다. IValueConverter와 VisualStateManager를 효과적으로 활용하고, 전략적으로 플랫폼을 선택함으로써 개발자들은 다양한 플랫폼에서 일관된 사용자 경험을 제공하는 고품질 애플리케이션을 개발할 수 있습니다.
리그 오브 레전드 프로젝트의 사례는 이러한 접근 방식의 실효성을 잘 보여주며, WPF, Uno Platform, WinUI3 간의 코드 공유와 크로스 플랫폼 확장성을 통해 XAML 기반 기술의 강력함과 WPF의 가치를 입증합니다.
앞으로 모바일과 웹 환경으로의 확장을 고려할 때, MAUI, Uno Platform, AvaloniaUI, OpenSilver 등 다양한 프레임워크의 발전과 함께 WPF 기술을 활용한 크로스 플랫폼 개발은 더욱 풍부해질 것입니다. 이는 XAML 기반 크로스 플랫폼 개발의 미래를 밝게 전망하게 하며, 개발자들에게 새로운 가능성을 제시합니다.