신호 형태: 미래의 반응 형태
Angular의 Reactive Forms는 다음을 기반으로 합니다. FormControl, FormGroup e
FormArray, 수년 동안 양식 관리의 표준을 대표해 왔습니다.
복잡한. 그러나, 신호 반응형 프리미티브로서
프레임워크의 핵심인 Angular 팀은 차세대 양식을 개발 중입니다.
완전히 Signal을 기반으로 함: i 신호 형태. 이 진화는 유망하다
양식 상태에 대한 RxJS에 대한 의존성을 제거하기 위해 상용구를 대폭 줄입니다.
세분화된 응답성을 양식 관리 시스템에 기본적으로 통합합니다.
이 시리즈의 다섯 번째 기사에서는 현대적인 각도, 한계를 분석하겠습니다 현재 Reactive Forms 중 Signal Forms에 대한 RFC 제안을 살펴보고 어떻게 새로운 기본 요소의 작동 방식 및 마이그레이션 준비 방법. 당신이 시작하든 새로운 프로젝트를 진행하거나 기존 엔터프라이즈 애플리케이션을 관리하는 경우 이 가이드는 다음과 같은 정보를 제공합니다. Angular 형태의 미래에 직면하는 데 필요한 지식입니다.
최신 Angular 시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 각도 신호 | 세분화된 응답성 |
| 2 | 영역 없는 변경 감지 | Zone.js 삭제 |
| 3 | 새로운 템플릿 @if, @for, @defer | 현대적인 제어 흐름 |
| 4 | 독립형 구성 요소 | NgModule이 없는 아키텍처 |
| 5 | 현재 위치 — 신호 양식 | 신호가 포함된 반응형 양식 |
| 6 | SSR 및 증분 수화 | 서버 측 렌더링 |
| 7 | Angular의 핵심 웹 바이탈 | 성능 및 지표 |
| 8 | 각도 PWA | 프로그레시브 웹 앱 |
| 9 | 고급 종속성 주입 | DI 트리 셰이크 가능 |
| 10 | Angular 17에서 21로 마이그레이션 | 마이그레이션 가이드 |
현재 반응형 양식의 한계
Reactive Forms는 Angular 2에서 도입되었으며 클래스 계층 구조를 기반으로 합니다.
초록 (AbstractControl, FormControl, FormGroup,
FormArray) RxJS를 사용하는 Observable 변경사항을 알려드리기 위해
가치와 지위. 강력하지만 몇 가지 구조적 한계가 있음이 분명해졌습니다.
복잡한 응용 프로그램에서.
반응형 형태의 구조적 문제
- RxJS에 대한 종속성: 모든 것이 괜찮습니다
FormControl노출하다valueChangesestatusChangesObservable과 같이 개발자가 구독, 연산자 및 메모리 누수를 관리하도록 강제합니다. - 약한 타이핑: 각도 14까지,
FormGroup반환any. Typed Forms는 상황을 개선했지만 구문은 여전히 장황합니다. - AbstractControl 계층 구조: 체인 상속으로 인해 컨트롤 동작을 확장하거나 사용자 정의하기가 어렵습니다.
- 과도한 상용구: 유효성 검사가 포함된 양식을 만들려면 템플릿을 터치하기 전에 수십 줄의 TypeScript 코드가 필요합니다.
- 암시적 상태: 다음과 같은 속성
dirty,touched,valid변경 가능하고 파생되지 않아 불일치가 발생합니다. - 신호와 통합되지 않음: Reactive Forms는 신호의 세분화된 반응성 시스템을 활용하지 않으므로 Observable과 Signal 간을 수동으로 변환해야 합니다.
- 복잡한 재설정: 양식 상태(값, 더티, 터치됨)를 재설정하려면 여러 번의 호출이 필요하며 항상 예상대로 작동하지는 않습니다.
// Un form di registrazione con i Reactive Forms attuali
@Component({
selector: 'app-registration',
standalone: true,
imports: [ReactiveFormsModule],
template: `...`
})
export class RegistrationComponent implements OnInit, OnDestroy {
private fb = inject(FormBuilder);
private destroy$ = new Subject<void>();
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
address: this.fb.group({
street: [''],
city: ['', Validators.required],
zip: ['', Validators.pattern(/^\d{5}$/)]
})
});
// Devo sottoscrivermi manualmente per reagire ai cambiamenti
ngOnInit() {
this.form.get('password')?.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.form.get('confirmPassword')?.updateValueAndValidity();
});
}
// Devo gestire manualmente la distruzione delle sottoscrizioni
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get nameErrors() {
const ctrl = this.form.get('name');
if (ctrl?.hasError('required')) return 'Nome obbligatorio';
if (ctrl?.hasError('minlength')) return 'Minimo 2 caratteri';
return '';
}
}
보시다시피 적당히 복잡한 형태라도 상당한 양의 작업이 필요합니다. 상용구 코드: 수동 구독, 수명주기 관리, i용 getter 오류 메시지, 관찰 가능한 값과 명령형 값 간의 변환. 시그널폼의 약속 이 모든 문제를 해결하기 위해.
신호 형식 제안: RFC 및 새로운 기본 요소
Angular 팀이 RFC(의견 요청) 신호 형태의 경우, 신호를 기반으로 한 양식 관리 시스템을 완전히 다시 작성했습니다. 목표 가장 중요한 것은 다음과 같은 API를 만드는 것입니다. 본질적으로 반응적, 의존하지 않고 양식 상태에 대한 RxJS의 성능과 유연성을 유지하면서 현재 반응형 양식.
새로운 원시 제안
| 원시 신호 형태 | 반응형 양식에 해당 | 설명 |
|---|---|---|
FormSignal |
FormControl |
단일 형식 값, 응답 상태의 쓰기 가능한 신호 |
FormRecord |
FormGroup |
양식 신호 그룹은 여러 FormSignal을 하나의 개체로 구성합니다. |
FormList |
FormArray |
반복 수집을 위한 신호 형식의 동적 목록 |
computed() |
valueChanges |
양식 값에서 자동 반응형 파생 |
effect() |
.subscribe() |
수동 관리 없이 자동 정리로 인한 부작용 |
// Lo stesso form di registrazione con Signal Forms
@Component({
selector: 'app-registration',
standalone: true,
template: `...`
})
export class RegistrationComponent {
// Dichiarazione concisa con validazione inline
name = formSignal('', {
validators: [required(), minLength(2)]
});
email = formSignal('', {
validators: [required(), email()]
});
password = formSignal('', {
validators: [required(), minLength(8)]
});
confirmPassword = formSignal('', {
validators: [required(), matchesField(this.password)]
});
address = formRecord({
street: formSignal(''),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [pattern(/^\d{5}$/)] })
});
// Lo stato è derivato automaticamente, nessuna sottoscrizione
isFormValid = computed(() =>
this.name.isValid() &&
this.email.isValid() &&
this.password.isValid() &&
this.confirmPassword.isValid() &&
this.address.isValid()
);
// Nessun ngOnInit, nessun ngOnDestroy, nessun Subject
}
신호 형태의 주요 이점
- 구독 없음: 아니요
subscribe(), 아니요takeUntil, 처리할 메모리 누수가 없습니다. - 파생된 상태: 다음과 같은 속성
isValid(),isDirty(),errors()그것들은 계산된 신호이며 항상 동기화됩니다. - 기본적으로 유형이 안전함: 값의 유형은 초기 값에서 자동으로 추론됩니다.
- 템플릿과 통합: 신호는 파이프나 변환 없이 템플릿에 직접 연결됩니다.
- 자동 정리: 구성요소 수명주기를 수동으로 관리하지 않음
신호에 따른 양식 상태
Signal Forms의 가장 중요한 변경 사항 중 하나는 다음과 같습니다. 양식의 전체 상태
신호가 되다. 더 이상 다음과 같은 변경 가능한 속성이 없습니다. .dirty o
.touched 내부적으로 업데이트되는 내용: 상태의 모든 측면과
computed 진실의 근원에서 파생된 신호.
const username = formSignal('', {
validators: [required(), minLength(3), maxLength(20)]
});
// Il valore corrente: WritableSignal<string>
console.log(username.value()); // ''
// Stato di validazione: Signal<boolean>
console.log(username.isValid()); // false
console.log(username.isInvalid()); // true
// Errori di validazione: Signal<ValidationErrors | null>
console.log(username.errors()); // { required: true }
// Stato di interazione: Signal<boolean>
console.log(username.isDirty()); // false
console.log(username.isPristine()); // true
console.log(username.isTouched()); // false
console.log(username.isUntouched()); // true
// Stato di attesa (validazione asincrona): Signal<boolean>
console.log(username.isPending()); // false
// Stato di disabilitazione: Signal<boolean>
console.log(username.isDisabled()); // false
console.log(username.isEnabled()); // true
// Aggiornamento programmatico del valore
username.value.set('angular_dev');
// Ora: isValid() === true, isDirty() === true, errors() === null
// Marcare come touched (ad esempio on blur)
username.markAsTouched();
// Ora: isTouched() === true
비교: 반응성 형태와 신호 형태의 상태
| 재산 | 반응형 양식 | 신호 형태 |
|---|---|---|
| Valore | control.value (변경 가능한 게터) |
control.value() (신호) |
| 효력 | control.valid (부울 속성) |
control.isValid() (계산된 신호) |
| 오류 | control.errors (객체 또는 null) |
control.errors() (신호) |
| 더러운 | control.dirty (부울 내부적으로 변경됨) |
control.isDirty() (계산된 신호) |
| 변화 관찰 | control.valueChanges.subscribe() |
effect(() => console.log(control.value())) |
| 대청소 | 수동(구독 취소, takeUntil) | 자동(DestroyRef에 연결됨) |
가장 큰 장점은 국가가 항상 일관됨. 반응형 형식에서는
중간 상태를 관찰할 수 있습니다. dirty 업데이트됐지만 errors
아직은 아닙니다. Signal Forms를 사용하면 계산된 모든 업데이트가 결함 없이 원자적으로 이루어집니다.
신호 스케줄링.
신호를 이용한 검증
Signal Forms는 검증 시스템을 완전히 재설정합니다. 검증인은
반환하는 순수 함수 오류 신호, 기본 지원 포함
다음과 같은 별도의 인터페이스가 필요 없는 동기식 및 비동기식 검증
Validator e AsyncValidator.
// Validatori built-in
const email = formSignal('', {
validators: [
required(), // Non può essere vuoto
email(), // Deve essere un'email valida
maxLength(100) // Massimo 100 caratteri
]
});
// Validatore personalizzato: funzione pura
function strongPassword(): ValidatorFn {
return (value: string) => {
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasDigit = /\d/.test(value);
const hasSpecial = /[!@#$%^&*]/.test(value);
if (hasUpperCase && hasLowerCase && hasDigit && hasSpecial) {
return null; // Valido
}
return {
strongPassword: {
hasUpperCase,
hasLowerCase,
hasDigit,
hasSpecial,
message: 'La password deve contenere maiuscole, minuscole, numeri e caratteri speciali'
}
};
};
}
const password = formSignal('', {
validators: [required(), minLength(8), strongPassword()]
});
// Accesso reattivo agli errori specifici
const passwordErrors = computed(() => {
const errs = password.errors();
if (!errs) return [];
const messages: string[] = [];
if (errs['required']) messages.push('Password obbligatoria');
if (errs['minlength']) messages.push('Minimo 8 caratteri');
if (errs['strongPassword']) messages.push(errs['strongPassword'].message);
return messages;
});
// Validatore asincrono: controlla se l'username è disponibile
function usernameAvailable(userService: UserService): AsyncValidatorFn {
return async (value: string) => {
if (value.length < 3) return null; // Troppo corto, saltiamo la verifica
const exists = await userService.checkUsername(value);
return exists
? { usernameTaken: { message: `L'username "${value}" è già in uso` } }
: null;
};
}
@Component({
selector: 'app-signup',
standalone: true,
template: `
<label>Username</label>
<input [formSignal]="username" />
@if (username.isPending()) {
<span class="checking">Verifica in corso...</span>
}
@if (username.errors()?.['usernameTaken']; as err) {
<span class="error">{{ err.message }}</span>
}
`
})
export class SignupComponent {
private userService = inject(UserService);
username = formSignal('', {
validators: [required(), minLength(3)],
asyncValidators: [usernameAvailable(this.userService)],
asyncDebounceMs: 300 // Debounce integrato!
});
}
신호 양식을 사용한 검증의 새로운 기능
- 통합 디바운스: 비동기 유효성 검사기 지원
asyncDebounceMs기본적으로 필요없이debounceTime()작성자: RxJS - 대응 보류 상태:
isPending()비동기 검증 중에 자동으로 업데이트되는 신호 - 교차 필드 유효성 검사기: 연결된 값이 변경되면 자동 업데이트를 통해 다른 FormSignals에 종속성으로 액세스할 수 있습니다.
- 구성: 유효성 검사기는 간단한 기능으로 쉽게 구성하고 테스트할 수 있습니다.
신호가 있는 그룹 형성: 구성 및 중첩
formRecord() 더 많은 전화를 걸 수 있습니다 formSignal 구조에서
계층적, 동등함 FormGroup 반응형 형태. 주요 차이점
기록의 상태는 이렇습니다 자동으로 파생 그의 아이들의 상태에서.
// Form completo con gruppi annidati
const profileForm = formRecord({
personalInfo: formRecord({
firstName: formSignal('', { validators: [required()] }),
lastName: formSignal('', { validators: [required()] }),
birthDate: formSignal<Date | null>(null)
}),
contactInfo: formRecord({
email: formSignal('', { validators: [required(), email()] }),
phone: formSignal('', { validators: [pattern(/^\+?\d{10,15}$/)] })
}),
preferences: formRecord({
newsletter: formSignal(false),
theme: formSignal<'light' | 'dark'>('dark'),
language: formSignal('it')
}),
// FormList per indirizzi multipli
addresses: formList([
formRecord({
type: formSignal<'home' | 'work'>('home'),
street: formSignal('', { validators: [required()] }),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [required()] })
})
])
});
// Lo stato si propaga automaticamente verso l'alto
const isProfileValid = profileForm.isValid();
// true solo se TUTTI i campi di TUTTI i gruppi sono validi
const isDirty = profileForm.isDirty();
// true se QUALSIASI campo in qualsiasi gruppo e stato modificato
// Accesso al valore completo come oggetto tipizzato
const formValue = profileForm.value();
// Tipo inferito automaticamente:
// {
// personalInfo: { firstName: string; lastName: string; birthDate: Date | null }
// contactInfo: { email: string; phone: string }
// preferences: { newsletter: boolean; theme: 'light' | 'dark'; language: string }
// addresses: Array<{ type: 'home' | 'work'; street: string; city: string; zip: string }>
// }
// Aggiungere un indirizzo alla lista
profileForm.controls.addresses.push(
formRecord({
type: formSignal<'home' | 'work'>('work'),
street: formSignal('', { validators: [required()] }),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [required()] })
})
);
자동 상태 전파
그룹 상태가 내부적으로 계산되는 반응형 양식과 달리
신호 형태에서 일시적인 불일치가 발생할 수 있습니다. formRecord
그것은 계산된 신호 그것은 그의 자녀에 달려 있습니다. 이는 다음을 의미합니다.
formRecord.isValid()ècomputed(() => children.every(c => c.isValid()))formRecord.isDirty()ècomputed(() => children.some(c => c.isDirty()))- 결함 없는 스케줄링 덕분에 일관성 없는 중간 상태가 없습니다.
- 레코드를 재설정하면 모든 하위 항목이 원자적으로 재설정됩니다.
model() 및 신호 형식을 사용한 양방향 바인딩
Angular에서 이 기능을 도입했습니다. model() 처럼 양방향 바인딩 가능 신호
구성 요소의 경우. Signal Forms는 다음과 완벽하게 통합됩니다. model(), 생성
필요 없이 템플릿과 양식 상태 간의 깔끔한 양방향 데이터 흐름
다음과 같은 특별한 지시문의 formControlName.
// Componente input personalizzato con model()
@Component({
selector: 'app-text-input',
standalone: true,
template: `
<div class="input-wrapper" [class.invalid]="hasError()">
<label>{{ label() }}</label>
<input
[value]="value()"
(input)="onInput($event)"
(blur)="onBlur()"
[placeholder]="placeholder()"
/>
@if (hasError() && isTouched()) {
<span class="error-message">{{ errorMessage() }}</span>
}
</div>
`
})
export class TextInputComponent {
// Two-way binding con il componente padre
value = model<string>('');
label = input<string>('');
placeholder = input<string>('');
hasError = input<boolean>(false);
isTouched = input<boolean>(false);
errorMessage = input<string>('');
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(target.value);
}
onBlur() {
// Notifica il padre che il campo e stato toccato
}
}
// Utilizzo nel componente padre con Signal Forms
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [TextInputComponent],
template: `
<form (ngSubmit)="submit()">
<app-text-input
[(value)]="name.value"
[label]="'Nome completo'"
[hasError]="name.isInvalid()"
[isTouched]="name.isTouched()"
[errorMessage]="nameError()"
/>
<app-text-input
[(value)]="email.value"
[label]="'Email'"
[hasError]="email.isInvalid()"
[isTouched]="email.isTouched()"
[errorMessage]="emailError()"
/>
<button [disabled]="!isFormValid()">Invia</button>
</form>
`
})
export class ContactFormComponent {
name = formSignal('', { validators: [required()] });
email = formSignal('', { validators: [required(), email()] });
nameError = computed(() => {
if (this.name.errors()?.['required']) return 'Nome obbligatorio';
return '';
});
emailError = computed(() => {
if (this.email.errors()?.['required']) return 'Email obbligatoria';
if (this.email.errors()?.['email']) return 'Email non valida';
return '';
});
isFormValid = computed(() =>
this.name.isValid() && this.email.isValid()
);
submit() {
console.log({ name: this.name.value(), email: this.email.value() });
}
}
model() + 신호 양식 통합의 이점
- 특별한 지시문이 없습니다: 필요하지 않습니다
[formControl]oformControlName템플릿에서 - 재사용 가능한 구성 요소: 사용자 정의 입력은 다음을 통해 값을 수신하고 반환합니다.
model(), 양식 논리와 완전히 독립적 - 완전한 유형 안전성: 값의 유형은 FormSignal에서 model()을 거쳐 템플릿으로 흐릅니다.
- 테스트 가능성: FormModule 없이 입력 구성 요소를 별도로 테스트할 수 있습니다.
오늘날의 실용적인 패턴: 하이브리드 신호 + 반응형 접근 방식
신호 양식이 아직 RFC 단계에 있는 동안 신호 활용을 시작할 수 있습니다.
현재 Reactive Forms를 사용하여 하이브리드 접근 방식 얼마나 쉬운가
향후 마이그레이션. 이는 다음을 사용하는 것을 의미합니다. toSignal() 변환하기 위해
신호의 형태를 관찰하고 파생된 상태를 구성할 수 있습니다. computed().
@Component({
selector: 'app-search',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="searchControl" placeholder="Cerca..." />
@if (isSearching()) {
<span class="spinner">Ricerca in corso...</span>
}
@if (hasResults()) {
<p>Trovati {{ resultCount() }} risultati per "{{ searchTerm() }}"</p>
}
@for (result of results(); track result.id) {
<app-result-card [result]="result" />
} @empty {
@if (searchTerm().length > 0) {
<p>Nessun risultato per "{{ searchTerm() }}"</p>
}
}
`
})
export class SearchComponent {
private searchService = inject(SearchService);
searchControl = new FormControl('', { nonNullable: true });
// Converto valueChanges in Signal
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
// Stato derivato con computed
private searchResult = toSignal(
toObservable(this.searchTerm).pipe(
switchMap(term =>
term.length > 2
? this.searchService.search(term)
: of({ items: [], total: 0 })
)
),
{ initialValue: { items: [], total: 0 } }
);
results = computed(() => this.searchResult().items);
resultCount = computed(() => this.searchResult().total);
hasResults = computed(() => this.resultCount() > 0);
isSearching = signal(false);
}
전환 전략: 반응형에서 신호형으로
- 1단계(오늘): 전환하다
valueChanges시그널에서toSignal(). 미국computed()파생 상태별 - 2단계(오늘): 양식 로직을 Signal을 노출하는 전용 서비스로 분리
- 3단계(사용 가능한 경우): FormControl을 FormSignals로 한 번에 하나씩 교체
- 4단계: 제거하다
ReactiveFormsModule및 양식과 관련된 RxJS 종속성
신호가 포함된 사용자 정의 양식 컨트롤
인터페이스 ControlValueAccessor (CVA)는 현재 생성 메커니즘입니다.
사용자 정의 양식 구성 요소. 신호 양식을 사용하면 이 패턴이 단순화됩니다.
과감하게. 그러나 오늘날 Signals를 활용하여 더욱 깔끔한 CVA를 구축하는 것이 이미 가능합니다.
// CVA attuale migliorato con Signals
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
<div class="star-rating" [class.disabled]="isDisabled()">
@for (star of stars(); track star) {
<button
class="star"
[class.filled]="star <= currentValue()"
[class.hovered]="star <= hoveredStar()"
(mouseenter)="hoveredStar.set(star)"
(mouseleave)="hoveredStar.set(0)"
(click)="selectRating(star)"
[disabled]="isDisabled()">
★
</button>
}
<span class="label">{{ currentValue() }} / {{ maxStars() }}</span>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true
}
]
})
export class StarRatingComponent implements ControlValueAccessor {
maxStars = input(5);
currentValue = signal(0);
hoveredStar = signal(0);
isDisabled = signal(false);
stars = computed(() =>
Array.from({ length: this.maxStars() }, (_, i) => i + 1)
);
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
writeValue(value: number): void {
this.currentValue.set(value || 0);
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.isDisabled.set(isDisabled);
}
selectRating(star: number): void {
if (this.isDisabled()) return;
this.currentValue.set(star);
this.onChange(star);
this.onTouched();
}
}
CVA 현재와 신호 형태의 미래
| 나는 기다린다 | 현재 CVA | 신호 형태(향후) |
|---|---|---|
| 구현할 인터페이스 | ControlValueAccessor (4가지 방법) |
인터페이스가 없고 그냥 model() |
| 등록 | NG_VALUE_ACCESSOR 공급자 |
바인딩을 통해 자동 [(value)] |
| onChange/onTouched 콜백 | 수동, 필수 | 자동, 신호를 통해 반응 |
| 상태가 비활성화됨 | setDisabledState() 피할 수 없는 |
신호 isDisabled() 부모로부터 |
| 상용구 | ~30줄의 CVA 코드 | model() 포함 ~5줄 |
// Futuro: nessun ControlValueAccessor, nessun provider
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
<div class="star-rating">
@for (star of stars(); track star) {
<button
class="star"
[class.filled]="star <= value()"
(click)="value.set(star)">
★
</button>
}
</div>
`
})
export class StarRatingComponent {
// model() gestisce tutto: two-way binding, notifiche, stato
value = model(0);
maxStars = input(5);
stars = computed(() =>
Array.from({ length: this.maxStars() }, (_, i) => i + 1)
);
// Fine. Nessun CVA, nessun provider, nessuna callback.
}
// Nel parent:
// <app-star-rating [(value)]="rating.value" />
계획된 마이그레이션: 예상되는 사항
Signal Forms는 현재 RFC 단계에 있으며 Angular 팀은 소개는 될 것이다 점진적이고 깨지지 않는. 반응형 양식은 계속됩니다 여러 주요 릴리스에 대해 지원되므로 점진적인 마이그레이션이 가능합니다.
예상 마이그레이션 일정
| 단계 | 예상 버전 | 무엇을 기대해야 할까요? |
|---|---|---|
| RFC 및 피드백 | 각도 19-20 | 커뮤니티로부터 피드백 수집, API 반복 |
| 개발자 미리보기 | 각도 20-21 | API를 사용할 수 있지만 변경될 수 있음 |
| 안정적인 | 각도 21-22 | 안정적인 API, 공식 마이그레이션 도식 |
| 추천 | 각도 22+ | 문서의 기본 접근 방식인 신호 형식 |
| RF 지원 중단 | 각도 24+ | 더 이상 사용되지 않는 것으로 표시된 반응형 양식(확장 지원 포함) |
마이그레이션 준비 전략
- 입력된 양식을 채택하십시오: 입력하지 않고 여전히 FormControl을 사용하는 경우 첫 번째 단계로 Angular 14에 도입된 Typed Reactive Forms로 마이그레이션하세요.
- 미국
nonNullable: 다음을 사용하여 FormControl을 구성합니다.nonNullable: trueSignal Forms의 동작에 맞추기 위해 - 양식 논리를 분리합니다. 양식 생성 및 관리를 전용 서비스로 이동하여 마이그레이션에는 구성 요소가 아닌 서비스만 포함됩니다.
- 사용을 줄이세요
valueChanges: 가능한 경우toSignal(control.valueChanges)Signal 작업을 시작하려면 - 순수 검증 기능을 선호합니다: 복잡한 종속성을 갖는 클래스가 아닌 오류를 반환하는 순수 함수로 유효성 검사기를 작성합니다.
- AbstractControl 확장을 피하세요. FormControl 또는 FormGroup의 하위 클래스를 생성하지 마십시오. 신호 양식에는 이에 상응하는 클래스가 없기 때문입니다.
- 유효성 검사 규칙을 문서화합니다. 변환을 용이하게 하기 위해 필드와 유효성 검사기 간의 명확한 매핑을 유지합니다.
// Service che isola la logica del form
// Oggi: usa Reactive Forms internamente, espone Signal
// Domani: sostituisci l'implementazione con Signal Forms
@Injectable({ providedIn: 'root' })
export class ProfileFormService {
private fb = inject(FormBuilder);
// Implementazione interna con Reactive Forms
private _form = this.fb.nonNullable.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
bio: ['', Validators.maxLength(500)]
});
// API pubblica basata su Signal
readonly name = toSignal(
this._form.controls.name.valueChanges,
{ initialValue: '' }
);
readonly email = toSignal(
this._form.controls.email.valueChanges,
{ initialValue: '' }
);
readonly isValid = toSignal(
this._form.statusChanges.pipe(map(s => s === 'VALID')),
{ initialValue: false }
);
readonly isDirty = toSignal(
this._form.valueChanges.pipe(map(() => this._form.dirty)),
{ initialValue: false }
);
// Metodi pubblici che restano invariati dopo la migrazione
setValues(data: Partial<ProfileData>) {
this._form.patchValue(data);
}
getValues(): ProfileData {
return this._form.getRawValue();
}
reset() {
this._form.reset();
}
}
Angular의 양식에 대한 모범 사례
템플릿 기반 양식, 반응형 양식 또는 Signal Forms를 준비 중입니다. 일부 모범 사례는 보편적이며 더욱 강력하고 유지 관리가 가능하며 미래에 대비한 양식을 작성하는 데 도움이 될 것입니다.
Angular Forms에 대한 10가지 모범 사례
- 올바른 양식 유형을 선택하세요. 간단한 양식을 위한 템플릿 기반(로그인, 연락처), 복잡한 양식을 위한 반응성/신호(다단계, 동적, 고급 검증 포함)
- 서비스 중인 로직을 분리합니다. 구성 요소에서 직접 FormGroup을 생성하지 마십시오. Signal을 노출하는 전용 서비스를 사용하여 테스트 및 재사용을 촉진합니다.
- 선언적 검증: 유효성 검사기를 순수하고 재사용 가능한 함수로 정의하세요. 구성 요소의 인라인 논리가 아님
-
터치 후에만 오류 표시: 다음까지 오류 메시지를 표시하지 마세요.
사용자가 필드와 상호 작용하지 않는 경우(
isTouched()oisDirty()) - 로딩 상태 관리: 동안 제출 버튼을 비활성화합니다. 비동기 유효성 검사 및 로딩 표시기를 보여줍니다.
-
미국
nonNullable: 항상 FormControl을 구성하십시오.nonNullable: true가치관을 피하기 위해null재설정 후 예기치 않은 -
효과 계산을 선호합니다: 양식의 상태(유효성, 오류,
완성)으로
computed()와보다는effect()osubscribe() - 격리된 테스트 검증기: 각 유효성 검사기에 대한 단위 테스트 작성 구성요소와 관계없이 순수 기능으로 맞춤화됨
- 제출물을 주의해서 관리하세요. 이중 제출 방지, HTTP 오류 처리, 사용자에게 명확한 피드백 표시(성공/오류)
-
마이그레이션을 준비합니다. Reactive Forms를 사용하는 경우 점진적으로 변환하세요.
valueChanges시그널에서toSignal()미래의 전환을 촉진하기 위해
언제 어떤 유형의 양식을 사용해야 할까요?
| 대본 | 템플릿 기반 | 반응형 양식 | 신호 형태(향후) |
|---|---|---|---|
| 간단한 로그인/문의 양식 | 조언 | 허용됨 | 조언 |
| 다단계 양식/마법사 | 권장되지 않음 | 조언 | 조언 |
| 동적 양식(생성된 필드) | 권장되지 않음 | 조언 | 조언 |
| 복잡한 교차 필드 검증 | 어려운 | 다루기 쉬운 | 단순한 |
| 풍부한 파생 상태가 있는 양식 | 제한된 | RxJS를 사용하면 | 네이티브(계산) |
| 신호와의 통합 | 부분 | toSignal()을 사용하면 | 토종의 |
| 성능(변화 감지) | Zone.js에 따라 다름 | Zone.js에 따라 다름 | 세분화된 |
양식에서 피해야 할 안티패턴
- 사용하지 마십시오
ngModel반응형 양식 사용: 동일한 형식에서 템플릿 기반과 반응형을 혼합하면 예측할 수 없는 동작이 발생합니다. - 정리를 잊지 마세요: 반응형 양식을 사용하면 모든
subscribe()avalueChanges으로 관리해야 합니다.takeUntilDestroyed()oDestroyRef - 클라이언트측만 검증하지 마세요: 클라이언트 측 검증은 UX를 향상시키지만 보안을 위해 서버 측 검증은 필수입니다.
- 접근성을 무시하지 마세요. 미국
aria-describedby오류 메시지를 필드에 연결하려면 earia-invalid잘못된 필드를 신고하려면 - 사용하지 마십시오
getRawValue()아무 이유 없이: 양식에 비활성화된 필드가 없는 경우.value그리고 충분하다
요약 및 다음 단계
신호 형식은 프레임워크를 향한 Angular의 자연스러운 진화 단계를 나타냅니다. 완전히 신호 기반입니다. 양식 상태에 대한 RxJS에 대한 종속성을 제거하여 감소 상용구와 기본적으로 세분화된 응답성을 통합하는 Signal Forms는 약속합니다. 양식 관리를 더욱 간단하고 안전하며 효율적으로 만듭니다.
이 기사의 주요 개념
- 반응형 양식 제한: RxJS에 대한 종속성, 과도한 상용구, 변경 가능한 상태, 엄격한 AbstractControl 계층 구조, 신호와의 통합 없음
- 신호 형식 RFC: 새로운 프리미티브(
formSignal,formRecord,formList) FormControl, FormGroup, FormArray를 대체할 신호 기반 - 반응 상태:
isValid(),isDirty(),isTouched(),errors()계산된 신호는 항상 일관성이 있기 때문에 - 단순화된 검증: 순수 함수로서의 검증기, 비동기용 디바운스 내장, 필드 간 반응성
- formRecord를 사용한 구성: 상태가 자식에서 부모로 자동 전파됨, 원자 재설정, 추론된 입력
- model() + 신호 형식: ControlValueAccessor가 없는 양방향 바인딩, 재사용 가능한 최소 양식 구성 요소
- 하이브리드 접근 방식: 사용
toSignal()지금 Reactive Forms를 사용하여 마이그레이션을 준비하세요 - 단순화된 CVA: 미래에,
model()ControlValueAccessor 인터페이스를 완전히 대체합니다. - 점진적 마이그레이션: Reactive Forms는 오랫동안 지원될 예정이며 마이그레이션은 공식 회로도의 지원을 받을 것입니다.
시리즈의 다음 기사에서 우리는 탐구할 것입니다 SSR 및 증분 수화, Angular가 성능을 달성할 수 있도록 하는 서버 측 렌더링 기술 점진적인 수분 공급으로 상호 작용 시간을 단축하는 뛰어난 로딩 시간 서버에서 클라이언트로 로직을 점진적으로 전송합니다.







