부모-자식 컴포넌트
EventCallback으로 부모-자식 컴포넌트 간 데이터를 양방향으로 전달하는 패턴을 익힙니다.
📌 학습 목표
- Blazor 컴포넌트 트리 구조와 부모-자식 관계를 이해하고 설명할 수 있습니다.
[Parameter]속성으로 부모 → 자식 방향의 데이터 전달을 구현할 수 있습니다.EventCallback으로 자식 → 부모 방향의 이벤트 알림을 처리할 수 있습니다.EventCallback<T>를 사용해 이벤트 발생 시 데이터를 함께 전달할 수 있습니다.@bind-PropertyName단축 문법으로 양방향 바인딩을 간결하게 구현할 수 있습니다.
📝 개념 설명
1. Blazor 컴포넌트 트리와 데이터 흐름
Blazor 앱은 컴포넌트 트리(Component Tree)로 구성됩니다. 최상위 App.razor를 시작으로 여러 컴포넌트가 부모-자식 관계를 형성하며 UI를 구성합니다. 이 트리 구조에서 데이터는 반드시 방향을 갖고 이동합니다.
- 부모 → 자식:
[Parameter]속성으로 값을 ‘주입’합니다. - 자식 → 부모:
EventCallback으로 이벤트를 ‘알립니다’.
이 두 방향을 함께 활용하면 완전한 양방향 데이터 흐름을 구현할 수 있습니다.
2. [Parameter] — 부모에서 자식으로 데이터 전달
자식 컴포넌트에 [Parameter] 속성을 선언하면, 부모 컴포넌트에서 태그 속성(Attribute) 형태로 값을 전달할 수 있습니다. 부모가 다시 렌더링될 때마다 자식의 파라미터 값도 자동으로 갱신됩니다.
@* ChildGreeting.razor (자식 컴포넌트) *@
<div>
<p>안녕하세요, @Name 님!</p>
<p>현재 점수: @Score점</p>
</div>
@code {
[Parameter]
public string Name { get; set; } = string.Empty;
[Parameter]
public int Score { get; set; }
}@* ParentPage.razor (부모 컴포넌트) *@
<ChildGreeting Name="홍길동" Score="@currentScore" />
@code {
private int currentScore = 95;
}[Parameter] 값을 직접 수정하는 것은 권장하지 않습니다. 부모가 다음에 렌더링될 때 원래 값으로 덮어씌워지기 때문입니다.3. EventCallback — 자식에서 부모로 이벤트 전달
EventCallback은 자식 컴포넌트가 부모에게 “무언가가 발생했다”고 알리는 메커니즘입니다. C#의 event와 유사하지만, Blazor 렌더링 사이클에 최적화되어 있습니다.
EventCallback의 핵심 특징:
- 자식 컴포넌트에
[Parameter]로 선언합니다. - 부모 컴포넌트에서 핸들러 메서드를 연결합니다.
- 자식이
InvokeAsync()를 호출하면 부모의 핸들러가 실행됩니다. - 이벤트 호출 후 부모의
StateHasChanged()가 자동으로 호출되어 UI가 갱신됩니다. EventCallback이 null이어도InvokeAsync()는 안전하게 동작합니다.
@* LikeButton.razor (자식 컴포넌트) *@
<button @onclick="HandleClick">❤️ 좋아요</button>
@code {
[Parameter]
public EventCallback OnLiked { get; set; }
private async Task HandleClick()
{
await OnLiked.InvokeAsync();
}
}@* ArticlePage.razor (부모 컴포넌트) *@
<h3>좋아요 수: @likeCount</h3>
<LikeButton OnLiked="@AddLike" />
@code {
private int likeCount = 0;
private void AddLike()
{
likeCount++;
}
}4. EventCallback<T> — 데이터와 함께 이벤트 전달
단순한 알림이 아닌, 값을 함께 부모에 전달해야 할 때는 제네릭 버전인 EventCallback<T>를 사용합니다. InvokeAsync(value)에 전달할 데이터를 인수로 넘깁니다.
@* SearchBox.razor (자식 컴포넌트) *@
<input type="text" @bind="keyword" placeholder="검색어 입력" />
<button @onclick="Search">검색</button>
@code {
private string keyword = string.Empty;
[Parameter]
public EventCallback<string> OnSearch { get; set; }
private async Task Search()
{
await OnSearch.InvokeAsync(keyword);
}
}@* SearchPage.razor (부모 컴포넌트) *@
<SearchBox OnSearch="@HandleSearch" />
@if (!string.IsNullOrEmpty(result))
{
<p>검색 결과: <strong>@result</strong></p>
}
@code {
private string result = string.Empty;
private void HandleSearch(string keyword)
{
result = $"'{keyword}'에 대한 결과를 표시합니다.";
}
}Parameter 값 설정
값 렌더링·처리
InvokeAsync() 호출
StateHasChanged() 자동
5. @bind-PropertyName — 양방향 바인딩 단축 문법
자식에 X라는 [Parameter]와 XChanged라는 EventCallback<T>가 쌍으로 존재할 때, 부모에서 @bind-X 단축 문법을 사용할 수 있습니다.
@* 단축 문법 *@
<QuantitySelector @bind-Quantity="quantity" MaxQuantity="10" />
@* 위 코드는 아래와 완전히 동일합니다 *@
<QuantitySelector
Quantity="@quantity"
QuantityChanged="@((val) => quantity = val)"
MaxQuantity="10" />@bind-Quantity가 작동하려면 EventCallback의 이름이 반드시 QuantityChanged여야 합니다. 파라미터 이름 + “Changed”가 Blazor의 명명 규칙입니다.💡 예제 & 실습 — 완성형 수량 선택기 컴포넌트
실무에서 자주 쓰이는 상품 수량 선택기를 통해 전체 통신 흐름을 단계별로 구현합니다.
Step 1: 자식 컴포넌트 (QuantitySelector.razor)
@* QuantitySelector.razor *@
<div class="qty-wrap">
<button @onclick="Decrease" disabled="@(Quantity <= 1)">−</button>
<span>@Quantity</span>
<button @onclick="Increase" disabled="@(Quantity >= MaxQuantity)">+</button>
</div>
@code {
[Parameter]
public int Quantity { get; set; } = 1;
[Parameter]
public int MaxQuantity { get; set; } = 99;
[Parameter]
public EventCallback<int> QuantityChanged { get; set; }
private async Task Increase()
{
if (Quantity < MaxQuantity)
await QuantityChanged.InvokeAsync(Quantity + 1);
}
private async Task Decrease()
{
if (Quantity > 1)
await QuantityChanged.InvokeAsync(Quantity - 1);
}
}Step 2: 부모 컴포넌트 (ShoppingCart.razor)
@* ShoppingCart.razor *@
<h3>장바구니</h3>
<p>상품명: Blazor 입문서</p>
<p>단가: 25,000원</p>
<label>수량:</label>
<QuantitySelector @bind-Quantity="quantity" MaxQuantity="10" />
<hr />
<p><strong>합계: @((quantity * 25000).ToString("N0"))원</strong></p>
@code {
private int quantity = 1;
}Step 3: 동작 흐름 해설
- 부모가 렌더링되며
Quantity=1,MaxQuantity=10을 자식에 전달합니다. - 사용자가 + 버튼을 클릭하면 자식의
Increase()가 실행됩니다. QuantityChanged.InvokeAsync(Quantity + 1)으로 새 수량(2)을 부모에 알립니다.@bind-Quantity의 내부 핸들러가quantity = 2로 갱신합니다.- Blazor가 자동으로 UI를 다시 렌더링하여 합계가 50,000원으로 표시됩니다.
⚠️ 자주 틀리는 것 / 주의사항
① 자식이 [Parameter]를 직접 수정하면 안 됩니다
자식 내부에서 파라미터 값을 직접 변경하면, 부모가 다음번에 렌더링될 때 원래 값으로 덮어씌워집니다. 상태는 항상 부모가 소유하고, 자식은 변경 요청만 EventCallback으로 알려야 합니다.
// ❌ 잘못된 패턴 — 부모가 모르는 상태 변경
[Parameter] public int Count { get; set; }
private void Wrong() { Count++; } // 다음 렌더링 때 원래 값으로 복원됨
// ✅ 올바른 패턴 — EventCallback으로 부모에 위임
[Parameter] public int Count { get; set; }
[Parameter] public EventCallback<int> CountChanged { get; set; }
private async Task Correct()
{
await CountChanged.InvokeAsync(Count + 1);
}② EventCallback은 async Task로 처리하세요
// ❌ await 없이 호출 — 컴파일러 경고 발생
private void HandleClick()
{
OnClicked.InvokeAsync(); // Task를 버림
}
// ✅ await 적용
private async Task HandleClick()
{
await OnClicked.InvokeAsync();
}③ Action이나 Func<Task> 대신 EventCallback을 사용하세요
Action이나 Func<Task>를 콜백으로 쓸 수도 있지만, EventCallback이 권장됩니다:
EventCallback은 이벤트 후 부모의StateHasChanged()를 자동으로 호출합니다.Action은 자동 렌더링이 없어 UI 갱신이 누락될 수 있습니다.EventCallback은null이어도 안전하게InvokeAsync()가 동작합니다.
④ @bind-X 명명 규칙을 지켜야 합니다
@bind-Value가 동작하려면 자식에 Value Parameter와 ValueChanged EventCallback이 정확히 이 이름으로 존재해야 합니다. 이름이 다르면 단축 문법은 컴파일 오류가 발생합니다.
🎯 마무리
[Parameter]와 EventCallback은 Blazor 컴포넌트 개발의 가장 기초가 되는 통신 패턴입니다. 부모가 상태를 소유하고 자식에게 값을 내려주며, 자식은 변화가 생기면 이벤트로 부모에 알리는 이 구조는 단방향 데이터 흐름(Unidirectional Data Flow)의 핵심 원리입니다.
- 재사용 가능한 UI 컴포넌트(버튼, 입력 폼, 모달, 슬라이더 등)는 항상 이 패턴으로 설계하세요.
- 자식이 복잡한 객체를 전달해야 한다면
EventCallback<MyClass>처럼 커스텀 클래스도 T로 지정할 수 있습니다. - 여러 단계를 건너 데이터를 전달해야 할 때는
CascadingValue/CascadingParameter나 별도의 상태 서비스를 고려하세요.
- [Parameter]: 부모 → 자식 단방향 데이터 전달. 자식 컴포넌트 속성에 선언하고 부모 태그에서 값을 설정합니다.
- EventCallback: 자식 → 부모 이벤트 알림.
InvokeAsync()호출 시 부모 핸들러 실행 + StateHasChanged() 자동 호출. - EventCallback<T>: 이벤트 발생 시 T 타입 데이터를 부모에게 함께 전달하는 제네릭 버전.
- @bind-X 단축 문법: 자식의 X Parameter + XChanged EventCallback<T> 쌍을 부모에서 간결하게 연결.
- 자식은 상태를 직접 수정하지 않습니다. 변경 요청은 반드시 EventCallback으로 부모에 위임합니다.