Skip to content

3. 폼 유효성 검사


애플리케이션에 폼을 구성할 때는 사용자가 필수 항목을 입력했는지, 입력된 데이터는 올바른지 검증해야 한다. 이 문서는 반응형 폼이나 템플릿 기반 폼에서 사용자가 입력한 내용을 검증하고 유효성 검사 결과를 화면에 표시하는 방법에 대해 다룬다.


1. 사전지식

폼 유효성 검사에 대해 알아보기 전에 이런 내용을 먼저 이해하는 것이 좋다.



Note


2. 템플릿 기반 폼 유효성 검사

템플릿 기반 폼의 유효성 검사를 적용하려면 기본 HTML 폼에서 유효성을 검사했던 것처럼 유효성 검사 어트리뷰트를 추가하면 된다. 폼에 이런 어트리뷰트를 사용하면 Angular가 적절한 디렉티브를 매칭해서 유효성을 검사한다.


Angular는 폼 컨트롤의 값이 바뀔 때마다 유효성 검사 로직을 실행하고 검사 결과를 반환하는데, INVALID 상태일 때는 에러 목록을 반환하며 VALID 상태일 때는 null을 반환한다.


이 때 개발자는 ngModel로 폼 컨트롤의 상태를 참조할 수 있다. 아래 예제 코드는 name 변수와 연결된 NgModel로 유효성을 검사하는 예제 코드이다.


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

<div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
  <div *ngIf="name.errors?.['required']">Name is required.</div>
  <div *ngIf="name.errors?.['minlength']">
    Name must be at least 4 characters long.
  </div>
  <div *ngIf="name.errors?.['forbiddenName']">Name cannot be Bob.</div>
</div>


이 예제 코드를 보면서 이런 내용을 확인해 보자.


  • <input> 엘리먼트에는 HTML 유효성 검사 어트리뷰트 requiredminLength가 사용되었다. 그리고 커스텀 유효성 검사 디렉티브 forbiddenName도 사용되었다.
  • 템플릿에서 #name="ngModel"라고 작성하면 폼 컨트롤에 적용된 NgModel 디렉티브가 템플릿 지역 변수(template local variable) name에 할당된다. NgModelFormControl 인스턴스의 상태를 관리하기 때문에 NgModel이 적용된 name 변수에 접근하면 validdirty와 같은 폼 컨트롤 상태를 참조할 수 있다.
  • <div> 엘리먼트에 사용된 *ngIfname으로 연결된 폼 컨트롤의 상태에 따라 관련된 메시지를 화면에 표시한다.
  • 폼에서 발생할 수 있는 에러에 해당되는 <div> 엘리먼트를 몇 개 추가했다. 이 메시지는 required, minLength, forbiddenName 에러가 발생할 때 각각 표시된다.


Note

  • 사용자가 폼 컨트롤에 아무것도 입력하지 않았는데도 화면에 에러가 표시되는 것을 방지하려면 폼 컨트롤 상태 중 dirtytouched를 활용하는 것이 좋다.
  • 사용자가 입력 필드의 값을 변경하면 폼 컨트롤이 dirty 상태가 된다.
  • 사용자가 폼 컨트롤에 접근했다가 포커스를 다른 곳으로 옮기면 폼 컨트롤이 touched 상태가 된다.


3. 반응형 폼 유효성 검사

반응형 폼에서는 컴포넌트 클래스가 데이터의 원천이다. 그래서 템플릿에 어트리뷰트를 적용하는 방식을 사용하지 않고 컴포넌트 클래스에서 폼 컨트롤 모델을 정의할 때 직접 유효성 검사 함수를 적용한다. 이 경우에도 폼 컨트롤의 값이 변경되면 유효성 검사 함수가 자동으로 실행된다.


1) 유효성 검사 함수

유효성 검사 함수는 동기/비동기 방식으로 동작한다.


  • 동기 유효성 검사 함수: 폼 컨트롤 인스턴스에 접근해서 유효성을 검사한 뒤 에러나 null을 동기 방식으로 반환하는 함수이다. 동기 유효성 검사 함수는 FormControl을 구성할 때 두 번째 인자에 지정한다.
  • 비동기 유효성 검사 함수: 폼 컨트롤 인스턴스에 접근해서 유효성을 검사한 뒤 에러나 null 값을 Promise나 Observable 형태로 반환하는 함수이다. 비동기 유효성 검사 함수는 FormControl을 구성할 때 세 번째 인자에 지정한다. Angular는 불필요한 성능 저하를 막기 위해 동기 유효성 검사 함수가 모두 성공했을 때만 비동기 유효성 검사 함수를 실행한다.


2) 기본 유효성 검사 함수

반응형 폼에서는 Angular가 제공하는 기본 유효성 검사 함수를 사용하거나 커스텀 유효성 검사 함수를 정의해서 사용할 수 있다.


템플릿 기반 폼에서 어트리뷰트로 적용하던 requiredminLength와 같은 기본 유효성 검사 함수는 Angular가 Validators 클래스로 제공한다.


히어로 폼을 반응형 폼으로 만들려면 아래 코드처럼 유효성 검사 함수를 적용하면 된다.


reactive/hero-form-reactive.component.ts (유효성 검사 함수)
ngOnInit(): void {
  this.heroForm = new FormGroup({
    name: new FormControl(this.hero.name, [
      Validators.required,
      Validators.minLength(4),
      forbiddenNameValidator(/bob/i) // <-- 커스텀 유효성 검사기의 인자는 이렇게 전달합니다.
    ]),
    alterEgo: new FormControl(this.hero.alterEgo),
    power: new FormControl(this.hero.power, Validators.required)
  });

}

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

get power() { return this.heroForm.get('power'); }


이 예제 코드에서 name 폼 컨트롤에는 Validators.requiredValidators.minLength(4) 기본 유효성 검사 함수, forbiddenNameValidator 커스텀 유효성 검사 함수가 지정되었다.


이 유효성 검사 함수들은 모두 동기 방식으로 동작하기 때문에 FormControl을 구성하면서 두 번째 인자로 지정했다. 두 번째 인자에 배열을 사용하면 유효성 검사 함수를 여러 개 지정할 수 있다.


그리고 이 예제 코드에는 게터(getter) 메서드가 몇 개 더 있다. 반응형 폼에서는 폼 컨트롤에 접근할 때 부모 그룹에 있는 get() 메서드를 사용해야 하기 때문에, 템플릿에서 폼 컨트롤을 활용하려면 게터 메서드를 정의해두는 것이 좋다.


이제 템플릿에 있는 name 입력 필드 값이 변경되었을 때 템플릿 기반 폼 때와 비슷한 결과가 표시되도록 만들어 보자.


reactive/hero-form-reactive.component.html (name 입력 필드와 에러 메시지)
<input
  type="text"
  id="name"
  class="form-control"
  formControlName="name"
  required
/>

<div
  *ngIf="name.invalid && (name.dirty || name.touched)"
  class="alert alert-danger"
>
  <div *ngIf="name.errors?.['required']">Name is required.</div>
  <div *ngIf="name.errors?.['minlength']">
    Name must be at least 4 characters long.
  </div>
  <div *ngIf="name.errors?.['forbiddenName']">Name cannot be Bob.</div>
</div>


코드를 보면, 이 예제 코드는 템플릿 기반 폼 때처럼 디렉티브로 폼 컨트롤에 접근하지 않는다. 반응형 폼은 디렉티브 대신 컴포넌트 클래스에 정의한 name 게터 메서드를 사용한다.


그런데 템플릿에 여전히 required 어트리뷰트가 지정되어 있는 것을 확인해 보자. 이 어트리뷰트는 반응형 폼이 동작할 때는 유효하지 않지만 접근성 관련 기능을 제공하려면 남겨둬야 한다.


4. 커스텀 유효성 검사 함수 정의하기

기본 유효성 검사 함수만으로 충분하지 않은 경우에는 커스텀 유효성 검사 함수를 정의할 수도 있다.


이전 섹션 반응형 폼 예제에 사용된 forbiddenNameValidator 함수를 살펴보자. 이 함수는 이렇게 정의되어 있다.


shared/forbidden-name.directive.ts (forbiddenNameValidator)
/** 히어로의 이름은 인자로 받은 정규표현식에 매칭되지 않아야 합니다.  */
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을 사용했다.


커스텀 비동기 유효성 검사 함수도 동기 유효성 검사 함수와 비슷하지만, 비동기 유효성 검사 함수는 반드시 Promise나 Observable에 null이나 에러 객체를 실어서 반환한다. 옵저버블을 반환한다면 이 옵저버블은 폼 유효성 검사가 모두 끝나기 전에 반드시 종료되어야 한다.


1) 반응형 폼에 커스텀 유효성 검사 함수 적용하기

반응형 폼에서는 FormControl에 커스텀 유효성 검사 함수를 직접 지정한다.


reactive/hero-form-reactive.component.ts (유효성 검사 함수)
this.heroForm = new FormGroup({
  name: new FormControl(this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i), // <-- 커스텀 유효성 검사기의 인자는 이렇게 전달합니다.
  ]),
  alterEgo: new FormControl(this.hero.alterEgo),
  power: new FormControl(this.hero.power, Validators.required),
});


2) 템플릿 기반 폼에 커스텀 유효성 검사 함수 적용하기

템플릿 기반 폼에서는 템플릿에 디렉티브를 적용하는 방식으로 사용해야 하기 때문에 유효성 검사 함수를 디렉티브로 랩핑해야 한다. 그래서 forbiddenNameValidatorForbiddenValidatorDirective와 같이 변환해야 사용할 수 있다.


유효성 검사 디렉티브는 디렉티브 자체에서 NG_VALIDATORS 프로바이더를 등록하기 때문에 Angular는 이 디렉티브가 폼 유효성 검사에 사용되는 디렉티브라는 것을 인식할 수 있다. NG_VALIDATORS는 기본 유효성 검사 목록을 확장할 때 사용하는 프로바이더이다.


shared/forbidden-name.directive.ts (providers)
providers: [
  {
    provide: NG_VALIDATORS,
    useExisting: ForbiddenValidatorDirective,
    multi: true,
  },
];


유효성 검사 디렉티브 클래스는 Angular 폼과 자연스럽게 어울려 동작하기 위해 Validator 인터페이스를 기반으로 구현한다. forbiddenNameValidator를 디렉티브 클래스로 랩핑한 코드는 이렇다.


shared/forbidden-name.directive.ts (디렉티브)
@Directive({
  selector: "[appForbiddenName]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: ForbiddenValidatorDirective,
      multi: true,
    },
  ],
})
export class ForbiddenValidatorDirective implements Validator {
  @Input("appForbiddenName") forbiddenName = "";

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


ForbiddenValidatorDirective를 한 번 정의하고 나면 디렉티브 셀렉터 appForbiddenName를 입력 엘리먼트에 추가하면 해당 엘리먼트에 유효성 검사 함수를 적용할 수 있다:


template/hero-form-template.component.html (유효성 검사 함수 적용하기)
<input
  type="text"
  id="name"
  name="name"
  class="form-control"
  required
  minlength="4"
  appForbiddenName="bob"
  [(ngModel)]="hero.name"
  #name="ngModel"
/>


Note

  • 커스텀 유효성 검사 디렉티브 인스턴스를 생성할 때 useClass를 사용하지 않고 useExisting을 사용했다.
  • 그래서 ForbiddenValidatorDirective의 인스턴스는 폼에서 "bob"를 바인딩할 때 사용된 forbiddenName 인스턴스와 같다.
  • 이 때 useExisting 대신 useClass를 사용하면 새로운 클래스 인스턴스가 생성되기 때문에 forbiddenName에 적용된 디렉티브 인스턴스와 연결되지 않는다.


5. CSS 클래스 활용하기

Angular는 폼 컨트롤의 상태에 따라 폼 컨트롤 엘리먼트에 CSS 클래스를 자동으로 지정한다. 그래서 이 클래스들을 활용하면 폼 컨트롤 상태에 따라 스타일을 다르게 지정할 수 있다. Angular는 이런 클래스들을 자동으로 지정한다.


  • .ng-valid
  • .ng-invalid
  • .ng-pending
  • .ng-pristine
  • .ng-dirty
  • .ng-untouched
  • .ng-touched
  • .ng-submitted (폼 엘리먼트에만 적용)


아래 예제는 히어로 폼에 자동으로 지정되는 .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: 0.5rem;
}

select {
  width: 100%;
  padding: 0.5rem;
}


6. 연관 필드 유효성 검사

연관 필드 유효성 검사(cross-field validation)는 커스텀 유효성 검사 함수를 활용해서 폼에 존재하는 필드 여러 개를 함께 검토하는 것을 의미한다. 서로 배타적인 옵션이 있다고 하면, A나 B는 선택할 수 있지만 A와 B를 함께 선택하는 것은 막아야 하는 조건이 이런 경우에 해당된다. 어떤 필드는 다른 필드에 종속된 경우도 있다. 사용자가 A 필드를 먼저 설정한 후에 B를 골라야 하는 경우가 그렇다.


예제를 보면서 이런 내용에 대해 알아보자:


  • 반응형 폼/팀플릿 기반 폼에서 연관된 폼 컨트롤 두 개를 함께 검사해 보자.
  • 사용자가 입력한 값이 유효성 검사를 통과하지 못했을 때 에러 메시지를 표시해 보자.


이번에 다룰 예제는 히어로가 자신의 진짜 정체를 폼에 작성하지 못하게 하는 것이다. 이 동작은 히어로의 이름과 별명을 다르게 입력해야 하는 조건으로 구현해 보자.


1) 반응형 폼에 연관 필드 유효성 검사 적용하기

폼이 이런 구조로 구성되었다고 하자:


const heroForm = new FormGroup({
  name: new FormControl(),
  alterEgo: new FormControl(),
  power: new FormControl(),
});


이 폼에서 namealterEgo가 이웃한 폼 컨트롤이다. 두 폼 컨트롤을 커스텀 유효성 검사 함수 하나로 검사하려면 이 유효성 검사 함수를 두 폼 컨트롤의 부모 폼 컨트롤, 이 경우에는 FormGroup에 적용해야 한다. 물론 FormGroup은 자식 폼 컨트롤에 접근해서 값을 참조할 수 있다.


FormGroup에 유효성 검사 함수를 적용하려면 폼 컨트롤 생성자의 두 번째 인자로 유효성 검사 함수를 지정하면 된다.


const heroForm = new FormGroup(
  {
    name: new FormControl(),
    alterEgo: new FormControl(),
    power: new FormControl(),
  },
  { validators: identityRevealedValidator }
);


이 유효성 검사 함수의 코드는 이렇다.


shared/identity-revealed.directive.ts
/** 히어로의 이름은 별명과 일치하지 않아야 합니다. */
export const identityRevealedValidator: ValidatorFn = (
  control: AbstractControl
): ValidationErrors | null => {
  const name = control.get("name");
  const alterEgo = control.get("alterEgo");

  return name && alterEgo && name.value === alterEgo.value
    ? { identityRevealed: true }
    : null;
};


identity 유효성 검사 함수는 ValidatorFn 인터페이스를 기반으로 구현한다. 이 인터페이스는 Angular 폼 컨트롤 객체를 인자로 받아서 유효성을 검사한 후 유효성 검사를 통과하면 null을 반환하고 유효성 검사를 통과하지 않으면 ValidationErrors를 반환한다.


그리고 이 유효성 검사 함수는 FormGroup이 제공하는 get 메서드로 자식 폼 컨트롤 namealterEgo 필드의 값을 가져온 후에 비교하는 동작을 한다.


두 필드의 값이 다르면 히어로의 정체는 비밀로 지켜지며 유효성 검사도 통과하면서 null을 반환한다.


하지만 두 필드의 값이 일치하면 히어로의 정체가 탄로나기 때문에 에러 객체를 반환해서 유효성 검사를 통과하지 못했다는 것을 알려야 한다.


이 때 사용자가 사용하기 편한 UX를 제공하기 위해 화면에 에러 메시지를 표시해서 폼 그룹에서 에러가 발생했다는 것을 알리는 것이 좋다.


reactive/hero-form-template.component.html
<div
  *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)"
  class="cross-validation-error-message alert alert-danger"
>
  Name cannot match alter ego.
</div>


*ngIfFormGroup에 적용된 identityRevealed 유효성 검사 결과에 따라 에러 메시지를 화면에 표시하는데, 이 에러 메시지는 사용자가 폼에 한 번 접근한 뒤에만 표시될 것이다.


2) 템플릿 기반 폼에 연관 필드 유효성 검사 적용하기

템플릿 기반 폼에서는 유효성 검사 함수를 디렉티브로 랩핑해야 한다. 그리고 이 디렉티브는 아래 예제처럼 NG_VALIDATORS 토큰으로 등록해야 템플릿에 사용할 수 있다.


shared/identity-revealed.directive.ts
@Directive({
  selector: "[appIdentityRevealed]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: IdentityRevealedValidatorDirective,
      multi: true,
    },
  ],
})
export class IdentityRevealedValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    return identityRevealedValidator(control);
  }
}


디렉티브를 정의하고 나면 HTML 템플릿에서 디렉티브를 적용해야 한다. 이 때 유효성 검사 함수는 폼 계층 가장 바깥에서 동작하기 때문에 아래 코드처럼 form 태그에 적용하면 된다.


template/hero-form-template.component.html
<form #heroForm="ngForm" appIdentityRevealed></form>


사용자가 편한 UX를 제공하려면 폼 상태가 유효하지 않을 때 적절한 에러 메시지를 표시하면 된다.


template/hero-form-template.component.html
<div
  *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)"
  class="cross-validation-error-message alert"
>
  Name cannot match alter ego.
</div>


템플릿 코드는 템플릿 기반 폼과 반응형 폼에서 같다.

7. 비동기 유효성 검사 함수 정의하기

비동기 유효성 검사 함수는 AsyncValidationFn 인터페이스 AsyncValidator 인터페이스를 기반으로 구현한다. 비동기 유효성 검사 함수는 동기 유효성 검사 함수와 거의 비슷하지만 이런 점이 다르다.


  • validate() 함수는 Promise나 Observable을 반환해야 한다.
  • 옵저버블을 반환한다면 이 옵저버블은 반드시 종료되어야 한다. 종료되지 않는 옵저버블을 종료시키려면 first, last, take, takeUntil과 같은 연산자를 활용하면 된다.


비동기 유효성 검사 함수는 동기 유효성 검사가 모두 에러없이 끝난 후에만 실행된다. 이것은 비동기 유효성 검사 함수가 HTTP 요청과 같이 무거운 비동기 로직을 실행할 수 있기 때문에 상대적으로 가벼운 동기 로직으로 유효성 검사를 우선 검사하려는 의도로 설계된 것이다.


비동기 유효성 검사 로직이 시작되고 나면 폼 컨트롤이 pending 상태가 된다. 그래서 이 상태를 활용하면 폼 컨트롤에 유효성 검사가 동작하고 있다는 것을 시각적으로 표현할 수도 있다.


가장 일반적인 UI 패턴은 비동기 로직이 실행되고 있을 때 로딩 인디케이터를 표시하는 것이다. 템플릿 기반 폼에서는 이렇게 구현할 수 있다.


<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator />
<app-spinner *ngIf="model.pending"></app-spinner>


1) 커스텀 비동기 유효성 검사 함수 구현하기

아래 예제는 히어로의 별명이 이미 사용되고 있는지 검사하는 비동기 유효성 검사 함수를 구현한 것이다. 새로운 히어로가 나타나면 히어로 목록에 추가되고 은퇴한 히어로는 목록에서 제외되기 때문에, 히어로의 별명을 사용할 수 있는지는 DB를 비동기로 확인해야 한다.


UniqueAlterEgoValidatorAsyncValidator 인터페이스를 활용해서 이런 기능을 구현한 유효성 검사 함수이다.


@Injectable({ providedIn: "root" })
export class UniqueAlterEgoValidator implements AsyncValidator {
  constructor(private heroesService: HeroesService) {}

  validate(
    ctrl: AbstractControl
  ): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
      map((isTaken) => (isTaken ? { uniqueAlterEgo: true } : null)),
      catchError(() => of(null))
    );
  }
}


유효성 검사 함수의 생성자에 의존성으로 주입되는 HeroesService는 이렇게 정의된 인터페이스이다.


interface HeroesService {
  isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}


실제로 운영되는 애플리케이션이라면 HeroesService가 새로운 히어로 별명을 사용할 수 있는지 확인하기 위해 HTTP 요청을 보내는 식으로 동작할 것이다. 하지만 이 문서는 유효성 검사 함수에 대해서만 다루기 때문에 서비스를 어떻게 구현해야 하는지는 중요하지 않다. 지금은 HeroesService 인터페이스가 이렇다는 것만 확인하면 된다.


유효성 검사 로직이 시작되면 UniqueAlterEgoValidator가 실행되면서 현재 폼 값을 기준으로 HeroesService의 isAlterEgoTaken() 메서드가 실행된다. 이 시점에 폼 컨트롤은 pending 상태로 변경되며 validate() 메서드가 종료되면서 옵저버블 체인으로 새로운 데이터가 전달되기 전까지 유지된다.


isAlterEgoTaken() 메서드는 히어로의 별명이 현재 사용 중인지 검사하기 위해 HTTP 요청을 보내고 응답이 오면 Observable<boolean>을 반환한다. validate() 메서드는 폼 유효성 검사 형식에 맞게 이 응답을 boolean 타입으로 변환한다.


일반 유효성 검사 함수와 마찬가지로, 유효성 검사를 통과하면 null을 반환하며 유효성 검사를 통과하지 못하면 ValidationErros 객체를 반환한다. 그리고 이 유효성 검사 함수에는 예상치 못하게 발생하는 에러를 방지하기 위해 catchError 연산자가 사용되었다. isAlterEgoTaken()이 정상적으로 실행된 경우 외에, HTTP 요청이 실패한 경우에는 폼에 입력한 값이 유효하다고 간주하고 null을 반환한다. HTTP 요청이 실패한 경우에 유효성 검사를 통과하지 못한 것으로 처리하려면 null 대신 ValidationError 객체를 반환하면 된다.


시간이 지난 후에 옵저버블 체인이 종료되면 비동기 유효성 검사도 종료된다. 이 때 pending 플래그는 false 값이 되며 폼 유효성 검사 결과도 갱신된다.


2) 비동기 폼 유효성 검사 성능 최적화하기

기본적으로 모든 유효성 검사 함수는 폼 값이 변경될 때마다 매번 실행된다. 이 때 동기 유효성 검사 함수는 애플리케이션 성능에 큰 영향을 주는 경우가 별로 없다. 하지만 비동기 유효성 검사 함수는 폼 컨트롤의 유효성을 검사하기 위해 HTTP 요청을 보내는 것과 같은 동작을 한다. 그런데 매번 키가 입력될 때마다 HTTP 요청이 발생하면 백엔드 API에 큰 부하를 줄 수 있기 때문에 이런 상황은 피하는 것이 좋다.


updateOn 프로퍼티를 기본값인 change에서 submit이나 blur로 바꾸면 폼 유효성 검사 로직이 동작하는 시점을 미룰 수 있다.


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


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


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


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


8. 기본 HTML 폼 유효성 검사와 상호작용하기

기본적으로 Angular는 <form> 엘리먼트 안쪽에 novalidate 어트리뷰트를 추가해서 기본 HTML 폼 유효성 검사를 비활성화시키고, 프레임워크에 있는 유효성 검사 함수를 적용한다. 만약 Angular가 제공하는 유효성 검사 기능과 기본 유효성 검사 기능을 함께 사용하려면 ngNativeValidate 디렉티브를 지정해서 비활성화된 기본 HTML 폼 유효성 검사를 다시 활성화시키면 된다.


References