이 글은 아래 글의 오류를 정정하기 위한 것입니다.
Static SSR 개념이 처음 나왔을 때 가볍게 훑어 본 후에 작성한 것이라, 오해가 많았으며, 그로 인해 혼란을 드린 점 사과 드립니다.
.Net 8.0 블레이저의 정적 SSR - 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 됩니다.
이때, 사용자가 [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 페이지로 이동했을 때 네트워크 로그는 아래와 같습니다.
이를 변경 전과 비교해보면, 달라진 게 보일 것입니다.
기능이 on 되어 있을 때(변경 전)에는 counter 로 이동했을 때, blazor.web.js 가 보낸 요청에 대한 서버 응답이 한 줄로 표시되는데, 이 요청을 보내고, 받은 응답을 바탕으로 DOM을 업데이트 시킨 것이 기능이 하는 역할입니다.
이 기능이 off 된 후에는 counter 전체 페이지와 자산(blazor.web.js 포함)을 전부 새로 받고 있습니다.
그 결과로 화면 전체가 refresh 됩니다.
즉, 이 기능을 꺼야 MVC와 같은 전통적인 웹앱과 같은 방식으로 동작하는 것입니다.
Enhanced Navigation 기능은 페이지 이동과 form 을 관장하는데, 구체적인 동작 방식은, 브라우저에서 페이지 이동 요청이 있을 경우, 이를 캡쳐해서 요소들의 주소라면, 변경될 부분만 서버로부터 받고 DOM을 업데이트 합니다. 변경되지 않는 부분은 주로 MainLayout 이고, 새로 받는 부분은 페이지 요소의 컨텐츠입니다.
DOM 만 업데이트하기 때문에, 화면 전체를 refresh 하는 것 보다 훨씬 효율적입니다.
이 부분을 보고 솔직히 많이 놀랐습니다. Interactive 렌더 모드를 설정할 필요가 있나 싶을 정도로요.
이 것도 어찌보면 interactive 이기도 하고, 전통적인 웹앱보다 효율적이기까지 하니까요.
물론 정말로 Interactive 한 화면을 구성하려면 Interactive 렌더 모드를 설정할 필요가 있기는 합니다.
도움이 되셨기를 바랍니다.