์ํฐํฐ ํ๋ ์์ํฌ ์ฝ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ํฐํฐ ์ ์ฅ ํ ์ฆ์ MediatR ์๋ฆผ ๋ณด๋ด๊ธฐ | no dogma blog
๋ช ์ฃผ ์ ์ ์ํฐํฐ ํ๋ ์์ํฌ์์ ๊ฐ์ฒด๋ฅผ ์ ์ฅํ ์งํ์ ์ก์ธ์คํ๋ ๋ฐฉ๋ฒ์ ๋ํ ๊ฒ์๋ฌผ์ ์์ฑํ๋๋ฐ, ํด๋น ๊ฒ์๋ฌผ์์๋ ์ ์ฅ๋ ๊ฐ ์ํฐํฐ์ ๋ํด ์ฝ์์ ํ ์ค์ ์ถ๋ ฅํ์ต๋๋ค.
์ํฐํฐ๊ฐ ์์ฑ/์ ๋ฐ์ดํธ๋์๋ค๋ ์๋ฆผ์ ๋ณด๋ด๋ ค๊ณ ํ ๋ ์ด ๋ฐฉ๋ฒ์ด ์ด๋ป๊ฒ ์ ์ฉํ ์ ์๋์ง ๋ณด์ฌ๋๋ฆฌ๋ ค๊ณ ํ์ต๋๋ค. ์ด๋ฒ ํฌ์คํ ์์๋ ์ํฐํฐ๊ฐ ์ ์ฅ๋ ์งํ MediatR ์๋ฆผ์ ๋ณด๋ด๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
์ฌ๊ธฐ์์๋ MediatR ์ฌ์ฉ ๋ฐฉ๋ฒ์ ๋ํด ์ค๋ช ํ์ง ์์ ๊ฒ์ด๋ฉฐ, ์ด์ ๋ํ ์ข์ ๋ฌธ์๊ฐ ์์ผ๋ฉฐ, ์ ๋ ์ด์ ๋ํ ๋ช ๊ฐ์ ๊ฒ์๋ฌผ์ ์์ฑํ์ต๋๋ค. ํธ๋ค๋ฌ๋ ProductNotificationHandler.cs ํ์ผ์์ ์ฐพ์ ์ ์์ผ๋ฉฐ, ๋งค์ฐ ๊ฐ๋จํ๋ฉฐ, ์ฝ์์ ํ ์ค์ ์ธ์ํฉ๋๋ค.
์ฌ๊ธฐ์๋ ์ํฐํฐ๊ฐ ์ ์ฅ๋ ์งํ ์๋ฆผ์ ์ ์กํ๊ธฐ ์ํด DbContext์ SaveChangesAsync ๋ฉ์๋๋ฅผ ์ฌ> ์ ์ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ๋๋ฆฌ๊ฒ ์ต๋๋ค.
๋ก์ง ์ฝ๋๋ฅผ ๋ทฐ๋ชจ๋ธ๋ก ์ฎ๊ธฐ๋ ์์ด๋์ด๋ฅผ ์ํด ๊ตณ์ด MVVM ํดํท์ ์ฌ์ฉํ ํ์๊ฐ ์๋์ง๋ ์๋ฌธ์ ๋๋ค.
MVVM ํดํท์ผ๋ก ์ธํด ๋ทฐ๋ชจ๋ธ์ ์ด ์ธ๊ฐ์ Notify ์ฝ๋๊ฐ ์ถ๊ฐ๋์ง๋ง, ๋ทฐ๊ฐ ๊ตฌ๋ ํ์ง ์๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ๋ ํธ๋ฆฌ๊ฑฐ๋์ง ์์ต๋๋ค.
// PropertyChanged = null
public async Task LoadDataAsync()
{
IsLoading = true; // Notify
try
{
// ...
Forecasts = await _weatherService.GetForecastAsync(DateTime.Now); // Notify
}
finally
{
IsLoading = false; // Notify
}
}
๊ฐ์ข์ ์ฝ๋๋ MVVM ํดํท์ด ์์ด๋ ๋ทฐ๊ฐ ๋ทฐ๋ชจ๋ธ์ ์ฌ์ฉํ๋ ๋ฐ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
internal sealed class WeatherViewModel
{
private readonly WeatherForecastService _weatherService;
public WeatherViewModel(WeatherForecastService weatherService)
{
_weatherService = weatherService;
}
public bool IsLoading;
public WeatherForecast[] Forecasts = Array.Empty<WeatherForecast>();
public async Task LoadDataAsync()
{
IsLoading = true;
try
{
await Task.Delay(TimeSpan.FromSeconds(2)); // simulate loading
Forecasts = await _weatherService.GetForecastAsync(DateTime.Now);
}
finally
{
IsLoading = false;
}
}
}
MVVM ํดํท์ด ๋ธ๋ ์ด์ ์์ ํ์ ๋ฐํํ๋ ค๋ฉด, ๋ธ๋ ์ด์ ๊ฐ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ง์ํด์ผ ํ ๊ฒ ๊ฐ์๋ฐ,
//.razor ์์
@BindingContext WeatherViewModel
//ํน์
@DataContext WeatherViewModel
(์๋ฐฉํฅ) ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ Notify => Read/Notify => Read ์ ๋ผ์ด๋ ํธ๋ฆฝ ๊ตฌ์กฐ์ด๊ธฐ์, ๋ธ๋ ์ด์ ์๋ฒ ์ฑ์์๋ SignalR ๋ฐ์ดํฐ๋์ด ํญ์ฆํ๋ ๋ถ์์ฉ์ด ์์ ๊ฒ ๊ฐ์ต๋๋ค.
๋ฌผ๋ก , ๋ธ๋ ์ด์ ์น์ด์ ๋ธ๋ฆฌ ์ฑ์์๋ ์ฑํํ ๋งํ ๊ฒ ๊ฐ์ต๋๋ค.
๋ฌธ์์ด ๋ณด๊ฐ์ ์ฆ๊ฒจ์ฐ๋ ๊ธฐ๋ฅ์ด์๋๋ฐ ์์๋ ์ง์ ํ ์ ์๋ค๋ ์์ฒญ ์ ๊ธฐํ๋ค์ ใ ใ
์๊ฒฌ ์ง์ ๊ฐ์ฌํฉ๋๋ค.
์ ๊ฐํ ๋ด์ฉ (ํนํ ์ฒซ ๋ฒ์งธ ๋ด์ฉ)์ด ์น์ ํ์ง ์์์ ์๊ธด ๋ฌธ์ ๋ก ๋ณด์ด๋ฉฐ ์ฌ์ค ์ฐ์๋ ์๋ฆฌ์ฆ์ ๋ง๋ฌด๋ฆฌ โ MVVM ํดํท์ผ๋ก ๋ช ๋ น์ด ์ฌ์ฉ ๋ฐ ๋น๋๊ธฐ ์ทจ์๊ฐ ์ ์๊ฐ ์ ๋ฌํ๊ณ ์ ํ๋ ์ฃผ๋ ๋ด์ฉ์ผ๋ก ์ดํด๋ฉ๋๋ค. ์ด๊ฒ ๋ง์ผ๋ก๋ Blazor์์ MVVM ํดํท์ ์ฌ์ฉํด์ ์ฝ๋๋ฅผ ์ข ๋ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋ค์ด ์ค๋ค๊ณ ์๊ฐํฉ๋๋ค.
...
<button disabled="@ViewModel.CountCommand.IsRunning" @onclick="() => ViewModel.CountCommand.ExecuteAsync(null)">Click!</button>
...
[RelayCommand]
public async Task OnCount()
{
for (var i = 0; i < 200; i++)
{
Count++;
await Task.Delay(10);
}
}
ํํธ ์ค์๊ฐ ๋ฐ์ดํฐ์ ํ์
๊ฐ ์๋ ์ด์ Blazor์์๋ ์
๋ ฅ(๋ฒํผ์ ํด๋ฆญ ๋ฑ)์ ์ด๋ฒคํธ์ ์ํด ํ๋ฉด ๊ฐฑ์ ์ด ์ด๋ฃจ์ด์ง๋ฏ๋ก INotifyPropertyChanged
์ด๋ฒคํธ๋ฅผ ๊ตฌ๋
ํ์ง ์์๋ ๋๋ถ๋ถ์ ์๋๋ฆฌ์ค์์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๊ฐ๋ น,
์์ await ์ด์ ์ IsLoading
์ ๋ณ๊ฒฝ์ด ํ๋ฉด์ (์๋์ผ๋ก) ์ ์ฉ๋๋ฉฐ LoadDataAsync()
๋ฉ์๋์ ์ฝ์ด ๋๋๋ ์์ ์ ๋ค์ IsLoading
์ ๋ณ๊ฒฝ๋ ์ํ๊ฐ ํ๋ฉด์ ์ ์ฉ๋ฉ๋๋ค.
ํ์ง๋ง ๋ฒํผ์ ๋๋ ์ ๋ 1๋ถํฐ 100๊น์ง 10ms ๋จ์๋ก ์ถ๋ ฅํ๋ ๋ฑ์ ๋์ํ์ง ์์ฃ . ์ด๋ถ๋ถ์ ๋ค์์ ๋ฐฉ์์ผ๋ก ๊ฐ๋จํ ํด๊ฒฐํ ์ ์์ต๋๋ค.
๋ค์์ฒ๋ผ ViewModel์ MVVM ํดํท์ผ๋ก ๊ตฌ์ฑํ ํ
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.ComponentModel;
namespace BlazorApp41.ViewModels;
public partial class IndexViewModel : ObservableObject
{
[ObservableProperty]
private int _count;
public Action StateHasChanged { get; set; } = () => { };
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
StateHasChanged();
}
[RelayCommand]
public async Task OnCount()
{
for (var i = 0; i < 200; i++)
{
Count++;
await Task.Delay(10);
}
}
}
StateHasChanged
Action ๋๋ฆฌ๊ฒ์ดํธ๋ฅผ ๋ค์์ฒ๋ผ ํ๋ฉด
@code {
protected override void OnInitialized()
{
ViewModel.StateHasChanged = () => InvokeAsync(StateHasChanged);
}
}
๋ฒํผ์ ๋๋ ์ ๋ ๋์์ด ์๋ฃ๋ ๋๊น์ง ๋ฒํผ์ ๋นํ์ฑํ ๋๋ฉฐ ์ซ์๊ฐ 10ms ๊ฐ๊ฒฉ์ผ๋ก ์ฆ๊ฐํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
@page "/"
@using BlazorApp41.ViewModels;
@inject IndexViewModel ViewModel
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<h2>@ViewModel.Count</h2>
<button disabled="@ViewModel.CountCommand.IsRunning" @onclick="async () => await ViewModel.CountCommand.ExecuteAsync(null)">Click!</button>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
@code {
protected override void OnInitialized()
{
ViewModel.StateHasChanged = () => InvokeAsync(StateHasChanged);
}
}
Blazor Webassembly์์๋ ๋ง์ํ์ ๊ฒ ์ฒ๋ผ ViewModel์ ์์ฑ์ด ๋ณ๊ฒฝ๋จ์ ๋ฐ๋ผ ๊ฐฑ์ ๋๋ ํ๋ฉด์ ๋ ๋๋ง์ ์น๋ธ๋ผ์ฐ์ ์์ ํ๋ฏ๋ก ๋ฌธ์ ๋ ๊ฒ์ด ์ ํ ์์ผ๋ Blazor Server ๊ฒฝ์ฐ html ๋ ๋๋ง์ ์๋ฒ์์ ํ๋ฉฐ ViewModel ์ธ์คํด์ค ์ญ์ ์๋ฒ์ ์์ด์ ViewModel์ ์์ฑ ๋ณ๊ฒฝ์ ์ํ ํ๋ฉด ๋ ๋๋ง์ ์ํ ๋ฐ์ดํฐ๋์ ์ฆ๊ฐ๋ ๋น์ฐํ ๋ฐ์ํ์ง๋ง ์ฐ๋ คํ์๋ ๊ฒ ์ฒ๋ผ ์ค์๊ฐ ๋ฐ์ดํฐ์ ํํ์ด ์๋ ์ด์ Blazor Server์ ์ฌ์ฉ๋ชฉ์ ์ ๋ฒ์ด๋์ง๋ ์๋๋ค๋ผ๋ ๊ฒ์ด ์ ์ ์๊ฒฌ์ ๋๋ค.
๊ฒฐ๋ก ์ MVVM ํดํท์ Blazor์์ ์ฌ์ฉํ๋๋ฐ ๋ฌธ์ ๊ฐ ์์ผ๋ฉฐ ๋ค๋ฅธ ํ๊ฒฝ๊ณผ ๋์ผํ ๋ฐ์ธ๋ฉ ํํ์ ๋๋ฆด ์ ์๋ค๊ณ ์๊ฐํฉ๋๋ค.
๊ด๋ จํด์ ์ด๋ ๊ฒ ์ฐ๋ฉด ๊ด์ฐฎ์ง ์์๊น ์ํ์ ๋ง๋ค์ด๋ดค์ต๋๋ค.
๊ธฐ๋ณธ ๊ตฌ์กฐ๋ ์๋์ ๊ฐ๊ณ ์,
ViewModel โ ViewModelBase โ IStateHasChanged
ViewModelComponent โ OwningComponentBase
IoC๋ฅผ ์ด์ฉํ๊ธฐ ์ํด Program.cs์ IndexViewModel
๋ฅผ ์ถ๊ฐํด์ผ ํ๊ณ
builder.Services.AddScoped<IndexViewModel>();
์ดํ ์ฌ์ฉ๋ฒ์ ๊ฑฐ์ ๋์ผํฉ๋๋ค.
| IndexViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using N31.BlazorServerMVVM.Base;
namespace N31.BlazorServerMVVM.ViewModels;
public partial class IndexViewModel : ViewModelBase
{
[ObservableProperty] private int _count;
[ObservableProperty] private int _inputCount = 200;
[RelayCommand]
public async Task OnCountAsync(CancellationToken ct)
{
try
{
for (var i = 0; i < InputCount; i++)
{
Count++;
await Task.Delay(10, ct);
}
}
catch (OperationCanceledException)
{
}
}
}
| Index.razor
@inherits ViewModelComponent<IndexViewModel>
@page "/"
@using N31.BlazorServerMVVM.Base;
@using N31.BlazorServerMVVM.ViewModels;
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<h2>@ViewModel.Count</h2>
<div>
<button disabled="@ViewModel.CountCommand.IsRunning" @onclick="() => ViewModel.CountCommand.ExecuteAsync(null)">Click!</button>
<button disabled="@(!ViewModel.CountCommand.CanBeCanceled)" @onclick="@ViewModel.CountCommand.Cancel">Cancel</button>
</div>
<div>
<input disabled="@ViewModel.CountCommand.IsRunning" @bind="@ViewModel.InputCount" @bind:event="oninput" />
<input disabled="@ViewModel.CountCommand.IsRunning" @bind="@ViewModel.InputCount" @bind:event="oninput" />
</div>
<SurveyPrompt Title="How is Blazor working for you?" />
Blazor๋ razor์ ์์ ํํ์ ์ ๋ค๋ฆญ์ ์ธ ์ ์์ด์ ViewModel์ ์ข ๋ ์ฌ์ฉํ๊ธฐ ํธํ ๋๋์ ๋๋ค.
@inherits ViewModelComponent<IndexViewModel>
์ ์ ์์ ์ด "๊ตฌ๋ ํ์ง ์์๋ ๋๋๋ฐ, ๊ตฌ๋ ์ ์ ์ ๋ก ํ ๋๊ตฌ๊ฐ ํ์ํ ๊น?"์ ๋๋ค.
MVVM ํดํท์ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ ์ ๋ก, ๋ฐ์ธ๋ฉ ์์ค(๋ทฐ๋ชจ๋ธ)๋ฅผ ์ํ ๋๊ตฌ์ ๋๋ค. ์ด ๋๊ตฌ๋ ๋ฐ์ธ๋ฉ ํ๊ฒ(๋ทฐ)์ด ์ ์ ํ ์ฅ์น๋ค( BindableProperty, Command, RoutedEvent, Binding ๋ฑ)์ ๊ฐ์ถ๊ณ ์์ ๋ ํธ๋ฆฌํ ๊ฒ์ด์ง, ์ด ์ฅ์น๋ค์ ์ฌ์ฉ์ ์ผ์ผ์ด ๊ตฌํํด์ผ ํ๋ค๋ฉด ๊ธ์ด ๋ถ์ค๋ผ์ ์ง๋์ง ์๊ฒ ์ฃ .
๋ฆด๋ ์ด ์ปค๋งจ๋ ์์ ์ฝ๋๊ฐ ์ด๋ฌํ ์ ์ ๋ณด์ฌ์ฃผ๊ณ ์์ต๋๋ค. ๋ธ๋ ์ด์ ์์๊ฐ Command ์์ฑ์ ์ ๊ณตํ๋ค๋ฉด, ์๋์ ๊ฐ์ด ๊ฐ๋จํ์ ์ฝ๋์ ๋๋ค.
<button @OnClickCommand = "BindingContext.CountCommand" disabled="BindingContext.CountCommand.IsRunning" ...
๋ํ, BindableProperty๋ ์ ๊ณตํ์ง ์๊ธฐ ๋๋ฌธ์, StateHasChanged ๋ฉ์๋๋ก ์ด๋ฅผ ํ๋ด๋ธ ๊ฒ์ด์ฃ .
์ด๋ฌํ ํ๋ด๊ฐ ์ฝ๊ฐ์ ๋ถํ๋ฅผ ๋๋ ค๋, ๊ธฐ๋ฅ ์ ๋์ผํ๊ฒ ๋์ํ๋ค๋ฉด, WPF/UWP/MAUI/Blazor WASM ์ฒ๋ผ ์ฑ๊ธ ์ธ์คํด์ค ๊ธฐ๋ฐ ์ฑ์์๋ ๊ตณ์ด ๋ถํ์ํ๋ค๊ณ ๋งํ ๊ฒ๊น์ง๋ ์์ ๊ฒ์ ๋๋ค. ๊ทธ๋ฌ๋, Blazor Server์ฑ์ ๋์ผํ ์ธ์คํด์ค๊ฐ ๋์์ ์๋ฐฑ์์ ์์ฒ๊ฐ๊น์ง ์กด์ฌํ๋ ๊ฒ์ ์ ์ ํ๊ธฐ ๋๋ฌธ์, ์ฝ๊ฐ์ ๋ถํ ์ฆ๊ฐ๋ ์ ์์ ์์ ์ ๋น๋กํ๊ฒ ์ฆ๊ฐํ๊ฒ ๋์ด, ์ ์คํ ํ์๊ฐ ์์ ๊ฒ์ ๋๋ค.
์ด ๊ธ๊ณผ ๊ด๋ จํ ๋ ผ์๋ฅผ ์ด์ด ๊ฐ๋ฉด์ ๋ฌธ๋
๋ธ๋ ์ด์ ์ ์ด๋ฒคํธ ์ ํ ์๋จ์ผ๋ก delegate ๊ฐ ์๋ EventCallback ์ ์ฑํํ ๊ฒ์ ๋ถํ ๋ถ๋ด์ด ๊ฐ์ฅ ํฐ ์์ธ์ด์ง ์์๊น?
ํ๋ ์๊ฐ๋ ๋ค์์ต๋๋ค. ์ฐธ์กฐ ๊ฐ์ฒด์ธ delegate๋ ์ํ๋ฅผ ์ ์ฅํด์ผ ํ๋ ๋ฐ๋ฉด, ๊ฐ ๊ฐ์ฒด์ธ EventCallback์ ๊ทธ๋ ์ง ์์ฃ .
๊ทธ๋ผ์๋, ์ ๊ธ์ ์๋ ์ค, ๋ทฐ๋ชจ๋ธ์ ๋ทฐ์ ์ํ๋ฅผ ๋ณด๊ดํ๋ ํจํด์ ๋ธ๋ ์ด์ ์ ์ํ ๊ด๋ฆฌ ์ธก๋ฉด์์๋ ํ๋ฅญํ ์ ํ์ง์์๋ ํ๋ฆผ์์ด ๋ณด์ ๋๋ค.
์ถ์ : ๊ทธ๋์ ๋ ์ด๋์ ํซ ๋ฆฌ๋ก๋ ๋ฌธ์ ๋ ์์ง๋ ํด๊ฒฐ์ด ์๋์ต๋๋ค. ๋น์ฃผ์ผ ์คํ๋์ค ๋ค์ ๊น์๋๋ฐ๋์. ใ ใ
VS 17.7 P1๋ถํฐ Blazor์์ ๋ค์ ํซ๋ฆฌ๋ก๋๊ฐ ์ ๋์ํฉ๋๋ค.
์ ๋ ์ค๋ ์๋ฒฝ์ ํ์ธํ์ต๋๋ค. ^^
๊ทผ๋ฐ, 17.6 ์์๋ ์ด๋จ ๋๋ ๋๋ค๊ฐ, ๋ค์ ์๋๋ ๊ฒฝํ์ ํ๊ธฐ์ ์กฐ๊ธ ์ง์ผ๋ด์ผ ํ ๊ฒ ๊ฐ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ , ์ด๋ฒ ๋ฒ์ ์ ๋๊ธฐ๋ ํ๋๋ฐ, ๋ญ๊ฐ ์ข ๋๋ฆฌ๋ค์. ๊ธธ๋ฉด 5์ด ์ ๋ ๊ฑธ๋ฆฌ๋ ๊ฒ ๊ฐ์ต๋๋ค.