Angular는 폼을 만드는 세 가지 방식으로 Signal Forms, 반응형 폼, 템플릿 기반 폼을 제공합니다. 각 방식은 상태, 유효성 검사, 데이터 흐름을 관리하는 패턴이 다릅니다. 이 가이드에서는 세 방식의 차이를 이해하고 프로젝트에 알맞은 방식을 선택하는 방법을 알아봅니다.
한눈에 비교하기
| 기능 | Signal Forms | 반응형 폼 | 템플릿 기반 폼 |
|---|---|---|---|
| 원본 데이터 | 사용자가 정의한 쓰기 가능한 시그널 모델 | FormControl/FormGroup |
컴포넌트의 사용자 모델 |
| 타입 안전성 | 모델에서 추론 | 타입이 지정된 폼으로 명시 | 제한적 |
| 유효성 검사 | 경로 기반 검증 함수를 사용하는 스키마 | 컨트롤에 전달하는 검증 함수 목록 | 디렉티브 기반 |
| 상태 관리 | 시그널 기반 | 옵저버블 기반 | Angular가 관리 |
| 구성 | 시그널과 스키마 함수 | FormControl 트리 | 템플릿의 NgModel |
| 적합한 용도 | 시그널 기반 애플리케이션 | 복잡한 폼 | 간단한 폼 |
| 학습 난이도 | 보통 | 보통~높음 | 낮음 |
| 상태 | 안정화(v22 이상) | 안정화 | 안정화 |
예제로 비교하기: 로그인 폼
세 방식으로 같은 폼을 구현한 예제를 보면 차이를 가장 쉽게 이해할 수 있습니다.
차이점 이해하기
세 방식은 설계가 다르기 때문에 폼을 작성하고 유지보수하는 방식에도 차이가 있습니다. 이런 차이는 폼 상태를 어디에 저장하고 유효성 검사를 어떻게 관리하는지에서 비롯됩니다.
폼 데이터를 저장하는 위치
가장 근본적인 차이는 폼 값의 원본 데이터(source of truth)를 어디에 두는지입니다.
Signal Forms는 값을 쓸 수 있는 시그널에 데이터를 저장합니다. 현재 폼 값이 필요하면 시그널을 실행합니다:
const credentials = this.loginModel(); // { email: '...', password: '...' }
이 방식은 폼 데이터를 하나의 반응형 컨테이너에 저장하고, 값이 변경되면 Angular에 자동으로 알립니다. 폼 구조는 데이터 모델과 정확히 일치합니다.
반응형 폼은 FormControl과 FormGroup 인스턴스에 데이터를 저장합니다. 폼 계층을 통해 값에 접근합니다:
const credentials = this.loginForm.value; // { email: '...', password: '...' }
이 방식은 폼 상태 관리를 컴포넌트의 데이터 모델과 분리합니다. 폼 구조를 명시적으로 정의하지만, 설정 코드가 더 많이 필요합니다.
템플릿 기반 폼은 컴포넌트 프로퍼티에 데이터를 저장합니다. 값에 직접 접근합니다:
const credentials = {email: this.email, password: this.password};
가장 직접적인 방식이지만, 여러 값을 한데 모아야 할 때는 직접 구성해야 합니다. Angular는 템플릿의 디렉티브를 통해 폼 상태를 관리합니다.
유효성 검사 방식
각 방식은 유효성 검사 규칙을 정의하는 방법이 다릅니다. 이에 따라 유효성 검사 로직의 위치와 유지보수 방법도 달라집니다.
Signal Forms는 스키마 함수에서 필드 경로에 검증 함수를 연결합니다:
loginForm = form(this.loginModel, (fieldPath) => {
required(fieldPath.email, {message: 'Email is required'});
email(fieldPath.email, {message: 'Enter a valid email address'});
});
모든 유효성 검사 규칙을 한곳에 모아 정의합니다. 스키마 함수는 폼을 생성할 때 한 번 실행되고, 필드 값이 변경되면 검증 함수가 자동으로 실행됩니다. 오류 메시지도 유효성 검사 정의에 포함합니다.
반응형 폼은 컨트롤을 생성할 때 검증 함수를 연결합니다:
loginForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
});
검증 함수는 폼 구조의 개별 컨트롤에 연결됩니다. 따라서 유효성 검사 로직이 폼 정의 곳곳에 나뉘어 배치됩니다. 오류 메시지는 일반적으로 템플릿에 작성합니다.
템플릿 기반 폼은 템플릿에서 디렉티브 어트리뷰트를 사용합니다:
<input [(ngModel)]="email" required email />
유효성 검사 규칙은 템플릿의 HTML과 함께 작성합니다. 유효성 검사 로직을 UI 가까이에 둘 수 있지만, 로직이 템플릿과 컴포넌트에 나뉘어 배치됩니다.
타입 안전성과 자동완성
각 방식은 TypeScript와 연동하는 방법이 크게 다르므로, 컴파일러가 오류를 방지하는 데 도움을 주는 정도도 다릅니다.
Signal Forms는 모델 구조에서 타입을 추론합니다:
const loginModel = signal({email: '', password: ''});
const loginForm = form(loginModel);
// TypeScript는 loginForm.email이 존재하며 FieldState<string>을 반환한다는 것을 알 수 있습니다.
시그널에서 데이터 구조를 한 번 정의하면 TypeScript가 필드의 존재 여부와 타입을 자동으로 알 수 있습니다.
존재하지 않는 loginForm.username에 접근하면 타입 오류가 발생합니다.
반응형 폼은 타입이 지정된 폼을 사용해서 타입을 명시적으로 지정해야 합니다:
const loginForm = new FormGroup({
email: new FormControl<string>(''),
password: new FormControl<string>(''),
});
// TypeScript는 loginForm.controls.email의 타입이 FormControl<string>이라는 것을 알 수 있습니다.
각 컨트롤의 타입을 개별적으로 지정합니다. TypeScript가 폼 구조를 검사하지만, 타입 정보는 데이터 모델과 별도로 관리해야 합니다.
템플릿 기반 폼은 제한적인 타입 안전성만 제공합니다:
email = '';
password = '';
// TypeScript는 문자열이라는 것만 알 수 있으며, 폼 수준의 타입 정보는 없습니다.
TypeScript는 컴포넌트 프로퍼티를 이해하지만, 폼 구조나 유효성 검사 정보는 알지 못합니다. 따라서 폼 관련 작업을 컴파일 시점에 검사할 수 없습니다.
구현 방식 선택하기
Signal Forms가 적합한 경우
- 시그널 기반의 새 애플리케이션을 만드는 경우(Angular v22 이상)
- 모델 구조에서 타입 안전성을 추론하고 싶은 경우
- 스키마 기반 유효성 검사 방식을 선호하는 경우
- 팀원들이 시그널에 익숙한 경우
반응형 폼이 적합한 경우
- 운영 환경에서 검증된 안정성이 필요한 경우
- 복잡하고 동적인 폼을 만드는 경우
- 옵저버블 기반 패턴을 선호하는 경우
- 폼 상태를 세밀하게 제어해야 하는 경우
- 기존 반응형 폼 코드를 유지보수하는 경우
템플릿 기반 폼이 적합한 경우
- 로그인, 문의, 검색과 같은 간단한 폼을 만드는 경우
- 프로토타입을 빠르게 만드는 경우
- 폼 로직이 단순한 경우
- 폼 로직을 템플릿에 두는 방식을 선호하는 경우
- 기존 템플릿 기반 폼 코드를 유지보수하는 경우
다음 단계
각 방식을 자세하게 알아보려면 다음 문서를 참고하세요: