폼과 유효성 검사
EditForm 컴포넌트와 DataAnnotations를 사용해 입력 폼을 만들고 유효성을 검사합니다.
📌 학습 목표
- EditForm 컴포넌트의 역할과 기본 구조를 이해합니다.
- DataAnnotations 어트리뷰트를 사용해 모델에 유효성 규칙을 선언합니다.
- ValidationSummary와 ValidationMessage로 오류 메시지를 화면에 출력합니다.
- OnValidSubmit과 OnInvalidSubmit 이벤트로 폼 제출을 처리합니다.
- 사용자 지정 유효성 검사 어트리뷰트를 직접 작성합니다.
(DataAnnotations)
컴포넌트 구성
입력
이벤트
유효성 판정
또는 OnInvalidSubmit
📝 개념 설명
1. EditForm 컴포넌트
Blazor에서 폼을 처리할 때는 HTML의 <form> 태그 대신 EditForm 컴포넌트를 사용합니다. EditForm은 단순한 래퍼가 아니라, 내부적으로 EditContext 객체를 생성해 폼 전체 상태(필드 수정 여부, 유효성 오류 목록 등)를 일괄 관리합니다.
EditForm의 주요 속성과 이벤트
- Model: 폼에 바인딩할 C# 데이터 모델 객체
- OnValidSubmit: 유효성 검사 통과 시 호출되는 이벤트 콜백
- OnInvalidSubmit: 유효성 검사 실패 시 호출되는 이벤트 콜백
- OnSubmit: 유효성 결과와 무관하게 항상 호출 (직접 검사 코드 작성 필요)
@* RegisterForm.razor — EditForm 기본 구조 *@
<EditForm Model="@registerModel" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>이름</label>
<InputText @bind-Value="registerModel.Name" />
<ValidationMessage For="@(() => registerModel.Name)" />
</div>
<button type="submit">제출</button>
</EditForm>
@code {
private RegisterModel registerModel = new();
private void HandleValidSubmit()
{
Console.WriteLine("폼 제출 성공!");
}
private void HandleInvalidSubmit()
{
Console.WriteLine("입력값을 확인해 주세요.");
}
}OnSubmit을 사용하면 DataAnnotationsValidator가 있어도 유효성 검사가 자동 실행되지 않습니다.
editContext.Validate()를 직접 호출해야 합니다. 특별한 이유가 없다면 OnValidSubmit / OnInvalidSubmit 조합을 사용하세요.2. DataAnnotations — 선언형 유효성 규칙
DataAnnotations는 C# 모델 클래스의 프로퍼티에 어트리뷰트(Attribute)를 부착해 유효성 규칙을 선언적으로 정의하는 방식입니다. System.ComponentModel.DataAnnotations 네임스페이스를 사용하며, ASP.NET Core 전반에서 폭넓게 활용됩니다.
주요 DataAnnotations 어트리뷰트
| 어트리뷰트 | 설명 | 주요 매개변수 |
|---|---|---|
[Required] | 필수 입력 (null·빈 값 불허) | ErrorMessage |
[StringLength(max)] | 문자열 최대·최소 길이 | MaximumLength, MinimumLength |
[Range(min, max)] | 숫자 범위 제한 | minimum, maximum |
[EmailAddress] | 이메일 형식 검사 | ErrorMessage |
[Phone] | 전화번호 형식 검사 | ErrorMessage |
[Url] | URL 형식 검사 | ErrorMessage |
[RegularExpression] | 정규식 패턴 검사 | pattern |
[Compare] | 다른 프로퍼티와 값 비교 | otherProperty |
[MinLength(n)] | 최소 길이 | length |
[MaxLength(n)] | 최대 길이 | length |
using System.ComponentModel.DataAnnotations;
public class RegisterModel
{
[Required(ErrorMessage = "이름을 입력해 주세요.")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "이름은 2~50자 사이여야 합니다.")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "이메일을 입력해 주세요.")]
[EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다.")]
public string Email { get; set; } = "";
[Required(ErrorMessage = "비밀번호를 입력해 주세요.")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "비밀번호는 8~100자 사이여야 합니다.")]
public string Password { get; set; } = "";
[Required(ErrorMessage = "비밀번호 확인을 입력해 주세요.")]
[Compare(nameof(Password), ErrorMessage = "비밀번호가 일치하지 않습니다.")]
public string ConfirmPassword { get; set; } = "";
[Range(14, 120, ErrorMessage = "나이는 14~120세 사이여야 합니다.")]
public int Age { get; set; }
}3. 유효성 검사 컴포넌트
① DataAnnotationsValidator
EditForm 내에 배치해 DataAnnotations 어트리뷰트 기반 검사를 활성화하는 컴포넌트입니다. EditForm 내에 반드시 배치해야 어트리뷰트 검사가 실행됩니다.
<DataAnnotationsValidator />② ValidationSummary
폼 전체의 유효성 오류 메시지를 <ul> 목록 형태로 한꺼번에 표시합니다. 폼 상단에 배치해 사용자가 모든 오류를 한눈에 파악하게 합니다.
<ValidationSummary />③ ValidationMessage
특정 프로퍼티의 오류 메시지만 해당 필드 옆에 표시합니다. For 속성에 람다 표현식으로 프로퍼티를 지정합니다.
<ValidationMessage For="@(() => registerModel.Email)" />두 컴포넌트를 함께 사용하면 동일한 오류가 두 곳에 중복 표시됩니다. 간단한 폼은 ValidationSummary(폼 상단 전체 목록)만, 복잡한 폼은 ValidationMessage(필드 옆 개별 오류)만 사용하는 것이 일반적입니다.
4. Blazor 입력(Input) 컴포넌트
Blazor는 HTML 입력 요소를 감싸는 전용 컴포넌트를 제공합니다. 이 컴포넌트들은 EditContext와 자동으로 연동되어 유효성 상태에 따라 CSS 클래스(valid / invalid / modified)가 자동 적용됩니다.
| Blazor 컴포넌트 | 대응 HTML 요소 | 바인딩 타입 |
|---|---|---|
InputText | <input type="text"> | string |
InputNumber<T> | <input type="number"> | int, decimal, double 등 |
InputDate<T> | <input type="date"> | DateTime, DateOnly |
InputCheckbox | <input type="checkbox"> | bool |
InputSelect<T> | <select> | enum, string 등 |
InputTextArea | <textarea> | string |
💡 예제 & 실습 — 회원가입 폼 완성하기
DataAnnotations 모델 정의, EditForm 구성, 오류 메시지 표시, 폼 제출 처리를 모두 포함한 완전한 회원가입 폼을 단계별로 작성합니다.
Step 1. 모델 클래스 정의 (RegisterModel.cs)
// Models/RegisterModel.cs
using System.ComponentModel.DataAnnotations;
public class RegisterModel
{
[Required(ErrorMessage = "이름을 입력해 주세요.")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "이름은 2~50자 사이여야 합니다.")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "이메일을 입력해 주세요.")]
[EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다.")]
public string Email { get; set; } = "";
[Required(ErrorMessage = "비밀번호를 입력해 주세요.")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "비밀번호는 8~100자 사이여야 합니다.")]
public string Password { get; set; } = "";
[Required(ErrorMessage = "비밀번호 확인을 입력해 주세요.")]
[Compare(nameof(Password), ErrorMessage = "비밀번호가 일치하지 않습니다.")]
public string ConfirmPassword { get; set; } = "";
[Range(14, 120, ErrorMessage = "나이는 14~120세 사이여야 합니다.")]
public int Age { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "이용약관에 동의해 주세요.")]
public bool AgreeToTerms { get; set; }
}Step 2. 폼 컴포넌트 작성 (Register.razor)
@page "/register"
<h2>회원가입</h2>
@if (isSuccess)
{
<p>@successMessage</p>
}
else
{
<EditForm Model="@registerModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label>이름 *</label>
<InputText @bind-Value="registerModel.Name" class="form-control" />
<ValidationMessage For="@(() => registerModel.Name)" />
</div>
<div class="form-group">
<label>이메일 *</label>
<InputText @bind-Value="registerModel.Email" class="form-control" />
<ValidationMessage For="@(() => registerModel.Email)" />
</div>
<div class="form-group">
<label>비밀번호 (8자 이상) *</label>
<InputText @bind-Value="registerModel.Password" type="password" class="form-control" />
<ValidationMessage For="@(() => registerModel.Password)" />
</div>
<div class="form-group">
<label>비밀번호 확인 *</label>
<InputText @bind-Value="registerModel.ConfirmPassword" type="password" class="form-control" />
<ValidationMessage For="@(() => registerModel.ConfirmPassword)" />
</div>
<div class="form-group">
<label>나이</label>
<InputNumber @bind-Value="registerModel.Age" class="form-control" />
<ValidationMessage For="@(() => registerModel.Age)" />
</div>
<div class="form-group">
<label>
<InputCheckbox @bind-Value="registerModel.AgreeToTerms" />
이용약관에 동의합니다 *
</label>
<ValidationMessage For="@(() => registerModel.AgreeToTerms)" />
</div>
<button type="submit">가입하기</button>
</EditForm>
}
@code {
private RegisterModel registerModel = new();
private bool isSuccess = false;
private string successMessage = "";
private void HandleValidSubmit()
{
// 유효성 검사 통과 — 실제 저장 로직 실행
isSuccess = true;
successMessage = $"{registerModel.Name}님, 회원가입이 완료되었습니다!";
}
}Step 3. 코드 해설
- Model=”@registerModel”: EditForm이 registerModel 객체를 추적 대상으로 등록하고 내부 EditContext를 생성합니다.
- DataAnnotationsValidator: 없으면 어트리뷰트 검사가 전혀 실행되지 않아 OnValidSubmit이 항상 성공으로 처리됩니다.
- ValidationSummary: 폼 상단에 모든 유효성 오류를 목록으로 표시합니다.
- @bind-Value: 입력 컴포넌트와 모델 프로퍼티를 양방향 바인딩합니다.
- ValidationMessage For=”@(() => …)”: 람다 표현식으로 특정 프로퍼티를 지정해 해당 필드의 오류 메시지만 표시합니다.
- OnValidSubmit: 모든 유효성 검사를 통과한 후에만 HandleValidSubmit이 호출됩니다.
Step 4. 사용자 지정(Custom) 유효성 검사 어트리뷰트
DataAnnotations로 표현하기 어려운 복잡한 규칙은 ValidationAttribute를 상속해 직접 작성할 수 있습니다.
// 한국 휴대폰 번호 형식 검사 어트리뷰트
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
public class KoreanPhoneAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(
object? value, ValidationContext validationContext)
{
if (value is null || string.IsNullOrEmpty(value.ToString()))
return ValidationResult.Success; // null 여부는 [Required]가 담당
string phone = value.ToString()!;
bool isValid = Regex.IsMatch(phone, @"^010[-]?[0-9]{4}[-]?[0-9]{4}$");
return isValid
? ValidationResult.Success
: new ValidationResult(
ErrorMessage ?? "올바른 휴대폰 번호를 입력해 주세요. (예: 010-1234-5678)");
}
}
// 모델에서 사용
public class ContactModel
{
[Required]
[KoreanPhone(ErrorMessage = "휴대폰 번호 형식이 올바르지 않습니다.")]
public string Phone { get; set; } = "";
}⚠️ 자주 틀리는 것 / 주의사항
EditForm 내에
<DataAnnotationsValidator />를 배치하지 않으면 DataAnnotations 어트리뷰트가 전혀 동작하지 않습니다. OnValidSubmit이 항상 성공으로 처리되는 현상이 발생합니다.폼 리셋 목적으로
registerModel = new RegisterModel()을 실행하면 EditForm의 내부 EditContext가 함께 초기화됩니다. 이는 의도된 동작이지만, EditForm이 변경을 감지하려면 StateHasChanged()를 함께 호출해야 합니다.C# 8 이상에서
string?(nullable)으로 선언된 프로퍼티는 [Required]가 있어도 null이 유효한 값으로 취급될 수 있습니다. string(non-nullable)으로 선언하고 = ""로 초기화하는 것이 권장 패턴입니다.For="@(registerModel.Name)"처럼 값을 직접 전달하면 컴파일 오류가 발생합니다. 반드시 For="@(() => registerModel.Name)"처럼 () => 람다 표현식으로 감싸야 합니다.Blazor 6 이하에서는
<InputText type="password">가 동작하지 않았습니다. Blazor 7 이상부터는 추가 어트리뷰트 전달(attribute splatting)이 지원되어 type="password"를 직접 사용할 수 있습니다. 하위 호환이 걱정된다면 <input type="password" @bind="model.Password" @bind:event="oninput">를 사용하되, 이 경우 ValidationMessage가 동작하지 않을 수 있으니 주의하세요.🎯 마무리
Blazor의 EditForm과 DataAnnotations를 조합하면 별도의 유효성 검사 로직 없이도 모델 클래스 선언만으로 강력한 폼 유효성 검사를 구현할 수 있습니다. 모델에 어트리뷰트를 부착하고, EditForm 내에 DataAnnotationsValidator를 배치하며, ValidationMessage로 오류를 표시하는 세 가지 패턴만 숙지해도 실무의 대부분 요구사항을 충족할 수 있습니다.
- EditForm은 Blazor의 폼 컴포넌트로, 내부적으로 EditContext를 생성해 폼 전체 상태를 관리합니다.
- 모델 프로퍼티에 DataAnnotations 어트리뷰트([Required], [StringLength], [EmailAddress], [Compare] 등)를 부착해 유효성 규칙을 선언합니다.
- DataAnnotationsValidator는 EditForm 내에 반드시 배치해야 어트리뷰트 검사가 실행됩니다.
- ValidationSummary는 폼 전체 오류 목록, ValidationMessage For=”@(() => 프로퍼티)”는 필드별 오류 메시지를 표시합니다.
- OnValidSubmit은 유효성 통과 시, OnInvalidSubmit은 실패 시 호출됩니다.
- 복잡한 유효성 규칙은 ValidationAttribute를 상속한 사용자 지정 어트리뷰트로 구현합니다.
- Blazor Input 컴포넌트(InputText, InputNumber, InputCheckbox 등)는 EditContext와 자동 연동되어
valid/invalidCSS 클래스가 자동 적용됩니다.