2024. 2. 19. 00:00ㆍASPNET/Blazor
Blazor web app 의 server 와 client 간의 rendering 을 함께 사용하다 보니
component 와 data 공유를 하는 방법이 필요 하다. (state management 라고도한다.)
Blazor 가 기존의 Server only 또는 Client only (WebAssembly) 같은 경우
state management 가 특별히 어려운 건 없다.
어차피 memory 상에서 처리 하면 되는 문제라서
이러한 경우는 그냥 기존대로 사용하면 된다.
문제는 Blazor 이 united 라는 새로운 template 를 만들면서
server 와 client 가 통합되어 사용되면 문제가 생기게 된다.
실제 현재 server 에서 client 로 data 를 전달하는 standard 한 방법이 없다.
MS 에서도 이야기 하지 않고 그들이 이야기 한데로 적용해도 동작하지 않는다.
거기에 Prerender (SSR) 가 활성화 되어 있는 경우 그에 의한 data 두번 호출에 문제도 있다.
이번 시간에는 이러 한 문제점들을 확인 하고 Blazor Web App 에서
Rendering mode 간에 data share 를 하는 방법을 알아보자
기본 project 생성시 만들어지는 Counter 앱을 기준으로 설명 하겠다.
1. prerender 시 data 처리 (중복 실행되지 않게 하기)
처음 실행시 SSR 모드로 실행되면 (Prerender) 이때 api 를 호출 하거나 하면
다음 실행시 (interactive mode) api 를 다시 호출하게 된다.
이러한 문제를 해결하기 위해서 PersitenComponent 를 활용한다.
@page "/counter/prerendered"
@implements IDisposable
@inject ILogger<PrerenderedCounter> Logger
@inject PersistentComponentState ApplicationState
@rendermode InteractiveAuto
<PageTitle>Prerendered Counter</PageTitle>
<h1>Prerendered Counter, @renderMode</h1>
<p role="status">@message @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount;
private string? message;
private Random r = new Random();
private PersistingComponentStateSubscription persistingSubscription;
protected override void OnInitialized()
{
persistingSubscription =ApplicationState.RegisterOnPersisting(PersistCount);
if (!ApplicationState.TryTakeFromJson<int>(
"count", out var restoredCount))
{
currentCount = r.Next(100);
message = "currentCount set to ";
Logger.LogInformation(message + currentCount );
}
else
{
currentCount = restoredCount!;
message = "currentCount restored to ";
Logger.LogInformation(message + currentCount);
}
}
private string renderMode = "SSR (not interactive)";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (OperatingSystem.IsBrowser())
{
renderMode = "WebAssembly interactive";
}
else
{
renderMode = "Server interactive";
}
await base.OnAfterRenderAsync(firstRender);
StateHasChanged();
}
}
private Task PersistCount()
{
ApplicationState.PersistAsJson("count", currentCount);
return Task.CompletedTask;
}
void IDisposable.Dispose()
{
persistingSubscription.Dispose();
}
private void IncrementCount()
{
currentCount++;
}
}
아래에서 저장하고
ApplicationState.PersistAsJson("count", currentCount);
아래에서 저장된 값을 가져온다.
ApplicationState.TryTakeFromJson<int>("count", out var restoredCount)
2. in memory 상에 data 처리
자 위와 같이 prerender 처리를 해도 counter 를 increment 한 후에
home 으로 이동했다가 다시 돌아오면 이전 값이 남아 있지 않다.
이 문제를 해결해 보자
@page "/counter/inmemory"
@rendermode InteractiveAuto
@inject IStateContainer<int> StateContainer
@implements IDisposable
<PageTitle>Inmemory Counter</PageTitle>
<h1>Inmemory Counter, @renderMode!</h1>
<p role="status">Current count: @currentCount </p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private string renderMode = "SSR (not interactive)";
private void IncrementCount()
{
StateContainer.Property = ++currentCount;
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (OperatingSystem.IsBrowser())
{
renderMode = "WebAssembly interactive";
}
else
{
renderMode = "Server interactive";
}
base.OnAfterRender(firstRender);
StateHasChanged();
}
}
protected override void OnInitialized()
{
StateContainer.OnChange -= StateHasChanged;
StateContainer.OnChange += StateHasChanged;
currentCount = StateContainer.Property;
base.OnInitialized();
}
public void Dispose()
{
StateContainer.OnChange -= StateHasChanged;
}
}
상태 저장을 위한 StateContainer 는 다음과 같다.
public interface IStateContainer<T>
{
T? Property { get; set; }
event Action? OnChange;
}
public class StateContainer<T>:IStateContainer<T>
{
private T? _value;
public T? Property
{
get => _value;
set
{
_value = value;
NotifyStateChanged();
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
이제 Server 와 client 에 각각 service 를 등록 하자
//server 쪽 program.cs
builder.Services.AddScoped(typeof(IStateContainer<>), typeof(StateContainer<>));
//client 쪽 program.cs (webassembly 는 scope == singleton 과 같다.)
builder.Services.AddSingleton(typeof(IStateContainer<>), typeof(StateContainer<>));
3. Render Mode 간에 Data 처리
그런데 이렇게 해도 의문이 든다. Server Interactive 한 상태에서 increment 한 data 는
client interactive 한 mode 로 전달이 되지 않는 것이다.
즉 두 app (Server, WebAssembly) 은 memory 상에 data 를 공유하지 않고
할수 있는 방법이 사실 없다.
자 이부분에서 ms 는 뭔가 방법을 알려줘야 할 것 같은데 특별이 방법은 없다.
그들이 만든 예제에서 된다고 하는 내용을 보면 실제 실행시 적용되지 않는다.
아마 이 부분을 염두하지 못한건지? 아니면 내가 찾지 못한 것 일수도 있다.
그래서 이걸 처리 위해 brower 에 sessionstorage 를 이용할 것이다.
blazor server 에서는 protected 된 storage 가 있지만 webassembly 는 사용 불가능 하다.
그래서 3rd party nuget package 를 이용할 것이다.
client project 로 이동해서 다음 명령을 실행하자
dotnet add package Blazored.SessionStorage
이제 Program.cs (client) 로 이동해서 다음을 추가하자
builder.Services.AddBlazoredSessionStorage();
shared 처리하는 counter 는 아래와 같다.
@page "/counter/shared"
@* @rendermode @(new InteractiveAutoRenderMode(prerender: false)) *@
@rendermode InteractiveAuto
@inject Blazored.SessionStorage.ISessionStorageService sessionStorage
<PageTitle>Shared Counter</PageTitle>
<h1>Counter, @renderMode!</h1>
<p role="status">Current count: @currentCount </p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private string renderMode = "SSR (not interactive)";
private async Task IncrementCount()
{
currentCount = await sessionStorage.GetItemAsync<int>("count");
await sessionStorage.SetItemAsync("count", ++currentCount);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (OperatingSystem.IsBrowser())
{
renderMode = "WebAssembly interactive";
}
else
{
renderMode = "Server interactive";
}
currentCount = await sessionStorage.GetItemAsync<int>("count") == 0 ? currentCount : await sessionStorage.GetItemAsync<int>("count");
await base.OnAfterRenderAsync(firstRender);
StateHasChanged();
}
}
// protected override async Task OnInitializedAsync()
// {
// // currentCount = await sessionStorage.GetItemAsync<int>("count") == 0 ? currentCount : await sessionStorage.GetItemAsync<int>("count");
// // StateHasChanged();
// // await base.OnInitializedAsync();
// }
}
주석 처리되어 있는 부분은 만약 prerender를 false 로 설정 하였을때 OnInitializeAsync 를 이용해서 처리 가능하다는 것을 보여준 것이다. 이제 data 가 공유가 가능해 졌다.
MS 에서 야심차게 준비한 Blazor Web App 이라는 통합(??) web frameworks 는 아직 많이 부족하다.
그들이 어디까지 생각하고 이것을 개발 한 것인지 모르겠으나
개발자도 그렇고 개발사도 그렇고 좀더 시간이 필요할 걸로 보인다..
개발자들은 각 mode 들이 어떻게 서로 상호작용하고 동작하는지
자세히 파고들지 않는다면 많은 문제에 직면할 것이다.
하지만 그러한 수고를 할 가치가 있는 web frameworks 임에는 분명하다
MS 가 조금더 빠르게 대응 하여 각각에 대한 reference 를 준비해야 할 거라고 생각한다.
그렇게 하지 않는다면 이미 사용하고 있는 frontend frameworks 에서 굳이 이동하려는 사람들은 없을 것이다.
관련영상
'ASPNET > Blazor' 카테고리의 다른 글
Blazor Web App - Register and Login (Identity Server) (0) | 2023.12.18 |
---|---|
Blazor Web App - 렌더링 개념 (1) | 2023.12.11 |
Blazor web app 이란 무엇인가? (dotnet 8) (0) | 2023.12.04 |
이벤트 처리 2 (0) | 2022.05.06 |
이벤트 처리 (0) | 2022.05.05 |