Blazor에서 구성 요소 상속

· 3분 읽기

저는 여러 개의 양식 페이지가 있는 프로젝트를 구축하고 있었는데, 모든 페이지마다 동일한 로드 상태 논리, 동일한 오류 처리 및 동일한 토스트 알림이 있었습니다. 모든 것을 복사하여 붙여넣는 것이 잘못된 것 같아서 Blazor에서 구성 요소 상속을 살펴보았습니다. Blazor 구성 요소는 C# 클래스이므로 매우 간단합니다.

기본

모든 Blazor 구성 요소는 기본적으로 ComponentBase에서 상속됩니다. ComponentBase을 확장하는 고유한 기본 클래스를 생성한 다음 구성 요소가 그로부터 상속받도록 할 수 있습니다.

대부분의 페이지에 로딩 상태와 오류 처리가 필요하다고 가정해 보겠습니다. 기본 클래스를 만들 수 있습니다:

using Microsoft.AspNetCore.Components;

public abstract class PageBase : ComponentBase
{
    protected bool IsLoading { get; set; } = true;
    protected string? ErrorMessage { get; set; }

    protected async Task LoadDataAsync(Func<Task> action)
    {
        try
        {
            IsLoading = true;
            ErrorMessage = null;
            await action();
        }
        catch (Exception ex)
        {
            ErrorMessage = ex.Message;
        }
        finally
        {
            IsLoading = false;
            StateHasChanged();
        }
    }
}

기본 클래스 사용

이제 모든 페이지 구성 요소에서 ComponentBase에서 상속하는 대신 PageBase에서 상속합니다.

@page "/users"
@inherits PageBase

@if (IsLoading)
{
    <div class="spinner"></div>
}
else if (ErrorMessage is not null)
{
    <div class="alert alert-danger">@ErrorMessage</div>
}
else
{
    <ul>
        @foreach (var user in users)
        {
            <li>@user.Name</li>
        }
    </ul>
}

@code {
    private List<User> users = new();

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync(async () =>
        {
            users = await Http.GetFromJsonAsync<List<User>>("api/users");
        });
    }
}

@inherits PageBase 지시문이 핵심입니다. 이는 Blazor에게 기본 ComponentBase 대신 기본 클래스를 사용하도록 지시합니다. 이제 상속받은 모든 페이지에서 IsLoading, ErrorMessageLoadDataAsync()를 무료로 얻을 수 있습니다.

기본 클래스에 서비스 주입

또한 기본 클래스에 서비스를 삽입하여 모든 하위 구성 요소에서 사용할 수 있도록 할 수도 있습니다.

public abstract class PageBase : ComponentBase
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    [Inject]
    protected IToastService Toast { get; set; } = default!;

    protected bool IsLoading { get; set; } = true;
    protected string? ErrorMessage { get; set; }

    protected void NavigateBack() => Navigation.NavigateTo("javascript:history.back()");

    protected void ShowSuccess(string message) => Toast.ShowSuccess(message);
}

PageBase에서 상속된 모든 구성 요소는 이제 아무것도 주입하지 않고도 Navigation, Toast, NavigateBack()ShowSuccess()에 액세스할 수 있습니다.

일반 기본 클래스에 대해 더 자세히 알아보기

일반적인 CRUD 패턴에 대한 일반 기본 클래스를 만들 수도 있습니다.

public abstract class CrudPageBase<T> : PageBase
{
    protected List<T> Items { get; set; } = new();
    protected T? SelectedItem { get; set; }

    protected abstract Task<List<T>> FetchItems();
    protected abstract Task DeleteItem(T item);

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync(async () =>
        {
            Items = await FetchItems();
        });
    }

    protected async Task OnDelete(T item)
    {
        await LoadDataAsync(async () =>
        {
            await DeleteItem(item);
            Items = await FetchItems();
            ShowSuccess("Item deleted.");
        });
    }
}

그러면 실제 페이지가 매우 깨끗해집니다.

@page "/products"
@inherits CrudPageBase<Product>

@* just the markup, all logic lives in the base class *@

@code {
    protected override Task<List<Product>> FetchItems()
        => Http.GetFromJsonAsync<List<Product>>("api/products");

    protected override Task DeleteItem(Product item)
        => Http.DeleteAsync($"api/products/{item.Id}");
}

사용해야 할 때와 사용하지 않을 때

구성 요소 상속은 로드 상태, 오류 처리, 인증 확인 또는 CRUD 패턴과 같은 공유 동작에 적합합니다. 그러나 깊은 상속 계층 구조를 지나치게 사용하지 마십시오. 두 수준 이상의 깊이로 진행되는 경우 구성을 사용하는 것이 더 나을 것입니다(예: 이전 게시물의 LoadingComponent 접근 방식).

나는 보통 페이지의 “유형"별로 하나의 기본 클래스를 유지합니다: 일반 페이지의 경우 PageBase, 양식의 경우 FormPageBase, 그게 전부입니다.

게시물이 마음에 드셨기를 바랍니다! @emimontesdeoca로 소셜 미디어를 통해 언제든지 저에게 연락해주세요.

리소스