심화 가이드
폼

폼 입력 유효성 검사하기

사용자 입력이 정확하고 완전한지 검사하면 전반적인 데이터 품질을 높일 수 있습니다. 이 문서에서는 반응형 폼과 템플릿 기반 폼에서 UI를 통해 받은 사용자 입력을 검증하고, 유용한 검사 메시지를 표시하는 방법을 알아봅니다.

템플릿 기반 폼에서 입력 검증하기

템플릿 기반 폼에 유효성 검사를 추가하려면 네이티브 HTML 폼 유효성 검사에서 사용하는 것과 같은 유효성 검사 어트리뷰트를 추가하면 됩니다. Angular는 디렉티브를 사용해서 이 어트리뷰트를 프레임워크의 검증 함수와 연결합니다.

폼 컨트롤의 값이 변경될 때마다 Angular는 유효성 검사를 실행합니다. 검사에 실패하면 유효성 검사 오류 목록을 생성하고 상태를 INVALID로 지정하며, 통과하면 null을 반환하고 상태를 VALID로 지정합니다.

ngModel을 템플릿 지역 변수로 내보내면 컨트롤 상태를 확인할 수 있습니다. 다음 예제에서는 NgModel을 name 변수로 내보냅니다:

actor-form-template.component.html (name)

<input
  type="text"
  id="name"
  name="name"
  class="form-control"
  required
  minlength="4"
  appForbiddenName="bob"
  [(ngModel)]="actor.name"
  #name="ngModel"
/>

@if (name.invalid && (name.dirty || name.touched)) {
  <div class="alert">
    @if (name.hasError('required')) {
      <div>Name is required.</div>
    }
    @if (name.hasError('minlength')) {
      <div>Name must be at least 4 characters long.</div>
    }
    @if (name.hasError('forbiddenName')) {
      <div>Name cannot be Bob.</div>
    }
  </div>
}

이 예제에서 다음 내용을 확인해 보세요.

  • <input> 엘리먼트에는 HTML 유효성 검사 어트리뷰트인 required와 minlength가 있습니다. 커스텀 검증 디렉티브인 forbiddenName도 있습니다. 자세한 내용은 커스텀 검증 함수 섹션을 참고하세요.

  • #name="ngModel"은 NgModel을 name 지역 변수로 내보냅니다. NgModel은 내부 FormControl 인스턴스의 여러 프로퍼티를 제공하므로, 템플릿에서 valid, dirty 등의 컨트롤 상태를 확인할 수 있습니다. 컨트롤 프로퍼티의 전체 목록은 AbstractControl API 레퍼런스를 참고하세요.

    • 가장 바깥쪽 @if는 name이 유효하지 않으면서 컨트롤이 dirty 또는 touched 상태일 때만 내부 메시지를 표시합니다.

    • 내부의 각 @if는 발생할 수 있는 유효성 검사 오류별로 메시지를 표시합니다. required, minlength, forbiddenName 오류에 대한 메시지가 있습니다.

참고: 사용자가 폼을 수정하기도 전에 오류가 표시되지 않도록 컨트롤이 dirty 또는 touched 상태인지 확인하는 것이 좋습니다.

  • 사용자가 감시 중인 필드의 값을 변경하면 컨트롤은 "dirty" 상태가 됩니다.
  • 폼 컨트롤 엘리먼트에서 포커스가 벗어나면 컨트롤은 "touched" 상태가 됩니다.

반응형 폼에서 입력 검증하기

반응형 폼에서는 컴포넌트 클래스가 원본 데이터를 관리합니다. 템플릿의 어트리뷰트로 검증 함수를 추가하는 대신, 컴포넌트 클래스의 폼 컨트롤 모델에 검증 함수를 직접 추가합니다. 그러면 Angular는 컨트롤 값이 변경될 때마다 이 함수를 실행합니다.

검증 함수

검증 함수는 동기 또는 비동기 방식으로 동작할 수 있습니다.

검증 함수 종류 설명
동기 검증 함수 컨트롤 인스턴스를 인자로 받고, 유효성 검사 오류 모음 또는 null을 즉시 반환하는 동기 함수입니다. FormControl 인스턴스를 생성할 때 두 번째 인자로 전달합니다.
비동기 검증 함수 컨트롤 인스턴스를 인자로 받고, 이후 유효성 검사 오류 모음 또는 null을 전달하는 Promise나 옵저버블을 반환하는 비동기 함수입니다. FormControl 인스턴스를 생성할 때 세 번째 인자로 전달합니다.

성능을 고려해서 Angular는 모든 동기 검증 함수를 통과한 경우에만 비동기 검증 함수를 실행합니다. 각 비동기 검증이 완료된 후에 오류가 설정됩니다.

내장 검증 함수

검증 함수를 직접 작성하거나 Angular에 내장된 검증 함수를 사용할 수 있습니다.

템플릿 기반 폼에서 어트리뷰트로 사용하는 required, minlength 등의 내장 검증 기능은 모두 Validators 클래스의 함수로도 사용할 수 있습니다. 전체 내장 검증 함수 목록은 Validators API 레퍼런스를 참고하세요.

배우 입력 폼을 반응형 폼으로 바꾸려면, 같은 내장 검증 기능을 다음 예제처럼 함수 형태로 사용하면 됩니다.

actor-form-reactive.component.ts (검증 함수)

actorForm = new FormGroup({
  name: new FormControl(this.actor.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator.
  ]),
  role: new FormControl(this.actor.role),
  skill: new FormControl(this.actor.skill, Validators.required),
});

get name() {
  return this.actorForm.get('name');
}

get skill() {
  return this.actorForm.get('skill');
}

이 예제의 name 컨트롤에는 내장 검증 함수인 Validators.required, Validators.minLength(4)와 커스텀 검증 함수인 forbiddenNameValidator를 설정합니다.

모두 동기 검증 함수이므로 두 번째 인자로 전달합니다. 여러 검증 함수를 사용하려면 함수 배열을 전달하면 됩니다.

이 예제에는 getter 메서드도 몇 개 추가했습니다. 반응형 폼에서는 상위 그룹의 get 메서드로 모든 폼 컨트롤에 접근할 수 있지만, 템플릿에서 간편하게 사용하도록 getter를 정의하면 편리할 때가 있습니다.

name 입력의 템플릿을 다시 보면 템플릿 기반 예제와 상당히 비슷합니다.

actor-form-reactive.component.html (name과 오류 메시지)

<input type="text" id="name" class="form-control" formControlName="name" required />

@if (name.invalid && (name.dirty || name.touched)) {
  <div class="alert alert-danger">
    @if (name.hasError('required')) {
      <div>Name is required.</div>
    }
    @if (name.hasError('minlength')) {
      <div>Name must be at least 4 characters long.</div>
    }
    @if (name.hasError('forbiddenName')) {
      <div>Name cannot be Bob.</div>
    }
  </div>
}

템플릿 기반 버전과 달리 이 폼에서는 디렉티브를 내보내지 않습니다. 대신 컴포넌트 클래스에 정의한 name getter를 사용합니다.

템플릿에 required 어트리뷰트가 여전히 남아 있다는 점도 확인해 보세요. 유효성 검사에는 필요하지 않지만, 접근성을 위해 유지해야 합니다.

커스텀 검증 함수 정의하기

내장 검증 함수가 애플리케이션의 요구사항에 정확히 맞지 않을 수 있으므로, 때로는 커스텀 검증 함수를 만들어야 합니다.

앞의 예제에 사용한 forbiddenNameValidator 함수를 살펴봅시다. 이 함수는 다음과 같이 정의합니다.

forbidden-name.directive.ts (forbiddenNameValidator)

/** An actor's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? {forbiddenName: {value: control.value}} : null;
  };
}

이 함수는 특정 금지된 이름을 찾을 정규 표현식을 받고, 검증 함수를 반환하는 팩토리입니다.

이 예제에서는 "bob"을 금지하므로, "bob"이 포함된 배우 이름을 허용하지 않습니다. 설정하는 정규 표현식에 따라 "alice"나 다른 이름을 허용하지 않도록 할 수도 있습니다.

forbiddenNameValidator 팩토리는 설정이 반영된 검증 함수를 반환합니다. 이 검증 함수는 Angular 컨트롤 객체를 받고, 컨트롤 값이 유효하면 null을, 유효하지 않으면 유효성 검사 오류 객체를 반환합니다. 일반적으로 오류 객체의 프로퍼티 이름은 'forbiddenName' 같은 유효성 검사 키이며, 값은 오류 메시지에 활용할 수 있는 {name} 같은 임의의 값 모음입니다.

커스텀 비동기 검증 함수도 동기 검증 함수와 비슷하지만, null이나 유효성 검사 오류 객체를 나중에 전달하는 Promise 또는 옵저버블을 반환해야 합니다. 옵저버블을 반환하는 경우에는 반드시 완료되어야 하며, 폼은 완료 시점에 마지막으로 전달된 값을 유효성 검사 결과로 사용합니다.

반응형 폼에 커스텀 검증 함수 추가하기

반응형 폼에서는 FormControl에 함수를 직접 전달해서 커스텀 검증 함수를 추가합니다.

actor-form-reactive.component.ts (검증 함수)

actorForm = new FormGroup({
  name: new FormControl(this.actor.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator.
  ]),
  role: new FormControl(this.actor.role),
  skill: new FormControl(this.actor.skill, Validators.required),
});

템플릿 기반 폼에 커스텀 검증 함수 추가하기

템플릿 기반 폼에서는 검증 함수를 감싼 디렉티브를 템플릿에 추가합니다. 예를 들어 ForbiddenValidatorDirective는 forbiddenNameValidator를 감싸는 역할을 합니다.

다음 예제처럼 디렉티브가 자신을 NG_VALIDATORS 프로바이더에 등록하면 Angular가 해당 디렉티브를 유효성 검사 과정에 사용하는 것으로 인식합니다. NG_VALIDATORS는 검증 함수를 확장해서 등록할 수 있도록 미리 정의된 프로바이더입니다.

forbidden-name.directive.ts (providers)

providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => ForbiddenValidatorDirective),
      multi: true,
    },
  ],
})
export class ForbiddenValidatorDirective implements Validator {
  readonly forbiddenName = input<string>('', {alias: 'appForbiddenName'});

  validate(control: AbstractControl): ValidationErrors | null {
    return this.forbiddenName()
      ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control)
      : null;
  }
}

디렉티브 클래스는 Validator 인터페이스를 구현해서 Angular 폼과 쉽게 연동할 수 있도록 합니다. 전체 구성을 이해할 수 있도록 디렉티브의 나머지 코드를 살펴봅시다.

forbidden-name.directive.ts (디렉티브)

@Directive({
  selector: '[appForbiddenName]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => ForbiddenValidatorDirective),
      multi: true,
    },
  ],
})
export class ForbiddenValidatorDirective implements Validator {
  readonly forbiddenName = input<string>('', {alias: 'appForbiddenName'});

  validate(control: AbstractControl): ValidationErrors | null {
    return this.forbiddenName()
      ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control)
      : null;
  }
}

ForbiddenValidatorDirective를 만들었다면 입력 엘리먼트에 셀렉터인 appForbiddenName을 추가해서 활성화할 수 있습니다. 예를 들면 다음과 같습니다:

actor-form-template.component.html (forbidden-name-input)

<input
  type="text"
  id="name"
  name="name"
  class="form-control"
  required
  minlength="4"
  appForbiddenName="bob"
  [(ngModel)]="actor.name"
  #name="ngModel"
/>

참고: 커스텀 유효성 검사 디렉티브는 useClass가 아니라 useExisting으로 등록합니다. 등록하는 검증 함수는 forbiddenName 프로퍼티가 "bob"에 바인딩된, 폼 안의 해당 ForbiddenValidatorDirective 인스턴스여야 합니다.

useExisting을 useClass로 바꾸면 forbiddenName 값이 없는 새 클래스 인스턴스가 등록됩니다.

컨트롤 상태 CSS 클래스

Angular는 여러 컨트롤 프로퍼티를 폼 컨트롤 엘리먼트의 CSS 클래스에 자동으로 반영합니다. 이 클래스를 사용하면 폼 상태에 따라 폼 컨트롤 엘리먼트에 스타일을 적용할 수 있습니다. 현재 다음 클래스를 지원합니다.

  • .ng-valid
  • .ng-invalid
  • .ng-pending
  • .ng-pristine
  • .ng-dirty
  • .ng-untouched
  • .ng-touched
  • .ng-submitted (컨트롤을 감싸는 form 엘리먼트에만 적용)

다음 배우 입력 폼 예제에서는 .ng-valid와 .ng-invalid 클래스로 각 폼 컨트롤의 테두리 색상을 지정합니다.

forms.css (상태 클래스)

.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
}

.alert div {
  background-color: #fed3d3;
  color: #820000;
  padding: 1rem;
  margin-bottom: 1rem;
}

.form-group {
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: .5rem;
}

select {
  width: 100%;
  padding: .5rem;
}

여러 필드를 함께 검증하기

교차 필드 검증 함수는 폼의 서로 다른 필드 값을 비교해서, 값의 조합이 유효한지 판단하는 커스텀 검증 함수입니다. 예를 들어 서로 양립할 수 없는 선택지를 제공해서 사용자가 A나 B 중 하나만 선택하고 둘을 함께 선택할 수 없도록 하는 폼이 있을 수 있습니다. 어떤 필드 값이 다른 필드에 의존할 수도 있습니다. 예를 들어 A를 선택한 경우에만 B를 선택할 수 있도록 할 수 있습니다.

다음 교차 검증 예제에서는 아래 내용을 알아봅니다:

  • 같은 수준에 있는 두 컨트롤의 값을 기준으로 반응형 폼이나 템플릿 기반 폼 입력을 검증합니다.
  • 사용자가 폼과 상호작용한 뒤 유효성 검사를 통과하지 못하면 설명 메시지를 표시합니다.

이 예제에서는 배우 입력 폼을 작성할 때 배우 이름과 배역 이름을 동일하게 사용하지 않도록 교차 검증합니다. 검증 함수는 배우 이름과 배역이 서로 다른지 확인합니다.

반응형 폼에 교차 검증 추가하기

폼 구조는 다음과 같습니다:

const actorForm = new FormGroup({
  'name': new FormControl(),
  'role': new FormControl(),
  'skill': new FormControl(),
});

name과 role은 같은 수준에 있는 컨트롤입니다. 하나의 커스텀 검증 함수로 두 컨트롤을 함께 검사하려면 공통 상위 컨트롤인 FormGroup에서 검사해야 합니다. FormGroup에서 하위 컨트롤을 조회하면 값을 비교할 수 있습니다.

FormGroup에 검증 함수를 추가하려면 생성할 때 두 번째 인자로 전달합니다.

const actorForm = new FormGroup(
  {
    'name': new FormControl(),
    'role': new FormControl(),
    'skill': new FormControl(),
  },
  {validators: unambiguousRoleValidator},
);

검증 함수의 코드는 다음과 같습니다.

unambiguous-role.directive.ts

/** An actor's name can't match the actor's role */
export const unambiguousRoleValidator: ValidatorFn = (
  control: AbstractControl,
): ValidationErrors | null => {
  const name = control.get('name');
  const role = control.get('role');

  return name && role && name.value === role.value ? {unambiguousRole: true} : null;
};

unambiguousRoleValidator 검증 함수는 ValidatorFn 인터페이스를 구현합니다. Angular 컨트롤 객체를 인자로 받고, 폼이 유효하면 null을, 그렇지 않으면 ValidationErrors를 반환합니다.

이 검증 함수는 FormGroup의 get 메서드로 하위 컨트롤을 가져온 뒤 name과 role 컨트롤의 값을 비교합니다.

값이 다르면 배역을 명확히 구분할 수 있고 두 값이 모두 유효하므로 null을 반환합니다. 값이 같으면 배우와 배역을 혼동할 수 있으므로, 오류 객체를 반환해서 폼이 유효하지 않다고 표시해야 합니다.

사용자 경험을 개선하기 위해 폼이 유효하지 않을 때 템플릿에 적절한 오류 메시지를 표시합니다.

actor-form-reactive.component.html

@if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {
  <div class="cross-validation-error-message alert alert-danger">
    Name cannot match role or audiences will be confused.
  </div>
}

이 @if는 FormGroup에 unambiguousRoleValidator가 반환한 교차 검증 오류가 있을 때 메시지를 표시합니다. 단, 사용자가 폼과 상호작용한 뒤에만 표시합니다.

템플릿 기반 폼에 교차 검증 추가하기

템플릿 기반 폼에서는 검증 함수를 감싼 디렉티브를 만들어야 합니다. 다음 예제처럼 NG_VALIDATORS 토큰을 사용해서 이 디렉티브를 검증 함수로 제공합니다.

unambiguous-role.directive.ts

@Directive({
  selector: '[appUnambiguousRole]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => UnambiguousRoleValidatorDirective),
      multi: true,
    },
  ],
})
export class UnambiguousRoleValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    return unambiguousRoleValidator(control);
  }
}

새 디렉티브를 HTML 템플릿에 추가해야 합니다. 이 검증 함수는 폼의 최상위 수준에 등록해야 하므로, 다음 템플릿에서는 form 태그에 디렉티브를 적용합니다.

actor-form-template.component.html

<form #actorForm="ngForm" appUnambiguousRole>

사용자 경험을 개선하기 위해 폼이 유효하지 않을 때 적절한 오류 메시지를 표시합니다.

actor-form-template.component.html

@if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {
  <div class="cross-validation-error-message alert">Name cannot match role.</div>
}

이 부분은 템플릿 기반 폼과 반응형 폼에서 동일합니다.

비동기 검증 함수 만들기

비동기 검증 함수는 AsyncValidatorFn 또는 AsyncValidator 인터페이스를 구현합니다. 동기 검증 함수와 매우 비슷하지만 다음과 같은 차이가 있습니다.

  • validate() 함수는 Promise 또는 옵저버블을 반환해야 합니다.
  • 반환하는 옵저버블은 유한해야 합니다. 즉, 언젠가는 완료되어야 합니다. 무한한 옵저버블을 유한한 옵저버블로 바꾸려면 first, last, take, takeUntil 같은 필터링 연산자를 파이프로 연결하면 됩니다.

비동기 유효성 검사는 동기 유효성 검사 이후에, 동기 검사를 통과한 경우에만 실행합니다. 따라서 기본적인 검사에서 이미 유효하지 않은 입력을 발견했다면 HTTP 요청처럼 비용이 많이 들 수 있는 비동기 검사 과정을 생략할 수 있습니다.

비동기 유효성 검사가 시작되면 폼 컨트롤은 pending 상태가 됩니다. 컨트롤의 pending 프로퍼티를 확인해서 검사가 진행 중임을 화면에 표시할 수 있습니다.

비동기 검사가 진행되는 동안 로딩 표시를 보여주는 것이 일반적인 UI 패턴입니다. 다음 예제는 템플릿 기반 폼에서 이를 구현하는 방법입니다.

<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator />

@if (model.pending) {
  <app-spinner />
}

커스텀 비동기 검증 함수 구현하기

다음 예제의 비동기 검증 함수는 배우가 아직 다른 배우에게 배정되지 않은 배역을 맡도록 합니다. 새 배우가 계속 오디션을 보고 기존 배우가 은퇴하므로, 선택할 수 있는 배역 목록을 미리 가져올 수는 없습니다. 입력한 배역을 검증하려면 현재 배역을 맡은 배우들의 중앙 데이터베이스를 조회하는 비동기 작업을 시작해야 합니다.

다음 코드는 AsyncValidator 인터페이스를 구현한 UniqueRoleValidator 검증 클래스를 만듭니다.

role.directive.ts

@Service()
export class UniqueRoleValidator implements AsyncValidator {
  private readonly actorsService = inject(ActorsService);

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.actorsService.isRoleTaken(control.value).pipe(
      map((isTaken) => (isTaken ? {uniqueRole: true} : null)),
      catchError(() => of(null)),
    );
  }
}

actorsService 프로퍼티는 다음 인터페이스를 정의하는 ActorsService 토큰의 인스턴스로 초기화합니다.

interface ActorsService {
  isRoleTaken: (role: string) => Observable<boolean>;
}

실제 애플리케이션에서는 ActorsService가 배우 데이터베이스에 HTTP 요청을 보내 배역을 선택할 수 있는지 확인합니다. 검증 함수에서는 서비스의 실제 구현이 중요하지 않으므로, 이 예제는 ActorsService 인터페이스를 기준으로 작성할 수 있습니다.

유효성 검사를 시작하면 UniqueRoleValidator는 현재 컨트롤 값을 ActorsService의 isRoleTaken() 메서드에 전달해서 작업을 위임합니다. 이때 컨트롤은 pending 상태가 되며, validate() 메서드가 반환한 옵저버블 체인이 완료될 때까지 이 상태를 유지합니다.

isRoleTaken() 메서드는 배역을 선택할 수 있는지 확인하는 HTTP 요청을 보내고 결과로 Observable<boolean>을 반환합니다. validate() 메서드는 응답을 map 연산자로 전달해서 유효성 검사 결과로 변환합니다.

그런 다음 다른 검증 함수와 마찬가지로 폼이 유효하면 null을, 그렇지 않으면 ValidationErrors를 반환합니다. 이 검증 함수는 발생할 수 있는 오류를 catchError 연산자로 처리합니다. 이 예제에서는 isRoleTaken()의 오류를 유효성 검사 통과로 처리합니다. 검사 요청에 실패했다고 해서 반드시 배역이 유효하지 않은 것은 아니기 때문입니다. 오류를 다르게 처리해서 ValidationError 객체를 반환하도록 할 수도 있습니다.

시간이 지나 옵저버블 체인이 완료되면 비동기 유효성 검사도 끝납니다. pending 값은 false가 되고 폼의 유효성 상태가 갱신됩니다.

반응형 폼에 비동기 검증 함수 추가하기

반응형 폼에서 비동기 검증 함수를 사용하려면 먼저 컴포넌트 클래스의 프로퍼티에 검증 객체를 주입합니다.

actor-form-reactive.component.ts

roleValidator = inject(UniqueRoleValidator);

그런 다음 검증 함수를 FormControl에 직접 전달해서 적용합니다.

다음 예제에서는 UniqueRoleValidator의 validate 함수를 컨트롤의 asyncValidators 옵션에 전달하고, ActorFormReactiveComponent에 주입한 UniqueRoleValidator 인스턴스에 바인딩해서 roleControl에 적용합니다. asyncValidators에는 비동기 검증 함수 하나 또는 함수 배열을 지정할 수 있습니다. FormControl 옵션에 대해 자세히 알아보려면 AbstractControlOptions API 레퍼런스를 참고하세요.

actor-form-reactive.component.ts

const roleControl = new FormControl('', {
  asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)],
  updateOn: 'blur',
});

템플릿 기반 폼에 비동기 검증 함수 추가하기

템플릿 기반 폼에서 비동기 검증 함수를 사용하려면 새 디렉티브를 만들고 NG_ASYNC_VALIDATORS 프로바이더에 등록합니다.

다음 예제에서 디렉티브는 실제 검증 로직을 가진 UniqueRoleValidator 클래스를 주입받습니다. Angular가 유효성 검사를 실행할 때 호출하는 validate 함수에서 이 객체를 사용합니다.

role.directive.ts

@Directive({
  selector: '[appUniqueRole]',
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: forwardRef(() => UniqueRoleValidatorDirective),
      multi: true,
    },
  ],
})
export class UniqueRoleValidatorDirective implements AsyncValidator {
  private readonly validator = inject(UniqueRoleValidator);

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.validator.validate(control);
  }
}

그런 다음 동기 검증 함수와 마찬가지로 입력 엘리먼트에 디렉티브 셀렉터를 추가해서 활성화합니다.

actor-form-template.component.html (unique-unambiguous-role-input)

<input
  type="text"
  id="role"
  name="role"
  #role="ngModel"
  [(ngModel)]="actor.role"
  [ngModelOptions]="{updateOn: 'blur'}"
  appUniqueRole
/>

비동기 검증 함수 성능 최적화하기

기본적으로 모든 검증 함수는 폼 값이 변경될 때마다 실행됩니다. 동기 검증 함수는 보통 애플리케이션 성능에 눈에 띄는 영향을 미치지 않습니다. 하지만 비동기 검증 함수는 컨트롤을 검증하기 위해 HTTP 요청을 보내는 경우가 많습니다. 키를 누를 때마다 HTTP 요청을 보내면 백엔드 API에 부담을 줄 수 있으므로 가능하면 피하는 것이 좋습니다.

updateOn 프로퍼티를 기본값인 change에서 submit 또는 blur로 바꾸면 폼 유효성 상태를 갱신하는 시점을 늦출 수 있습니다.

템플릿 기반 폼에서는 템플릿에 프로퍼티를 설정합니다.

<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}" />

반응형 폼에서는 FormControl 인스턴스에 프로퍼티를 설정합니다.

new FormControl('', {updateOn: 'blur'});

반응형 폼에서 검증 함수를 동적으로 관리하기

복잡한 반응형 폼에서는 사용자 입력이나 애플리케이션 상태에 따라 검증 함수를 추가하거나 제거하거나 변경해야 할 수 있습니다. Angular는 폼 컨트롤을 다시 생성하지 않고도 런타임에 검증 함수를 관리할 수 있도록 AbstractControl에 여러 메서드를 제공합니다.

검증 함수 추가하고 제거하기

addValidators와 removeValidators 메서드를 사용하면 초기화 이후에도 컨트롤의 검증 함수를 변경할 수 있습니다. Angular는 검증 함수를 참조로 비교하므로 removeValidators에는 addValidators에 전달했던 것과 동일한 함수를 전달해야 합니다.

private readonly usPostalCode = Validators.pattern(/^\d{5}$/);

onCountryChange(country: string) {
    const postalCodeControl = this.profileForm.get('postalCode');

    if (country === 'US') {
      // 미국 우편번호 검증 함수를 추가합니다.
      postalCodeControl.addValidators([Validators.required, this.usPostalCode]);
    } else {
      // 미국이 아니면 검증 함수를 제거합니다.
      postalCodeControl.removeValidators([Validators.required, this.usPostalCode]);
    }

    postalCodeControl.updateValueAndValidity();
}

모든 검증 함수 교체하기

컨트롤의 기존 동기 검증 함수를 모두 교체하려면 setValidators를 사용하고, 모두 제거하려면 clearValidators를 사용합니다.

toggleStrictNameValidation(isStrict: boolean) {
  const nameControl = this.profileForm.get('name');

  if (isStrict) {
    // 엄격한 유효성 검사 규칙을 설정합니다.
    nameControl.setValidators([
      Validators.required,
      Validators.minLength(3),
      Validators.pattern(/^[a-zA-Z]+$/),
    ]);
  } else {
    // 모든 검증 함수를 제거합니다.
    nameControl.clearValidators();
  }

  nameControl.updateValueAndValidity();
}

비동기 검증 함수에도 addAsyncValidators, removeAsyncValidators, setAsyncValidators, clearAsyncValidators를 사용해서 같은 패턴을 적용할 수 있습니다.

유효성 검사 상태 갱신하기

검증 함수를 변경한 뒤에는 updateValueAndValidity를 실행해서 컨트롤의 유효성 검사 상태를 다시 계산합니다. 이 메서드는 갱신 동작을 제어하는 옵션을 받습니다.

// 컨트롤을 갱신하고 상위 컨트롤에 알립니다.
control.updateValueAndValidity();

// 상위 컨트롤에 알리거나 이벤트를 보내지 않고 이 컨트롤만 갱신합니다.
control.updateValueAndValidity({onlySelf: true, emitEvent: false});

네이티브 HTML 폼 유효성 검사와 함께 사용하기

Angular는 기본적으로 바깥쪽 <form>에 novalidate 어트리뷰트를 추가해서 네이티브 HTML 폼 유효성 검사를 비활성화하고, 디렉티브로 유효성 검사 어트리뷰트를 프레임워크의 검증 함수와 연결합니다. Angular 유효성 검사와 네이티브 유효성 검사를 함께 사용하려면 ngNativeValidate 디렉티브로 네이티브 검사를 다시 활성화할 수 있습니다. 자세한 내용은 API 문서를 참고하세요.