포스트

[Angular 마스터하기] Day 12 - Reactive Forms, 복잡한 폼 마스터하기

[Angular 마스터하기] Day 12 - Reactive Forms, 복잡한 폼 마스터하기

이제와서 시작하는 Angular 마스터하기 - Day 12 “더 강력하고 유연한 Reactive Forms를 마스터하세요! 🎯”

오늘 배울 내용

  • Reactive Forms vs Template-driven Forms
  • FormGroup, FormControl
  • FormBuilder 사용
  • 커스텀 유효성 검사
  • 동적 폼

1. Reactive Forms 시작하기

ReactiveFormsModule 임포트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <input formControlName="email" placeholder="이메일">
      <input formControlName="password" type="password" placeholder="비밀번호">
      <button type="submit" [disabled]="loginForm.invalid">로그인</button>
    </form>
  `
})
export class ReactiveFormComponent {
  loginForm = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [Validators.required, Validators.minLength(8)])
  });

  onSubmit() {
    if (this.loginForm.valid) {
      console.log(this.loginForm.value);
    }
  }
}

FormBuilder로 간단하게

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-form-builder-example',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <input formControlName="name" placeholder="이름">
      <input formControlName="email" placeholder="이메일">
      <input formControlName="age" type="number" placeholder="나이">

      <button type="submit" [disabled]="userForm.invalid">제출</button>

      <div class="debug">
        <p>폼 값: </p>
        <p>유효성: </p>
      </div>
    </form>
  `
})
export class FormBuilderExampleComponent {
  private fb = inject(FormBuilder);

  userForm = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    age: [0, [Validators.required, Validators.min(1), Validators.max(120)]]
  });

  onSubmit() {
    console.log(this.userForm.value);
  }
}

2. 유효성 검사와 에러 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Component({
  selector: 'app-validation-example',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <form [formGroup]="form">
      <div class="form-group">
        <label>이메일</label>
        <input formControlName="email" placeholder="example@email.com">

        @if (email.invalid && email.touched) {
          <div class="error">
            @if (email.errors?.['required']) {
              <span>이메일은 필수입니다</span>
            }
            @if (email.errors?.['email']) {
              <span>올바른 이메일 형식이 아닙니다</span>
            }
          </div>
        }
      </div>

      <div class="form-group">
        <label>비밀번호</label>
        <input formControlName="password" type="password">

        @if (password.invalid && password.touched) {
          <div class="error">
            @if (password.errors?.['required']) {
              <span>비밀번호는 필수입니다</span>
            }
            @if (password.errors?.['minlength']) {
              <span>최소 {{ password.errors?.['minlength'].requiredLength }}자 필요</span>
            }
          </div>
        }
      </div>
    </form>
  `,

  styles: [`
    .error {
      color: #f44336;
      font-size: 0.9em;
      margin-top: 5px;
    }
  `]
})
export class ValidationExampleComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]]
  });

  // FormControl에 쉽게 접근
  get email() {
    return this.form.get('email')!;
  }

  get password() {
    return this.form.get('password')!;
  }
}

3. 커스텀 Validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// 비밀번호 일치 검사
export function passwordMatchValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const password = control.get('password');
    const confirmPassword = control.get('confirmPassword');

    if (!password || !confirmPassword) {
      return null;
    }

    return password.value === confirmPassword.value ? null : { passwordMismatch: true };
  };
}

// 사용 예
@Component({
  template: `
    <form [formGroup]="registerForm">
      <input formControlName="password" type="password">
      <input formControlName="confirmPassword" type="password">

      @if (registerForm.errors?.['passwordMismatch'] && registerForm.touched) {
        <div class="error">비밀번호가 일치하지 않습니다</div>
      }
    </form>
  `
})
export class RegisterComponent {
  private fb = inject(FormBuilder);

  registerForm = this.fb.group({
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirmPassword: ['', Validators.required]
  }, { validators: passwordMatchValidator() });
}

4. 동적 폼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { FormArray } from '@angular/forms';

@Component({
  selector: 'app-dynamic-form',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <form [formGroup]="form">
      <h3>연락처 목록</h3>

      <div formArrayName="contacts">
        @for (contact of contacts.controls; track $index; let i = $index) {
          <div [formGroupName]="i" class="contact-item">
            <input formControlName="name" placeholder="이름">
            <input formControlName="phone" placeholder="전화번호">
            <button type="button" (click)="removeContact(i)">삭제</button>
          </div>
        }
      </div>

      <button type="button" (click)="addContact()">연락처 추가</button>
    </form>
  `
})
export class DynamicFormComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    contacts: this.fb.array([])
  });

  get contacts() {
    return this.form.get('contacts') as FormArray;
  }

  addContact() {
    const contactForm = this.fb.group({
      name: ['', Validators.required],
      phone: ['', Validators.required]
    });
    this.contacts.push(contactForm);
  }

  removeContact(index: number) {
    this.contacts.removeAt(index);
  }
}

📝 정리

Template-driven vs Reactive Forms

항목 Template-driven Reactive Forms
설정 위치 템플릿 컴포넌트
데이터 모델 양방향 바인딩 명시적 관리
유효성 검사 디렉티브 함수
테스트 어려움 쉬움
복잡한 폼 어려움 적합

체크리스트

  • Reactive Forms를 만들 수 있나요?
  • FormBuilder를 사용할 수 있나요?
  • 커스텀 Validator를 만들 수 있나요?
  • 동적 폼을 만들 수 있나요?

📚 다음 학습

다음 시간에는 파이프를 배웁니다!


“Reactive Forms로 더 강력한 폼을 만드세요!” 🎯

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.