Angular 14부터 반응형 폼에는 기본적으로 엄격한 타입이 적용됩니다.
이 가이드를 읽기 전에 Angular 반응형 폼에 익숙해야 합니다.
타입이 지정된 폼 개요
Angular 반응형 폼을 사용하면 폼 모델 을 명시적으로 지정합니다. 간단한 예로 다음과 같은 기본 로그인 폼을 살펴봅시다:
const login = new FormGroup({
email: new FormControl(''),
password: new FormControl(''),
});
Angular는 이 FormGroup을 다룰 수 있는 다양한 API를 제공합니다.
예를 들어 login.value, login.controls, login.patchValue 등을 사용할 수 있습니다.
전체 API 레퍼런스는 API 문서를 참고하세요.
이전 Angular 버전에서는 이런 API 대부분의 타입에 any가 포함되어 있어서, 컨트롤 구조나 값을 다룰 때 타입 안전성을 보장하지 못했습니다.
예를 들어 다음과 같이 잘못된 코드를 작성할 수 있었습니다:
const emailDomain = login.value.email.domain;
엄격한 타입이 적용된 반응형 폼에서는 email에 domain 프로퍼티가 없기 때문에 위 코드를 컴파일할 수 없습니다.
타입을 사용하면 안전성이 높아질 뿐 아니라 IDE의 자동완성이 개선되고 폼 구조를 명시적으로 지정할 수 있는 등 여러 이점이 있습니다.
이런 개선 사항은 현재 반응형 폼에만 적용되며, 템플릿 기반 폼에는 적용되지 않습니다.
타입이 지정되지 않은 폼
타입이 지정되지 않은 폼도 계속 지원하며 이전과 같은 방식으로 동작합니다.
이 방식을 사용하려면 @angular/forms에서 Untyped로 시작하는 심볼을 로드해야 합니다:
const login = new UntypedFormGroup({
email: new UntypedFormControl(''),
password: new UntypedFormControl(''),
});
각 Untyped 심볼은 이전 Angular 버전과 정확히 같은 방식으로 동작합니다.
Untyped 접두사를 제거하면서 점진적으로 타입을 적용할 수 있습니다.
FormControl 시작하기
가장 간단한 폼은 컨트롤 하나로 구성합니다:
const email = new FormControl('angularrox@gmail.com');
이 컨트롤의 타입은 자동으로 FormControl<string|null>로 추론됩니다.
TypeScript는 email.value, email.valueChanges, email.setValue(...) 등 FormControl API 전체에서 이 타입을 적용합니다.
null 허용 여부
컨트롤 타입에 null이 포함된 이유가 궁금할 수 있습니다.
reset을 실행하면 언제든 컨트롤 값이 null이 될 수 있기 때문입니다:
const email = new FormControl('angularrox@gmail.com');
email.reset();
console.log(email.value); // null
TypeScript는 컨트롤 값이 null일 가능성을 항상 처리하도록 요구합니다.
컨트롤이 null 값을 갖지 않도록 하려면 nonNullable 옵션을 사용하면 됩니다.
이 옵션을 지정하면 컨트롤을 초기화할 때 null 대신 초기값으로 되돌립니다:
const email = new FormControl('angularrox@gmail.com', {nonNullable: true});
email.reset();
console.log(email.value); // angularrox@gmail.com
다시 말해 이 옵션은 .reset()을 실행할 때 폼의 런타임 동작에도 영향을 미치므로, 주의해서 변경해야 합니다.
타입을 명시적으로 지정하기
타입 추론에 의존하지 않고 타입을 직접 지정할 수도 있습니다.
초기값이 null인 컨트롤을 생각해 봅시다.
초기값이 null이면 TypeScript는 원하는 범위보다 좁은 FormControl<null>로 타입을 추론합니다.
const email = new FormControl(null);
email.setValue('angularrox@gmail.com'); // 에러가 발생합니다!
이 문제를 방지하려면 타입을 string|null로 명시합니다:
const email = new FormControl<string | null>(null);
email.setValue('angularrox@gmail.com');
FormArray: 같은 타입의 컨트롤을 동적으로 관리하기
FormArray에는 개수가 정해지지 않은 컨트롤 목록을 담습니다.
타입 인자는 내부의 각 컨트롤 타입에 해당합니다:
const names = new FormArray([new FormControl('Alex')]);
names.push(new FormControl('Jess'));
여러 항목을 한 번에 추가하려면 aliases.push()에 컨트롤 배열을 전달합니다.
const aliases = new FormArray([new FormControl('ng')]);
aliases.push([new FormControl('ngDev'), new FormControl('ngAwesome')]);
이 FormArray 내부 컨트롤의 타입은 FormControl<string|null>입니다.
배열에 서로 다른 여러 타입의 항목을 담으려면 UntypedFormArray를 사용해야 합니다.
TypeScript가 각 위치에 어떤 타입의 항목이 올지 추론할 수 없기 때문입니다.
FormArray는 내부의 모든 컨트롤을 제거하는 clear() 메서드도 제공합니다:
const aliases = new FormArray([new FormControl('ngDev'), new FormControl('ngAwesome')]);
aliases.clear();
console.log(aliases.length); // 0
FormGroup과 FormRecord
Angular는 키 목록이 정해진 폼에 사용할 FormGroup 타입과, 키가 정해지지 않았거나 동적으로 구성하는 그룹에 사용할 FormRecord 타입을 제공합니다.
일부 값만 포함하는 경우
로그인 폼을 다시 살펴봅시다:
const login = new FormGroup({
email: new FormControl('', {nonNullable: true}),
password: new FormControl('', {nonNullable: true}),
});
모든 FormGroup에서는 컨트롤을 비활성화할 수 있습니다.
비활성화된 컨트롤은 그룹의 값에 포함되지 않습니다.
따라서 login.value의 타입은 Partial<{email: string, password: string}>입니다.
이 타입의 Partial은 각 프로퍼티의 값이 undefined일 수 있다는 의미입니다.
구체적으로 login.value.email의 타입은 string|undefined이며, TypeScript는 strictNullChecks가 활성화되어 있다면 값이 undefined일 가능성을 처리하도록 요구합니다.
비활성화된 컨트롤의 값까지 포함해서 가져오고, 이로 인해 필드가 undefined가 되는 것을 피하려면 login.getRawValue()를 사용하면 됩니다.
선택적 컨트롤과 동적 그룹
어떤 폼에는 존재할 수도 있고 존재하지 않을 수도 있는 컨트롤이 있으며, 런타임에 이를 추가하거나 제거할 수 있습니다. 이런 컨트롤은 선택적 필드 로 표현합니다:
interface LoginForm {
email: FormControl<string>;
password?: FormControl<string>;
}
const login = new FormGroup<LoginForm>({
email: new FormControl('', {nonNullable: true}),
password: new FormControl('', {nonNullable: true}),
});
login.removeControl('password');
이 폼에서는 타입을 명시적으로 지정해서 password 컨트롤을 선택적으로 사용하도록 설정합니다.
TypeScript는 선택적 컨트롤만 추가하거나 제거할 수 있도록 제한합니다.
FormRecord
키를 미리 알 수 없는 경우에는 앞에서 살펴본 FormGroup 사용 방식이 맞지 않을 수 있습니다.
FormRecord 클래스는 이런 경우에 사용하도록 설계되었습니다:
const addresses = new FormRecord<FormControl<string | null>>({});
addresses.addControl('Andrew', new FormControl('2340 Folsom St'));
이 FormRecord에는 값의 타입이 string|null인 컨트롤을 추가할 수 있습니다.
동적이면서 키가 정해지지 않고, 컨트롤마다 타입도 다른 FormGroup이 필요하다면 타입 안전성을 더 높일 수 없으므로 UntypedFormGroup을 사용해야 합니다.
FormBuilder로 FormRecord를 생성할 수도 있습니다:
const addresses = fb.record({'Andrew': '2340 Folsom St'});
FormBuilder와 NonNullableFormBuilder
FormBuilder 클래스도 앞의 예제와 같은 방식으로 새로운 타입을 지원하도록 개선되었습니다.
추가로 NonNullableFormBuilder도 사용할 수 있습니다.
이 타입은 모든 컨트롤에 {nonNullable: true}를 지정하는 것을 간단하게 표현하며, null을 허용하지 않는 대규모 폼에서 반복 코드를 크게 줄일 수 있습니다.
FormBuilder의 nonNullable 프로퍼티로 접근할 수 있습니다:
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
위 예제의 내부 컨트롤 두 개는 모두 null을 허용하지 않습니다.
즉, nonNullable이 설정됩니다.
NonNullableFormBuilder라는 이름으로 직접 주입할 수도 있습니다.