Blazor 처음부터: 3장 — 확장 가능한 컴포넌트

· 3분 읽기

Blazor 처음부터 3장에 오신 것을 환영합니다. 아직 2장을 읽지 않았다면 먼저 읽고 기본 프로젝트를 준비해 주세요.

2장에서는 앱을 실행했습니다. 3장에서는 앱을 유지보수 가능한 구조 로 바꿉니다.

Blazor 프로젝트는 페이지마다 거대한 .razor 파일이 되면 금방 복잡해집니다. 컴포넌트는 이를 막아줍니다. 일관성, 재사용성, 그리고 UI 책임 분리가 좋아집니다.


Blazor 컴포넌트란 무엇인가

컴포넌트는 다음을 할 수 있는 .razor 파일입니다.

  • 마크업 렌더링
  • 로컬 상태 보관
  • 파라미터로 입력 받기
  • 부모 컴포넌트로 이벤트 전달
  • 자식 콘텐츠 렌더링

런타임에서 Blazor는 각 컴포넌트를 작은 상태 머신처럼 다룹니다. 상태가 바뀌면 다시 렌더링하고 DOM diff를 적용합니다.

즉, 명확한 입력과 예측 가능한 동작을 설계하는 것이 핵심입니다.


1단계: 목적이 분명한 컴포넌트부터 시작

Components/Common/SectionHeader.razor를 만듭니다.

<header class="section-header">
    <h2>@Title</h2>
    @if (!string.IsNullOrWhiteSpace(Subtitle))
    {
        <p>@Subtitle</p>
    }
</header>

@code {
    [Parameter] public string Title { get; set; } = string.Empty;
    [Parameter] public string? Subtitle { get; set; }
}

Components/Pages/Home.razor에서 사용:

@page "/"

<PageTitle>Blazor 처음부터</PageTitle>

<SectionHeader
    Title="Blazor 처음부터"
    Subtitle="3장은 컴포넌트를 다룹니다." />

작은 예제지만 핵심은 분명합니다. 컴포넌트는 파라미터만 봐도 의도를 이해할 수 있어야 합니다.


2단계: 파라미터로 동작을 명시적으로 표현

재사용 가능한 버튼 컴포넌트를 만들어 봅니다.

Components/Common/AppButton.razor:

<button class="app-button @VariantCssClass" @onclick="OnClick">
    @Text
</button>

@code {
    [Parameter] public string Text { get; set; } = "Button";
    [Parameter] public string Variant { get; set; } = "primary";
    [Parameter] public EventCallback OnClick { get; set; }

    private string VariantCssClass => Variant.ToLowerInvariant() switch
    {
        "secondary" => "app-button--secondary",
        "danger" => "app-button--danger",
        _ => "app-button--primary"
    };
}

사용 예:

@code {
    private int _savedCount;

    private void Save()
    {
        _savedCount++;
    }
}

<AppButton Text="Save" Variant="primary" OnClick="Save" />
<p>저장 횟수: @_savedCount</p>

핵심은 계약(Contract) 설계입니다.

  • 적고 명확한 파라미터
  • 암묵적 동작 대신 명시적 이름
  • 안전한 기본값

3단계: RenderFragment로 레이아웃 컴포지션

RenderFragment를 사용하면 부모가 자식에게 UI 블록을 전달할 수 있습니다.

Components/Common/Card.razor 생성:

<article class="card">
    <header class="card__header">@Title</header>
    <section class="card__body">
        @ChildContent
    </section>
</article>

@code {
    [Parameter] public string Title { get; set; } = string.Empty;
    [Parameter] public RenderFragment? ChildContent { get; set; }
}

사용 예:

<Card Title="Roadmap">
    <ul>
        <li>컴포넌트</li>
        <li>Data binding</li>
        <li>Routing</li>
    </ul>
</Card>

이 패턴은 래퍼 마크업 반복 없이 일관된 페이지 구조를 만드는 데 매우 유용합니다.


4단계: 거대한 페이지보다 컴포지션

페이지가 커지면 책임 기준으로 분리하세요.

  • ProfileSummary (상단 요약)
  • ProfileStats (지표)
  • ProfileActivityList (최근 활동)

그 결과 페이지는 구현 상세가 아니라 조합/오케스트레이션 역할을 하게 됩니다.


5단계: 마크업과 로직의 균형

단순한 컴포넌트는 inline @code로 충분합니다.

커지기 시작하면 code-behind로 분리하세요.

  • UserCard.razor
  • UserCard.razor.cs

마크업과 C# 로직 모두 읽기 쉬워집니다.


6단계: 실용적인 폴더 구조

확장에 유리한 구조 예시:

  • Components/Pages/ -> 라우팅 가능한 페이지
  • Components/Layout/ -> 앱 셸/내비게이션
  • Components/Common/ -> 공용 범용 컴포넌트
  • Components/Features/<FeatureName>/ -> 기능별 컴포넌트

초기에 자주 하는 실수

  • 전용 모델 없이 파라미터를 너무 많이 전달
  • 페이지 컴포넌트에 비즈니스 규칙을 직접 작성
  • 수백 줄짜리 “God component” 생성
  • 반복되는 마크업을 추출하지 않음

다음 장

4장에서는 데이터 바인딩과 이벤트를 다룹니다. @bind, 이벤트 처리, 양방향 바인딩의 트레이드오프, 예측 가능한 상태 관리 패턴을 살펴봅니다.