Skip to content

2. 반응형 폼


반응형 폼은 화면에 있는 입력 필드의 값이 변경될 때마다 원하는 로직이 실행되게 하는 모델-드리븐 방식의 폼이다. 이 문서에서는 폼 컨틀롤을 만드는 방법부터 시작해서 폼 컨트롤의 값을 갱신하는 방법, 폼 컨트롤 여러 개를 그룹으로 묶어서 한 번에 처리하는 방법, 폼 컨트롤의 유효성을 검사하는 방법, 동적으로 폼 컨트롤을 추가하고 제거하는 방법에 대해 안내한다.


Note


1. 사전지식

반응형 폼에 대해 알아보기 전에 이런 내용을 미리 알아두는 것이 좋다:


  • TypeScript 프로그래밍
  • Angular 개요 문서에서 설명하는 Angular 앱 설계 개념
  • 폼 소개 문서에서 설명하는 폼 구성 개념


2. 반응형 폼 개요

반응형 폼이란 원하는 시점에 명시적으로 폼에 접근해서 상태를 참조하는 방식을 이야기한다. 반응형 폼에서 폼 상태가 변경되면 새로운 상태를 반환하기 때문에 전체 폼 모델 중 어느 부분이 변경되었는지 추적할 수 있다. 반응형 폼은 옵저버블 스트림을 기반으로 동작하기 때문에, 폼에 입력되는 값도 스트림 형태로 전달된다.


반응형 폼을 사용하면 개발자가 의도한 대로만 데이터가 변경되며, 현재 상태를 쉽게 예측할 수 있기 때문에 테스트하기도 편하다. 데이터가 변경되는 것을 감지하는 쪽에서 값을 받아 반응하는 것도 쉽다.


반응형 폼은 여러 가지 면에서 템플릿 기반 폼과 다르다. 반응형 폼은 데이터 모델에 동기 방식으로 접근할 수 있기 때문에 동작을 예측하기 쉬우며, 옵저버블 연산자를 활용해서 조작할 수 있고, 옵저버블 스트림을 추적하는 방식으로 변화를 감지할 수도 있다.


반면에 템플릿 기반 폼은 템플릿 안에서만 동작하며 템플릿 안에 있는 디렉티브를 기반으로 동작하기 때문에 동작을 폼 모델에 직접 접근할 수 없으며 비동기 방식으로만 변화를 감지할 수 있다.


3. 기본 폼 컨트롤 추가하기

폼 컨트롤은 3단계를 거쳐 정의한다.


1] 앱에 반응형 폼 모듈을 로드한다. 이 모듈은 반응형 폼에서 사용하는 디렉티브를 제공하는 모듈이다.

2] 컴포넌트에 FormControl 인스턴스를 정의한다.

3] 템플릿에 FormControl을 등록한다.


그러면 컴포넌트 템플릿에서 폼이 동작하는 것을 확인할 수 있다.


아래 예제는 폼 컨트롤 하나를 정의하는 예제 코드이다. 이 예제 코드에서 사용자가 입력 필드에 이름을 입력하면 폼이 이벤트를 감지해서 현재 값을 화면에 표시한다.


반응형 폼 모듈 로드하기

반응형 폼 컨트롤을 사용하려면 @angular/forms 패키지가 제공하는 ReactiveFormsModule을 로드해서 NgModuleimports 배열에 추가하면 된다.


src/app/app.module.ts (일부)
import { ReactiveFormsModule } from "@angular/forms";

@NgModule({
  imports: [
    // 다른 모듈들...
    ReactiveFormsModule,
  ],
})
export class AppModule {}


FormControl 생성하기

Angular CLI 명령 ng generate를 실행하면 프로젝트에 컴포넌트를 추가할 수 있다.


ng generate compontn NameEditor


그리고 폼 컨트롤을 하나 추가하려면 이 컴포넌트 파일에 FormControl 클래스를 로드하고 클래스 프로퍼티로 FormControl을 선언하면 된다.


src/app/name-editor/name-editor.component.ts
import { Component } from "@angular/core";
import { FormControl } from "@angular/forms";

@Component({
  selector: "app-name-editor",
  templateUrl: "./name-editor.component.html",
  styleUrls: ["./name-editor.component.css"],
})
export class NameEditorComponent {
  name = new FormControl("");
}


그러면 FormControl의 생성자가 실행되면서 인자로 전달한 빈 문자열이 초기값으로 설정된다. 이렇게 클래스 프로퍼티에 폼 컨트롤을 선언하면 폼에서 입력되는 이벤트나 값, 유효성 겁사 결과를 컴포넌트 코드에서 간단하게 확인할 수 있다.


템플릿에 폼 컨트롤 연결하기

컴포넌트 클래스에 폼 컨트롤을 추가하고 나면, 컴포넌트 템플릿에서도 폼 컨트롤을 연결해야 이 폼 컨트롤을 사용할 수 있다. 그래서 FormControlDirective를 의미하는 formControl을 입력 필드에 지정하면 템플릿과 컴포넌트의 폼 컨트롤을 연결할 수 있다. 이 디렉티브는 ReactiveFormsModule에서 제공하는 디렉티브이다.


src/app/name-editor/name-editor.component.html
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name" />

Note

  • ReactiveFormsModule에서 제공하는 클래스나 디렉티브에 대해 좀 더 알아보려면 아래 반응형 폼 API 섹션을 참고한다.


이렇게 템플릿 바인딩 문법을 사용하면 템플릿에 있는 name 입력 필드가 폼 컨트롤과 연결된다. 이제 폼 컨트롤과 DOM 엘리먼트는 서로 연동되는데, 화면에서 값이 변경되면 모델값도 변경되고, 모델의 값이 변경되면 화면의 값도 변경된다.


컴포넌트 표시하기

이제 NameEditorComponent를 템플릿에 추가하면 다음과 같은 입력 필드가 화면에 표시된다.


src/app/app.component.html (NameEditorComponent)
<app-name-editor></app-name-editor>


001


1) 폼 컨트롤 값 표시하기

폼 컨트롤의 값을 표시하려면 다음과 같은 방법을 사용할 수 있다:


  • 폼 값이 변경된 것을 알려주는 valueChanges 옵저버블을 AsyncPipe 파이프와 함께 사용할 수 있다. 이 옵저버블은 컴포넌트 클래스에서 subscribe() 메서드로 구독해도 된다.
  • 폼 컨트롤의 현재 값을 표현하는 value 프로퍼티를 사용해도 된다.


다음 예제는 폼 컨트롤의 갑을 문자열 삽입으로 템플릿에 표시하는 예제 코드이다.


src/app/name-editor/name-editor.component.html (값 표시하기)
<p>Value: {{ name.value }}</p>


그리고 이 값은 폼 컨트롤 엘리먼트의 값이 변경될 때마다 자동으로 갱신된다.


반응형 폼에서는 각 폼 컨트롤의 프로퍼티나 메서드를 사용해서 폼 컨트롤이 제공하는 정보에 접근할 수 있다. 프로퍼티와 메서드는 AbstractControl 클래스에 정의되어 있으며, 폼 컨트롤의 값을 확인하는 것외에도 폼 컨트롤의 상태를 확인하거나 유효성을 검사하는 용도로도 사용할 수 있다.


2) 폼 컨트롤 값 변경하기

반응형 폼에서 제공하는 메서드를 사용하면 사용자의 동작 없이도 폼 컨트롤의 값을 컴포넌트 코드에서 변경할 수 있다. 이 중 setValue() 메서드를 사용하면 폼 컨트롤의 값을 변경할 수 있으며, 이 때 폼 컨트롤의 구조에 맞지 않는 데이터가 전달되는 유효성 검사도 수행한다. 그래서 백엔드 API나 서비스에서 데이터를 가져온 후에 setValue() 메서드를 사용하면 현재 폼 컨트롤의 값 전체를 한 번에 변경할 수 있다.


다음 예제 코드는 setValue() 메서드를 사용해서 폼 컨트롤의 값을 Nancy로 변경하는 예제 코드이다.


src/app/name-editor/name-editor.component.ts (값 변경하기)
updateName() {
  this.name.setValue('Nancy');
}


이 메서드를 실행할 수 있도록 템플릿을 수정한다. 다음과 같이 구현한 후에 Update Name 버튼을 클릭하면, 폼 컨트롤 엘리먼트에 입력된 값에 관계없이 지정된 값으로 변경된다.


src/app/name-editor/name-editor.component.html (값 변경하기)
<button (click)="updateName()">Update Name</button>


그리고 이 때 폼 컨트롤의 값은 폼 모델과 연결되어 있기 때문에, 버튼을 눌러서 폼 컨트롤의 값이 변경되면 컴포넌트 클래스에 있는 폼 모델 값도 함께 변경된다.


Note

  • 이 예제에서는 폼 컨트롤 하나를 대상으로 setValue() 메서드를 사용했지만, 이 메서드의 인자로 전달하는 타입에 따라 폼 그룹이나 폼 배열에도 사용할 수 있다.


4. 연관된 폼 컨트롤 묶기

폼은 보통 몇 개 폼이 연관되는 방식으로 구성된다. 반응형 폼을 사용하면 두 가지 방법으로 폼 컨트롤을 묶어서 관리할 수 있다.


  • 관련된 폼 컨트롤을 그룹으로 정의할 수 있다. 이번 섹션에서는 폼 그룹에 대해 이야기 해보자. 폼이 복잡하다면 중첩된 폼 그룹을 구성할 수도 있다.
  • 폼이 동적으로 변경된다면 폼 배열을 정의할 수 있다. 그리고 이 경우에도 폼이 복잡하다면 폼 배열을 중첩할 수도 있다.


입력 필드 하나를 의미하는 폼 컨트롤 여러 개는 함께 묶어서 폼 그룹으로 처리할 수 있다. 실제로 폼을 구성할 때도 개별 폼 컨트롤을 처리하지 않고 폼 그룹으로 한 번에 처리하는 것이 효율적이며, 폼 그룹에 포함된 폼 컨트롤은 이름으로 구분한다. 폼 컨트롤 여러 개를 폼 그룹으로 한 번에 처리하는 방법을 알아보자.


다음 명령을 실행해서 ProfileEditor 컴포넌트를 생성하고 이 컴포넌트에 @angular/forms 패키지에서 제공하는 FormGroup 클래스와 FormControl 클래스를 로드한다.


ng generate component ProfileEditor


src/app/profile-editor/profile-editor.component.ts (심볼 로드하기)
import { FormGroup, FormControl } from "@angular/forms";


폼 그룹은 다음과 같은 단계로 추가한다.


1] FormGroup 인스턴스를 추가한다.

2] FormGroup 모델과 화면을 연결한다.

3] 폼 데이터를 저장한다.


FormGroup 인스턴스 생성하기

컴포넌트 클래스에 profileForm 프로퍼티를 선언하고 이 프로퍼티에 폼 그룹 인스턴스를 생성해서 할당한다. 폼 그룹을 생성하려면 폼 컨트롤과 키를 매칭시켜서 생성자에 인자로 전달하면 된다.


그러면 이 폼 안에 firstName 인스턴스와 lastName 인스턴스가 선언된다.


src/app/profile-editor/profile-editor.component.ts (폼 그룹)
import { Component } from "@angular/core";
import { FormGroup, FormControl } from "@angular/forms";

@Component({
  selector: "app-profile-editor",
  templateUrl: "./profile-editor.component.html",
  styleUrls: ["./profile-editor.component.css"],
})
export class ProfileEditorComponent {
  profileForm = new FormGroup({
    firstName: new FormControl(""),
    lastName: new FormControl(""),
  });
}


이제 폼 컨트롤을 그룹으로 묶었다. 폼 컨트롤을 그룹으로 묶으면 폼 컨트롤의 값을 번거롭게 하나씩 참조할 필요 없이 폼 그룹에서 제공하는 모델을 사용할 수 있다. 폼 그룹 인스턴스도 폼 컨트롤 인스턴스와 비슷하게 valueuntouched 프로퍼티, setValue() 메서드를 제공한다.


FormGroup 모델과 화면 연결하기

폼 그룹도 각각의 폼 컨트롤의 상태와 변화를 추적한다. 그래서 폼 컨트롤 중 하나의 상태나 값이 변경되면 이 폼 컨트롤을 감싸고 있는 부모 폼 컨트롤도 새로운 상태로 변경되고 값도 변경된다. 이 폼 그룹이 관리하는 모델은 각각의 폼 컨트롤로 구성된다. 그래서 이 모델을 템플릿에 연결하면 폼 컨트롤을 처리했던 것과 비슷하게 폼 그룹을 처리할 수 있다.


src/app/profile-editor/profile-editor.component.html (폼 그룹 템플릿)
<form [formGroup]="profileForm">
  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName" />

  <label for="last-name">Last Name: </label>
  <input id="last-name" type="text" formControlName="lastName" />
</form>


폼 그룹은 폼 컨트롤의 묶음을 표현하며, 이 예제 코드에서는 FormGroup 타입의 profileFormform 엘리먼트와 바인딩되어 폼 모델과 입력 필드를 연결한다. 이 코드에 사용된 formControlNameFormControlName 디렉티브의 셀렉터이며, 개별 입력 필드를 FormGroup 안에 있는 폼 컨트롤과 연결하는 동작을 수행한다. 그러면 각각의 폼 컨트롤에 연결된 엘리먼트는 해당 폼 컨트롤이 관리하며, 폼 그룹 인스턴스를 통해 전체 모델에 접근할 수도 있다.


데이터 저장하기

이 예제에서는 ProfileEditor 컴포넌트가 사용자의 입력을 받기만 하지만, 실제 애플리케이션이라면 폼에 입력된 값을 컴포넌트 외부로 전달해서 어떤 동작을 수행해야 할 것이다. FormGroup 디렉티브는 form 엘리먼트에서 발생하는 submit 이벤트를 감지하며, submit 이벤트가 발생했을 때 폼에 바인딩된 콜백 함수를 실행하기 위해 ngSubmit 이벤트를 새로 발생시킨다.


다음과 같이 form 태그에 ngSubmit 이벤트를 바인딩해서 onSubmit() 콜백 메서드가 실행되게 작성해 보자.


src/app/profile-editor/profile-editor.component.html (폼 제출 이벤트 처리)
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()"></form>


그러면 ProfileEditor 컴포넌트에 정의된 onSubmit() 메서드가 profileForm의 현재 값을 읽어서 특정 동작을 실행하게 할 수 있다. 이 때 컴포넌트와 폼의 캡슐화를 보장하기 위해 EventEmitter를 사용해서 폼 값을 컴포넌트 외부로 전달하는 것이 좋다. 다음 예제는 이렇게 가져온 폼 값을 console.warn 함수로 브라우저 콘솔에 출력하는 예제이다.


src/app/profile-editor/profile-editor.component.ts (폼 제출 메소드)
onSubmit() {
  // TODO: EventEmitter로 폼 내용 보내기
  console.warn(this.profileForm.value);
}


submit 이벤트는 form 태그에서 발생하는 네이티브 DOM 이벤트이다. 이 이벤트는 submit 타입의 버튼을 클릭했을 때 발생하며, 사용자가 폼 내용을 입력하고 엔터 키를 눌렀을 때도 발생한다.


이 이벤트를 발생시키기 위해 폼 아래에 button 엘리먼트를 다음과 같이 추가한다.


src/app/profile-editor/profile-editor.component.html (폼 제출 버튼)
<p>Complete the form to enable button.</p>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>


Note

  • 예제로 구현한 버튼은 profileForm의 유효성 검사 결과가 유효하지 않을 때 disabled 어트리뷰트를 지정하도록 바인딩되어 있다.
  • 아직 유효성 검사 로직은 아무것도 적용하지 않았기 때문에 버튼은 항상 활성화되어 있을 것이다.


컴포넌트 표시하기

ProfileEditor 컴포넌트를 화면에 표시하기 위해 AppComponent 템플릿에 다음과 같이 컴포넌트를 추가한다.


src/app/app.component.html (개인정보 수정 화면)
<app-profile-editor></app-profile-editor>


그러면 ProfileEditor 컴포넌트가 화면에 표시되며, 이 컴포넌트에 정의된 폼 그룹 안에 있는 firstNamelastName 폼 컨트롤도 화면에 함께 표시된다.


002


1) 중첩 폼 그룹(Neting form groups) 만들기

폼 그룹은 폼 그룹 안에 있는 개별 폼 컨트롤 인스턴스에 직접 접근할 수 있으며 다른 폼 그룹도 자식 폼 컨트롤로 가질 수 있다. 그래서 폼 그룹을 논리적으로 구성하면 폼 전체를 한 번에 관리하기도 편하다.


복잡한 폼을 구성할 때는 관련된 영역끼리 작게 묶어서 관리하는 것이 편하다. 결국 관련된 영역을 작게 묶는 것이 폼 그룹의 역할이다.


복잡한 폼을 구성할 때는 이렇게 구현해 보자.


1] 중첩된 그룹을 정의한다.

2] 템플릿에서도 중첩된 그룹을 묶어서 관리한다.


데이터들 중에는 같은 그룹으로 묶는 것이 자연스러운 경우가 있다. 사용자의 이름과 주소가 그런 경우에 해당된다.


중첩 그룹 만들기

profileForm 폼 안에 중첩된 폼을 만들기 위해 address 그룹을 이렇게 구성해 보자.


src/app/profile-editor/profile-editor.component.ts (중첩된 폼 그룹)
import { Component } from "@angular/core";
import { FormGroup, FormControl } from "@angular/forms";

@Component({
  selector: "app-profile-editor",
  templateUrl: "./profile-editor.component.html",
  styleUrls: ["./profile-editor.component.css"],
})
export class ProfileEditorComponent {
  profileForm = new FormGroup({
    firstName: new FormControl(""),
    lastName: new FormControl(""),
    address: new FormGroup({
      street: new FormControl(""),
      city: new FormControl(""),
      state: new FormControl(""),
      zip: new FormControl(""),
    }),
  });
}


이 예제에서 firstNamelastName은 폼 컨트롤이지만 addressstreetcity, state, zip으로 이루어진 폼 그룹이다. addressFormGroup은 그 자체로도 폼 그룹이지만 부모 폼 그룹인 profileForm의 자식 폼 그룹이며, 기존에 사용하던 폼 상태와 값이 변경되는 상황도 이전과 마찬가지로 참조할 수 있다. 자식 객체에서 발생하는 상태/값 변화는 부모 폼 그룹으로 전파되기 때문에, 전체 폼 그룹을 참조해도 개별 값이 변경되는 것을 감지할 수 있다.


템플릿에서 중첩 폼 묶기

컴포넌트 클래스의 모델을 수정하고 나면, 템플릿에도 이 내용과 관련된 폼 그룹 인스턴스와 입력 엘리먼트를 수정해야 한다.


ProfileEditor 템플릿에 다음과 같이 address 폼 그룹을 추가한다.


src/app/profile-editor/profile-editor.component.html (중첩된 폼 그룹 템플릿)
<div formGroupName="address">
  <h2>Address</h2>

  <label for="street">Street: </label>
  <input id="street" type="text" formControlName="street" />

  <label for="city">City: </label>
  <input id="city" type="text" formControlName="city" />

  <label for="state">State: </label>
  <input id="state" type="text" formControlName="state" />

  <label for="zip">Zip Code: </label>
  <input id="zip" type="text" formControlName="zip" />
</div>


그러면 ProfileEditor 폼에서 address 부분이 다시 한 번 그룹으로 묶여 다음과 같이 화면에 표시된다.


003


Note

  • 폼 그룹의 내용을 컴포넌트 템플릿에 표시하려면 value 프로퍼티에 JsonPipe를 사용하면 된다.


2) 데이터 모델 일부만 갱신하기

폼 그룹을 처리하면서 매번 전체 폼 값을 한 번에 갱신하는 것은 번거롭기 때문에, 일부 폼 컨트롤의 값만 변경하는 것이 더 편한 경우도 있다. 이번에는 폼 모델의 일부 값만 변경하는 방법을 알아보자.


모델 값은 두 가지 방법으로 변경할 수 있다.


  • 폼 컨트롤에 setValue() 메서드를 사용할 수 있다. setValue() 메서드는 폼 그룹의 구조와 정확하게 일치하는 객체를 받았을 때만 폼 그룹 전체값을 한 번에 갱신한다.
  • 폼 모델의 특정 부분만 변경하려면 patchValue() 메서드를 사용할 수 있다.


setValue() 메서드를 사용하면 인자의 형태가 복잡한 폼 구조에 맞을 때만 값을 변경할 수 있다. 반면에 patchValue() 메서드는 폼 그룹의 구조와 다른 인자가 전달되어도 에러를 발생하지 않는다.


아래 예제는 ProfileEditorComponent에 선언된 updateProfile 메서드를 사용해서 사용자의 이름과 주소를 변경하는 예제 코드이다.


src/app/profile-editor/profile-editor.component.ts (일부 값 변경하기)
updateProfile() {
  this.profileForm.patchValue({
    firstName: 'Nancy',
    address: {
      street: '123 Drew Street'
    }
  });
}


그리고 이 메서드를 실행하는 버튼을 템플릿에 다음과 같이 추가한다.


src/app/profile-editor/profile-editor.component.html (값 변경하기)
<button (click)="updateProfile()">Update Profile</button>


이제 사용자가 이 버튼을 클릭하면 profileForm 모델의 firstName 값과 street 값이 새로운 값으로 변경된다. 이 때 폼 그룹의 구조에 맞추기 위해 street 필드의 값은 address 객체 안에 지정했다. patchValue() 메서드는 전달된 인자 중에서 폼 모델의 구조에 맞는 값만 변경한다.


5. FormBuilder로 폼 컨트롤 생성하기

폼 컨트롤을 매번 직접 생성해야 하는데, 이런 폼이 여러 개라면 귀찮은 반복작업이 될 것이다. 이 때 FormBuilder 서비스를 사용하면 좀 더 편하게 폼을 구성할 수 있다.


FormBuilder 서비스는 이렇게 사용한다.


1] FormBuilder 클래스를 로드한다.

2] FormBuilder 서비스를 의존성으로 주입한다.

3] FormBuilder 서비스로 폼을 구성한다.


아래 예제를 보면서 ProfileEditor 컴포넌트에서 폼을 정의하는 부분을 폼 빌더 서비스를 사용하는 방식으로 리팩토링하려면 어떻게 해야 하는지 확인해 보자.


FormBuilder 클래스 로드하기

먼저 @angular/forms 패키지에서 FormBuilder 클래스를 로드한다.


src/app/profile-editor/profile-editor.component.ts (심볼 로드하기)
import { FormBuilder } from "@angular/forms";


FormBuilder 서비스를 의존성으로 주입하기

폼 빌더는 ReactiveFormsModule에서 제공하며, 의존성으로 주입할 수 있도록 서비스로 정의되어 있다. 다음과 같이 컴포넌트 생성자에 FormBuilder 서비스를 주입한다.


src/app/profile-editor/profile-editor.component.ts (생성자)
constructor(private fb: FormBuilder) { }


폼 컨트롤 생성하기

FormBuilder 서비스는 control(), group(), array() 메서드를 제공하는데, 이 메서드는 각각 폼 컨트롤, 폼 그룹, 폼 배열 인스턴스를 생성해서 반환하는 팩토리 메서드이다.


group() 메서드를 사용해서 profileForm 컨트롤을 만들어 보자.


src/app/profile-editor/profile-editor.component.ts (폼 빌더)
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";

@Component({
  selector: "app-profile-editor",
  templateUrl: "./profile-editor.component.html",
  styleUrls: ["./profile-editor.component.css"],
})
export class ProfileEditorComponent {
  profileForm = this.fb.group({
    firstName: [""],
    lastName: [""],
    address: this.fb.group({
      street: [""],
      city: [""],
      state: [""],
      zip: [""],
    }),
  });

  constructor(private fb: FormBuilder) {}
}


위 코드에서 group() 메서드에 전달하는 객체의 구조는 폼 모델의 구조와 같다. 그리고 프로퍼티 값으로는 배열을 전달하는데, 폼 컨트롤의 초기값은 배열의 첫 번째 항목으로 전달한다.


Note

  • 폼 컨트롤의 초기값만 지정한다면 배열을 사용하지 않아도 된다.
  • 하지만 폼 컨트롤에 유효성 검사기를 지정하려면 프로퍼티 값에 배열을 사용해야 한다.
  • 이 때 동기 유효성 검사기는 두 번째 인자로, 비동기 유효성 검사기는 세 번째 인자로 지정한다.


폼 컨트롤을 직접 생성하는 방식과 폼 빌더를 사용하는 방식이 어떻게 다른지 비교해 보자.


profileForm = new FormGroup({
  firstName: new FormControl(""),
  lastName: new FormControl(""),
  address: new FormGroup({
    street: new FormControl(""),
    city: new FormControl(""),
    state: new FormControl(""),
    zip: new FormControl(""),
  }),
});
profileForm = this.fb.group({
  firstName: [""],
  lastName: [""],
  address: this.fb.group({
    street: [""],
    city: [""],
    state: [""],
    zip: [""],
  }),
});


6. 폼 유효성 검사하기

폼 유효성 검사(Form validation)는 사용자가 필수 항목을 모두 입력했는지, 입력한 내용이 올바른지 검증하는 것이다. 이 섹션에서는 폼 컨트롤에 유효성 검사기를 하나 붙여서 폼 상태를 화면에 표시해 보자.


폼 유효성 검사기는 다음과 같은 과정으로 적용한다.


1] 폼 컴포넌트에 유효성 검사 함수를 로드한다.

2] 원하는 폼 필드에 유효성 검사기를 적용한다.

3] 폼 유효성 검사 결과에 따라 적절한 처리 로직을 추가한다.


가장 일반적인 유효성 검사는 필드를 필수 항목으로 만드는 것이다. 아래 예제를 보면서 firstName 폼 컨트롤에 필수항목 유효성 검사기를 추가하고 유효성 검사 결과를 화면에 표시하는 방법에 대해 알아보자.


유효성 검사 함수 로드하기

반응형 폼 모듈은 다양한 유효성 검사 함수도 함께 제공한다. 이 함수들은 폼 컨트롤 인스턴스를 인자로 받으며, 유효성 검사에 실패한 경우에 에러 객체를 반환하고 유효성 검사를 통과하면 null을 반환한다.


먼저, @angular/forms 패키지에서 Validators 클래스를 로드한다.


src/app/profile-editor/profile-editor.component.ts (심볼 로드하기)
import { Validators } from "@angular/forms";


필수 항목 지정하기

ProfileEditor 컴포넌트에서 firstName 폼 컨트롤을 생성하던 배열의 두 번째 항목으로 Validators.required를 추가한다.


src/app/profile-editor/profile-editor.component.ts (required 유효성 검사기)
profileForm = this.fb.group({
  firstName: ["", Validators.required],
  lastName: [""],
  address: this.fb.group({
    street: [""],
    city: [""],
    state: [""],
    zip: [""],
  }),
});


폼 상태 표시하기

폼 컨트롤을 필수항목으로 지정하면 이 폼 컨트롤의 초기 상태는 유효하지 않은 것이 된다. 폼 컨트롤의 상태는 부모 폼 그룹 엘리먼트로 전파되며, 결과적으로 폼 그룹 전체가 유효성 검사를 통과하지 못한 것으로 처리된다. 그러면 폼 그룹 인스턴스의 status 프로퍼티를 참조해서 이 폼의 상태를 확인할 수 있다.


profileForm의 상태를 표시하려면 다음과 같이 템플릿을 작성한다.


src/app/profile-editor/profile-editor.component.html (상태 표시하기)
<p>Form Status: {{ profileForm.status }}</p>


004


firstName 폼 컨트롤이 필수항목으로 지정되었지만 유효성 검사를 통과하지 못했기 때문에 profileForm 전체가 유효하지 않은 것으로 판단되고, Submit 버튼도 비활성화된다. firstName 입력 필드에 데이터를 입력하면 폼 전체의 유효성 검사가 통과되면서, Submit 버튼도 활성화될 것이다.


7. 동적 폼 구성하기

폼 컨트롤에 이름이 없고 폼 컨트롤 개수가 변한다면 FormGroup 대신 FormArray를 사용하는 방법도 고려해볼만 하다. 폼 그룹 인스턴스와 마찬가지로 폼 배열도 폼 컨트롤은 동적으로 추가하거나 제거할 수 있으며, 자식 폼 컨트롤을 모아 값이나 유효성 검사 결과를 한 번에 참조할 수도 있다. 폼 배열은 이름을 지정하는 방식으로 정의하지 않는데, 자식 폼 컨트롤의 개수가 몇 개인지 몰라도 된다는 점에서 이 방식이 유리하다.


동적 폼은 이런 순서로 구현한다.


1] FormArray 클래스를 로드한다.

2] FormArray 컨트롤을 정의한다.

3] 게터 메서드를 사용해서 FormArray 컨트롤에 접근한다.

4] 템플릿에 폼 배열을 연결한다.


폼 배열을 어떻게 활용할 수 있는지 예제를 보면서 확인해 보자.


FormArray 클래스 로드하기

먼저 @angular/forms 패키지에서 FormArray 클래스를 로드한다. 이것은 컴포넌트 클래스에서 타입 참조를 위해 로드한 것이며, FormBuilder는 이 과정이 없어도 내부적으로 FormArray 타입을 지원한다.


src/app/profile-editor/profile-editor.component.ts (심볼 로드하기)
import { FormArray } from "@angular/forms";


FormArray 정의하기

폼 배열은 배열로 초기화하며, 이 때 배열의 길이는 어떠한 것이라도 가능하다. profileFormaliases 프로퍼티를 추가하고, 이 프로퍼티를 폼 배열로 정의해 보자.


FormBuilder를 사용한다면 array() 메서드로 폼 배열을 정의할 수 있으며, FormBuilder.control() 메서드를 사용해서 기본 폼 컨트롤을 생성한다.


src/app/profile-editor/profile-editor.component.ts (aliases 폼 배열)
profileForm = this.fb.group({
  firstName: ["", Validators.required],
  lastName: [""],
  address: this.fb.group({
    street: [""],
    city: [""],
    state: [""],
    zip: [""],
  }),
  aliases: this.fb.array([this.fb.control("")]),
});


그러면 aliases 폼 컨트롤은 FormGroup 안에 폼 배열로 선언되며, 이후에 동적으로 추가되기 전까지는 폼 컨트롤 1개로 구성된다.


FormArray에 접근하기

폼 배열을 사용한다면 폼 배열 안의 각 인스턴스를 profileForm.get() 메서드로 참조하는 것보다 게터(getter) 함수를 사용하는 것이 편하다. 게터 함수를 사용하면 폼 배열 안에 있는 폼 컨트롤의 개수에 관계없이 간단하게 폼 컨트롤의 값을 참조할 수 있고, 반복문을 작성할 때도 편하다.


게터 함수는 다음과 같이 정의한다.


src/app/profile-editor/profile-editor.component.ts (aliases 게터)
get aliases() {
  return this.profileForm.get('aliases') as FormArray;
}


Note

  • 폼 컨트롤을 참조하기 위해 get() 메서드를 사용하면 AbstractControl 타입으로 폼 컨트롤을 받는다.
  • 공통 메서드를 사용한다면 이대로 활용해도 되지만, FormArray에 해당되는 메서드를 사용하려면 타입 캐스팅해야 한다.


이번에는 FormArray에 동적으로 폼 컨트롤을 추가하는 함수를 정의해 보자. 폼 배열에 새로운 폼 컨트롤을 추가할 때는 FormArray.push() 메서드를 사용한다.


src/app/profile-editor/profile-editor.component.ts (항목 추가)
addAlias() {
  this.aliases.push(this.fb.control(''));
}


이제 이렇게 만든 폼 배열을 템플릿에 표시해 보자.


템플릿에 폼 배열 표시하기

사용자가 폼 모델에 aliases 값을 입력하려면 이 폼 컨트롤을 템플릿에 추가해야 한다. 이전 예제에서 FormGroupNameDirectiveformGroupName 어트리뷰트로 바인딩했던 것과 비슷하게, FormArrayNameDirective가 제공하는 formArrayName 어트리뷰트를 사용해서 FormArray를 템플릿에 바인딩하면 된다.


formGroupName <div> 엘리먼트 뒤에 다음과 같은 템플릿을 추가한다.


src/app/profile-editor/profile-editor.component.html (aliases 폼 배열의 템플릿)
<div formArrayName="aliases">
  <h2>Aliases</h2>
  <button (click)="addAlias()" type="button">+ Add another alias</button>

  <div *ngFor="let alias of aliases.controls; let i=index">
    <!-- alias 폼 배열이 반복되는 부분 -->
    <label for="alias-{{ i }}">Alias:</label>
    <input id="alias-{{ i }}" type="text" [formControlName]="i" />
  </div>
</div>


*ngFor 디렉티브는 폼 배열 인스턴스의 각 폼 컨트롤 인스턴스를 순회한다. 폼 배열 안에 있는 항목은 이름이 없기 때문에 인덱스를 변수 i에 활용해서 formControlName으로 바인딩했다.


005


이제는 폼 배열의 개수가 변하더라도 각각의 폼 컨트롤은 인덱스로 관리된다. 이 폼 배열이나 전체 폼 그룹의 값, 상태를 확인할 때도 같은 인덱스를 활용할 수 있다.


별칭 추가하기

이 폼의 Alias 필드는 기본적으로 폼 컨트롤이 하나 존재한다. 그리고 Add Alias 버튼을 누르면 폼 컨트롤이 추가되며, 이렇게 추가된 폼 컨트롤의 유효성 상태도 템플릿 아래에 추가했던 Form Value 부분을 통해 확인할 수 있다.


Note

  • aliases 필드는 폼 컨트롤 단위로 추가할 수도 있지만 폼 그룹 단위로 추가할 수도 있다.
  • 폼 컨트롤을 정의하고 활용하는 방법은 같다.


8. 반응형 폼 API

아래 표는 반응형 폼을 구현할 때 자주 사용하는 클래스와 서비스 목록을 나열한 것이다.


클래스 설명
AbstractControl 폼 컨트롤을 표현하는 FormControl, FormGroup, FormArray의 추상 클래스이다. 폼 컨트롤의 공통 기능과 프로퍼티를 정의한다.
FormControl 개별 폼 컨트롤의 값과 유효성 검사 상태를 관리하는 클래스이다. 이 클래스는 HTML 문서의 <input>이나 <select> 엘리먼트와 연동된다.
FormGroup 연관된 AbstractControl 인스턴스를 그룹으로 관리할 때 사용하는 클래스이다. 이 그룹은 자식 폼 컨트롤을 프로퍼티로 관리하며, 그룹에 접근해도 자식 폼 컨트롤의 값이나 유효성 검사 상태도 확인할 수 있다. 컴포넌트의 최상위 폼도 FormGroup이다.
FormArray AbastractControl을 배열 형태로 관리할 때 사용하는 클래스이다.
FormBuilder 폼 컨트롤 인스턴스를 간편하게 만들 때 사용하는 서비스이다.
디렉티브 설명
FormControlDirective 개별 FormControl 인스턴스와 폼 컨트롤 엘리먼트를 연결한다.
FormControlName 이름을 기준으로 FormGroup 안에 있는 FormControl을 연결한다.
FormGroupDirective FormGroup 인스턴스를 DOM 엘리먼트와 연결한다.
FormGroupName 중첩된 FormGroup 인스턴스를 DOM 엘리먼트와 연결한다.
FormArrayName FormArray 인스턴스를 DOM 엘리먼트와 연결한다.

References