Blazor 처음부터: 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.razorUserCard.razor.cs
마크업과 C# 로직 모두 읽기 쉬워집니다.
6단계: 실용적인 폴더 구조
확장에 유리한 구조 예시:
Components/Pages/-> 라우팅 가능한 페이지Components/Layout/-> 앱 셸/내비게이션Components/Common/-> 공용 범용 컴포넌트Components/Features/<FeatureName>/-> 기능별 컴포넌트
초기에 자주 하는 실수
- 전용 모델 없이 파라미터를 너무 많이 전달
- 페이지 컴포넌트에 비즈니스 규칙을 직접 작성
- 수백 줄짜리 “God component” 생성
- 반복되는 마크업을 추출하지 않음
다음 장
4장에서는 데이터 바인딩과 이벤트를 다룹니다. @bind, 이벤트 처리, 양방향 바인딩의 트레이드오프, 예측 가능한 상태 관리 패턴을 살펴봅니다.