사용자가 폼을 제출하면 애플리케이션은 보통 여러 작업을 동시에 처리해야 합니다: 유효성 검사 결과를 표시하고, 중복 제출을 방지해야 하며, 서버로 데이터를 보내는 등의 작업이 그렇습니다. 이런 작업을 각각 수동으로 처리하는 것은 번거로운 일이며, 오류 가능성도 올리는 일입니다.
시그널 폼은 폼 제출 과정을 단순하게 관리하도록 submit() 함수를 제공합니다.
이 문서에서는 submit() 함수의 사용 방법을 알아봅시다.
submit() 함수는 뭘 하나요?
submit() 함수는 이런 과정을 수행합니다:
- 상호작용 가능한 필드를
touched상태로 바꿉니다 — touched 상태가 된 뒤에만 오류를 표시하는 필드에도 이제 유효성 검사 오류가 표시됩니다. 숨겨진 필드, 비활성화된 필드, 읽기 전용 필드는 이 처리에서 제외합니다. - 유효성 검사를 실행합니다 — 유효성 검사가 실패하면 폼 제출을 멈추고
action함수가 실행되지 않습니다. action함수를 실행합니다 —action함수는 폼의 현재 값을 사용하여 실행됩니다. 이 함수가 실행되는 동안submitting()함수는true값을 반환합니다.- 결과를 처리합니다 —
action함수가 오류를 반환하면 오류가 해당 필드로 전달됩니다. 오류가 발생하지 않으면 폼 제출이 처리됩니다.
submit() 함수는 Promise<boolean> 타입을 반환합니다. action 함수가 오류 없이 완료되면 true로 이행되고, 유효성 검사에 실패하거나 action 함수가 오류를 반환하거나 이미 제출이 진행 중이면 false로 이행됩니다. action 함수가 예외를 던지면 Promise는 그 오류로 거부(reject)됩니다.
FormRoot에 폼 제출 방법 등록하기
submit() 함수를 사용하는 가장 일반적인 방법은 FormRoot 디렉티브를 통하는 것입니다.
FormRoot 디렉티브가 <form> 엘리먼트에 바인딩되면 다음 작업을 자동으로 처리합니다:
novalidate를 설정합니다 — 브라우저의 기본 유효성 검사를 비활성화하여 시그널 폼이 유효성 검사를 처리하도록 합니다.- 기본값 방지 — 브라우저의 내장 폼 제출 기능을 방지합니다
submit()함수를 실행합니다 — 폼을submission옵션으로 생성한 경우, 사용자가 폼을 제출하면 제출 과정을 시작합니다.
참고: FormRoot 디렉티브는 form 엘리먼트에 novalidate 어트리뷰트를 자동으로 지정합니다.
수동으로 지정할 필요는 없습니다.
FormRoot가 폼 제출 이벤트를 받더라도 개발자는 폼 데이터로 무엇을 할지 지정해야 합니다.
일반적으로 3가지가 필요합니다:
- 폼과
FormRoot디렉티브를 바인딩합니다. form()함수에submission옵션을 전달합니다.- 폼 제출 데이터를 관리하기 위해
submission옵션에action함수를 정의합니다.
import {Component, signal} from '@angular/core';
import {form, FormField, FormRoot, required} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField, FormRoot],
template: `
<form [formRoot]="contactForm">
<label>
Name
<input [formField]="contactForm.name" />
</label>
<label>
Email
<input type="email" [formField]="contactForm.email" />
</label>
<button type="submit">Send</button>
</form>
`,
})
export class Contact {
contactModel = signal({
name: '',
email: '',
});
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to submit form'};
},
},
},
);
}
action 함수는 유효성 검사가 모두 통과할 때만 실행됩니다.
기본적으로, 비동기 유효성 검사는 폼 제출을 차단하지 않습니다.
자세한 내용은 유효성 검사 타이밍 제어 섹션을 참고하세요.
action 함수는 필드 트리와 함께 root와 submitted 필드 트리를 표현하는 detail 객체를 인자로 받습니다.
유효성 검사가 통과된 후에도, 네트워크 오류나 중복 오류와 같은 상황으로 action 함수는 실패할 수 있습니다.
이런 경우에는 오류를 반환해서 화면에 오류를 표시할 수 있습니다.
반대로, 오류가 아님을 표현하려면 action 함수가 null 이나 undefined을 반환하거나, 빈 값으로 return 을 실행하면 됩니다.
폼 제출 상태 표시하기: submitting()
폼 제출이 진행되는 상태를 추적하려면 시그널 폼의 submitting() 시그널을 확인하면 됩니다.
action 함수가 실행되는 중에는 이 시그널이 true를 반환합니다.
이 시그널은 로딩 중임을 표시하거나 중복 제출을 막기 위해 폼 제출 버튼을 비활성화할 때 사용하면 좋습니다.
<button type="submit" [disabled]="contactForm().submitting()">
@if (contactForm().submitting()) {
Sending...
} @else {
Send
}
</button>
action 함수 실행이 완료되거나 오류를 반환하면 submitting() 시그널은 false를 반환합니다.
폼 제출 오류 관리하기
서버 오류
action 함수가 서버와 통신하고 나서 서버가 특정 필드에서 오류가 발생했다고 결과를 반환할 수 있습니다.
이 경우에는 서버에서 받은 오류를 action으로 반환해서 화면에 있는 필드에서 오류가 발생했다고 표시하면 됩니다.
필드 오류
기본적으로 action 함수에서 반환하는 오류는 제출 대상 필드, 즉 submit()에 전달한 필드 트리에 할당됩니다:
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to submit form'};
};
특정 필드에서 발생한 오류
오류가 발생한 것을 특정 필드에 표시하려면 fieldTree 프로퍼티로 해당 필드를 가리키면 됩니다:
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'taken', message: result.message, fieldTree: field.email};
};
동시에 발생한 오류
여러 필드에서 오류가 동시에 발생한 것을 표시하려면 오류를 배열 형태로 반환하면 됩니다:
action: async (field) => {
const result = await registerUser(field().value());
if (result.ok) return;
return result.errors.map((err: {field: string; message: string}) => ({
kind: 'serverError',
message: err.message,
fieldTree: field[err.field as keyof typeof field],
}));
};
폼 제출 오류 초기화하기
사용자가 필드값을 수정하면 폼 제출 오류가 자동으로 초기화됩니다.
만약 email 필드에서 발생한 오류가 action으로 반환되었다면, 사용자가 email 필드의 값을 변경하면서 오류가 사라집니다.
사용자가 필드값을 변경했을 때 실행되는 유효성 검사와는 다릅니다. 유효성 검사 규칙은 필드값이 변경될 때마다 실행되며 같은 오류가 계속 발생할 수 있습니다. 폼 제출 오류는 서버에서 한 번 전달된 결과입니다. 사용자가 필드 값을 수정해 오류가 지워지면, 폼을 다시 제출하기 전까지는 다시 나타나지 않습니다.
팁: 폼 제출 오류는 필드의 errors() 시그널로 유효성 검사 오류와 함께 전달됩니다.
템플릿에 오류를 표시하는 방법은 필드 상태 관리 문서를 참고하세요.
유효성 검사 오류 처리하기: onInvalid
유효성 검사가 실패하면 action 함수는 실행되지 않습니다.
오류가 발생한 첫번째 위치로 이동하거나, 토스트를 표시하고, 오류가 발생한 필드에 포커스를 옮기는 등 폼 제출 단계에서 발생한 오류에 반응할 때는 onInvalid 콜백을 사용합니다.
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
await saveContact(field().value());
},
onInvalid: (field) => {
const firstError = field().errorSummary()[0];
firstError?.fieldTree().focusBoundControl();
},
},
},
);
onInvalid 콜백 함수는 action과 마찬가지로 (field, detail) 인자를 받습니다.
onInvalid 함수는 모든 필드가 touched로 처리된 후 실행되기 때문에, 이 함수가 실행되는 시점에는 화면에 이미 오류가 표시되어 있을 것입니다.
유효성 검사 타이밍 제어하기: ignoreValidators
기본적으로 submit()은 진행 중인 유효성 검사는 무시합니다.
그리고 실패한 유효성 검사가 없다면, 비동기 유효성 검사가 진행 중이더라도 action이 실행됩니다.
이 동작을 변경하려면 ignoreValidators 옵션을 지정하면 됩니다.
| 값 | 동작 |
|---|---|
'pending' |
보류 중인 유효성 검사가 있어도, 유효성 검사가 실패하지 않으면 폼을 제출합니다(기본값) |
'none' |
모든 유효성 검사가 통과된 후에 폼을 제출합니다. 비동기 유효성 검사가 진행 중이면 폼은 제출되지 않습니다. |
'all' |
유효성 검사 결과와 관계없이 폼을 제출합니다. |
contactForm = form(
this.contactModel,
(schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
},
{
submission: {
action: async (field) => {
await saveContact(field().value());
},
ignoreValidators: 'none',
},
},
);
폼에서 사용자 이름을 중복 체크하는 것과 같은 비동기 유효성 검사가 포함되어 있으면 모든 유효성 검사가 종료된 후에 폼을 제출하는 것이 좋습니다.
이 경우에는 'none' 값을 사용하세요.
유효성 검사 상태에 관계없이 일단 데이터를 저장해야 하는 경우라면 'all' 값을 사용하세요.
수동 폼 제출: submit()
일반적으로 폼을 제출하려면 FormRoot 디렉티브를 사용하지만, submit()을 직접 실행할 수도 있습니다.
여러 단계로 구성된 마법사, 자동 저장, 폼 엘리먼트 밖에서 폼 제출을 제어할 때 이 방식이 유용합니다.
import {Component, signal} from '@angular/core';
import {form, FormField, required, submit} from '@angular/forms/signals';
@Component({
selector: 'app-contact',
imports: [FormField],
template: `
<label>
Name
<input [formField]="contactForm.name" />
</label>
<label>
Email
<input type="email" [formField]="contactForm.email" />
</label>
<button (click)="onSave()">Save</button>
`,
})
export class Contact {
contactModel = signal({
name: '',
email: '',
});
contactForm = form(this.contactModel, (schemaPath) => {
required(schemaPath.name);
required(schemaPath.email);
});
async onSave() {
// `submit()`을 직접 실행하면, 두번째 인자로 `FormOptions` 대신 실행 함수를 전달합니다.
const success = await submit(this.contactForm, async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return {kind: 'serverError', message: 'Failed to save'};
});
if (success) {
// 폼 제출에 성공했을 때 로직을 작성합니다 — 화면 이동, 확인 메시지 등
}
}
}
추가 동작 실행하기
submit() 함수는 Promise<boolean>을 반환합니다. action이 오류 없이 종료되면 true를 반환하고, 유효성 검사에 실패하거나 action이 오류를 반환하거나 이미 제출이 진행 중이면 false를 반환합니다. action이 예외를 던지면 Promise는 거부(reject)됩니다.
네비게이션이나 사용자 알림을 수행하려면 이 방식을 활용하면 됩니다.
async onSave() {
const success = await submit(this.contactForm, async (field) => {
await saveContact(field().value());
});
if (success) {
await this.router.navigate(['/confirmation']);
}
}
추가 동작을 실행하면서 데이터를 전달하려면 함수 안에서 처리하면 됩니다:
async onSave() {
await submit(this.contactForm, async (field) => {
const contact = await createContact(field().value());
await this.router.navigate(['/confirmation', contact.id]);
});
}
FormRoot를 사용할 때도 후속 동작을 action 안에 작성하세요. FormRoot가 내부에서 submit()을 호출하기 때문입니다.
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) {
await this.router.navigate(['/confirmation']);
return;
}
return {kind: 'serverError', message: 'Failed to submit form'};
},
}
동시 제출
폼 제출이 진행 중일 때 같은 폼이나 하위 폼에서 다시 한 번 submit()을 실행하면 폼 제출 함수가 실행되지 않고 false 값을 즉시 반환합니다.
이 동작은 사용자가 폼 제출 버튼을 빠르게 여러 번 누르는 경우 발생할 수 있는 폼 중복 제출을 방지하기 위한 것입니다.
다음 단계
이 문서에서는 폼을 제출하고 그 과정에서 발생하는 오류를 처리하는 방법을 알아봤습니다. 이런 내용도 확인해 보세요: