Blazor web app 렌더링 간 data 공유

2024. 2. 19. 00:00ASPNET/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 에서 굳이 이동하려는 사람들은 없을 것이다. 

 

 

관련영상

https://youtu.be/cmFdyeRAja8

 

반응형

'ASPNET > Blazor' 카테고리의 다른 글