.net 8.0 출시 기념: 블레이저 앱을 찐 크로스 플랫폼 UI 앱으로 만들기

블레이저 웹어셈블리 프로젝트를 조금만 수정하면, 웹은 물론이고, MAUI의 BlazorWebView 콘트롤에 호스팅 가능한 그야 말로 찐 크로스 플랫폼 앱을 빌드하는 프로젝트 구조를 갖출 수 있습니다.

마우이가 리눅스는 지원하지 않아 리눅스 UI 앱은 이 글의 대상이 아닙니다. 그러나, 블레이저를 리눅스 앱에 사용하는 것이 불가능한 것은 아닙니다. 이와 관련해서 별도의 글을 통해 다루겠습니다.

아래 글은 .net 8.0 기준입니다. (.net 7.0도 같습니다)

웹어셈블리 프로젝트 생성

이미 작성된 블레이저 WASM 프로젝트를 변경하는 것을 전제하는 것이라, 임의로 하나 생성합니다.

VS2022 에서 Blazor WebAssembly Standalone App 을 선택해서 솔루션을 생성합니다.
이 프로젝트에 Components 라는 이름을 붙이겠습니다.

프로젝트 생성 시 [추가 정보] 선택 창에서 아래와 같이 선택합니다.

image

  • Https 는 선택하지 않습니다.
    Https 를 선택하지 않는 이유는 https 는 호스팅 서버의 책임이기 때문입니다.
    사실, 호스팅과 앱을 분리한다는 취지로 블레이저 Server가 아닌 WASM 앱을 선택한 것입니다.

  • 기존에 존재하는 프로젝트를 흉내내기 위해, 샘플 페이지를 넣습니다.

RCL 프로젝트로 변경

첫 번째 단계로 블레이저 WASM 앱을 Razor Class Library(RCL)로 변경합니다.

Components.csproj 파일의 내용을 아래의 내용으로 바꿉니다.

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework> // sdk 버전에 따라 타겟 프레임워크를 변경합니다.
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>


  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0" />
  </ItemGroup>

</Project>

보시다시피, Razor Class Library 프로젝트 템플릿의 .csproj 의 내용과 동일합니다.

변경 후 저장을 누르면, VS 2022 가 아래와 같은 경고를 보여줍니다.

프로젝트 시스템을 통한 생성이 아니기 때문에 발생한 문제인데, 제일 오른쪽에 있는 [Reload projects] 를 누르면 해결됩니다.

자 이제, RCL 과 관련 없는 프로젝트 멤버들에 대한 칼질을 시작합니다.

Program.cs 제거

라이브러리이기 때문에 실행 엔트리 포인트가 필요 없죠.

index.html 변경

wwwroot 폴더에 있는 index.html 을 head.html 로 변경하고, 파일의 내용을 모두 지운 후, 아래의 내용으로 채워 넣습니다.

    <!--<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />-->
    <link rel="stylesheet" href="_content/Components/css/bootstrap/bootstrap.min.css" />
    <!--<link rel="stylesheet" href="css/app.css" />-->
    <link rel="stylesheet" href="_content/Components/css/app.css" />
    <!--<link rel="icon" type="image/png" href="favicon.png" />-->
    <link rel="icon" type="image/png" href="_content/Components/favicon.png" />

이 파일은 앱이 사용하는 것이 아니고, 다른 프로젝트에서 복사해가기 편하도록 저장하는 용도입니다.
주석문은 바뀌기 전의 내용을 보여주기 위한 목적일 뿐, 지우셔도 됩니다.

보시다 시피, 모든 내용을 다 지우고 정적 파일의 경로를 수정했습니다.

왜 이렇게 바꿔야 하는지는 아래 글을 참고 하세요.

프로젝트를 다시 빌드해서 문제가 없는 지 확인합니다.
나타날 수 있는 문제는 대부분, 네임 스페이스 관련 문제입니다.
보통 _imports.razor 이나 globalUsings.cs 파일에서 나타납니다.

예를 들어, RCL 이 참조하지 않은 WebAssembly 관련 네임스페이스를 _Imports.razor 에서 지웁니다.

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@* @using Microsoft.AspNetCore.Components.WebAssembly.Http : 삭제*@
@using Microsoft.JSInterop
@using Components
@using Components.Layout

문제가 해결되면, 모든 준비가 다 된 것입니다.

RCL 프로젝트는 앱의 실행에 관한 모든 것을 담고 있습니다. 앱을 수정하려면, RCL 프로젝트를 수정하면 됩니다.

원하는 실행체에서 이 프로젝트를 참조하여 빌드하기만 하면 되는데, 그 방법에 대해 살펴 봅니다.

이하에서는 이 프로젝트를 RCL, RCL 프로젝트 혹은 RCL 앱으로 칭합니다.

프론트 웹앱으로 빌드

RCL 앱을 프론트 웹앱으로 빌드하기 위해, 솔루션에 블레이저 웹어셈블리 프로젝트를 추가합니다.
저는 기본 이름인 BlazorApp1 을 선택했습니다.

RCL 프로젝트 참조

당연히 RCL 프로젝트 참조를 시켜야겠지요?

// BlazorApp1.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
...
  <ItemGroup>
    <ProjectReference Include="..\Components\Components.csproj" />
  </ItemGroup>

</Project>

Program.cs 수정

Program.cs 의 내용을 아래와 같이 수정합니다.

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
//builder.RootComponents.Add<App>("#app"); <= 변경 전
builder.RootComponents.Add<Components.App>("#app"); // <=변경 후
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

주석 처리된 부분은 변경 포인트를 보여 주기 위한 것이라, 지우셔도 됩니다.

보시다시피, 프론트 웹앱의 루트 컴포넌트로 RCL (Components)프로젝트에 있는 App.razor 를 사용하라는 의미입니다.

index.html 수정

RCL 프로젝트에 있는 .css 을 이 프로젝트에서 참조할 수 있도록 index.html 을 수정해야 합니다.

Components.wwwroot 폴더의 head.html 파일의 내용을 복사하여, BlazorApp1.wwwroot 폴더에 있는 index.html 파일의 <head> 에 붙여 넣어, 아래와 같은 내용이 되도록 수정합니다.

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorApp1</title>
    <base href="/" />
    <!--<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="css/app.css" />
    <link rel="icon" type="image/png" href="favicon.png" />-->
    <link rel="stylesheet" href="_content/Components/css/bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="_content/Components/css/app.css" />
    <link rel="icon" type="image/png" href="_content/Components/favicon.png" />
    <link href="BlazorApp1.styles.css" rel="stylesheet" /> <!-- <=중요-->
</head>

위 파일에서, <link href=“BlazorApp1.styles.css” rel=“stylesheet” /> 는 반드시 있어야 합니다.
이 파일은 빌드 타임에 생성되는 것인데, css 격리와 관련이 있습니다.
이 파일에는 이 프로젝트와 참조 중인 RCL 프로젝트의 .razor.css 파일의 내용이 담기게 되는데, 만약 이 링크가 없으면, RCL 프로젝트의 css 격리가 적용되지 않습니다.

이제 앱을 빌드해서 실행해 보면, RCL 앱이 실행됨을 알 수 있습니다.

파일 정리

앱이 사용하는 모든 레이저 컴포넌트와 정적 자원은 RCL(Components)프로젝트가 제공하므로, 이 프로젝트에는 빌드 관련한 것 외에 뭔가를 더 둘 이유가 없습니다.

BlazorApp1 프로젝트에는 wwwroot 폴더의 index.html 과 Program.cs 를 제외한 모든 파일을 지웁니다.
지운 상태에서의 프로젝트는 아래와 같습니다.

image

이제 앱을 실행시켜 정상 동작하는지 다시 확인 합니다.

앱이 정상 동작하지 않는다면, 프로젝트 별로 '다시 빌드’를 실행하세요.
간혹 다시 빌드가 안 먹는 경우가 있습니다. 될때까지 다시 빌드하세요.

정적 자원 참조 정리

앱을 실행해 보면, 다른 것은 문제가 없는데, Weather 페이지에서 에러가 납니다.

에러의 원인을 살펴 보기 위해 브라우저 디버그 도구를 열어 봅니다.

404 에러가 보이고, Weather.razor 파일의 44 번째 행에 문제가 있다고 알려 줍니다.
뭐가 문제인지 알아 보기 위해, 디버그 도구의 [네트워크] 탭을 열고 페이지를 새로고침 해봅니다.

weather.json 파일을 못 받았기 때문인 것 같습니다.

위에 있는 링크의 글처럼, RCL의 정적 파일에 대한 참조는 언제나 ‘_content/{프로젝트 이름}/’ 의 접두어를 사용해야 합니다. RCL 내부에서건 외부에서건 마찬가지입니다.

Components.Pages.Weather.razor 파일의 44행은 이러한 규칙을 위배하고 있습니다.
문제가 되는 곳을 수정합니다. 아래를 참고하세요.

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        // forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json"); <= 변경 전
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("_content/Components/sample-data/weather.json"); // 변경 후
    }

다시 실행시켜 보면 에러가 사라진 것을 볼 수 있을 것입니다.

에러가 사라지지 않는다면, RCL 프로젝트를 ‘다시 빌드’ 하세요.

크로스 플랫폼 앱으로 빌드

이제 RCL 프로젝트를 마우이 앱에서 사용하여 크로스 플랫폼 앱을 빌드하는 방법에 관해 알아 보겠습니다. 솔루션에 Maui Blazor Hybrid App 을 추가합니다.

이전과 동일합니다.

  • Components 프로젝트 참조 추가.
  • index.html 변경

그런데, 마우이 하이브리드 프로젝트는 BlazorWebView 콘트롤이 블레이저를 호스팅하는데, 템플릿 프로젝트에서는 MainPage.xaml 가 이 콘트롤을 사용합니다.
MainPage.xaml 을 아래와 같이 변경합니다.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiBlazorApp1"
             xmlns:rcl="clr-namespace:Components;assembly=Components" // <= 추가
             x:Class="MauiBlazorApp1.MainPage"
             BackgroundColor="{DynamicResource PageBackgroundColor}">

    <BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
        <BlazorWebView.RootComponents>
            <!--<RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />-->
            <RootComponent Selector="#app" ComponentType="{x:Type rcl:App}" />
        </BlazorWebView.RootComponents>
    </BlazorWebView>

</ContentPage>

앱을 빌드하고 실행시켜 보면 정상 동작하는 것을 알 수 있습니다.

파일 정리

프론트앱과 마찬가지로, 마우이 하이브리드 앱이 필요로 하는 레이저 요소와 정적 자원은 RCL이 보유하고 있기에 마우이 프로젝트에 블레이저 관련한 파일들이 존재할 이유가 없습니다.
따라서, 블레이저 관련 파일들을 모두 지웁니다.

마우이 관련 파일을 지우면 안됩니다.

지우고 난 후의 프로젝트 모습입니다.

image

마치며

지금까지 앱의 모체는 RCL 프로젝트에 두고 빌드 형태만 달리 하여 원하는 플랫폼의 앱으로 만드는 방법을 살펴 봤습니다.

그런데, 마우이 앱을 실행해 보면, Weather.razor 의 44 행에서 문제가 발생함을 알 수 있습니다.
앞서 블레이저 WASM 앱에서 문제를 해결했음에도 불구하고 다시 문제가 발생한 것입니다.

사실 두 문제는 동일 코드로 인해 유발되었지만, 그 이면의 내용은 전혀 다릅니다.

이 문제를 해결하기 위해서는 RCL 앱의 구조를 크로스 플랫폼에 맞도록 변경해야 합니다. 이와 관련해서는 별도의 글로 다루겠습니다.

14개의 좋아요

기본 RCL 템플릿으로 만들지 않고 이렇게 만들면 이점이 있나요?
제가 이해하기로는 기본 UI템플릿을 가져오는 것 말고는 없는 듯 한데 다른 점이 있는지 궁금합니다!

6개의 좋아요

.razor, .razor.css, .css, index.html, .json 을 작성하기 귀찮아서 입니다. ^^

그리고 되는 것과 안되는 것을 보여주기 위함입니다.

6개의 좋아요

감사합니다 ㅎㅎ

4개의 좋아요

오해가 없도록 글의 내용을 수정했습니다.

4개의 좋아요