Razor 컴포넌트 기초
.razor 파일의 기본 문법과 컴포넌트 생명주기를 이해합니다.
📌 학습 목표
.razor파일의 세 가지 구성 요소(마크업·@code 블록·지시어)를 설명할 수 있습니다.- Razor 문법(
@기호)을 사용해 C# 값 출력, 이벤트 연결, 조건/반복 렌더링을 구현할 수 있습니다. - 컴포넌트 매개변수(
[Parameter])를 선언하고 부모 컴포넌트에서 자식으로 값을 전달할 수 있습니다. - Blazor 컴포넌트 생명주기 메서드의 호출 순서를 이해하고 상황에 맞는 메서드를 선택할 수 있습니다.
- 완전하고 실행 가능한 Razor 컴포넌트를 직접 작성할 수 있습니다.
📝 개념 설명
Razor 컴포넌트란?
Blazor 애플리케이션의 UI는 Razor 컴포넌트(Razor Component)라는 단위로 구성됩니다. 확장자가 .razor인 파일 하나가 곧 하나의 컴포넌트이며, HTML 마크업과 C# 코드를 한 파일 안에 작성할 수 있습니다.
React의 함수형 컴포넌트, Angular의 컴포넌트와 유사한 개념이지만, 별도의 템플릿 언어 없이 C#을 그대로 사용한다는 점이 핵심 차별점입니다.
@변수로 C# 값을 출력하고 @onclick 등으로 이벤트를 연결합니다.@page로 라우팅 경로, @using으로 네임스페이스, @inject로 서비스 주입을 선언합니다..razor 파일 기본 구조
다음은 세 가지 구성 요소를 모두 담은 가장 기본적인 Razor 컴포넌트의 전체 구조입니다.
@* 1. 지시어 — 파일 최상단에 위치 *@
@page "/hello"
@using System.Collections.Generic
@* 2. 마크업 — HTML과 Razor 문법 혼합 *@
<h1>안녕하세요, @Name!</h1>
<p>현재 카운트: @count</p>
<button @onclick="Increment">클릭 (+1)</button>
@* 3. @code 블록 — C# 로직 *@
@code {
[Parameter]
public string Name { get; set; } = "World";
private int count = 0;
private void Increment()
{
count++;
}
}
Razor 문법 핵심
① 값 출력 — @표현식
@변수명으로 C# 변수를 HTML에 직접 출력합니다. 복잡한 표현식은 괄호로 감쌉니다.
<p>이름: @userName</p>
<p>날짜: @DateTime.Now.ToString("yyyy-MM-dd")</p>
<p>계산 결과: @(price * quantity)원</p>
② 이벤트 핸들러 — @이벤트명
HTML 이벤트 이름 앞에 @를 붙여 C# 메서드를 연결합니다.
<button @onclick="HandleClick">클릭</button>
<input @oninput="HandleInput" placeholder="입력" />
<select @onchange="HandleChange"></select>
③ 조건부 렌더링 — @if
@if (isLoggedIn)
{
<p>환영합니다!</p>
}
else
{
<p>로그인이 필요합니다.</p>
}
④ 반복 렌더링 — @foreach
<ul>
@foreach (var item in items)
{
<li>@item.Name — @item.Price원</li>
}
</ul>
컴포넌트 매개변수 (Parameter)
부모 컴포넌트에서 자식 컴포넌트로 값을 전달할 때는 [Parameter] 특성(Attribute)을 사용합니다. 반드시 public 속성으로 선언해야 합니다.
자식 컴포넌트 — Alert.razor
<div class="alert alert-@Type">
@Message
</div>
@code {
[Parameter]
public string Message { get; set; } = string.Empty;
[Parameter]
public string Type { get; set; } = "info"; // "info" | "warning" | "danger"
}
부모에서 사용
<Alert Message="저장이 완료되었습니다." Type="success" />
<Alert Message="오류가 발생했습니다." Type="danger" />
컴포넌트 생명주기 (Lifecycle)
Blazor 컴포넌트는 생성부터 소멸까지 정해진 순서로 생명주기 메서드가 호출됩니다. 각 메서드를 오버라이드(override)하여 원하는 시점에 코드를 실행합니다.
OnInitializedAsync — 초기 데이터 로딩
protected override async Task OnInitializedAsync()
{
// 최초 1회만 실행
products = await ProductService.GetAllAsync();
}
OnParametersSetAsync — 매개변수 변경 대응
protected override async Task OnParametersSetAsync()
{
// 매개변수가 바뀔 때마다 실행
filteredList = await DataService.GetByCategoryAsync(Category);
}
OnAfterRenderAsync — JavaScript 호출
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 무한 루프 방지를 위해 firstRender 조건 필수
await JSRuntime.InvokeVoidAsync("initChart", "#myChart");
}
}
IDisposable — 리소스 정리
@implements IDisposable
@code {
private Timer? _timer;
protected override void OnInitialized()
{
_timer = new Timer(Tick, null, 0, 1000);
}
private void Tick(object? state)
{
InvokeAsync(StateHasChanged); // UI 갱신
}
public void Dispose()
{
_timer?.Dispose();
}
}
💡 예제 & 실습 — 완전한 스마트 카운터
지금까지 배운 매개변수, 생명주기, 이벤트, Dispose를 모두 통합한 완전한 컴포넌트 예제입니다.
@* SmartCounter.razor *@
@page "/smart-counter"
@implements IDisposable
<h2>스마트 카운터</h2>
<p>현재 값: <strong>@count</strong> / 최대: @MaxCount</p>
<progress value="@count" max="@MaxCount" style="width:100%"></progress>
<div style="margin-top:8px">
<button @onclick="Decrement" disabled="@(count <= MinCount)">−</button>
<button @onclick="Increment" disabled="@(count >= MaxCount)">+</button>
<button @onclick="Reset">초기화</button>
</div>
@if (count == MaxCount)
{
<p style="color:green;font-weight:bold">✅ 최대값 도달!</p>
}
@if (count == MinCount)
{
<p style="color:red">⚠️ 최솟값입니다.</p>
}
<p style="color:gray;font-size:0.85em">경과: @elapsed초</p>
@code {
[Parameter] public int MaxCount { get; set; } = 10;
[Parameter] public int MinCount { get; set; } = 0;
[Parameter] public int InitialValue { get; set; } = 0;
private int count;
private int elapsed;
private Timer? _timer;
// ① 초기화 — 최초 1회
protected override void OnInitialized()
{
count = InitialValue;
_timer = new Timer(_ => InvokeAsync(() =>
{
elapsed++;
StateHasChanged();
}), null, 1000, 1000);
}
// ② 매개변수 변경 시 범위 보정
protected override void OnParametersSet()
{
count = Math.Clamp(count, MinCount, MaxCount);
}
private void Increment() => count = Math.Min(count + 1, MaxCount);
private void Decrement() => count = Math.Max(count - 1, MinCount);
private void Reset() => count = InitialValue;
// ⑤ 소멸 시 타이머 정리
public void Dispose() => _timer?.Dispose();
}
부모 페이지에서 사용
@page "/"
<SmartCounter MaxCount="5" MinCount="-5" InitialValue="0" />
<SmartCounter MaxCount="100" MinCount="0" InitialValue="50" />
단계별 해설
- @page “/smart-counter”: 이 컴포넌트를
/smart-counterURL로 직접 접근할 수 있게 합니다. - @implements IDisposable: 타이머를 사용하므로 컴포넌트 제거 시
Dispose()로 정리합니다. - [Parameter] 세 개:
MaxCount,MinCount,InitialValue를 외부에서 주입하여 동작을 제어합니다. - OnInitialized: 초기값 설정 + 1초마다 경과 시간을 업데이트하는 타이머를 시작합니다.
- InvokeAsync + StateHasChanged: 타이머는 백그라운드 스레드이므로 Blazor 동기화 컨텍스트로 전환 후 UI를 갱신합니다.
- OnParametersSet: 부모가 MaxCount/MinCount를 변경하면 현재 count를 유효 범위 안으로 자동 조정합니다.
⚠️ 자주 틀리는 것 / 주의사항
[Parameter]가 붙은 속성을 private으로 선언하면 부모에서 값을 전달할 수 없습니다. Blazor 5.0 이후 private [Parameter]에는 컴파일 경고가 발생합니다. 반드시 public get; set; 형태로 선언하세요.타이머 콜백, 소켓 이벤트 등 Blazor 동기화 컨텍스트 밖에서 UI를 변경할 때는 반드시
await InvokeAsync(StateHasChanged)를 사용해야 합니다. 직접 StateHasChanged()만 호출하면 스레드 안전성 문제가 발생할 수 있습니다.OnInitializedAsync는 컴포넌트 생애에서 단 한 번만 실행됩니다. 부모가 매개변수를 변경할 때마다 데이터를 다시 로드해야 한다면 OnParametersSetAsync를 사용해야 합니다. 이 둘을 혼동하면 데이터가 갱신되지 않는 버그가 발생합니다.OnAfterRender 안에서 조건 없이 StateHasChanged()를 호출하면 렌더 → OnAfterRender → StateHasChanged → 렌더 → … 무한 루프가 발생합니다. 반드시 if (firstRender) 같은 가드 조건을 걸어야 합니다.SmartCounter.razor 파일은 자동으로 <SmartCounter /> 태그가 됩니다. Blazor는 태그명과 파일명의 대소문자를 완전히 일치시킵니다. 반드시 파스칼 케이스(PascalCase)를 사용하세요. 소문자로 시작하면 일반 HTML 요소로 인식될 수 있습니다.🎯 마무리
Razor 컴포넌트는 Blazor 개발의 핵심 단위입니다. .razor 파일 하나가 하나의 독립적인 UI 조각이 되며, 마크업과 C# 코드를 한 파일에서 관리할 수 있습니다. 매개변수(Parameter)로 재사용 가능한 컴포넌트를 설계하고, 생명주기 메서드로 정확한 시점에 코드를 실행하는 패턴을 익히면 복잡한 UI도 체계적으로 구현할 수 있습니다.
실전에서 가장 중요한 판단은 어떤 생명주기 메서드를 언제 쓰느냐입니다. 초기 데이터 로드는 OnInitializedAsync, 매개변수 변경 대응은 OnParametersSetAsync, JavaScript 호출은 OnAfterRenderAsync, 리소스 정리는 Dispose라는 패턴을 체득하면 대부분의 상황에 대응할 수 있습니다.
.razor파일 = 마크업 +@code블록 + 지시어(@page · @using · @inject)@변수로 값 출력,@onclick으로 이벤트 연결,@if/@foreach로 조건·반복 렌더링[Parameter] public T 속성명 { get; set; }— 부모→자식 값 전달의 기본 규칙- 생명주기 순서: OnInitialized → OnParametersSet → ShouldRender → OnAfterRender → Dispose
- 외부 스레드에서 UI 갱신 시
await InvokeAsync(StateHasChanged)반드시 사용 - 파일명 = 컴포넌트 태그명 (반드시 PascalCase 준수)