Angular 21에서 Signal Forms

발행: (2026년 2월 12일 오후 05:02 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역하고자 하는 본문 텍스트를 제공해 주시겠어요?
코드 블록이나 URL은 그대로 유지하고, 본문만 한국어로 번역해 드리겠습니다.

Angular 21 Signal Forms – 새로운 사고 모델

수년간 Angular 폼은 하나의 의미만을 가지고 있었습니다: FormGroup, FormControl, valueChanges, 그리고 우리가 거의 기계적으로 탐색하는 AbstractControl 트리.
오랫동안 Angular를 사용해 왔다면, 그곳에 매우 익숙할 것입니다.

하지만 Angular 21은 단순히 새로운 API를 도입하는 것이 아니라 다른 사고 모델을 제시합니다.

Signal Forms는 Reactive Forms의 진화가 아닙니다. 비‑트리비얼한 시나리오에서 사용해 보면 폼이 프레임워크 기능처럼 느껴지는 것이 사라짐을 알게 될 것입니다.

Source:

“로그인 폼”에서 실제 결제 폼까지

클래식한 “로그인 폼” 예제 대신, 실제 프로덕션에 가까운 결제 폼을 만들어 보겠습니다. 중첩 객체, 조건부 결제 로직, 그리고 필드 간 검증이 포함됩니다.
우리는 정말 중요한 모델부터 시작합니다.

import { signal } from '@angular/core';

export interface CheckoutModel {
  customer: {
    firstName: string;
    lastName: string;
    email: string;
  };
  shipping: {
    street: string;
    city: string;
    zip: string;
    country: string;
  };
  billingSameAsShipping: boolean;
  billing: {
    street: string;
    city: string;
    zip: string;
    country: string;
  };
  payment: {
    method: 'card' | 'paypal';
    cardNumber: string;
    expiry: string;
    cvv: string;
  };
  acceptTerms: boolean;
}

checkoutModel = signal({
  customer: {
    firstName: '',
    lastName: '',
    email: ''
  },
  shipping: {
    street: '',
    city: '',
    zip: '',
    country: ''
  },
  billingSameAsShipping: true,
  billing: {
    street: '',
    city: '',
    zip: '',
    country: ''
  },
  payment: {
    method: 'card',
    cardNumber: '',
    expiry: '',
    cvv: ''
  },
  acceptTerms: false
});

왜 이것이 좋은가

  • 컨트롤 트리가 아니라 타입이 지정된 시그널입니다.
  • 단일 진실 원천(single source of truth)입니다.
  • computed, effect, 그리고 zoneless Angular와 자연스럽게 통합됩니다.
  • 보조 추상화 레이어가 없습니다.

Signal Forms는 모델을 반영하는 FieldTree를 생성합니다.

import { form, required, email, validate } from '@angular/forms/signals';

checkoutForm = form(this.checkoutModel, (path) => {
  required(path.customer.firstName);
  required(path.customer.lastName);
  required(path.customer.email);
  email(path.customer.email);

  required(path.shipping.street);
  required(path.shipping.city);
  required(path.shipping.zip);
  required(path.shipping.country);

  required(path.acceptTerms);
});

스키마 함수는 매우 중요합니다 – 검증을 UI, 컴포넌트, 혹은 추상 클래스가 아니라 데이터 구조에 가깝게 유지하도록 강제합니다. 검증은 모델 형태에 대해 선언되므로, 아키텍처적 명확성을 제공합니다.

템플릿 – formGroupName 또는 formControlName 없음

  • 중첩된 그룹을 “얻지” 않습니다.
  • AbstractControl 트리를 탐색하지 않습니다.
  • 구조는 이미 존재하는데, 이는 신호 모델을 그대로 반영하기 때문입니다.

이는 프레임워크 설정보다 순수 TypeScript에 더 가깝게 느껴집니다.

동적 결제 수단

  Card
  PayPal

@if (checkoutForm.payment.method().value() === 'card') {
  
  
  
}
  • 구독이 없습니다.
  • valueChanges가 없습니다.
  • async 파이프가 없습니다.

UI가 반응하는 이유는 Signals가 반응하기 때문입니다. 이는 Signals가 도입된 이후 Angular가 추구해 온 아키텍처 일관성입니다.

교차 필드 검증이 간단해집니다

Reactive Forms에서 가장 어려운 부분 중 하나는 교차 필드 검증이었습니다 – 형제 컨트롤을 검사하고 오류 맵을 반환하는 폼 수준 검증자를 작성하게 됩니다.

Signal Forms에서는 모델이 이미 시그널이므로, 그냥 읽기만 하면 됩니다.

validate(path.payment.cardNumber, ({ value }) => {
  const model = this.checkoutModel();

  if (model.payment.method !== 'card') {
    return null;
  }

  return value().length  {
  const { customer, shipping } = this.checkoutModel();

  return {
    fullName: `${customer.firstName} ${customer.lastName}`,
    destination: `${shipping.city}, ${shipping.country}`
  };
});
  • 폼 변경을 수신할 필요가 없습니다.
  • valueChanges가 없습니다.
  • 모델이 변경되면 → 계산된 값이 다시 계산되고 → 템플릿이 업데이트됩니다.

반응성은 앱 전반에 걸쳐 일관됩니다.

Effects – 파생 상태 처리 (예: “청구 주소와 배송 주소가 동일”)

일반적인 요구사항: 청구 주소가 배송 주소와 동일합니다. Reactive Forms에서는 값을 수동으로 패치하고 순환 업데이트를 조심히 피해야 합니다. Signals를 사용하면 이것이 매우 간단해집니다:

import { effect } from '@angular/core';

constructor() {
  effect(() => {
    const model = this.checkoutModel();

    if (model.billingSameAsShipping) {
      this.checkoutModel.update(current => ({
        ...current,
        billing: { ...current.shipping }
      }));
    }
  });
}

당신은 상태를 업데이트하고 있으며, “컨트롤을 패치”하는 것이 아닙니다. 이 구분은 시스템을 바라보는 방식을 바꿉니다.

필드 상태 검사

checkoutForm.customer.email().valid();   // true | false
checkoutForm.customer.email().invalid(); // true | false

각 필드는 반응형 상태를 직접 노출합니다.

요약

Signal Forms는 폼을 타입이 지정된, 반응형 데이터 모델로 변환합니다. 이를 통해 다른 애플리케이션 상태와 마찬가지로 검증, 계산, 효과 추적을 할 수 있습니다. 그 결과 더 깔끔한 사고 모델, 줄어든 보일러플레이트, 그리고 앱 전반에 걸친 아키텍처 일관성을 얻을 수 있습니다.

tForm.customer.email().touched();
checkoutForm.customer.email().errors();

템플릿 예시

@if (checkoutForm.customer.email().invalid() &&
     checkoutForm.customer.email().touched()) {
  @for (error of checkoutForm.customer.email().errors(); track error) {
    
{{ error.message }}

  }
}

차이점은 이 상태가 시그널 기반이며, 명령형이 아니라는 것입니다.

  • 폼에게 재계산을 요청하지 않습니다.
  • 변경 감지를 트리거하지 않습니다.
  • 그냥 반응합니다.

가장 큰 변화는 문법이 아니라 개념입니다.

Reactive Forms가 도입한 병렬 추상화

  • 컨트롤 트리
  • 검증 시스템
  • 상태 시스템
  • 이벤트 시스템

Signal Forms가 이를 모두 압축한 형태

  • 반응형 상태 + 스키마 검증

이미 다음을 사용하고 있다면:

  • Signals
  • Computed values
  • Effects
  • Zoneless Angular

Signal Forms는 일관된 느낌을 주며, 대규모 코드베이스에서 이 일관성은 매우 중요합니다.

Note: Signal Forms는 Angular 21에서 아직 실험 단계입니다. 내일 대규모 엔터프라이즈 앱을 전체 재작성하진 않겠지만, 새로운 Angular 21 프로젝트라면 확실히 이걸로 시작하겠습니다.

Signal Forms를 도입할 시점

  • 이미 시그널 우선 전략을 사용 중일 때
  • 무거운 RxJS 폼 로직을 피하고 싶을 때
  • 더 단순한 사고 모델을 원할 때
  • 세밀한 반응성을 중시할 때

API는 작고, 개념은 일관되며, 타입은 강력하고, 정신적 부담이 적습니다.

수년간 Angular 폼은 강력하지만 무거운 느낌이었습니다.
Signal Forms는 더 가볍고, 현대 Angular와 더 잘 맞으며, 명시적이고, 마법 같은 요소가 적습니다. 도메인을 올바르게 모델링하도록 유도하고, 컨트롤 트리를 얽히게 하지 않습니다.

동적 검증 로직이 있는 깊게 중첩된 Reactive Form을 디버깅해 본 적이 있다면, 차이를 바로 이해하게 될 것입니다.

이것은 단순히 새로운 폼 API가 아닙니다. Signals가 도입되면서 시작된 Angular의 변화를 마무리하는 것입니다. 폼은 이제 별도의 서브시스템이 아니라 반응형 상태일 뿐이며, 저는 이것이 올바른 방향이라고 확신합니다.

0 조회
Back to Blog

관련 글

더 보기 »