최근 Blazor 를 공부하고 있는데 자료를 추천해주실 수 있으실까요?

안녕하세요,

최근 웹 프론트엔드 개발에 흥미가 생겨서 제목 그대로 Blazor WebAssembly 를 공부하고 있는데, 자료를 추천받고자 합니다.

먼저 저의 학습 스타일이 핵심 동작원리를 문서로 깊게 파악한 뒤, 토이 프로젝트를 진행하면서 원리가 이해되지 않는 부분은 프레임워크 소스 코드를 들쳐보면서 공부를 합니다.

그래서 핵심 동작원리를 이해하는데 좋은 자료가 무엇일지 추천을 받고자 합니다. 일단 제가 살펴봤을 때는 MS 문서 중 아래 문서들이 동작 원리를 이해하는데 좋지 않을까 해서 하나씩 읽고 있습니다.

ASP.NET Core Razor component lifecycle
ASP.NET Core Blazor layouts
ASP.NET Core Blazor component rendering

그 외에 전체적인 프로젝트 흐름을 이해하기 위해 blazor-workshop 소스 코드는 전부 살펴보았습니다.

시작부터 모든 문법을 알고 시작할 필요는 없을 것 같아, Razor syntax reference for ASP.NET Core 는 필요할 때만 찾아보려고 합니다.

UI 를 개발하는 패턴을 익히려고 ant-design-blazorfast-blazor 의 소스코드를 분석하고 있는데 둘의 개발 스타일이 너무 달라서 UI 컴포넌트는 어떻게 설계해서 개발하고 관리해야 하는건지 고민이 됩니다.

그리고 Razor 파일이 어떻게 CS 파일로 변환되는지 설명하는 문서 또는 예제를 찾아보고 있지만 찾기가 너무 어렵습니다. ComponentBase 를 상속하겠지 예상하고는 있는데, HTML 로 작성된 부분은 어떻게 CS 코드가 되는지, 그리고 @code 에 작성한 코드는 HTML 을 CS 로 만든 코드를 참조할 수 있는지 여부 등을 알고 싶습니다. (React 같은 경우 ref 로 하위 컴포넌트의 참조를 만들어서 하던데 Blazor 도 비슷한게 있나요?)

정리하자면,

  1. MS 공식 문서 중 Blazor 를 이해하기 위해 반드시 숙지해야 하는 문서는 무엇일까요?
  2. UI 컴포넌트 아키텍처는 어떻게 설계해야 할까요? Best practices 라고 할만한 프로젝트 또는 소스코드가 있을까요?
  3. Razor 에서 CS 로 변환되는 과정을 구체적으로 설명한 문서나 예제가 있을까요?

긴 글 읽어주셔서 감사합니다.

인도인의 억양에 자막없는 영어 강의도 이해할 수 있으니, 도움이 되는 자료라면 추천 부탁드립니다.

좋아요 4
  1. MS 문서의 내용을 따라 읽어보면 대부분의 막히는 문제는 해결이 될 것 같습니다. 저 같은 경우 작년에 Blazor Server를 이용해서 대략 6개월 넘게 프로젝트를 진행했는데요, 거의 대부분 MS 문서를 통해 막혔던 부분을 해결했습니다.

  2. 위 문서의 Component를 보시면 도움이 됩니다. 이 문서를 대략 훑으신 후, 필요할 때 찾아 보시면 되지 않을까 합니다. 그리고 컴포넌트관련 잘 만들어진 Github의 소스코드를 찾으시려면 Awesome Blazor을 통해 Github로 접근하시는 걸 추천합니다. Component bundles에서 컴포넌트 라이브러리를 선택해 둘러보실 수 있으며, 가령, 예를 들어 MatBlazor의 경우, Components의 경로에서 탐색할 수 있고요, 거의 대부분의 컴포넌트 라이브러리들이 이런 구조입니다.
    Blazor의 razor는 기본으로 ComponentBase - Rendering를 상속 받고 레이아웃의 경우 Layouts - LayoutComponentBase를 상속 받습니다. 당연히 Blazor 컴포넌트 라이브러리들 역시 이런 구조를 가집니다. ComponentBaseLayoutComponentBase를 재정의 해서 자신들의 컴포넌트 기본 상속구조를 가지게 합니다.

@namespace MatBlazor
@inherits BaseMatDomComponent
<a @attributes="@Attributes" href="@GetHref()" id="@Anchor">@ChildContent</a>
  1. Razor는 컴파일 시점에서 CS 파일로 변환된 후 컴파일 됩니다. 변환된 코드의 기본 구조는 RenderTreeBuilder 에 의한 트리 생성입니다. 관련해서 Blazor RenderTree Explained 문서가 도움이 될 것 같습니다.
좋아요 2

컴포넌트에 대해 부연 설명하자면, Blazor의 컴포넌트는 razor파일 자체입니다. 만약 상호작용하지 않는 단순한 컴포넌트의 경우, 그냥 razor파일을 태그로 넣어주는 것으로 달성됩니다. 컴포넌트는 RenderTreeBuilder의 OpenComponent(), CloseComponent()에 의해 RenderTree에 포함됩니다.

<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />

가, cs로 변환되면,

            __builder.AddMarkupContent(0, "<h1>Hello, world!</h1>\r\n\r\nWelcome to your new app.\r\n\r\n");
            __builder.OpenComponent<WebApplication4.Shared.SurveyPrompt>(1);
            __builder.AddAttribute(2, "Title", "How is Blazor working for you?");
            __builder.CloseComponent();

가 됩니다. 이때, 컴포넌트는 OpenCompoment() → AddAttribute() → ClsoeComponent() 순으로 컴포넌트를 생성하고 관련된 인자를 넘기며, 최종 builder에 의해서 해당 컴포넌트의 RenderTree가 구성되도록 합니다.

이와 별개로 @Body등 razor에서 동적으로 추가할 수 있는 것은 RenderFragment로 가능합니다. RenderFragment는 RenderTreeBuilder를 이용해 트리를 구성하는 대리자이며, 사용자가 직접 만들수도 있습니다. 또는, razor의 HTML 태그를 바로 RenderFragment로 대입도 가능합니다.

※ 설명이 좀 혼란스러울 수 있어서 기존 내용을 수정 하였습니다. 기존 텍스트는 오른쪽 상단의 연필모양을 클릭하면 확인할 수 있습니다.

좋아요 2

Blazor Server/Webassembly 프로젝트를 빌드 한 후 obj\Debug\net5.0\Razor에 가시면 razor가 컴파일된 g.cs파일을 살펴보실 수 잇습니다. 그중에 하나를 예시로,

#pragma checksum "W:\Enjoy\WebApplication4\WebApplication4\Pages\Counter.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "5456520839664fbbeb726a4af0946602a9d68f5c"
// <auto-generated/>
#pragma warning disable 1591
namespace WebApplication4.Pages
{
    #line hidden
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using System.Net.Http;

#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using System.Net.Http.Json;

#line default
#line hidden
#nullable disable
#nullable restore
#line 3 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using Microsoft.AspNetCore.Components.Forms;

#line default
#line hidden
#nullable disable
#nullable restore
#line 4 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using Microsoft.AspNetCore.Components.Routing;

#line default
#line hidden
#nullable disable
#nullable restore
#line 5 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using Microsoft.AspNetCore.Components.Web;

#line default
#line hidden
#nullable disable
#nullable restore
#line 6 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using Microsoft.AspNetCore.Components.Web.Virtualization;

#line default
#line hidden
#nullable disable
#nullable restore
#line 7 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using Microsoft.AspNetCore.Components.WebAssembly.Http;

#line default
#line hidden
#nullable disable
#nullable restore
#line 8 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using Microsoft.JSInterop;

#line default
#line hidden
#nullable disable
#nullable restore
#line 9 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using WebApplication4;

#line default
#line hidden
#nullable disable
#nullable restore
#line 10 "W:\Enjoy\WebApplication4\WebApplication4\_Imports.razor"
using WebApplication4.Shared;

#line default
#line hidden
#nullable disable
    [Microsoft.AspNetCore.Components.RouteAttribute("/counter")]
    public partial class Counter : Microsoft.AspNetCore.Components.ComponentBase
    {
        #pragma warning disable 1998
        protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.AddMarkupContent(0, "<h1>Counter</h1>\r\n\r\n");
            __builder.OpenElement(1, "p");
            __builder.AddContent(2, "Current count: ");
            __builder.AddContent(3, 
#nullable restore
#line 5 "W:\Enjoy\WebApplication4\WebApplication4\Pages\Counter.razor"
                   currentCount

#line default
#line hidden
#nullable disable
            );
            __builder.CloseElement();
            __builder.AddMarkupContent(4, "\r\n\r\n");
            __builder.OpenElement(5, "button");
            __builder.AddAttribute(6, "class", "btn btn-primary");
            __builder.AddAttribute(7, "onclick", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, 
#nullable restore
#line 7 "W:\Enjoy\WebApplication4\WebApplication4\Pages\Counter.razor"
                                          IncrementCount

#line default
#line hidden
#nullable disable
            ));
            __builder.AddContent(8, "Click me");
            __builder.CloseElement();
        }
        #pragma warning restore 1998
#nullable restore
#line 9 "W:\Enjoy\WebApplication4\WebApplication4\Pages\Counter.razor"
       
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }

#line default
#line hidden
#nullable disable
    }
}
#pragma warning restore 1591

템플릿으로 기본 생성되는 파일 중 Counter.razor를 컴파일 한 g.cs파일이고요, razor와 동일한 이름의 클래스가 ComponentBase 상속으로 표현되었음을 알 수 있습니다. 특히 BuildRenderTree()를 보시면, builder에 의해서 razor로 표현된 HTML이 builder의 다양한 메소드 호출을 통해서 RenderTree로 구성됨을 알 수 있습니다.

좋아요 3

참고로 RenderFragment는 단지 RenderTreeBuilder를 받는 delegate입니다.

public delegate void RenderFragment(RenderTreeBuilder builder);
좋아요 2

razor 을 컴파일해서 만든 소스코드는 obj 폴더에 들어가는군요 ㅎㅎ Winform 이나 WPF, Xamarin 은 designer.csxaml.cs 를 소스코드와 같은 폴더에서 만드는데 왜 razor 는 없나 했더니 obj 폴더로 가는군요.

좋아요 1

RenderFragment 가 그럼 class 가 아니라 메서드인거군요. 이건 처음보는 패턴이라, 어떻게 설계되어 있고 활용되는지 호기심을 자극해서 아래와 같이 찾아보았습니다. 혹시 틀린 점이 있다거나 추가로 알아야 할 부분을 알려주시면 감사하겠습니다.

aspnetcore 에서 RenderFragment.csComponentBase.cs 에서 참조하고 있고 아래처럼 활용하는군요.

/// <summary>
/// Constructs an instance of <see cref="ComponentBase"/>.
/// </summary>
public ComponentBase()
{
    _renderFragment = builder =>
    {
        _hasPendingQueuedRender = false;
        _hasNeverRendered = false;
        BuildRenderTree(builder);
    };
}

/// <summary>
/// Notifies the component that its state has changed. When applicable, this will
/// cause the component to be re-rendered.
/// </summary>
protected void StateHasChanged()
{
    if (_hasPendingQueuedRender)
    {
        return;
    }

    if (_hasNeverRendered || ShouldRender() || _renderHandle.IsHotReloading)
    {
        _hasPendingQueuedRender = true;

        try
        {
            _renderHandle.Render(_renderFragment);
        }
        catch
        {
            _hasPendingQueuedRender = false;
            throw;
        }
    }
}

내부적으로는 ComponentBaseRenderFragment 로 렌더링을 하는 것처럼 보이네요.

좋아요 1

제가 설명을 정확하게 못드린 것 같습니다. RenderTree는 RenderFragment로 구성된다기 보다는 RenderTree를 구성하기 위해 RenderFragment를 통해 대리하여 트리를 구성하도록 위임한다고 표현하는게 맞는것 같습니다. 가령, razor에 포함된 컴포넌트가 있다고 할 때 해당 컴포넌트의 razor에서 builder를 받아 자체 구성하는 식입니다.

위에 모호한 표현은 제가 제거하도록 하겠습니다.

좋아요 2

그렇군요 ㅎㅎ RenderFragmentdelgate 로 정의되어 있으니까요. RenderTreeBuilderComponentRenderFragment 를 순회하면서 완성되는 설계군요.

좋아요 2

네 맞습니다. 트리를 결국에 완성하는 주체는 RenderTreeBuilder이고 이걸 대리자로 전달하여 builder를 이용해 추가하는 식입니다.

좋아요 2

위의 나눈 이야기를 정리하자면 다음과 같이 압축할 수 있을 것 같습니다.

  • razor 컴파일 결과를 확인하여 RenderTree가 어떻게 구성되는지룰 추척할 수 있다. 이때 기 구현된 컴포넌트 라이브러리 및 샘플 파일을 컴파일 하여 CS로 변환된 내부코드를 통해 동작 방식을 더 깊게 이해할 수 있다
  • 위의 행위와 MS 문서를 병행해서 보면 거의 대부분의 Blazor 및 컴포넌트 동작 방식을 이해할 수 있다.

위의 제 설명이 장황한 면이 있어서 다시 한번 정리해봅니다.

좋아요 3

저의 경우에는 작년에 Blazor-workshop (시드니) 참가해서 Steve 에게 여러가지 질문을 하면서 많은 것을 배웠습니다. 그의 블로그 역시 좋은 자료중에 하나 입니다. ( https://blog.stevensanderson.com/ ) 무엇보다 직접 무엇간을 만들어 보시면 더 많은것을 얻을슬수 있을것 입니다.

좋아요 4

@David Blazor 를 시작하신 Steven Sanderson 님 (twitter) 블로그도 Feedly 에서 RSS 피드 등록해놓고 하나씩 보려고 하는데, Deep 한 내용들이 꽤 많으셔서 MS 문서가 익숙해지고 난 다음에 참고하려고 합니다. 관심가져주시고 좋은 내용 공유해주셔서 감사합니다.

직접 만드는건 이번에 제가 시나리오 기획하고 Adobe XD 로 프로토타입 디자인한 앱을 Blazor 로 개발해보려고 합니다. 지금은 커뮤니티에 도움을 받는 입장이지만, 나중에 커뮤니티에 기여하는 개발자가 되도록 노력하겠습니다.

좋아요 4