.NET 9 Blazor 탐구 - slog

목적

.NET 9 Blazor의 추가, 변경된 기능 탐구

목표

  • Static SSR 및 하이브리드 동작성 확인
  • 간단한 블로그 서비스 만들기
4개의 좋아요

렌더 모드

1개의 좋아요

@BigSquare 님이 예전에 Static SSR의 기본 동작성을 확인하셨습니다.

Static SSR는 서버 또는 클라이언트에서 이전 Blazor 페이지 처럼 상호 작용하는 것과는 다르게 – 여기서 상호작용이란 상태가 유지되고 클릭 등에 의해 상태가 변경되며 그 상태를 담고 있는 인스턴스가 계속 유지되는 것을 의미합니다 – Static SSR은 서버에서 페이지를 렌더링 하고 난 후 인스턴스는 폐기되는 방식 입니다.

다만 Blazor에서 제공하는 Static SSR의 경우 전체 페이지가 리플레시 되어 사용성이 저하되기 때문에 Blazor에서는 향상된 양식 처리를 통해 이를 보완합니다.

2개의 좋아요

.NET MAUI Blazor 하이브리드 웹 앱 템플릿

.NET MAUI 워크로드를 설치한 후

dotnet workload install maui

다음의 명령을 이용해서 Blazor 웹앱 MAUI 템플릿으로 프로젝트를 시작할 수 있습니다.

dotnet new maui-blazor-web

이 템플릿으로 앱을 실행하면 Blazor로 MAUI 환경에서 애플리케이션을 개발할 수 있으며 Server 또는 WebAssembly 방식으로 동작하는 것이 아니라 인베딩 된 WebView에서 렌더링 하는 방식으로 동작하게 됩니다.

1개의 좋아요

9.0 에서 소개된 이 솔루션 템플릿은 웹앱과 크로스 플랫폼 데스크탑 앱의 UI를 Blazor 하나로 작성할 수 있는 방법을 알려주는 것 같습니다.

  • Maui Blazor Hybrid
    • Window
    • Mac
    • Andriod
    • iOS
    • Tizen
  • Blazor Web App
    • Blazor Webassembly
  • RCL

8.0 까지만 해도, 위와 같은 구성을 하기 위해서는 아래처럼 구성을 했어야 했습니다.

  • Maui Blazor Hybrid
    • Window (선택적)
    • Mac
    • Andriod
    • iOS
    • Tizen
  • WPF Blazor Hosting (선택적)
  • Windows Forms Blazor Hosting (선택적)
  • Blazor Webassembly Standalone
  • RCL
  • Asp.Net Core Web api

위와 같이 구성한 이유는 Blazor hybrid/Hosting 과 Blazor Webassembly의 인터렉티브 모드가 기본적으로 같기 때문입니다.

실제로 해보시면, 서버 측 인터렉티브와 클라이언트 측 인터렉티브를 모두 만족하는 레이저 요소를 RCL에 작성하는 것은 사실 꽤 어렵다는 점을 알게 됩니다.

9.0 템플릿은 서버 측 렌더링이 낄 수 밖에 없어 보이는데, 아니나 다를까 렌더 모드 처리에 관한 주의 사항이 문서에 많이 나오네요.

프리뷰 딱지 떼고 나오면, 어떻게 구성했는지 살펴 봐야겠습니다.

1개의 좋아요

하이브리드에서 렌더 모드 적용이 아직은 전역만 지원한다는 내용은 확인하였는데 어떤 부분이 RCL에서 인터렉티브 구현을 어렵게 하는지 궁금합니다.

제가 이해한 바로는 Blazor 하이브리드 방식은 Server / WebAssembly 방식이 아니라 WebView를 직접 사용해서 렌더링하는 것으로 알고 있는데요 이는 WebAssembly보다 Server의 인터렉티브 동작에 가깝겠지만 어쨌든 Server 모드는 아닙니다.

관련해서 조금 더 설명 부탁 드립니다.

RCL에 있는 요소들은 기본적으로 인터렉티브 모드를 지정하지 않습니다. (그렇게 권고되고 있습니다)

문제는 요소가 Routable Component(페이지)이고, 블레이저 웹앱에서 가져다 쓰는 경우입니다.

웹앱에서 렌더링 모드를 지정하지 않으면, Static SSR 이 되는데, 이 경우, 요소의 인터렉티브 코드가 동작하지 않습니다. (form 태그만 동작합니다)

인터렉티브 코드가 동작하기 위해서는, 웹앱의 인터렉티브 모드를 전역으로 하나를 지정해야 합니다. 이 지정에 따라서, 서버 측 프로젝트나 클라이언트 측 프로젝트 중 하나만 사용할 수 있습니다.

문서에 보니, 여기에 더해 MAUI에서 렌더 모드 관련 에러도 있네요.

The app automatically adopts global interactivity, which is important because MAUI apps always run interactively and throw errors on Razor component pages that explicitly specify a render mode. For more information, see BlazorWebView needs a way to enable overriding ResolveComponentForRenderMode (dotnet/aspnetcore #51235).

이러한 고민을 없애려면, RCL 에는 인터렉티브 모드를 가변할 수 있는 non-routable 요소만 정의해야 하는데, 이 경우, 모든 실행 프로젝트에 동일한 페이지 요소들을 중복적으로 넣어 줘야 하는 불편이 발생합니다.

웹뷰와 브라우저는 같은 실행 컨텍스트라 할 수 있습니다.
즉, 블레이저가 클라이언트 측에서 실행됨을 의미합니다.

원 댓글에 제시한 8.0 솔루션 구조는, 아예 서버 측 컨텍스트를 배제하여, 클라이언트 측 하나로 실행 컨텍스트를 단순화한 것입니다. (이 때문에, 서버 측 블레이저를 가정하는 Blazor Wep app을 사용하지 않게 됩니다.)

이러한 가정이 있으면, RCL의 모든 요소는 하나의 실행 컨테스트만 가정하여 통일적으로 작성할 수 있고, 모든 클라이언트 측 프로젝트에서 사용할 있게 되는 것이죠.

이러한 통일성으로 인해, RCL 프로젝트는 페이지는 물론, App.razor 까지 보유할 수 있게 됩니다. RCL 이 App 의 모든 로직과 자산을 갖게 되는 것이죠.

이는 MAUI 나 XAMARIN 에서 공유 프로젝트에 App 클래스를 두는 것과 같은 방식으로, 하나의 UI 코드를 (프론트)웹앱, 데스트 탑 앱, 모바일 앱에서 (코드 수정 없이) 사용하는 구조입니다.

2개의 좋아요

렌더 모드에 따른 인스턴스 확인

렌더 모드가 interactive이면서 prerender가 true(기본 설정) 일 경우 페이지는 사용자에게 빠르게 보이기 위해 상호작용 전 정적으로 먼저 렌더링을 한 이후 상호작용이 가능한 페이지로 다시 렌더링을 해서 총 두 번 해당 페이지 (또는 컴포넌트)의 인스턴스가 생성됩니다.

이에 따라 화면에 보여지는 값이 유지 되지 않는데 이를 확인하는 간단한 방법은 다음의 코드로 확인할 수 있습니다.

@page "/counter"
@rendermode InteractiveAuto
@*@rendermode InteractiveWebAssembly*@
@* @rendermode InteractiveServer *@

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>
<p>@RendererInfo.Name</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    int currentCount = Random.Shared.Next(0, 100);

    void IncrementCount()
    {
        currentCount++;
    }
}

@rendermode가 InteractiveServer일 경우 빠르게 화면이 전환되기 때문에 Auto나 WebAssembly이어야 전환 되는 것을 확인할 수 있습니다.

최초에는 아래처럼 Static으로 렌더링이 되었고 초기 카운트 값이 82였는데

페이지가 모두 준비가 되면 WebAssembly으로 렌더링 되고 초기 값이 91로 바뀐 것을 확인할 수 있습니다.

미리 렌더링된 상태 유지

미리 렌더링된 상태를 계속 유지하려면 PersistentComponentState 서비스를 아래와 같이 이용할 수 있습니다.

@page "/counter"
@implements IDisposable
@rendermode InteractiveAuto
@*@rendermode InteractiveWebAssembly*@
@* @rendermode InteractiveServer *@
@inject PersistentComponentState ApplicationState

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>
<p>@RendererInfo.Name</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingComponentStateSubscription;

    protected override void OnInitialized()
    {
        persistingComponentStateSubscription = ApplicationState.RegisterOnPersisting(PersistCount);

        if (!ApplicationState.TryTakeFromJson<int>(nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
        }
        else
        {
            currentCount = restoredCount;
        }
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }


    private void IncrementCount()
    {
        currentCount++;
    }

    void IDisposable.Dispose() => persistingComponentStateSubscription.Dispose();
}

| 미리 렌더링

| 상호작용 적용 후

PersistentComponentState에 관련된 닷넷데브 글은 @BigSquare 님의 블레이저 8.0 선택 장애 에서 찾을 수 있습니다.

1개의 좋아요

PersistentComponentState 서비스는 어떻게 미리 렌더링된 시점에서의 상태를 보존할 수 있을까요? 미리 렌더링된 html을 살펴보면 다음의 주석이 눈에 띕니다.

...
<!--Blazor-Server-Component-State:CfDJ8K93lMGTYKpAgncimEFm28D7H3mLLAO3PvR2WrIlslhYg7Vd82+05p2l...
<!--Blazor-WebAssembly-Component-State:eyJfX1Jlc291cmNlQ29sbGVjdGlvblVybCI6Iklp...

이 중 Blazor-WebAssembly-Component-State를 base64 복호화 하면,

{...,"currentCount":"NDk="}

즉, ApplicationState.PersistAsJson(nameof(currentCount), currentCount);에 의해서 JSON으로 저장된 currentCount 값이라는 것을 확인할 수 있습니다.

Blazor-Server-Component-State의 경우 base64 디코딩을 하더라도 암호화 되어 있는데 이는 서버에서 복호화 해서 사용해야 할 상태이므로 클라이언트에서 변조할 수 없도록 되어 있는것으로 보입니다.

2개의 좋아요

정적 웹 자산 전달 최적화

기존 app.UseStaticFiles()에서 app.MapStaticAssets()으로 정적 자료에 접근하는 방식이 달라졌습니다. 다만 UseStaticAssets()는 컴파일 및 게시 시점에서 정적 자원을 최적화 하므로 런타임 시 등록되는 정적 자료에 접근하기 위해서는 UseStaticFiles()를 여전히 사용해야 한다고 합니다.

그렇다면 정적 자산은 정적 파일과 어떤 차이가 있을까요? 자원을 압축하고 또 제대로 캐싱될 수 있도록 하는 일련의 처리가 됩니다.

https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-9.0#optimizing-static-web-asset-delivery

2개의 좋아요

런타임 시 렌더링 관련 정보 제공

RendererInfo 속성을 이용하면 런타임 시 렌더링 관련 정보를 얻을 수 있습니다.

2개의 좋아요