이번 포스팅에서는 Blazor를 사용하여 사용자 인터페이스를 구축하는 방법에 관해 간단히 알아보고자 합니다. 또한 Blazor만이 가진 특징과 함께 나름대로의 장·단점에 대해서도 함께 살펴보고자 합니다.
우선 웹서버나 웹브라우저에서 실행할 수 있는 Blazor component에 관한 것부터 알아 볼 것입니다. Blazor component는 2가지 방법으로 제공될 수 있는데 첫번째로 Blazor Server로 호스트되는 경우로서 브라우저상의 사용자 인터페이스 변경을 위해 SignalR을 사용할 수 있는 반면 Blazor WebAssembly로 호스트되는 경우에는 Web Browser상에서 코드가 실행되고 서버와의 상호작용을 위해 HTTP요청을 만들어 호출하는 방법을 사용하게 됩니다.
1. Blazor의 이해
Blazor는 기본적으로 전통적으로 사용되던 클라이언트 웹브라우저 프로그래밍 언어인 Javascript대신 C#만을 사용하여 필요한 Component만들어 공유하고 웹 사용자 인터페이스의 상호작용을 구현할 수 있도록 합니다.
● JavaScript
전통적으로 웹 브라우저에서 실행되어야할 프로그램은 Javascript나 혹은 변환이나 컴파일이 필요한 Vue 또는 React와 같은 Framework가 사용되었습니다. 이는 현존하는 모든 웹브라우저가 기본적으로 Javascript를 지원하고 있기 때문이며 클라이언트영역에서 비지니스 로직을 구현하기 위한 최소한의 기준익 되기 때문입니다.
하지만 Javascript는 언뜻 C언어와 비슷한 고급언어처럼 보이지만 사실 클래스상속과 같은 객체지향 방식이 아닌 prototype을 사용하는 동적형식의 가상함수 프로그래밍언어이며 이는 Javascript가 사용되고 운영될때 크고작은 문제점을 드러내기도 합니다.
물론 다수의 대안으로 몇가지 기술이 등장하기는 했지만 이런 상황에서 Blazor는 기존의 서버용 프로그램 작성을 위해 사용하던 C#과 같은 고급언어를 웹브라우저에서 실행하는 로직으로 구현하게 함으로서 많은 대안중 제시되었습니다.
● 웹 Plugin 프로그램
오래전부터 클라이언트영역에서 비지니스 로직을 구현하기 위한 Javascript의 대체방안으로 Flash가 Silverlight와 같은 기술이 사용되기도 하였습니다. 그러나 보안적인 문제와 웹표준화등에 따른 이유로 현재는 거의 쓰이지 않는 기술입니다.
● WebAssembly
WebAssembly는 2017년 처음 발표되었으며 Internet Explorer를 제외한 Chromium 브라우저(Chrome, Edge, Opera 등), Firefox, WebKet(Safari)등 거의 모든 주요 브라우저에서 WebAssembly를 사용할 수 있습니다.
WebAssembly(줄여서 Wasm)은 가상머신을 위한 바이너리 명령 형식이며 가상머신은 웹상에서 거의 native속도에 가깝게 코드를 실행할 수 있는 수단으로 제공됩니다. 여기서 Wasm은 C#고 같은 고수준언어의 컴파일을 거쳐 가상머신에서 실행될 수 있도록 설계된 것입니다.
● Blazor Hosting 모델
Blazor는 서버 쪽을 실행하거나 브라우저에서 클라이언트 쪽을 실행하도록 설계된 웹 프레임워크이며 다음과 같은 호스팅 모델을 통해 서비스될 수 있습니다.
- Blzor Server : 서버에서 동작하므로 별도의 인증절차 없이 C#코드는 서버의 모든 리소스에 완벽하게 접근할 수 있습니다. 또한 사용자의 웹브라우저 상에서 사용자 인터페이스는 SignalR을 사용하여 필요한 정보를 전달할 수 있습니다. 이때 서버는 SignalR을 통한 연결을 유지하며 연결된 모든 클라이언트의 상태를 추적하기 때문에 많은 수의 클라이언트가 연결되는 경우 성능적으로 문제를 야기할 수 있습니다. Blazor Server는 ASP.NET Core 3.0의 일부로 2019년에 처음 발표되었습니다.
- WebAssembly : 사용자의 웹브라우저에서 동작하는 방식입니다. 때문에 C#코드는 웹브라우저에 한정된 자원에만 접근할 수 있으며 서버의 자원접근을 위해서는 별도의 인증을 위해 또는 서버와의 상호작용을 위해 필요한 HTTP요청을 만들어여 호출해야 합니다. WASM은 2020년 ASP.NET Core 3.1이 장기지원버전이 아닌 탓에 3.2버전의 확장형으로 발표되었으며 처음 3.2버전은 Mono Runtime과 Mono Library를 사용했으나 .NET5부터는 Mono Runtime과 .NET5 Library를 사용하게 되었습니다.
참고로 WASM은 JIT없이 .NET IL interpreter상에서 동작합니다. 성능적인 이슈사항이 있지만 .NET5와 .NET6를 거치면서 성능적인 향상을 거듭해 가고 있습니다.
- .NET MAUI Blazor App (Blazor Hybrid) : .NET MAUI app에서 호스트되는 것으로 local interop channel을 통해 Web UI를 Web View Control에 렌더링합니다. Node.js를 사용하는 Electron App과 비슷한 방식으로 비교될 수 있습니다.
호스트되는 모델의 특징에 따라 설계할때 다소 주의를 필요로 하지만 일단 Blazor component를 하나 만들게 되면 서버, 클라이언트, Desktop App에서 동일하게 동작시킬 수 있다는 장점이 있습니다.
참고로 Blazor Server는 Internet Explorer 11에서 동작이 가능하지만 Blazor WebAssembly는 Internet Explorer를 지원하지 않습니다. 또한 WASM은 선택적으로 PWAs(Progressive Web Apps)를 지원하므로 클라이언트는 WebBrowser 메뉴를 통해 자신들의 Desktop에 App을 추가하고 Offline에서 App을 동작시키는 것도 가능합니다.
● Blazor component
component라는 개념은 Blazor를 사용해 사용자 인터페이스를 만드는데 있어서 중요한 개념입니다. Component는 사용자 인터페이스의 정의와 사용자 이벤트에 대한 반응을 정의하며 배포를 위해 컴파일된 NuGet Razor class Library로 구성될 수 있습니다.
예륻 들어 다음과 같이 Car.razor이라는 Component를 만드는 경우
<h3>Car</h3>
<label>속도 : </label> <span>@Speed</span><br />
<label>색상 : </label> <span>@Color</span>
@code {
[Parameter]
public int Speed { get; set; }
[Parameter]
public string? Color { get; set; }
}
Web Page 어디든 완성한 Component를 아래와 같이 사용할 수 있습니다.
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<Car Speed="80" Color="Red"></Car>
<SurveyPrompt Title="How is Blazor working for you?" />
직접 Component를 만드는것 이외에도 <title>과 같은 내장된 Blazor의 많은 Component들이 있고 Devexpress와 같은 회사에서도 다수의 Component를 개발해 판매하고 있습니다.
가까운 미래에 Component는 웹 사용자 인터페이스를 제작하는것 이외에 Blazor Mobile Binding을 사용해 기존의 HTML과 CSS를 대체하여 모바일에서의 사용자 인터페이스지원과 XAML과 .NET MAUI를 사용한 크로스-플렛폼 GUI를 개발하는데까지 활용될 것입니다.
● Blazor와 Razor의 차이
Razor는 MVC View나 Partial View등의 .cshtml파일에서 C#과 HTML을 혼용해 사용할 수 있는 markup 구문입니다. 다만 기본적으로 비지니스 로직은 Controller Class로 분리될 수 있으며 Vew로 모델을 투영하여 Web Page에서 출력을 처리하게 됩니다.
반면 Blazor Component는 .razor 파일에서 사용하며 Web Page로의 직접적인 출력을 담당하지 않습니다. Blazor Component는 그저 Web Page내부에서 Component를 감싸는 형태로 사용될 뿐입니다. 이때 @page 지시자는 Web Page에서 Component를 표시하기 위한 URL경로를 나타내는 라우트를 정의하는데 사용될 수 있습니다.
2. Blazor Project 살펴보기
앞서 언급한 것처럼 Blazor가 호스트되는데는 모델에는 Server와 WebAssembly 2가지 방식이 존재합니다. 이 모델중 실제 어떤 모델을 선택할지에 대해서는 직접 2개의 기본적인 템플릿을 생성해 봄으로서 차이를 확인하면 알 수 있을 것입니다.
● Blazor Server
우선 Visual Studio 2022를 사용해 다음과 같이 기본 템프릿을 선택하는 방법으로 Blazor Server프로젝트를 생성합니다.
프로젝트는 ASP.NET Core의 일반적인 프로젝트를 생성하는 것과 거의 비슷합니다. 모든 선택사항에서 기본값을 그대로 두고 프로젝트명만 적절하게 지정하여 프로젝트를 생성하면 됩니다.
물론 다른 프로젝트와는 다른 차이는 존재합니다. 일단 프로젝트를 생성한뒤 프로젝트의 Program.cs파일을 열어보면 서비스 설정영역에서 AddServerSideBlazor() 메서드를 호출하고 HTTP Pipeline설정 영역에서 Map으로 시작하는 2개의 메서드를 호출하고 있음을 알 수 있습니다.
using BlazorApp.Data;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
특히 MapBlazorHub()메서드는 ASP.NET Core App이 Blazor Component로 들어오는 SignalR 연결을 받아들일 수 있도록 하는 메서드이며 반면 MapFallbackToPage()메서드는 이외 모든 요청이 _Host.cshtml로 전달되도록 합니다.
Pages폴더에 있는 _Host.cshtml파일은 Layout페이지로 _Layout을 지정하고 있으며 Blazor Component를 표시하고 있습니다.
@page "/"
@namespace BlazorApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_Layout";
}
<component type="typeof(App)" render-mode="ServerPrerendered" />
이때 component는 render-mode로 서버에서 사전렌더되는 App Type이 지정되어 있습니다.
같은 Pages폴더안에는 공유 Layout 페이지인 _Layout.cshtml도 존재하며
@using Microsoft.AspNetCore.Components.Web
@namespace BlazorApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="BlazorApp.styles.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@RenderBody()
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
_Layout.cshtml에서는 <div id="blazor-error-ui">를 통해 오류 발생시 오류사항을 표시하고 있는 부분을 확인할 수 있습니다. 또한 Server에 대한 SignalR연결을 관리하도록 하기 위해 blazor.server.js라는 script파일이 추가됨을 알 수 있습니다.
이번에는 프로젝트 루트폴더로 넘어와 App.razor 파일을 살펴보겠습니다.
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
현재 Assembly내에 있는 모든 Component의 Router를 정의하고 있는데 요청에 다른 적절한 route가 발견되면 RouteView가 실행되어 DefaultLayout에서 정의한 MainLayout을 기본 Layout으로 설정하고 모든 route 데이터 매개변수를 Component로 전달합니다.
반면 적절한 route를 찾지 못하면 LayoutView가 살행되어 MainLayout안에서 내부의 HTML을 표시하게 됩니다.
App.razor에서 Layout으로 사용하고 있는 MainLayout.razor는 Shared 폴더에서 찾을 수 있습니다.
@inherits LayoutComponentBase
<PageTitle>BlazorApp</PageTitle>
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
MainLayout에서는 <main>이나 <article>과 같은 몇몇 HTML5 마크업과 함께 navigation menu를 포함하는 sidebar라는 <div>태그가 정의되어 있습니다. 여기서 navigation menu는 NavMenu.razor component 파일에서 구현된 것입니다.
참고로 MainLayout.razor는 MainLayout.razor.css파일과 연결되어 있는데 이 파일은 component를 위해 독립적으로 적용되는 css입니다.
NavMenu.razor는 MainLayout과 함께 Shared폴더에 있으며
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorApp</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
Home(Index)과 Counter그리고 fatchdata로 연결되는 세개의 menu item을 가지고 있습니다. 이들은 기본으로 제공되는 NavLink라는 Blazor component를 사용해 구현되었습니다.
위에서 생성된 Home, Counter, fetchdata 파일은 모두 Pages폴더에 존재하며 이 중 FetchData.razor파일은
@page "/fetchdata"
<PageTitle>Weather forecast</PageTitle>
@using BlazorApp.Data
@inject WeatherForecastService ForecastService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
의존성 주입을 통해 weather service의 객체를 가져와 weather forecasts를 가져오는 component를 정의하고 있습니다.
weather service는 Data폴더안에 WeatherForecastService.cs파일에 정의되어 있는데
namespace BlazorApp.Data
{
public class WeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
{
return Task.FromResult(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());
}
}
}
Web API Controller가 아닌 일반적인 Class로 랜덤한 weather data를 반환하고 있습니다.
프로젝트에 있는 파일중 어떤 파일의 경우에는 자체적으로 css나 혹은 javascript파일을 연결하고 있음을 알 수 있습니다.
CSS는 component자체적으로 스타일을 제공하기 위한 파일이며 javascript는 browser API와 같은 순수한 C#만으로는 수행할 수 없는 기능을 위한 것입니다. 이들 CSS와 Javascript는 프로젝트단위에서 전체적으로 제공하는 CSS와 Javascript와의 충돌을 피하기 위해 격리된 형태로 제공되며 더 높은 우선순위로 component에 적용됩니다.
3. Page Component의 Blazor routing
위에서 봤었던 App.razor파일의 Router component는 말 그대로 component를 Routing하기 위한 것입니다. component의 instance를 생성하는 HTML 태그와 비슷한 markup의 이름은 component type이며 component는 Web Page안에서 포함될 수 있고 Razor Page나 MVC Controller처럼 Route될 수 있습니다.
● Route를 위한 Page Component
route가능한 page component를 만들기 위한 방법은 @page 지시자를 사용하는 것입니다.
@page "cars"
이는 MVC Controller에서 특정 Class의 route를 위해 다음과 같이 decorate한것과 동일한 효과를 가지게 되는데
[Route("Cars")]
public class CarsController
{
}
Router Component는 AppAssembly매개변수의 Assembly에서 [Route] 특성으로 decorate된 compoent찾아 해당하는 URL경로를 등록하게 됩니다.
모든 single-page component는 등록하고자 하는 route만큼 여러개의 @page 지시자를 사용할 수 있으며 실행단계에서 page compoent는 MVC View나 Razor Page에서 했던 것처럼 지정한 Layout과 병합되어 렌더링됩니다. Blazor Server의 기본 템플릿 프로젝트에서 Page Component Layout으로는 기본적으로 MainLayout.razor페이지가 정의되어 있으며 관례적으로 route가능한 page component는 Pages폴더에 저장됩니다.
마이크로소프트는 Blazor Routing과 NaviLink Component를 위한 NavigationManager라는 의존성 서비스를 제공하고 있는데 해당 서비스의 NavigateTo() 메서드는 특정한 URL로의 이동을 위해 사용될 수 있습니다.
Blazor route는 대소문자를 구분하지 않는 매개변수를 포함할 수 있는데 [Parameter] attribute를 사용한 속성을 통해 매개변수를 바인딩함으로서 매개변수의 실제 값에 접근할 수 있습니다.
@page "/{MyParam}"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<span>MyParam Value : @MyParam</span>
<SurveyPrompt Title="How is Blazor working for you?" />
@code {
[Parameter]
public string? MyParam { get; set; }
}
물론 ?를 사용해 값이 넘어오지 않는 경우를 대비하거나
@page "/{MyParam?}"
OnParametrsSet() 메서드를 통해 값이 넘어오지 않을 경우 대신 사용될 값을 지정해 줄 수 있습니다.
@code {
[Parameter]
public string? MyParam { get; set; }
protected override void OnParametersSet()
{
MyParam = MyParam ?? "999";
}
}
여기서 사용된 OnParametersSet() 메서드는 ComponentBase로 부터 상속하는 기반 클래스에 의해 정의되었으며 이 밖에 ComponentBase는 다음과 같은 몇가지 유용한 메서드를 지원하고 있습니다.
Method | Description |
InvokeAsync | renderer의 synchronization context의 함수를 실행하기 위해 호출됩니다. |
OnAfterRender, OnAfterRenderAsync |
component가 매번 render되고 난 이후 code를 실행하기 위해 메서드를 재정의합니다. |
OnInitialized, OnInitializedAsync |
component가 render tree의 부모로 부터 초기 매개변수를 전달받고 난 후 code를 실행하기 위해 메서드를 재정의합니다. |
OnParametersSet, OnParametersSetAsync |
component가 매개변수를 전달받고 값이 속성에 할당되고 나면 실행할 code를 위해 메서드를 재정의합니다. |
ShouldRender | component가 Render되어야 하는지의 여부를 여부를 나타내기 위해 메서드를 재정의합니다. |
StateHasChanged | component를 다시 렌더링하려면 해당 메서드를 호출합니다. |
또한 Blazor component는 MVC View나 Razor Page와 비슷한 공통Layout을 가질 수 있습니다. 예를 들어 공통 레이아웃 페이지는 다음과 같이 LayoutComponentBase를 상속하는 razor페이지가 될 수 있고
@inherits LayoutComponentBase
<hr />
@Body
<hr />
해당 Layout을 사용하려는 component에서는 위에서 생성한 Layout을 지정해 주면 @Body부분에서 설정된 html을 render할 것입니다.
@page "/"
@layout MyLayout
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
Route관련하여 자주 사용되는 또 다른 component로는 NavLink component가 있습니다. 예를 들어 HTML에서 특정 페이지의 이동을 위해서 다음과 같이 <a>태그를 사용했다면
<a href="/car">Car</a>
Blazor에서는 같은 방법으로 NavLink component가 사용됩니다.
<NavLink href="car">Car</NavLink>
물론 <a>를 사용해도 여전히 원하는 페이지로의 이동이 가능하긴 하지만 NavLink는 앵커 태그를 렌더링 하고 ' href '가 현재 URI와 일치 하는지 여부에 따라 해당 '활성'클래스를 자동으로 전환합니다. 만약 CSS에서 다른 클래스이름을 사용한다면 해당 클래스의 이름은 NavLink.ActiveClass 속성으로 설정할 수 있습니다.
기본적으로 매칭알고리즘에서 href는 경로에 대한 접두사에 해당합니다. 따라서 NavLink의 href에서 '/car'는 아래 경로에 모두 일치하고 이들 모두에 active 클래스가 적용됩니다.
/car/detail /car/detail/10 /car/sedan |
위에서 처럼 접두사와 관련된 모든 경로에 대해 NavLink의 href경로와 일치하는 것으로 설정을 잡고자 한다면 Match속성에 NavLinkMatch.All값을 부여하면 됩니다.
<NavLink href="/car" Match="NavLinkkMatch.All">Car</NavLink>
이외에 <a>태그에서 사용되는 target과 같은 속성은 그대로 사용할 수 있으며 <a>로 랜더링될때 자동으로 반영됩니다.
● Blazor WebAssembly
이전에는 Blazor Server와 관련된 프로젝트를 생성했는데 이번에는 Server가 아닌 WebAssembly형식의 프로젝트를 생성해 보고자 합니다.
우선 프로젝트 템플릿으로 WebAssembly를 선택하고
프로젝트 이름을 지정합니다.
다음 단계에서는 ASP.NET Core hosted와 Progressive Web Application을 체크하고
프로젝트를 생성합니다.
프로젝트는 총 3개가 생성되는데
여기에서 Client가 Blazor WebAssembly 프로젝트이며 Server는 weather service를 호스팅하는 ASP.NET Core Web API 프로젝트로서 같은 프로젝트의 Shared와 Client 그리고 Server Side에서 Blazor WebAssembly지원을 위한 패키지를 참조하고 있습니다. 마지막 세번째 Shared는 weather service를 위한 Model등 필요한 공유 Class Library를 가진 프로젝트에 해당합니다.
Blazor WebAssembly App을 배포할때는 Web Server상에 각각의 프로젝트의 배포파일을 배치함으로서 Client 프로젝트를 배포하고 또한 Client App을 참조하여 서비스를 제공하는 Server프로젝트를 배포하여 Server와 Client모두를 호스팅하도록 합니다.
프로젝트의 Client프로젝트에서 파일(csproj)을 열어보면
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\BlazorAppWebAssembly.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
</Project>
우선 Blazor WebAssembly SDK를 사용하고 2개의 WebAssembly 패키지와 Shared 프로젝트, 그리고 PWA지원을 위한 service worker를 참조하고 있음을 알 수 있습니다.
그 다음 Program.cs 파일에서는 host builder로 server-side ASP.NET Core가 어난 WebAssembly를 사용하며
using BlazorAppWebAssembly.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
Blazor WebAssembly App의 일반적인 요구사항인 HTTP 요청을 위한 의존성 서비스를 등록하고 있습니다.
Client에 있는 wwwroot폴더에는 index.html파일이 있는데
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>BlazorAppWebAssembly</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="BlazorAppWebAssembly.Client.styles.css" rel="stylesheet" />
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>
파일을 보면 offline동작을 위한 manifest.json과 service-worker.js파일이 사용되고 있음을 알 수 있습니다. 참고로 blazor.webassembly.js는 Blazor WebAssembly를 위한 NuGet Package를 내려받는 스크립트파일입니다.
그리고 나머지 .razor파일들은 이전에 생성했던 Blazor Server 프로젝트와 동일한 이름으로 생성되어 있는데 Pages폴더에서 FetchData.razor파일을 열어보면
@page "/fetchdata"
@using BlazorAppWebAssembly.Shared
@inject HttpClient Http
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
}
HTTP요청을 만들기 위한 의존성서비스가 사용된것을 제외하고 전체적으로 Blazor Server에서 생성된 내용과 거의 비슷하게 되어 있음을 알 수 있습니다.
프로젝트에는 이전 Server와 동일하게 Blazor Component Code는 사용자의 Web Browser에서 실행되고 weather service는 Web Server하에서 동작합니다.
4. Blazor Server에서의 Component 만들기
해당 섹션에서는 이전에 사용하던 Northwind DB를 통해 Region의 항목을 나열하고, 추가, 수정하는 Component를 만들것입니다. 처음에는 Blazor Server에서 해당 기능을 구현할 것이지만 후에는 Blazor Server와 Blazor WebAssembly모두에서 동작할 수 있도록 refactor할 것입니다.
● 필요한 component 추가하기
우선 이전에 만든 Blazor Server프로젝트의 Pages폴더에 Blazor Component로 MyRegion.razor이라는 새로운 파일을 추가합니다.(파일명은 대문자로 시작해야 합니다.)
그리고 아래 구문을 추가하여 Region값을 표시하고 값을 저장하기 위한 속성을 정의합니다.
<h3>MyRegion - @(string.IsNullOrEmpty(Region) ? "-" : Region) </h3>
@code {
[Parameter]
public string? Region { get; set; }
}
그 다음 Index.razor 파일에서는 위에서 추가한 MyRegion 컴포넌트를 추가합니다.
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<MyRegion></MyRegion>
혹은 다음과 같이 component를 추가합니다.
<MyRegion Region="Etc"></MyRegion>
● Route가능한 page component 만들기
위에서 만든 component는 route parameter를 사용한 routeble page component로 손쉽게 전환할 수 있습니다.
우선 MyRegion.razor파일에서 아래와 같이 region 매개변수를 사용한 route를 추가하고
@page "/myregion/{region?}"
Shared 폴더에서 NavMenu.razor파일을 수정하여 사용자에게 위에서 추가한 routeble page component가 - 문자 또는 지정한 Region값을 보여줄 수 있도록 2개이 list item menu를 추가하고 프로제트를 실행하면 해당 Route로 정상 이동됨을 확인할 수 있습니다.
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="myregion" Match="NavLinkMatch.All">
<span class="oi oi-list-rich" aria-hidden="true"></span> MyRegion -
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="myregion/etc">
<span class="oi oi-list-rich" aria-hidden="true"></span> MyRegion Etc
</NavLink>
</div>
● Database 사용
Northwind Database를 사용하기 위해서는 몇가지 추가적인 작업이 필요합니다. 해당 작업과정은 아래 글에서 'DB 사용하기'부분을 참고해 주시기 바랍니다.
[.NET/ASP.NET Core] - [ASP.NET Core] Razor Page로 웹프로젝트 만들기
위 글에서는 AddNorthwindContext()메서드를 호출하여 서비스를 추가하는 것까지만 따라간뒤 다음 사항을 이어서 구현하시면 됩니다.
서비스를 추가하고 _Imports.razor 파일을 열어 다음과 같이 Namespace를 추가합니다. 이 작업은 프로젝트에 있는 각각의 component에서 DB객체가 필요한 경우 일일이 동일한 Namespace를 추가할 필요가 없도록 만들기 위함입니다.
@using BlazorApp.Data
참고로 _Imports.razor에서 사용한 Namespace는 오로지 razor파일에서만 유효합니다. 만약 component의 code를 별도의 .cs파일에서 구현하고 여기에서 DB객체가 필요한 경우 해당 파일에는 별도의 Namespace를 구현하거나 분리된 파일에서 global using을 사용해 Namespace를 Import해야 합니다.
이제 위에서 만든 MyRegion.razor에서는 DB에 연결하기 위한 inject를 추가합니다. 그 다음 DB객체를 가져오고 추가적인 HTML과 code를 구현하여 기존의 Region항목을 나열하도록 합니다.
@page "/myregion/{region?}"
@using Microsoft.EntityFrameworkCore
@inject NorthwindContext DB
<h3>MyRegion</h3>
@if (region == null)
{
<span>Nothing</span>
}
else
{
<table>
<thead>
<tr>
<th>ID</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach(var r in region)
{
<tr>
<td>@r.RegionId</td>
<td>@r.RegionDescription</td>
</tr>
}
</tbody>
</table>
}
@code {
[Parameter]
public string? Region { get; set; }
private IEnumerable<Region>? region;
protected override async Task OnParametersSetAsync()
{
if (string.IsNullOrEmpty(Region))
{
region = await DB.Region.ToListAsync();
}
else
{
region = await DB.Region.Where(x => x.RegionDescription == Region).ToListAsync();
}
}
}
5. Blazor Component를 위한 서비스 추상화
위 예제에서는 Blazor Component가 직접적으로 Northwind DB를 호출하여 Region 항목을 나열하고 있습니다. 이 구조는 Blazor Server의 Component가 Server상에서 실행되는데에는 아무런 문제가 없지만 해당 Component가 WebAssembly로 호스트되는 경우에는 작동할 수 없습니다. 이 문제는 Component의 재사용을 위해 지역 의존성 서비스를 생성함으로서 해결할 수 있습니다.
우선 프로젝트의 Data폴더에 IMyRegion.cs라는 이름으로 인터페이스 파일을 생성하고 다음과 같이 Region목록을 가져오는 메서드를 추가합니다. 이 메서드의 동작은 바로전에 MyRegion.razor파일에서 작성한 로직과 동일한 것입니다.
namespace BlazorApp.Data
{
public interface IMyRegion
{
Task<List<Region>> GetAllRegion();
Task<List<Region>> GetRegions(string r);
}
}
그리고 인터페이스에 따른 메서드를 구현하는 클래스를 작성합니다.
using Microsoft.EntityFrameworkCore;
namespace BlazorApp.Data
{
public class MyRegion : IMyRegion
{
private readonly NorthwindContext DB;
public MyRegion(NorthwindContext _DB)
{
DB = _DB;
}
public Task<List<Region>> GetAllRegion()
{
return DB.Region.ToListAsync();
}
public Task<List<Region>> GetRegions(string r)
{
return DB.Region.Where(x => x.RegionDescription == r).ToListAsync();
}
}
}
이제 Program.cs의 서비스 설정영역에서 IMyRegion인터페이스를 구현하는 transient service로서 NorthwindService를 등록하기만 하면 됩니다.
builder.Services.AddTransient<IMyRegion, MyRegion>();
여기서 Service를 AddTransient로 등록하면 DbContextExtensions에서 AddDbContext를 호출할때 ServiceLifetime.Transient 옵션을 설정하여 객체의 생명주기를 맞춰줘야 합니다. Transient는 HTTP요청을 발생할때마다 객체의 인스턴스를 새롭게 생성하지만 다른 설정인 Scorpe는 처음 HTTP요청시 인스턴스를 생성하고 동일한 페이지에 한해 생성된 인스턴스를 다시 사용한다는 차이가 있습니다. Singleton은 단 하나의 인스턴스로만 사용한다는 개념인데 정말 특별한 경우가 아니면 이 방식은 거의 사용하지 않습니다.
services.AddDbContext<NorthwindContext>(options => options.UseSqlServer(Configuration.GetConnectionString("NorthwindConnectionString")), ServiceLifetime.Transient);
MyRegion.razor에서 구현했던 Northwind Database Context를 Inject하는 부분을 위에서 등록한 Northwind Service로 변경하고 관련 구문도 함께 변경합니다.
@page "/myregion/{region?}"
@using Microsoft.EntityFrameworkCore
@inject IMyRegion service
<h3>MyRegion</h3>
@if (region == null)
{
<span>Nothing</span>
}
else
{
<table>
<thead>
<tr>
<th>ID</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach(var r in region)
{
<tr>
<td>@r.RegionId</td>
<td>@r.RegionDescription</td>
</tr>
}
</tbody>
</table>
}
@code {
[Parameter]
public string? Region { get; set; }
private IEnumerable<Region>? region;
protected override async Task OnParametersSetAsync()
{
if (string.IsNullOrEmpty(Region))
{
region = await service.GetAllRegion();
}
else
{
region = await service.GetRegions(Region);
}
}
}
● EditForm Component
마이크로소프트는 데이터 편집을 위해 사전에 미리 만들어진 EditForm Component를 제공하고 있습니다. 따라서 해당 Component와 데이터 입력을 위한 InputText등의 몇몇 Element를 조합하여 사용하면 Region 항목을 추가하거나 수정하는 기능을 구현할 수 있습니다.
EditForm는 또한 객체의 속성을 바인딩할 수 있도록 Model을 설정할 수 있으며 Validation을 위한 Event Handler를 설정할 수 있습니다.
프로젝트의 Shared폴더에서 RegionDetail.razor라는 이름의 새로운 파일을 생성하고 다음과 같이 EditForm Component를 작성합니다.
<EditForm Model="@region" OnValidSubmit="@OnValidationSubmit">
<DataAnnotationsValidator></DataAnnotationsValidator>
<InputNumber id="id" @bind-Value="@region.RegionId"></InputNumber>
<ValidationMessage For="@(() => region.RegionId)"></ValidationMessage>
<br />
<InputText id="desc" @bind-Value="@region.RegionDescription"></InputText>
<ValidationMessage For="@(() => region.RegionDescription)"></ValidationMessage>
<br />
<button type="submit">@buttonText</button>
</EditForm>
@code {
[Parameter]
public Region region { get; set; } = null!;
[Parameter]
public string? buttonText { get; set; } = "등록";
[Parameter]
public EventCallback OnValidationSubmit { get; set; }
}
필요에 따라서는 ValidationSummary대신 ValidationMessage component를 사용해 각각의 Element에서 개별적인 오류메세지를 표시할 수 있습니다.
이전에 MyRegion을 구현할때는 Region을 가져오는 것만 구현했었는데 새로운 Region을 추가하기 위한 메서드를 다음과 같이 추가하고
public Task<Region> SetRegion(Region r)
{
DB.Region.Add(r);
DB.SaveChangesAsync();
return Task.FromResult(r);
}
IMyRegion 인터페이스에도 메서드를 추가해 줍니다.
Task<Region> AddRegion(Region r);
위와 같이 component를 작성하고 나면 새로운 Region을 등록할 AddRegion.razor파일을 Pages폴더에 추가하고 component를 연결합니다.
@page "/addregion"
@inject IMyRegion service
@inject NavigationManager navigation
<RegionDetail buttonText="등록" region="@region" OnValidationSubmit="@Add"></RegionDetail>
@code {
private Region region = new();
private async Task Add()
{
await service.AddRegion(region);
navigation.NavigateTo("myregion");
}
}
MyRegion.razor에는 해당 페이지로의 이동을 위하 링크를 생성하고
<a href="/addregion">Region 추가</a>
프로젝트를 실행하여 Region추가를 확인합니다.
Region변경의 경우에는 기존내용을 수정한다는 개념만 다를뿐 프로그램을 구성하는 방식은 생성과 거의 동일합니다. 우선 기존의 것을 수정하기 위해 수정할 대상을 가져와야 하는데 Data폴더의 MyRegion.cs파일에서 GetRegion이름의 메서드를 아래와 같이 추가합니다.
public Task<Region?> GetRegion(int r)
{
return DB.Region.Where(x => x.RegionId == r).FirstOrDefaultAsync();
}
다음으로 가져온 대상을 변경하기 위한 메서드를 추가해 줍니다.
public Task<Region> EditRegionAsync(Region r)
{
DB.Entry(r).State = EntityState.Modified;
DB.SaveChanges();
return Task.FromResult(r);
}
MyRegion.cs를 변경하였으면 거기에 맞춰 인터페이스도에도 변경사항을 반영해 줍니다.
namespace BlazorApp.Data
{
public interface IMyRegion
{
Task<List<Region>> GetAllRegion();
Task<Region?> GetRegions(int r);
Task<Region> AddRegion(Region r);
Task<Region> EditRegionAsync(Region r);
}
}
이제 EditRegion.razor이름의 Blazor Component 파일을 Pages폴더에 생성하고 위에서 추가한 메서드를 호출하여 Region을 변경할 수 있도록 합니다.
@page "/editregion/{regionid:int}"
@inject IMyRegion service
@inject NavigationManager navigation
<RegionDetail buttonText="수정하기" region="@region" OnValidationSubmit="@Update"></RegionDetail>
@code {
[Parameter]
public int RegionId { get; set; }
private Region? region = new();
protected async override Task OnParametersSetAsync()
{
region = await service.GetRegion(RegionId);
}
private async Task Update()
{
if (region is not null)
{
await service.EditRegionAsync(region);
}
navigation.NavigateTo("myregion");
}
}
그리고 변경하길 원하는 Region을 선택할 수 있도록 MyRegion.razor Component에서 Region항목을 Table로 가져올때 <a>태그를 추가해 줍니다.
<tbody>
@foreach(var r in region)
{
<tr>
<td><a href="/editregion/@r.RegionId">@r.RegionId</a></td>
<td>@r.RegionDescription</td>
</tr>
}
</tbody>
프로젝트를 실행하고 결과를 확인합니다.
마지막으로 삭제입니다. MyRegion.cs에서 삭제에 필요한 메서드와
public Task RemoveRegionAsync(int regionId)
{
Region? region = DB.Region.Where(x => x.RegionId == regionId).FirstOrDefault();
if (region == null)
{
return Task.CompletedTask;
}
else
{
DB.Region.Remove(region);
return DB.SaveChangesAsync();
}
}
삭제에 필요한 RemoveRegion.razor Component를 추가합니다.
@page "/removeregion/{regionid:int}"
@inject IMyRegion service
@inject NavigationManager navigation
<RegionDetail buttonText="삭제하기" region="@region" OnValidationSubmit="@Remove"></RegionDetail>
@code {
[Parameter]
public int RegionId { get; set; }
private Region? region = new();
protected async override Task OnParametersSetAsync()
{
region = await service.GetRegion(RegionId);
}
private async Task Remove()
{
if (region is not null)
{
await service.RemoveRegionAsync(RegionId);
}
navigation.NavigateTo("myregion");
}
}
MyRegion.razor에서도 마찬가지로 삭제에 필요한 <a>태그를 추가한뒤
<table>
<thead>
<tr>
<th>ID</th>
<th>Description</th>
<td>삭제</td>
</tr>
</thead>
<tbody>
@foreach(var r in region)
{
<tr>
<td><a href="/editregion/@r.RegionId">@r.RegionId</a></td>
<td>@r.RegionDescription</td>
<td><a href="/removeregion/@r.RegionId">삭제</a></td>
</tr>
}
</tbody>
</table>
프로젝트를 실행해 결과를 확인합니다.
6. Blazor WebAssembly에서의 Component
Blazor Server의 경우 App자체가 Server에서 실행되므로 DB에 직접 접근하든 인터펭스를 통해 Repository를 구성하든 동일할 테지만 Client에서 구동되는 WebAssembly라면 직접적인 DB접근을 피하고 별도의 API를 통해서 DB의 접근이 이루어 져야 합니다.
이전 프로젝트에서는 IMyRegion 인터페이스를 사용한 의존성 서비스의 추상화를 통해 MyRegion클래스처럼 추가적인 요소를 제외하고는 대부분의 Component를 그대로 재사용할 수 있었습니다. 이제 WebAssembly에서는 직접적으로 NorthwindContext를 호출하여 사용하는 대신 서버에서의 Web Api Controller를 호출하도록 바꿔볼 것입니다.
● Blazor WebAssembly를 위한 API 작성
새로운 WebAPI프로젝트를 생성하고 Data폴더를 만들어 NothwindContext와 기타 테이블에 대한 클래스를 'EF Core Power Tools'을 통해 생성합니다.
이제까지 했던 방법과 동일하게 appsettings.json파일에 DB연결정보를 추가하고 DbContextExtensions.cs에 AddNorthwindContext 메서드를 작성한뒤 Program.cs파일에 서비스로 등록합니다.
Controllers폴더에는 기존파일을 삭제하고 RegionController라는 새로운 API Controller를 추가한뒤 Region의 CRUD를 위한 각각의 API 메서드를 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RegionAPI.Data;
namespace RegionAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class RegionController : ControllerBase
{
private readonly NorthwindContext db;
public RegionController(NorthwindContext _db)
{
this.db = _db;
}
[HttpGet]
public Task<List<Region>> GetAllRegion()
{
return db.Region.ToListAsync();
}
[HttpGet("r/{r}")]
public Task<List<Region>> GetRegions(string r)
{
return db.Region.Where(x => x.RegionDescription == r).ToListAsync();
}
[HttpGet("{id:int}")]
public Task<Region?> GetRegion(int r)
{
return db.Region.Where(x => x.RegionId == r).FirstOrDefaultAsync();
}
[HttpPut]
public Task<Region> EditRegionAsync(Region r)
{
db.Entry(r).State = EntityState.Modified;
db.SaveChanges();
return Task.FromResult(r);
}
[HttpPost]
public Task<Region> AddRegion(Region r)
{
db.Region.Add(r);
db.SaveChangesAsync();
return Task.FromResult(r);
}
[HttpDelete("{id:int}")]
public Task RemoveRegionAsync(int regionId)
{
Region? region = db.Region.Where(x => x.RegionId == regionId).FirstOrDefault();
if (region == null)
{
return Task.CompletedTask;
}
else
{
db.Region.Remove(region);
return db.SaveChangesAsync();
}
}
}
}
작성된 파일은 [HttpGet]과 같은 설정을 제외하고 이전 프로젝트에서의 이름과 동직이 정확히 동일한 것을 알 수 있습니다.
MyRegion.cs는 API를 통해 CRUD를 수행할 수 있도록 아래와 같이 수정하고
using Microsoft.EntityFrameworkCore;
namespace BlazorApp.Data
{
public class MyRegion : IMyRegion
{
private readonly HttpClient http;
public MyRegion(HttpClient _http)
{
http = _http;
}
public Task<List<Region>?> GetAllRegion()
{
return http.GetFromJsonAsync<List<Region>>("https://localhost:7199/api/region");
}
public Task<List<Region>?> GetRegions(string r)
{
return http.GetFromJsonAsync<List<Region>>($"https://localhost:7199/api/region/r/{r}");
}
public Task<Region?> GetRegion(int r)
{
return http.GetFromJsonAsync<Region>($"https://localhost:7199/api/region/{r}");
}
public async Task<Region?> EditRegionAsync(Region r)
{
HttpResponseMessage response = await http.PutAsJsonAsync("https://localhost:7199/api/region", r);
return await response.Content.ReadFromJsonAsync<Region>();
}
public async Task<Region?> AddRegion(Region r)
{
HttpResponseMessage response = await http.PostAsJsonAsync("https://localhost:7199/api/region", r);
return await response.Content.ReadFromJsonAsync<Region>();
}
public async Task RemoveRegionAsync(int regionId)
{
HttpResponseMessage response = await http.DeleteAsync($"https://localhost:7199/api/region/{regionId}");
}
}
}
인터페이스를 동일하게 맞춰준 후 Program.cs에서는 AddTransient로된 서비스를 AddHttpClient로 변경해 줍니다.
builder.Services.AddHttpClient<IMyRegion, MyRegion>();
참고로 위에서 사용된 https://localhost:7199 주소는 WebApi 프로젝트의 실행 URL이며 이는 프로젝트를 실행하는 컴퓨터마다 다를 수 있으니 주의하시기 바랍니다.
물론 API URL은 위 예제에서 처럼 일일이 Full URL을 명시하는 대신 Program.cs에서 다음과 같이 미리 지정해 두는 방법을 사용할 수도 있습니다.
builder.Services.AddHttpClient<IMyRegion, MyRegion>(options => { options.BaseAddress = new Uri("https://localhost:7199/api"); });
프로젝트를 테스트하려면 WebAPI를 먼저 구동시키고 그 다음 BlazorApp프로젝트를 실행하면 결과를 확인할 수 있습니다.
7. AOT를 통한 성능향상
Blazor WebAssembly에서 사용하는 .NET Runtime은 기본적으로 WebAssembly에서 작성된 번역기를 통해 IL번역을 수행합니다. 다른 App과는 달리 JIT(Just-in-time) Compiler를 사용하지 않기 때문에 CPU에 걸리는 작업처리성능은 기대한것보다 낮을 수 있습니다.
때문에 .NET6에서 마이크로소프트는 AOT(ahead of time)이라는 컴파일방식을 지원하기 시작했습니다. 다만 프로젝트의 규모에 따라 AOT 컴파일시간이 오래걸릴 수 있으므로 자동으로 수행되지는 않고 AOT 컴파일 사용을 명시해야 합니다.
컴파일된 크기 또한 AOT를 사용하지 않을때 보다 거의 2배정도 커진다는 단점또한 존재합니다. 이는 브라우저에서 내려 받아야 하는 App의 크기를 늘리고 덩달아 내려받는 시간도 늘리게 되므로 AOT사용은 신중하게 결정해야 합니다.
AOT사용을 위해서는 우선 아래명령을 통해 '.NET WebAssembly build tools'를 내려받고 설치해야 합니다.
dotnet workload install wasm-tools |
다음으로 AOT를 사용하고자 하는 Blazor WebAssembly프로젝트파일(csproj)을 열어 PropertyGroup에서 RunAOTCompilation설정을 추가합니다.
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
그리고 다시 프로젝트를 빌드합니다.
dotnet publish -c |
예제에서는 대략 29개 정도의 어셈블리가 적용되었음을 알 수 있습니다. 물론 빌드시간도 꽤 걸립니다.
8. browser compatibility analyzer
.NET6에 이르러 마이크로소프트는 .NET관련 모든 .NET Library를 통합했습니다. 따라서 이론적으로 Blazor WebAssembly App은 완전한 .NET API의 접근성을 가지게 되었지만 실제로는 Web Browser의 sendbox안에서 실행되므로 한계가 있습니다.
따라서 지원하지 않는 API를 호출하는 경우 PlatformNotSupportedException예외를 일으킬 수 있기 때문에 브라우저에 의해 지원하지 않는 API가 호출되는 경우 browser compatibility analyzer를 추가하여 사전에 경고를 줄 수 있도록 해줄 필요가 있습니다.
Blazor WebAssembly App과 Razor Class Library 프로젝트 템블릿은 자동적으로 browser 호환성을 확인하지만 다음과 같은 방법을 통해 명시적으로 호환성을 확인할 수도 있습니다.
첫번째는 프로젝트파일(csproj)을 수정해 SupportedPlatform 요소를 추가하는 것이고
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
<SupportedPlatform Include="browser" />
</ItemGroup>
두번째는 특정 메서드에 대해 UnsupportedOSPlatform을 decorate하는 것입니다.
[UnsupportedOSPlatform("browser")]
public void Something()
{
}
9. Javascript와의 상호운영성
기본적으로 Blazor component는 alert와 같은 간단한 javascript함수부터 local storage, geolocation이나 React, Vue와 같은 Javascript library등 웹브라우저의 영역에는 접근할 수 없습니다. 만약 이러한 기능이 필요하다면 Javascript Interop을 통해서 해당 기능을 구현해야 합니다.
먼저 프로젝트의 wwwroot 폴더에서 javascripts라는 폴더를 만들고 여기에 아래 내용으로 interop.js라는 javascript파일을 생성합니다. 되도록이면 해당 위치에 javascript파일을 생성할것을 권장하지만 파일이름은 임의할 수 있습니다.
var message = function (msg) {
window.alert(msg);
}
그리고 _Layout.cshtml 파일(혹은 wwwroot폴더의 index.html과 같은 파일에서도 가능)에서는 위에서 만든 script파일을 불러올 수 있도록 추가한뒤
<script src="scripts/interop.js"></script>
실행하고자 하는 Blazor component에서 아래와 같은 방법으로 위에서 작성한 message javascript함수를 호출하도록 합니다.
<button type="button" @onclick="alert">실행</button>
@code {
[Inject]
public IJSRuntime JSRuntime { get; set; } = null!;
public async Task alert()
{
await JSRuntime.InvokeVoidAsync("message", "abc");
}
}
10. 기타 Blazor Component
DevExpress와 같은 회사에서 유료로 사용가능한 Blzor Component를 만들기도 하고 아래 Open Source로 진행중인 것도 있으니 참고히시기 바랍니다.
Free Blazor Components | 60+ controls by Radzen
The Top 962 Blazor Open Source Projects on Github (awesomeopensource.com)
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] 시작하기 (0) | 2022.07.29 |
---|---|
[ASP.NET Core] ASP.NET Core 개요 (0) | 2022.07.27 |
[ASP.NET Core] ASP.NET Core Web API (2) | 2022.03.25 |
[ASP.NET Core] MVC패턴 웹프로젝트 만들기 (0) | 2022.03.04 |
[ASP.NET Core] HttpContext.User (0) | 2022.02.10 |