소개
핵심 가이드

시그널 폼

시그널 폼은 Angular 시그널을 사용해서 폼 상태를 관리하며, 데이터 모델과 UI를 동기화할 때도 Angular 시그널을 활용합니다.

이 문서에서는 시그널 폼을 어떻게 만드는지 핵심을 알아봅시다. 이렇게 구현합니다:

첫번째 폼 만들기

1. 폼 모델 생성하기: signal()

모든 폼은 폼 데이터 모델을 저장할 시그널을 만드는 것부터 시작합니다:

interface LoginData {
  email: string;
  password: string;
}

const loginModel = signal<LoginData>({
  email: '',
  password: '',
});

2. form()을 실행해서 폼 모델을 FieldTree로 변환하기

그리고 폼 모델을 form() 함수로 전달해서 이 폼 모델을 필드 트리 로 변환합니다. 필드 트리는 데이터 모델을 표현하는 객체이며, 객체이기 때문에 객체 프로퍼티처럼 접근할 수 있습니다.

최상위 폼 객체와 그 안에 중첩된 프로퍼티는 모두 FieldTree 노드입니다:

const loginForm = form(loginModel);

loginForm; // FieldTree입니다.
loginForm.email; // 이 값도 FieldTree입니다.

3. [formField] 디렉티브로 HTML 입력 필드를 바인딩합니다.

그다음에는 HTML 입력 엘리먼트에 [formField] 디렉티브를 양방향 바인딩해서 폼과 연결합니다:

<input type="email" [formField]="loginForm.email" />
<input type="password" [formField]="loginForm.password" />

그러면 사용자가 입력 엘리먼트의 값을 변경하면 폼에 있는 데이터도 자동으로 갱신됩니다.

참고: [formField] 디렉티브는 required, disabled, readonly와 같은 어트리뷰트 값도 동기화합니다.

4. FieldTree 시그널로 상태 읽기

FieldTree 노드를 함수처럼 실행하면 트리의 어느 부분에서든 상태에 접근할 수 있습니다. 반환된 상태 객체에는 값, 유효성 검사 상태, 상호작용 상태를 표현하는 반응형 시그널이 포함됩니다:

loginForm(); // 폼 전체의 상태를 반환합니다.
loginForm.email(); // 이메일 필드의 상태를 반환합니다.

현재 값을 읽으려면 value() 시그널을 읽으면 됩니다:

<!-- 사용자가 입력한 값을 화면에 렌더링합니다. -->
<p>Form value: {{ loginForm().value() | json }}</p>
<p>Email: {{ loginForm.email().value() }}</p>
// 현재값을 참조합니다.
const currentEmail = loginForm.email().value();

5. 값 변경하기: set()

어떤 노드에서든 value.set() 메서드를 사용해서 코드로 값을 변경할 수 있습니다. 이 메서드를 실행하면 FieldTree와 모델 시그널 값을 모두 변경합니다:

// 코드로 필드 값을 변경합니다.
loginForm.email().value.set('alice@wonderland.com');

그러면 필드 값과 모델 시그널 값이 모두 변경된 것을 확인할 수 있습니다:

// 모델 시그널도 함께 변경됩니다.
console.log(loginModel().email); // 'alice@wonderland.com'

전체 예제

활용 방법

[formField] 디렉티브는 표준 HTML 입력 필드라면 어디에라도 사용할 수 있습니다. 이렇게 사용하면 됩니다:

텍스트 입력

텍스트 입력은 다양한 type과 함께 사용할 수 있습니다:

<!-- 텍스트, 이메일 -->
<input type="text" [formField]="form.name" />
<input type="email" [formField]="form.email" />

숫자

숫자 입력 필드에 바인딩하면 문자열 타입과 숫자 타입을 자동으로 변환합니다:

<!-- 숫자 - 숫자 타입으로 자동 변환합니다. -->
<input type="number" [formField]="form.age" />

날짜, 시각

날짜 입력 필드는 YYYY-MM-DD 문자열 타입으로 값을 저장하며, 시각 입력 필드는 HH:mm 형식으로 값을 저장합니다:

<!-- 날짜, 시각 - ISO 문자열 형식으로 값을 저장합니다. -->
<input type="date" [formField]="form.eventDate" />
<input type="time" [formField]="form.eventTime" />

문자열 타입 날짜를 Date 객체로 변환하려면 Date() 생성자를 활용하면 됩니다:

const dateObject = new Date(form.eventDate().value());

여러 줄 텍스트

문자 입력 필드와 동일합니다:

<!-- Textarea -->
<textarea [formField]="form.message" rows="4"></textarea>

체크박스

체크박스는 불리언 값을 바인딩합니다:

<!-- 단일 체크박스 -->
<label>
  <input type="checkbox" [formField]="form.agreeToTerms" />
  I agree to the terms
</label>

여러 체크박스

체크박스가 여러 개라면 개별 필드마다 불리언 값을 연결합니다:

<label>
  <input type="checkbox" [formField]="form.emailNotifications" />
  Email notifications
</label>
<label>
  <input type="checkbox" [formField]="form.smsNotifications" />
  SMS notifications
</label>

라디오 버튼

라디오 버튼은 체크박스와 비슷하게 동작합니다. 라디오 버튼의 [formField] 값이 같다면, 시그널 폼은 이 라디오 버튼에 같은 name 어트리뷰트 값을 바인딩합니다:

<label>
  <input type="radio" value="free" [formField]="form.plan" />
  Free
</label>
<label>
  <input type="radio" value="premium" [formField]="form.plan" />
  Premium
</label>

이렇게 구현하면 사용자가 라디오 버튼을 클릭했을 때 폼의 field가 개별 라디오 버튼의 value 어트리뷰트값을 저장합니다. 그래서 "Premium" 버튼을 선택하면 form.plan().value() 값은 "premium"이 됩니다.

드롭다운 셀렉트

셀렉트 엘리먼트는 정적인 옵션과 동적인 옵션을 모두 사용할 수 있습니다:

<!-- 정적 옵션 -->
<select [formField]="form.country">
  <option value="">Select a country</option>
  <option value="us">United States</option>
  <option value="ca">Canada</option>
</select>

<!-- @for 를 사용하는 동적 옵션 -->
<select [formField]="form.productId">
  <option value="">Select a product</option>
  @for (product of products; track product.id) {
    <option [value]="product.id">{{ product.name }}</option>
  }
</select>

참고: 셀렉트 엘리먼트에 multiple 옵션을 지정하면 [formField] 디렉티브를 사용할 수 없습니다.

유효성 검사, 유효성 검사 상태

시그널 폼은 폼 필드에 사용할 수 있는 유효성 검사 함수를 기본으로 제공합니다. 폼 필드에 유효성 검사 함수를 적용하려면 form() 함수를 실행할 때 두번째 인자로 스키마 함수(schema function)를 전달하면 됩니다:

const loginForm = form(loginModel, (schemaPath) => {
  debounce(schemaPath.email, 500);
  required(schemaPath.email);
  email(schemaPath.email);
});

스키마 함수는 유효성 검사 규칙을 지정하는 스키마 경로(schema path) 를 인자로 받습니다.

이런 함수를 자주 사용합니다:

  • required() - 필드에 값이 존재해야 합니다.
  • email() - 이메일 형식인지 검사합니다.
  • min() / max() - 입력된 숫자의 범위를 검사합니다.
  • minLength() / maxLength() - 문자열이나 배열의 길이를 검사합니다.
  • pattern() - 정규표현식 형식에 맞는지 검사합니다.

그리고 유효성 검사 함수의 두번째 인자로 옵션 객체를 전달하면서 오류 메시지를 커스터마이징할 수 있습니다:

required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Please enter a valid email address'});

FieldTree의 각 노드는 유효성 검사 상태와 상호작용 상태를 반응형 시그널로 표현합니다.

FieldTree 상태 시그널

최상위 폼 객체를 포함한 트리의 모든 노드는 상태를 확인하는 동일한 시그널을 제공합니다. 모든 노드가 FieldTree이므로 어느 단계에서든 같은 API로 유효성 검사 상태와 상호작용 상태를 확인할 수 있습니다.

상태 설명
valid() 노드가 모든 유효성 검사 규칙을 통과하면 true를 반환합니다.
invalid() 유효성 검사 오류가 있으면 true를 반환합니다.
pending() 비동기 유효성 검사가 진행 중이면 true를 반환합니다.
touched() 사용자가 해당 필드나 자식 필드에 접근했다가 빠져나가면 true를 반환합니다.
dirty() 사용자가 값을 변경했으면 true를 반환합니다.
disabled() 노드가 비활성화되면 true를 반환합니다.
readonly() 노드가 읽기 전용이면 true를 반환합니다.
errors() kind, message 프로퍼티가 있는 유효성 검사 오류 배열을 반환합니다.

전체 예제

다음 단계

시그널 폼이 동작하는 것을 더 자세하게 알아보려면 이런 내용을 확인해 보세요:

  • 개요 - 시그널 폼이 무엇인지, 언제 사용하는지 설명합니다.
  • 폼 모델 - 폼 데이터를 시그널로 생성하고 관리합니다.
  • 필드 상태 관리 - 유효성 검사 결과를 활용하는 방법, 사용자 상호작용 상태 확인, 필드의 가시성을 관리합니다.
  • 유효성 검사 - 기본 유효성 검사 함수, 커스텀 유효성 검사, 비동기 유효성 검사를 다룹니다.