Angular Reactive Form을 활용한 동적 데이터 관리 및 유효성 검사 기법

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 코드 내에서 선언적으로 관리할 수 있습니다. 이는 코드의 가독성을 높일 뿐만 아니라, 동적인 폼 변화에 유연하게 대응할 수 있는 기반이 됩니다.

태그: Angular ReactiveForms FormControl FormArray FormBuilder

5월 25일 12:29에 게시됨