Micro Frontend 패턴 블레이저 앱

마이크로 프론트엔드

마이크로 프론트엔드는 마이크로 서비스의 개념을 (자바스크립트로 쓰여진) 프론트엔드 앱에 도입한 것입니다.

개별 프론트엔드는

  • 독립적으로 호스팅되어 사용될 수도 있고,
  • 다른 앱의 페이지로 사용될 수도 있어,

개발과 서비스에 대한 자유도를 높입니다.

RCL

Razor Class Library (RCL)프로젝트를 이용하면, 블레이저 앱을 마이크로 프론트 엔드 패턴으로 쉽게 설계가 가능합니다.

Consume ASP.NET Core Razor components from a Razor class library (RCL) | Microsoft Learn

블레이저에 마이크로 프론트엔드 패턴을 적용하는 핵심은 페이지 요소(와 그 요소를 위한 콘트롤 요소)를 RCL로 작성하고, 블레이저 앱에서 이를 참조하는 것입니다.

예를 들어, Counter.razor를 위한 RCL을 만드는 경우를 가정한다면 프로젝트를 아래와 같이 만들 수 있습니다.

Counter.csproj (RCL)
Counter.razor
Count.razor (Counter의 자식 요소)

설계 측면에서 주의해야 할 점은, Counter.razor 가 프로젝트에서 최상위 렌더 트리가 되어야 한다는 점입니다.

즉, Counter 프로젝트에서는 @page 특성이 붙는 페이지 요소는 Counter.razor 가 유일해야 합니다.

만약 또 다른 페이지 요소가 필요하다면, 그것을 별도의 RCL프로젝트로 정의하는 것이 이 패턴에 충실한 설계 방식이라 할 수 있을 것입니다.

물론 여려 RCL에서 사용하는 공용 콘트롤을 위한 RCL을 별도로 정의할 수도 있습니다.

블레이저 앱에서 RCL 소비

이렇게 만들어진, Counter 프로젝트/패키지는 블레이저 앱에서 사용할 수 있습니다.

예를 들어, Main이라는 블레이저 앱 프로젝트에서 RCL을 사용하려면,

  1. Counter 프로젝트나 패키지 참조를 하고,
  2. App.razor의 라우터에 어셈블리 정보를 제공합니다.
@* Main.App.razor *@
<Router AppAssembly="@this.GetType().Assembly"
        AdditionalAssemblies="@_componentAssemblies">
....
// Main.App.razor.cs
    private readonly Assembly[] _componentAssemblies = new Assembly[]
    {
        typeof(Counter).Assembly,
    };

마이크로 프론트엔드 패턴을 블레이저에 적용하면 아래와 같은 시나리오가 가능합니다.

  1. 정적인 웰컴 페이지만 배포하는 메인 블레이저 앱을 만들어 배포한다.

  2. 각 세부 페이지를 개별 프로젝트로 개발한다.

  3. 세부 페이지가 완료되는 대로,
    3.1 메인 앱에서 사용할 수도 있고,
    3.2 다른 앱을 통해 배포할 수도 있다.

세부 페이지는 팀 별로 분산하는 수평적 분업과 미래로 유보하는 수직적 분업이 가능합니다.

어떤 아이디어가 떠 올라 핵심 기능만 구현해서 이를 바로 배포한 후에 시장 반응을 먼저 확인해 보고, 반응이 좋다면 점점 업그레이드를 하는 개발 패턴을 취할 때 빛을 발할 것 같습니다.

의존성 주입

의존성 주입을 고려할 때 한 가지 고려 사항이 있습니다.

  1. RCL 전용의 로컬 서비스인가?
  2. Main 앱 수준의 전역 서비스인가?

기본적으로, 1번을 가정한다면,

Counter/
–Plugs/ICounterOwnService.cs
–Plugins/CounterOwnService.cs
–Plugins/CounterExtensions.cs

// CounterExtensions.cs
...
public static void AddCounterOwnService(this IServieCollection container)
{
    container.AddSinglton<ICounterOwnService, CounterOwnService>();
}
// Counter.razor.cs
...
[Inject]
public ICounterOwnService OwnService { get; set; } = null!;
...
// Main.Program.cs
using Counter.Plugins;
using Counter.Plugs;
...
 services.AddCounterOwnService();
...

2번의 대표적인 케이스가 지역화입니다.

지역화 서비스의 제공은 Main 앱에 두고, 모든 RCL은 이 서비스의 소비자가 되는 것입니다.

지역화에 관해 제가 애정하는 Localization 패키지를 사용하는 것을 가정합니다.

이 패키지는 아래 두 가지 중 하나를 선택할 수 있습니다.

위에 있는 것은 플러그(인터페이스)만 간추려 놓은 것이고, 아래에 있는 것은 플러그인(인터페이스 구현체)을 포함한 것입니다.

Counter RCL은 지역화 서비스의 소비자 입장이라, 플러그 패키지인 Abstractions 만을 사용해도 됩니다.
위에 있는 패키지를 프로젝트에 참조 추가시키고,

// Counter.razor.cs
using Microsoft.Extentions.Localization.Abstractions;
...
...
[Inject]
public IStringLocaliation<Counter> Localiaztion { get; set; } = null!;
/*Counter.razor*/
...
<h1>Localization["카운터 페이지"]</h1>
...

이 서비스의 구현체는 Main 앱에서 제공합니다.

이를 위해, Microsoft.Extentions.Localization을 참조하고, Main 앱의 서비스 컨테이너에 이 서비스를 등록합니다.

// Main.Program.cs
using Microsoft.Extentions.Localization;
...
 services.AddLocalization();
...

물론, 이 서비스가 사용할 .resx 파일들도 Main 프로젝트에 존재해야겠죠?

RCL 정적 자원

블레이저에 마이크로 프론트엔드 패턴을 적용할 때 주의해야 할 점은 정적 자원, 특히 스타일링을 위한 css의 참조입니다.

기본적으로 RCL 의 정적 자원은 프로젝트의 wwwroot 에 저장하면 됩니다.

Counter.csproj (RCL)
wwwroot/
– css/page.css
– img/image1.png
Counter.razor
Count.razor (Counter의 자식 요소)

Counter.razor 에서 page.css를 사용하려면, 아래와 같이 해야 합니다.

/* Counter.razor */
<link href="_content/Counter/css/page.css" rel="stylesheet" />
...

page.css 의 URL을 보시면, “_content/{패키지 ID}/{파일 경로}” 의 형태로 되어 있는데, 이는

RCL의 정적 자원 참조 규칙이라 반드시 따라야 합니다.
RCL 내부에서건 외부에서건 동일합니다.

참고로, 패키지ID 는 누겟 패키지 ID로 누겟 저장소 단위로 고유해야 하며, 프로젝트 파일에 명시됩니다.

  <PropertyGroup>
      <PackageId>{닷넷 네임스페이스 형식의 문자열}</PackageId>
  </PropertyGroup>

프로젝트 파일에 명시되지 않으면 기본값은 '어셈블리 이름"이 됩니다.

Counter 프로젝트의 경우, 별 다른 네이밍 변칙을 적용하지 않았기 때문에 프로젝트 명과 어셈블리 명이 같습니다.

이제 스타일링에 관해 세부적인 고려 사항을 살펴 보겠습니다.

::deep

Counter.razor 의 스타일 시트 링크를 다시 보면,

/* Counter.razor */
<link href="_content/Counter/css/page.css" rel="stylesheet" />
...

이 페이지 요소의 자식 요소들은 이 페이지와 함께 렌더링되기 때문에 page.css 파일의 규칙들을 사용할 수 있습니다.

즉, page.css 에 나열된 규칙들은 Counter 프로젝트 내부에서 전역적으로 적용됩니다.

만약, page.css 의 내용을 Counter.razor.css 로 옮겨 css 격리를 적용하면, 자식 요소에서는 이 규칙을 사용할 수 없게 됩니다.

여전히 전역적이기를 원한다면, Counter.razor.css 의 모든 css 규칙에 아래의 접두어를 붙여야 하는 불편을 감수해야 합니다.

::deep {식별자} {
...
}

css 재정의 충돌

만약, 스타일 시트의 링크를 아래와 같이 정의하는 경우,

/* Counter.razor */
<HeadContent>
    <link href="_content/Counter/page.css" rel="stylesheet" />
</HeadContent>
...

위 링크는, Main 앱이

블레이저 웹어셈블리라면, wwwroot/index.html 의,

블레이저 서버라면, _host.cshtml의,

head 태그의 제일 마지막에 삽입됩니다.

그런데, 만약, RCL에서 제공한 페이지 요소를 콘트롤 요소로 사용한 경우, css 규칙 재정의가 발생할 수 있습니다.

// Counter/wwwroot/css/page.css
h1 {
    color: red;
}
// FetchData/wwwroot/css/page.css
h1 {
    color: green;
}
/* Main.Index.razor */
<Counter />
<FetchData/>

아시죠? css 충돌나면… (할많하않)
대충, 강한 형식 언어의 위대함을 새삼 깨닫게 됩니다.

어찌저찌 찾는다 해도, RCL을 프로젝트가 아닌 패키지로 참조한 경우, 즉시 수정할 수도 없습니다.

개인적으로, css 격리에서 deep 을 제어할 수 있는 스위치가 있으면 좋을 것 같습니다.

// Counter.razor.css

// 여기까지는 Counter.razor 에만 적용되는 규칙들.
isolation{
    deep: true;
}
// 여기부터는 Counter.razor 의 하위 요소에도 적용되는 규칙들.

RCL 자원은 복사되지 않는다.

각 RCL에 포함된 자원은 RCL 패키지에 포함될 뿐, 앱의 정적 자원 폴더에 복사되지는 않습니다.

즉, RCL을 업데이트 해도, 앱이 패키지 업데이트를 하지 않으면 업데이트가 적용이 안됩니다.

이 경우, 앱 프로젝트 빌드 시에 패키지 최신 버전으로 Restore 하도록 빌드 태스크를 정의해야 합니다.

== To do ==

5개의 좋아요