[정정] .Net 8.0 블레이저의 Static SSR

이 글은 아래 글의 오류를 정정하기 위한 것입니다.
Static SSR 개념이 처음 나왔을 때 가볍게 훑어 본 후에 작성한 것이라, 오해가 많았으며, 그로 인해 혼란을 드린 점 사과 드립니다.

.Net 8.0 블레이저의 정적 SSR - :thread: Slog - 닷넷데브 (dotnetdev.kr)

렌더 모드를 통한 호스팅 모델 결정

8.0 이전의 블레이저는 호스팅 모델이 사전에 결정되는 방식이었습니다.
그래서, 블레이저 서버냐 블레이저 와즘이냐를 프로젝트 생성 단계에서 사전에 결정하고 이를 바꿀 수 없었습니다.

8.0 부터는 모든 요소에 "렌더 모드"라는 개념을 도입하여, 호스팅 모델을 요소 단위에서 결정할 수 있도록 변경되었습니다.

요소가 가질 수 있는 렌더 모델은 아래 네 가지인데, 그 중에 기본 값이 Static Server 이고, Static SSR 모드를 가리킵니다.

위 표에서 Render Location 열은 레이저 요소를 Html로 렌더링하는 주체를 가리키는데, 블레이저 호스팅 모델과 같은 의미입니다.

보시다시피, Static SSR은 서버(Asp.Net Core 런타임)가 렌더링 주체입니다.

Static SSR(Server Side Rendering)

정적인 SSR은

  • 서버가
  • 요청을 받으면,
  • “정적인” Html 문서를 생성한다

는 의미입니다.

Static SSR은 매 요청 시마다, Html 문서를 (새롭게) 생성한다는 점에서, 저장 중인 "파일"만 제공하는 정적 웹 서버와 차이가 있습니다.

설정 방법

8.0 블레이저 웹앱 프로젝트 템플릿을 사용해 프로젝트를 생성할 때, 전역 렌더링 모드를 None 으로 선택하면 됩니다.

이렇게 선택하면, 서브 측 프로젝트에 있는 레이저 요소는 자신의 렌더 모드를 Static SSR 과 Server Interactivity (Interactive SSR) 중 하나를 선택할 수 있습니다.

선택은 RenderModelAttribute 를 통해 수행됩니다.

아래와 같이 특성을 지정하면 Server Interactivity 입니다.

 @rendermode InteractiveServer 

위와 같은 특성이 없는 경우에는 Static SSR 이거나, 이 요소를 포함하고 있는 부모 요소의 렌더 모드를 따릅니다. 다만, Client 측 프로젝트에 있는 레이저 요소는 서버라는 컨텍스트 자체가 존재하지 않기 때문에, 기본적으로 Webassembly Interactive 렌더 모드입니다.

정적

정적인 SSR에서 "정적"이라는 의미는 DOM과 "C# 코드가 협업하지 않음"을 의미합니다.
이와 반대되는 의미는 "동적"이 아니라 둘 사이에 “상호 작용이 있는(Interactive)” 입니다.

정적으로 렌더링된 Html 문서라 하더라도,브라우저 내부에서 DOM 과 javascript 코드는 상호작용이 가능하기 때문에, "C# 코드와의 협업"이 중요 문맥입니다.

SPA가 아니다

Static SSR 은 요청이 있을 때만 Html 문서를 생성하는데, 이는 전통적인 웹앱(MVC 등)의 렌더링 방식과 기본적으로 같은 것입니다.

결과적으로, 페이지 간 이동은 Anchor 태그에 의해 이뤄지며, 이동이 있을 때 마다 Http 요청과 Http 응답이 일어납니다.

렌더링 모드를 None으로 설정한 프로젝트를 실행시키고 네비게이션 메뉴를 누르면 아래와 같이 누를 때 마다 Request/Reponse 가 일어남을 알 수 있습니다.

즉, 전역으로 Static SSR 로 설정된 블레이저 앱은 SPA가 아니라 전통적 웹앱과 유사하다고 할 수 있습니다.

상호 작용 없음

서버가 Html 문서를 생성할 때 사용하는 주 재료는 Uri 와 그 Uri 를 담당하는 레이저 페이지 요소입니다.

예를 들어, 프로젝트 템플릿에 포함되는 Counter 요소는 아래와 같이 정의되어 있습니다.

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

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

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

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

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

Counter 요소의 DOM 객체 p, button 태그는 C# 코드와 상호 작용하도록 정의되어 있습니다.

이 요소의 렌더링 모드가 Static SSR 인 경우, 서버가 생성한 Html 문서는 브라우저에 의해 아래와 같이 Display 됩니다.

image

이때, 사용자가 [Click me] 버튼을 눌러도 Current count 가 증가하지 않습니다.

브라우저를 이용해서, 위 화면의 코드를 살펴 보면,

<!-- 생략 -->

<h1>Counter</h1>

<p role="status">Current count: 0</p>

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

<!-- 생략 -->

서버가 렌더링한 Html 문서에는 상호작용을 위한 어떤 인프라도 포함되어 있지 않은 것을 알 수 있습니다.

그 결과로, 사용자의 버튼 클릭에도 어떠한 반응이 일어 나지 않는 것입니다.

참고로, 버튼을 누를 때 버튼이 깜빡깜빡하는 건 style 때문에 그런 것이지 javascript 혹은 C# 과의 협업의 결과물은 아닙니다.

블레이저가 아니다.

렌더 모드가 Static SSR 로 설정된 요소를 Html로 렌더링하는 주체는 "서버 측 요소 렌더링 서비스"입니다.

이 서비스의 등록은 아래의 확장 메서드를 통해 이뤄집니다.

// Program.cs

// Add services to the container.
builder.Services.AddRazorComponents(); 

8.0 이전에는 단순히 프로젝트에 전역으로 설정된 호스팅 모델을 기반으로 상호 작용 인프라를 추가했는데, 8.0 블레이저에서는 렌더 모드 별로 세분화되었습니다.

Microsoft.AspNetCore.Components.Endpoints;
AddRazorComponents()
레이저 요소(Razor component)를 html 로 변환하는 역할만 함.

Microsoft.AspNetCore.Components.Server;
AddInteractiveServerComponents()
Server Interactivity를 위한 제반 인프라를 Html에 추가함.

Microsoft.AspNetCore.Components.WebAssembly.Server;
AddInteractiveWebAssemblyComponents()
Webassembly Interactivity를 위한 제반 인프라를 Html에 추가함

Static SSR 관점에서 눈 여겨 봐야 할 점은 AddRazorComponents 로 등록되는 서비스는 단순히 Razor Component 들을 Html 로 렌더링(변환)하는 역할만 한다는 점입니다.

예전에 레이저 요소를 html 로 렌더링하는 별도 실행 파일에 대한 링크를 닷넷 문서 어디 쯤인가에서 본 기억이 있는데, 지금은 그 내용을 찾을 수가 없네요. 이 서비스는 그 파일에 포함된 렌더링 로직을 사용하는 것이라 추측됩니다.

이에 반해, AddInteractiveServerComponents로 등록되는 서비스는 (웹소켓을 통해) 서버의 코드와 상호 작용하는 블레이저의 인프라를 추가합니다.

이렇게 구분된 구조를 통해 유추할 수 있는 사실은, 서버 측 렌더링 관점에서 Static SSR 은 블레이저 영역이 아니라는 점입니다.

그렇기 때문에, 블레이저에서 제공하는 라우팅과 인증과 관련된 모든 기능이 동작하지 않습니다. 그러나, Asp.Net Core 미들웨어에서 제공하는 라우팅, 인증 정보는 사용할 수 있습니다.

요소의 수명 주기

Static SSR 로 설정된 요소 객체는 Html 렌더링 과정 중에 생성되고 폐기됩니다.

이는 아래 두 가지의 의미가 있습니다.

  • 요소의 상태가 유지되지 않음
  • 생명 주기 메소드가 한번 씩만 호출됨. ( == 리렌더링이 일어 나지 않음)

예를 들어, Weather 요소의 아래 수명 주기 코드는 한 번은 실행되기 때문에 정상적인 데이터를 생성하지만, 렌더링 후 요소 객체는 바로 폐기되기 때문에 forecasts 필드의 데이터(상태)는 유지되지 않습니다.

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(500);

        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToArray();
    }

그런데, Static SSR 에 관한 닷넷 문서에서는 요소의 인스턴스가 생성되지 않는다고 표현했는데, 그 언급은 잘못된 것 같습니다.

생성이 안된다면 수명 주기 메서드가 호출되지 않아 Weather 요소는 날씨 정보를 포함할 수 없게 되니까요.

Prerendering 이 결부되지 않음

참고적인 내용입니다.

Static SSR 에서는 Prerendering 이라는 개념이 없습니다.
이 개념은 렌더링 모드를 Static SSR 이 아닌, 다시 말하면, 아래 중 하나를 선택했을 때만 도입되는 개념이니 문서를 읽을 때 참고하기기 바랍니다.

  • Server
  • WebAssembly
  • Auto

Static SSR의 장점

정적 렌더링된 페이지의 가장 큰 장점은 SEO 인데, 이는 곧 SPA의 가장 큰 단점이기도 합니다.

이러한 장점을 온전히 누리기 위해서는, Static SSR로 설정된 랜딩 페이지를 Index.razor 파일로 정의하는 것이 좋을 듯 합니다.

이 요소는 MainLayout 을 사용하지 않도록 해야 상호 작용 모드와 간섭이 없을 것입니다.
돌이켜 보면, 이 방식은 8.0 이전의 블레이저 서버의 _host.cshtml 과 유사한 방식인 것 같습니다.

또한 SEO와 관련해서는 Prerendering 과도 연결되어 있다는 점도 알 필요가 있습니다.

호스팅 모델 유지

마지막으로 위의 모든 사항은 8.0 블레이저 웹앱에만 적용된다는 점을 기억해야 합니다.

여전히 호스팅 모델이 사전에 정의된 블레이저 서버 앱(사용할 일은 없겠죠)과 블레이저 웹어셈블리 앱을 사용할 있는데, 위 개념을 그 앱들에 적용하면 안됩니다.

저도 최근에 7.0 블레이저 웹 어셈블리 앱을 8.0 으로 마이그레이션 하면서 가장 기초적인 문맥을 파악하지 못해 고생 좀 했기에, 정리한 사항을 공유하게 되었습니다.

Enhanced Navigation

이 부분이 가장 중요한 부분일 것 같아, 잠시 조사 좀 더 해보고 추가합니다.

Enhance Navigation 은 Static SSR 과 전통적 웹앱을 구분짓는 구분점이 될 것 같습니다.

이 기능은 블레이저 웹앱에 기본적으로 On 되어 있어, 이 기능이 없을 때 일어나는 일을 먼저 알아 봅니다.

이 기능을 끄기 위해 App.razor 에서 body 태그의 내용을 아래와 같이 변경합니다.
(제가 만든 게 아니고, 닷넷 문서에 나와 있는 내용입니다)

// App.razor
// ...

<body>
    <Routes />
    @* <script src="_framework/blazor.web.js"></script> *@
    <script src="_framework/blazor.web.js" autostart="false"></script>
    <script>
        Blazor.start({
        ssr: { disableDomPreservation: true }
        });
    </script>
</body>

</html>

이 상태로 앱을 실행하고 counter 페이지로 이동했을 때 네트워크 로그는 아래와 같습니다.

image

이를 변경 전과 비교해보면, 달라진 게 보일 것입니다.

기능이 on 되어 있을 때(변경 전)에는 counter 로 이동했을 때, blazor.web.js 가 보낸 요청에 대한 서버 응답이 한 줄로 표시되는데, 이 요청을 보내고, 받은 응답을 바탕으로 DOM을 업데이트 시킨 것이 기능이 하는 역할입니다.

이 기능이 off 된 후에는 counter 전체 페이지와 자산(blazor.web.js 포함)을 전부 새로 받고 있습니다.
그 결과로 화면 전체가 refresh 됩니다.

즉, 이 기능을 꺼야 MVC와 같은 전통적인 웹앱과 같은 방식으로 동작하는 것입니다.

Enhanced Navigation 기능은 페이지 이동과 form 을 관장하는데, 구체적인 동작 방식은, 브라우저에서 페이지 이동 요청이 있을 경우, 이를 캡쳐해서 요소들의 주소라면, 변경될 부분만 서버로부터 받고 DOM을 업데이트 합니다. 변경되지 않는 부분은 주로 MainLayout 이고, 새로 받는 부분은 페이지 요소의 컨텐츠입니다.

DOM 만 업데이트하기 때문에, 화면 전체를 refresh 하는 것 보다 훨씬 효율적입니다.

이 부분을 보고 솔직히 많이 놀랐습니다. Interactive 렌더 모드를 설정할 필요가 있나 싶을 정도로요.

이 것도 어찌보면 interactive 이기도 하고, 전통적인 웹앱보다 효율적이기까지 하니까요.
물론 정말로 Interactive 한 화면을 구성하려면 Interactive 렌더 모드를 설정할 필요가 있기는 합니다.

도움이 되셨기를 바랍니다.

9개의 좋아요

이제 razor page 는 버려도 되는 건가요

1개의 좋아요

좋은 글 감사합니다. 많은 정보 얻어 갑니다!

2개의 좋아요

GitHub 블로그에 Jekyll 을 사용한 정적 HTML 생성 방식을 사용해서 만들게 되는데,

이를 대체 할 수 있게 된다면 굉장히 좋을 것 같습니다.

2개의 좋아요

저는 MVC에서 블레이저로 넘어 왔기 때문에, 웹 페이지는 잘 알지 못합니다.
구조적인 관점에서 비교 한 번 해주세요.

1개의 좋아요

저는 클라우드 플래어만 사용합니다.

말씀 하신 방식에 관해 글 한번 올려 주세요. ^^

아쉽게도 Blazor에서 소개하는 Static이라는 개념은 일반적으로 프론트엔드에서 통용되는 Static Site Generation(SSG)에 사용되는 Static과는 다릅니다.

말씀하신 Jekyll같은 프론트엔드의 SSG에서 Static은 정적 파일을 빌드 시에 생성하는 방식을 의미하지만, Blazor의 Static SSR은 프론트엔드에서의 No JavaScript개념과 비슷하다고 생각하시면 됩니다. 빌드 시에 정적 파일을 생성하는 것이 아니라 서버에서 요청을 받으면 정적 파일을 만들어서 응답합니다.

아쉽게도 Blazor에서 SSG개념을 사용하는 것은 유료입니다 :sob: ServiceStack의 Razor SSG

5개의 좋아요

헉 제가 찾던 기능중 하나였는데 유료군요 ㅠㅠ
그래도 찾아주셔서 감사합니다…
그나마 개인적으로 쓰려던 목적이 커서, 그나마…

Individual License
The Individual License Key can only be used in single developer projects where you are the sole Author, Contributor and Copyright Owner of its source code

개인용으로는 그래도 찍먹이 가능하겠네요 ㅠㅠ

오늘 좀 자세히 살펴보았습니다.
일단 용어가 문제가 많습니다. 닷넷은 왜이리 용어를 중복으로 쓰는지…
말씀하신대로 전통적인 Static 은 .html 파일이나 정적리소스(이미지파일등)을 말하는건데 말이죠.

Server Side Rendering 은 그때 그때 서버가 html dom을 만들어 뱉어내죠. PHP 처럼요.
Blazor (static) SSR은 기본적으로 이겁니다. 이건 스태틱이 아니라 동적생성웹페이지(다이나믹)이죠.

이게 static이냐? 하면 앞서말한 전통적인 .html이 아니기때문에 아니라고 해야겠지만, .Net 8 SSR 이전에 이미 Blazor Server 라는 용어때문인지 그와의 차이를 이해시키기위해 Static 이란 용어를 붙인것 같습니다. (굳이 붙여서 헤깔리게시리…)

중요 시사점은 Blazor SSR은 Blazor Server와 다르게 SEO에 유리하도록 검색엔진이 읽을 수 있는 DOM을 뱉어낸다는거죠.

이게 Static하다는 말은 C#으로 구현한 버튼기능등이 동작을 못한다는거고요. 당연히도 js로 대체하면 가능하고요 .
JS 안쓰고 여전히 C#을 쓰려면 해당 부분(컴포넌트)만 Blazor Server 컴포넌트로 렌더링 모드를 지정하면 되는군요. 그럼 그부분만 SignalR 커넥션이 열리고요.

좋아졌다고 봅니다. 웹에 공개할건 무조건 static SSR 옵션(None)으로 시작하면 되겠네요. 서버의 C#쓸곳은 Server 렌더링 컴포넌트를 넣으면되고, 서버와 통신이 필요없는 부분은 WASM 클라이언트를 껴넣을 수 있으니까요.

다른 웹프레임워크 (Next.js등)에도 이런게 가능한지 궁금하군요. 그런건 애초에 js쓰니 그럴필요가 없을수도 있고요.

5개의 좋아요

다른 js프레임워크도 SignalR 혹은 자체 구현 웹소켓을 사용할 수 있으니 블레이저 서버와 동일하게 동작하게 만들 수 있습니다.

js 를 사용한다면 굳이 와즘을 사용할 필요가 없기 때문에, 블레이저 와즘을 구현할 필요는 없죠.

블레이저의 장점은 뭐니 뭐니 해도 “코드 보안”(과 그로 인한 서버 보안)이라고 생각합니다.
js 와 wasm 코드는 모두 유저에게 노출되니까요.

3개의 좋아요

기존의 JS 영역의 SSR 프레임워크들은 Hydration이라는 기법을 썼습니다. 서버에서는 HTML을 렌더링하고 클라이언트에서 UI에서 렌더링을 다시 해서 이벤트나 상태관리를 하는 방식입니다(웹에서 렌더링 문서에서 설명을 잘 하고 있습니다.).

Blazor같이 통신으로 서버와 연결해서 변경점을 주고받는 방식은 JS 영역의 SSR 프레임워크에서 React Server Components나 Resumablity라는 형식으로 최근 받아들여지고 있습니다(출처 - LiveView 1.0 출시 - LiveView goes mainstream). 하지만 서버에서 변수나 DOM의 상태는 Blazor처럼 받아오지만 Hydration은 하고 있습니다.

다른 패러다임이 나오지 않는 이상 비슷하게 진행될 것 같습니다.

4개의 좋아요