폼은 사용자가 로그인할 때, 개인정보를 수정할 때, 추가 정보를 입력하는 등 데이터 입력이 필요한 작업에 다양하게 사용됩니다.
Angular는 반응형 폼과 템플릿 기반 폼을 기본으로 지원합니다.
두 방식 모두 뷰에서 사용자 입력 이벤트를 감지해서 유효성을 검사하고 폼과 데이터 모델을 구성하며, 변동사항이 있는지 추적합니다.
참고: 시그널 폼을 찾고 있다면 시그널 폼 핵심 문서를 참고하세요!
이 문서는 어떤 상황에서 어떤 방식의 폼이 나은지 설명하고, 방식별 구성 요소를 소개합니다. 그리고 두 방식이 어떻게 다른지 구성방식, 데이터 흐름, 테스트 관점에서 설명합니다.
폼 종류 선택하기
반응형 폼과 템플릿 기반 폼은 폼을 처리하는 방법이나 폼 데이터를 관리하는 방식이 다릅니다. 각각의 장점을 확인해 보세요.
| 방식 | 설명 |
|---|---|
| 반응형 폼(Reactive forms) | 폼 객체 모델을 명시적으로 선언하여 직접 접근합니다. 템플릿 기반 폼에 비해 직관적이고, 확장하기 쉬우며, 재사용하기 좋고, 테스트하기도 편합니다. 애플리케이션에서 폼이 차지하는 비중이 크거나, 반응형 디자인 패턴으로 애플리케이션을 구성하고 있다면, 반응형 폼을 사용하세요. |
| 템플릿 기반 폼(Template-driven forms) | 객체 모델을 구성하기 위해 템플릿 안에서 디렉티브를 활용합니다. 이메일로 로그인하는 정도의 간단한 폼을 구성하는 경우에 유용합니다. 반응형 폼만큼 확장성이 좋지는 않습니다. 구성하려는 폼이 아주 단순하거나, 템플릿 안에서만으로도 유효성 검사를 할 수 있다면 템플릿 기반 폼을 선택하는 것이 나을 수 있습니다. |
차이점
반응형 폼과 템플릿 기반 폼은 이런 점이 다릅니다.
| 반응형 폼 | 템플릿 기반 폼 | |
|---|---|---|
| 폼 모델 구성 | 컴포넌트 클래스에서 명시적으로 선언 | 디렉티브로 묵시적 선언 |
| 데이터 모델 | 구조가 명확하며 바뀌지 않음(immutable) | 구조가 명확하지 않으며 바뀔 수 있음(mutable) |
| 데이터 흐름 | 동기 | 비동기 |
| 유효성 검사 | 함수 | 디렉티브 |
확장성
애플리케이션에서 폼이 중요한 요소라면, 확장성이 아주 중요합니다. 컴포넌트와 관계없이 폼 모델을 재사용할 수 있다는 점이 중요한 포인트가 될 수 있습니다.
반응형 폼은 템플릿 기반 폼보다 확장하기 쉽습니다. 폼 API를 직접 사용하며, 뷰와 데이터 모델의 데이터 흐름이 동기 방식이기 때문에 복잡한 폼을 직관적으로 구성할 수 있습니다. 그리고 반응형 폼은 테스트할 때 설정할 것이 별로 없으며, 폼 값이 변경되거나 유효성 검사가 진행되는 동안 변경 감지를 신경 쓰지 않아도 잘 동작합니다.
템플릿 기반 폼은 간단한 경우에 주로 사용하며, 재사용하기 어렵습니다. 폼 API는 추상적이고 뷰와 데이터 모델의 데이터 흐름은 비동기 방식입니다. 템플릿 기반 폼의 추상적인 특성은 테스트에도 영향을 줍니다. 폼을 정상적으로 테스트하려면 변경 감지를 수동으로 실행해야 하고, 반응형 폼보다 설정해야 할 것도 많습니다.
폼 모델 구성하기
반응형 폼과 템플릿 기반 폼은 모두 사용자가 입력하는 폼 입력 엘리먼트와 컴포넌트에 있는 폼 데이터를 연결한다는 점에서 비슷합니다. 하지만, 폼 컨트롤 인스턴스를 어떻게 생성하고 관리하는지는 다릅니다.
폼을 구성하는 클래스
반응형 폼과 템플릿 기반 폼 모두 아래 클래스를 기반으로 구성됩니다.
| 클래스 | 설명 |
|---|---|
FormControl |
개별 폼 컨트롤의 값과 유효성 상태를 추적합니다. |
FormGroup |
폼 컨트롤 묶음의 값과 유효성 상태를 추적합니다. |
FormArray |
폼 컨트롤 배열의 값과 유효성 상태를 추적합니다. |
ControlValueAccessor |
Angular FormControl 인스턴스와 표준 DOM 엘리먼트를 연결합니다. |
반응형 폼 구성하기
반응형 폼을 사용하는 경우에는 컴포넌트 클래스에 폼 모델을 직접 정의하면 됩니다.
그리고 템플릿에서 [formControl] 디렉티브를 사용하면 FormControl 인스턴스와 폼 엘리먼트를 연결할 수 있습니다.
아래 컴포넌트 코드를 보면, 반응형 폼 방식으로 입력 필드가 하나 있습니다.
폼 모델은 FormControl 인스턴스입니다.
import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
@Component({
selector: 'app-reactive-favorite-color',
template: ` Favorite Color: <input type="text" [formControl]="favoriteColorControl" /> `,
imports: [ReactiveFormsModule],
})
export class FavoriteColorReactive {
favoriteColorControl = new FormControl('');
}
중요: 반응형 폼에서는 폼 모델이 원본 데이터를 관리합니다. 이 폼 모델은 <input> 엘리먼트에 [formControl] 디렉티브로 연결해서 값을 반영하거나 유효성 검사 상태를 표현할 수 있습니다.
템플릿 기반 폼 구성하기
템플릿 기반 폼을 사용하는 경우에는 폼 모델을 명시적으로 선언하지 않습니다.
폼 엘리먼트에 NgModel 디렉티브를 사용하면 이 디렉티브가 FormControl 인스턴스를 생성하고 관리합니다.
아래 컴포넌트 코드를 보면, 위에서 본 코드와 마찬가지로 템플릿 기반 폼으로 입력 필드 하나가 있습니다.
import {Component, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-template-favorite-color',
template: ` Favorite Color: <input type="text" [(ngModel)]="favoriteColor" /> `,
imports: [FormsModule],
})
export class FavoriteColorTemplate {
favoriteColor = signal('');
}
중요: 템플릿 폼에서는 템플릿이 원본 데이터를 관리합니다. FormControl 인스턴스는 NgModel 디렉티브가 자동으로 관리합니다.
데이터 흐름
애플리케이션에 폼이 있다면 화면에서 변경된 사항을 컴포넌트 모델에 반영해야 하고, 컴포넌트 모델에서 변경된 사항을 화면에 반영해야 합니다. 화면에서 사용자가 무언가 입력하거나 선택하면, 새로운 값이 데이터 모델에 반영되어야 한다는 뜻입니다. 비슷하게, 어떤 로직으로 데이터 모델의 값이 변경되었다면 이 값은 화면에 반영되어야 합니다.
반응형 폼이나 템플릿 기반 폼은 사용자가 입력한 것과 로직으로 데이터를 처리하는 방식이 다릅니다. 아래 다이어그램을 보면서 어떻게 진행되는지 확인해 봅시다.
반응형 폼의 데이터 흐름
반응형 폼 방식에서는 화면에 있는 개별 폼 엘리먼트가 FormControl 인스턴스 폼 모델에 직접 연결됩니다.
화면에서 변경된 사항이 폼 모델에 반영되고, 폼 모델이 변경되었을 때 화면이 변경되는 것은 동기 방식으로 동작하며, 화면이 어떻게 렌더링되었는지는 영향을 받지 않습니다.
화면에서 입력 필드 값이 변경되었을 때 모델로 전달되는 단계는 이렇습니다:
- 입력 엘리먼트에 값을 입력합니다. 이 경우에는 Blue 를 입력했다고 합시다.
- 폼 입력 엘리먼트가 "input" 이벤트를 발생시키고, 이 이벤트 객체에 마지막 값을 함께 전달합니다.
ControlValueAccessor가 이벤트를 감지하면FormControl인스턴스에 새 값을 전달합니다.FormControl인스턴스가 새로 변경된 값으로valueChanges옵저버블에 새 값을 전달합니다.valueChanges옵저버블 구독자들이 새 값을 받습니다.
그리고 로직으로 폼 모델 값이 변경되었을 때 화면에 반영되는 단계는 이렇습니다.
- 사용자가
favoriteColorControl.setValue()메서드를 실행하면서FormControl값을 변경합니다. FormControl인스턴스가valueChanges옵저버블로 새 값을 전달합니다.valueChanges옵저버블 구독자들이 새 값을 받습니다.- 폼 입력 엘리먼트와 연결된 컨트롤 값 접근자(control value accessor)가 새 값을 엘리먼트에 반영합니다.
템플릿 기반 폼의 데이터 흐름
템플릿 기반 폼에서는 개별 폼 엘리먼트마다 연결된 디렉티브가 폼 모델을 내부적으로 관리합니다.
화면에서 모델로 값이 전달되는 과정은 이렇습니다.
- 사용자가 입력 엘리먼트에 Blue 라고 입력합니다.
- 입력 엘리먼트에서 "input" 이벤트가 발생합니다.
- 입력 엘리먼트에 연결된 컨트롤 값 접근자(control value accessor)가
FormControl인스턴스의setValue()메서드를 실행합니다. FormControl인스턴스가valueChanges옵저버블 이벤트로 새 값을 전달합니다.valueChanges옵저버블 구독자가 새 값을 받습니다.- 컨트롤 값 접근자는
NgModel.viewToModelUpdate()메서드를 실행하면서ngModelChange이벤트도 함께 발생합니다. favoriteColor프로퍼티는 컴포넌트 템플릿에서 양방향 바인딩되었기 때문에, 컴포넌트의favoriteColor프로퍼티 값은ngModelChange에 반응하여 새 값(Blue)으로 변경됩니다.
favoriteColor 프로퍼티 값이 Blue 에서 Red 로 변경될 때 모델에서 화면으로 값이 전달되는 과정은 이렇습니다.
- 컴포넌트에서
favoriteColor값이 변경됩니다. - 변경 감지 싸이클이 시작됩니다.
- 입력값이 변경되었기 때문에
ngModel디렉티브의ngOnChanges라이프사이클 훅이 실행됩니다. ngOnChanges()메서드가 내부FormControl인스턴스의 값을 설정하는 비동기 작업을 큐에 추가합니다.- 변경 감지 싸이클이 종료됩니다.
- 다음 틱(tick)에
FormControl인스턴스의 값을 변경합니다. FormControl인스턴스가valueChanges옵저버블 이벤트를 보냅니다.valueChanges옵저버블 구독자가 새 값을 받습니다.- 컨트롤 값 접근자가 화면에 있는 폼 입력 엘리먼트의 값을 변경합니다.
참고: NgModel은 ExpressionChangedAfterItHasBeenChecked 오류를 방지하기 위해 변경 감지 싸이클을 분리합니다.
데이터 모델의 불변성
변경 감지 싸이클은 효율적으로 동작합니다.
| 방식 | 설명 |
|---|---|
| 반응형 폼 | 데이터 모델을 이뮤터블로 유지합니다. 데이터 모델이 변경될 때마다 FormControl 인스턴스가 기존 데이터 모델을 수정하는 대신 새로운 데이터 모델을 반환합니다. 그래서 컨트롤의 옵저버블을 통해 어떤 값이 변경되었는지 명확하게 확인할 수 있기 때문에, 변경 감지도 더 효율적으로 동작합니다. 변경되는 값을 옵저버블로 받으면 데이터를 가공하기도 좋습니다. |
| 템플릿 기반 폼 | 템플릿에서 값이 변경되면 양방향 바인딩된 객체의 값을 변경합니다. 왜냐하면, 양방향 데이터 바인딩한 경우에는 데이터 모델이 그대로 유지되기 때문이며, 그래서 값이 실제로 변경되어 화면에 반영해야 하는지 판단해야 하기 때문에 변경 감지 효율이 약간 떨어집니다. |
위에서 살펴본 예제로 다른 점을 설명해보면 이렇습니다.
- 반응형 폼에서는,
FormControl인스턴스가 언제나 새 값을 반환합니다. - 템플릿 기반 폼에서는 favorite 프로퍼티 의 값이 변경됩니다.
폼 유효성 검사
유효성 검사는 폼 전체를 다루는 부분입니다. 필수 입력 필드를 입력했는지 검사하거나, 외부 API를 활용해서 중복된 이름을 사용하지 않았는지 검사할 수 있고, Angular가 다양한 유효성 검사 함수를 제공하지만, 원하는 로직으로 커스텀 유효성 검사 함수를 만들 수도 있습니다.
| 방식 | 설명 |
|---|---|
| 반응형 폼 | 폼 컨트롤을 인자로 받는 함수 로 커스텀 유효성 검사 함수를 정의합니다 |
| 템플릿 기반 폼 | 템플릿 디렉티브 에서 처리하거나, 유효성 검사 함수를 디렉티브로 랩핑해서 정의합니다 |
자세한 내용은 폼 유효성 검사 문서를 참고하세요.
테스트
테스트는 복잡한 애플리케이션일수록 중요합니다. 가장 간단하게는, 폼 기능이 제대로 동작하는지 검사할 수 있습니다. 반응형 폼이나 템플릿 기반 폼은 폼 컨트롤과 폼 필드의 변경사항을 감지하는 방식이 다르기 때문에 UI 렌더링에 의존하는 정도가 다릅니다. 아래 예제를 보면서 반응형 폼과 템플릿 기반 폼을 어떻게 테스트할 수 있을지 알아봅시다.
반응형 폼 테스트
반응형 폼은 폼과 데이터 모델이 동기 방식으로 동작하기 때문에 테스트 코드를 작성하기 간편하며, UI를 렌더링하지 않아도 테스트를 실행할 수 있습니다. 폼 컨트롤의 상태를 확인하거나 값을 변경하는 것은 모두 변경 감지 싸이클을 고려하지 않아도 됩니다.
위에서 살펴본 예제로 화면에서 모델로, 모델에서 화면으로 값이 잘 전달되는지 테스트해 봅시다.
화면 - 모델 데이터 흐름 검증하기
첫 번째 예제는 다음 단계로 화면에서 모델로 데이터가 전달되는지 검증합니다.
- 폼 입력 엘리먼트를 참조하고 커스텀 "input" 이벤트를 만듭니다.
- Red 라고 새 값을 설정하고, "input" 이벤트를 발생시킵니다.
- 컴포넌트의
favoriteColorControl프로퍼티 값이 입력 엘리먼트 값과 같은지 검사합니다.
선호 색상 테스트 - 화면에서 모델로
it('should update the value of the input field', () => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
expect(fixture.componentInstance.favoriteColorControl.value).toEqual('Red');
});
다음 예제는 모델에서 화면으로 데이터가 제대로 전달되는지 검사해 봅시다.
favoriteColorControl,FormControl인스턴스로 새 값을 설정합니다.- 폼 입력 엘리먼트를 참조합니다.
- 입력 엘리먼트에 새 값이 반영되었는지 검사합니다.
선호 색상 테스트 - 모델에서 화면으로
it('should update the value in the control', () => {
component.favoriteColorControl.setValue('Blue');
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
});
템플릿 기반 폼 테스트하기
템플릿 기반의 폼을 테스트하려면 변경 감지가 어떻게 실행되는지 확실하게 알아야 하며, 엘리먼트를 참조하고, 테스트하고, 변화를 감지하기 위해 개별 변경 감지 싸이클을 어떻게 활용해야 하는지 이해해야 합니다.
아래 예제 코드는 이전에 살펴본 선호 색상 컴포넌트에서 데이터가 화면에서 모델로, 모델에서 화면으로 제대로 전달되는지 테스트하는 코드입니다.
화면에서 모델로 전달되는 것부터 검사해 봅시다.
선호 색상 테스트 - 화면에서 모델로
it('should update the favorite color in the component', async () => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
await fixture.whenStable();
expect(component.favoriteColor()).toEqual('Red');
});
화면에서 모델로 전달되는 것은 이런 순서로 테스트합니다.
- 폼 입력 엘리먼트를 쿼리하고 커스텀 "input" 이벤트를 만듭니다.
- 이벤트에 새 값으로 Red 를 설정하고 폼 입력 엘리먼트로 "input" 이벤트를 보냅니다.
await fixture.whenStable()을 사용해서 변경 사항이 처리될 때까지 기다립니다.- 컴포넌트의
favoriteColor프로퍼티 값이 입력 엘리먼트와 같은지 검사합니다.
이제 모델에서 화면으로 전달되는 데이터를 검사해 봅시다.
선호 색상 테스트 - 모델에서 화면으로
it('should update the favorite color on the input field', async () => {
component.favoriteColor.set('Blue');
await fixture.whenStable();
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
});
이런 순서로 테스트합니다.
- 컴포넌트 인스턴스로
favoriteColor프로퍼티 값을 설정합니다. - 다음 렌더링까지 기다리기 위해
await fixture.whenStable()를 실행합니다. - 폼 입력 엘리먼트를 참조합니다.
- 엘리먼트의 값이 컴포넌트 인스턴스의
favoriteColor프로퍼티 값과 같은지 검사합니다.
다음 단계
반응형 폼을 더 알아보려면 이 문서들을 확인해 보세요:
템플릿 기반 폼을 더 알아보려면 이 문서들을 확인해 보세요: