Angular의 반응형 폼(Reactive Forms)은 폼의 상태 변화를 명시적으로 관리하고, 데이터 모델을 직접 제어할 수 있는 강력한 방식을 제공합니다. 템플릿 기반 폼에 비해 확장성과 재사용성이 뛰어나며, 단위 테스트 작성이 용이하여 복잡한 비즈니스 로직이 포함된 대규모 프로젝트에 적합합니다.
1. 핵심 클래스 및 개념 이해
반응형 폼을 구성하는 네 가지 핵심 요소는 다음과 같습니다.
FormControl
개별 폼 컨트롤의 값과 유효성 상태를 관리하는 최소 단위입니다.
// 초기값 설정 및 비활성화 상태 정의
const userId: FormControl = new FormControl({ value: 'admin_01', disabled: true });
FormGroup
여러 개의 FormControl이나 다른 FormGroup, FormArray를 하나의 그룹으로 묶어 관리합니다. 전체 폼의 상태를 한눈에 파악할 때 사용합니다.
const profileForm = new FormGroup({
userName: new FormControl('홍길동'),
userAge: new FormControl(30)
});
FormArray
폼 컨트롤들의 배열을 관리합니다. 동적으로 추가되거나 삭제되는 리스트 형태의 폼(예: 테이블 행 추가)을 구현할 때 필수적입니다.
this.orderForm = this.fb.group({
items: this.fb.array([])
});
get itemEntries() {
return this.orderForm.get('items') as FormArray;
}
// 행 추가 로직
this.itemEntries.push(this.fb.group({
productName: [null],
quantity: [1]
}));
FormBuilder
FormControl이나 FormGroup 인스턴스를 수동으로 생성하는 번거로움을 줄여주는 서비스입니다. 가독성 높은 코드로 폼 구조를 설계할 수 있게 도와줍니다.
// FormBuilder를 사용한 간결한 선언
this.mainForm = this.fb.group({
staffName: ['', Validators.required],
position: [{ value: '사원', disabled: false }]
});
2. 폼 유효성 검사 (Validators)
유효성 검증기는 입력값이 올바른지 확인하고, 오류가 있을 경우 null이 아닌 에러 객체를 반환합니다.
- 동기 검증기: 값을 즉시 검사합니다 (예:
Validators.required). - 비동기 검증기: HTTP 요청 등을 통해 서버 데이터와 중복 여부를 확인할 때 사용하며, Promise나 Observable을 반환합니다.
커스텀 유효성 검사기 구현 예시
// 특정 길이를 엄격히 제한하는 검증기
customLengthValidator() {
return (control: AbstractControl): ValidationErrors | null => {
const isValid = control.value && control.value.length === 8;
return isValid ? null : { 'lengthMismatch': true };
};
}
3. 주요 메서드 및 속성
| 메서드/속성 | 설명 |
|---|---|
| patchValue() | 폼 모델의 일부 값만 업데이트할 때 사용합니다. |
| setValue() | 폼 모델 전체의 구조와 일치하는 값을 한꺼번에 설정해야 합니다. |
| reset() | 폼의 값을 초기화하고 pristine, untouched 상태로 되돌립니다. |
| updateValueAndValidity() | 값의 변화를 수동으로 반영하고 유효성 검사를 다시 실행합니다. |
| dirty / pristine | 사용자가 값을 변경했는지 여부를 나타내는 속성입니다. |
| touched / untouched | 사용자가 컨트롤에 포커스를 주었다가 벗어났는지 여부를 나타냅니다. |
4. 실무 응용 시나리오
시나리오: 조건부 필드 활성화 및 테이블 내 폼 배열 관리
계약 유형에 따라 시작일과 종료일 입력창을 활성화하고, 날짜의 선후 관계를 검증하는 복합적인 예제입니다.
TypeScript 로직:
this.contractForm = this.fb.group({
contractRows: this.fb.array([])
});
// 동적 행 추가 및 조건부 유효성 설정
addContractRow() {
const row = this.fb.group({
category: ['정규직', Validators.required],
beginDate: [{ value: null, disabled: true }],
endDate: [{ value: null, disabled: true }]
});
// 카테고리 변경 감지하여 날짜 필드 제어
row.get('category')?.valueChanges.subscribe(val => {
const begin = row.get('beginDate');
const end = row.get('endDate');
if (val === '계약직') {
begin?.enable();
end?.enable();
begin?.setValidators([Validators.required]);
} else {
begin?.disable();
end?.disable();
begin?.clearValidators();
}
begin?.updateValueAndValidity();
});
this.contractRows.push(row);
}
// 날짜 선후 관계 검증기
dateRangeValidator(control: AbstractControl): ValidationErrors | null {
const start = control.get('beginDate')?.value;
const end = control.get('endDate')?.value;
return start && end && new Date(start) > new Date(end) ? { 'invalidRange': true } : null;
}
HTML 템플릿:
<form [formGroup]="contractForm">
<table>
<tbody formArrayName="contractRows">
<tr *ngFor="let row of contractRows.controls; let i = index" [formGroupName]="i">
<td>
<select formControlName="category">
<option value="정규직">정규직</option>
<option value="계약직">계약직</option>
</select>
</td>
<td>
<input type="date" formControlName="beginDate">
</td>
<td>
<input type="date" formControlName="endDate">
<div *ngIf="row.hasError('invalidRange')">종료일은 시작일보다 빠를 수 없습니다.</div>
</td>
</tr>
</tbody>
</table>
</form>
반응형 폼을 사용하면 복잡한 데이터 구조에서도 비즈니스 로직을 HTML 템플릿이 아닌 TypeScript 코드 내에서 선언적으로 관리할 수 있습니다. 이는 코드의 가독성을 높일 뿐만 아니라, 동적인 폼 변화에 유연하게 대응할 수 있는 기반이 됩니다.