후속: Angular Signal Forms에서 validateStandardSchema를 사용한 Zod 검증 단순화

발행: (2025년 12월 26일 오후 05:00 GMT+9)
14 min read
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line and all formatting exactly as you requested.

Source: https://youtu.be/C0Oxa1PtrbQ

최근에 Zod 검증을 Angular Signal Forms와 함께 사용하는 튜토리얼을 공개했는데, 완벽하게 동작했습니다.

하지만 Reddit 댓글러가 정중히 지적했듯이, 전체를 과도하게 설계했었다는 사실을 알게 되었습니다!

그 영상에서는 스키마 검증을 수동으로 연결하고, 오류를 매핑하고, 성공 상태를 처리하며, 필드 이름을 번역했지만—Angular가 이미 Zod와 같은 스키마 검증기를 위해 특별히 설계된 내장 헬퍼를 제공한다는 것을 몰랐습니다.

네, 완전히 놓쳤습니다.

그래서 오늘은 이를 바로잡습니다. 많은 코드를 삭제하고, 올바른 API로 전환하여 Angular Signal Forms에서 Zod 검증을 거의 창피할 정도로 간단하게 만들겠습니다.

계속 지켜보세요 – 최종 솔루션이 놀라울 정도로 깔끔합니다.

이 글에서는 내장 validateStandardSchema() API를 사용하여 Angular Signal Forms에서 Zod 검증을 적용하는 권장 방법을 보여줍니다.

Angular Signal Forms에서 Zod 검증이 작동하는 방식 (리팩터링 전)

앱이 현재 어떻게 동작하는지부터 살펴보겠습니다.

이것은 이미 새로운 Signal Forms API를 사용하도록 업데이트된 간단한 회원가입 폼입니다:

사용자 이름과 이메일 입력 필드가 있는 회원가입 폼

폼을 제출하려고 하면 두 필드 모두에 대한 검증 오류가 즉시 표시됩니다:

검증 오류가 표시된 상태로 흐릿하게 보이는 사용자 이름 및 이메일 필드가 있는 회원가입 폼

이 오류들은 현재 Signal Form에 연결된 Zod 스키마에서 발생합니다.

이제 유효한 사용자 이름과 이메일을 입력해 보겠습니다:

유효한 값이 입력된 후 검증 오류가 사라진 회원가입 폼

오류가 자동으로 사라지는 것을 볼 수 있습니다 – 좋은 신호입니다.

폼을 다시 제출하면 실제로 데이터가 전송되며, 이는 콘솔 로그를 통해 확인할 수 있습니다:

폼 데이터가 전송되는 것을 보여주는 콘솔 로그

즉, 기능적으로는 모든 것이 정상적으로 작동합니다.
그리고 바로 이 점이 문제를 교묘하게 만드는 이유입니다 – 구현을 훨씬 더 깔끔하게 만들 수 있기 때문이죠.

Source:

Angular Signal Forms용 Zod 검증 스키마 정의

현재 이 모든 작업을 가능하게 하는 코드를 살펴보겠습니다.
먼저 스키마 파일부터 시작합니다:

import { z } from 'zod';

export const signupSchema = z.object({
  username: z
    .string()
    .min(3, 'Username must be at least 3 characters long')
    .regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores are allowed'),
  email: z.string().email('Please enter a valid email address'),
});

이 부분은 훌륭합니다 – 선언적이고, 프레임워크에 구애받지 않으며, 테스트하기 쉽고, 스키마를 정의하는 정확한 방식입니다.

문제점

아래로 스크롤하면 이전 버전에서 추가한 사용자 정의 validateSignup() 함수를 볼 수 있습니다:

export function validateSignup(value: SignupModel) {
  const result = signupSchema.safeParse(value);

  if (result.success) {
    return {
      success: true as const,
      data: result.data,
      errors: {} as ZodErrorMap,
    };
  }

  const errors = result.error.issues.reduce((acc, issue) => {
    const field = issue.path[0]?.toString() ?? '_form';
    (acc[field] ??= []).push(issue.message);
    return acc;
  }, {});

  return {
    success: false as const,
    data: null,
    errors,
  };
}

이 함수는:

  1. safeParse를 호출합니다.
  2. 검증이 성공했는지 확인합니다.
  3. Zod 이슈들을 사용자 정의 오류 맵으로 축소합니다.
  4. 모든 것을 Angular이 이해할 수 있는 형식으로 재구성합니다.

동작은 하지만, 이제 파일이 검증 규칙 정의보다 훨씬 더 많은 일을 하고 있습니다 – 이것이 우리의 첫 번째 실제 문제입니다.

validateTree()를 사용한 수동 Zod 검증 (왜 이것이 과도한가)

Signal‑ 기반 폼 모델

import { signal } from '@angular/core';
import { SignupModel } from './form.schema';

protected readonly model = signal({
  username: '',
  email: '',
});

이 signal은 폼 상태에 대한 단일 진실 소스입니다.
Signal Forms는 이 signal을 관찰하고 변경 사항에 자동으로 반응합니다.

form()으로 폼 생성

import { form, validateTree } from '@angular/forms/signals';
import { ValidationError } from '@angular/forms/signals';
import { validateSignup, ZodErrorMap } from './form.schema';

protected readonly form = form(this.model, s => {
  validateTree(s, ctx => {
    const result = validateSignup(ctx.value());

    if (result.success) {
      return undefined;
    }

    const zodErrors: ZodErrorMap = result.errors;
    const errors: ValidationError.WithOptionalField[] = [];

    const getFieldRef = (key: string) => {
      switch (key) {
        case 'username':
          return ctx.field.username;
        case 'email':
          return ctx.field.email;
        default:
          return null;
      }
    };

    for (const [fieldKey, messages] of Object.entries(zodErrors)) {
      const fieldRef = getFieldRef(fieldKey);
      if (fieldRef) {
        errors.push(
          ...messages.map(message => ({
            kind: `zod.${fieldKey}` as const,
            message,
            field: fieldRef,
          }))
        );
      }
    }

    return errors.length ? errors : undefined;
  });
});

우리는 validateTree()(Angular의 이스케이프‑해치 검증 API)를 사용합니다. 이를 통해 얻을 수 있는 것은:

  • 전체 폼 값에 대한 접근
  • 개별 필드 레퍼런스
  • 검증 동작에 대한 완전한 제어

강력하지만, 이 접근 방식은 많은 보일러플레이트 코드를 작성하도록 강요합니다.

보일러플레이트 문제

커스텀 검증기가 실행된 후, 성공 케이스를 다음과 같이 처리합니다:

if (result.success) {
  return undefined;
}

그 다음 Zod 오류를 Angular ValidationError 객체로 수동 매핑합니다:

for (const [fieldKey, messages] of Object.entries(zodErrors)) {
  const fieldRef = getFieldRef(fieldKey);
  if (fieldRef) {
    errors.push(
      ...messages.map(message => ({
        kind: `zod.${fieldKey}` as const,
        message,
        field: fieldRef,
      }))
    );
  }
}

이러한 반복 작업은 규모를 확장하기 어렵고, Reddit 댓글자가 지적한 바로 그 문제입니다.

Built‑In Schema Validation: validateStandardSchema()

Angular Signal Forms는 Zod와 같은 스키마 검증기를 위한 도우미 validateStandardSchema() 를 제공합니다.
이 함수는 다음을 수행합니다:

  • 스키마 실행
  • 오류 해석
  • 오류를 올바른 필드에 매핑
  • 값이 유효해지면 오류 삭제

요컨대, Angular는 이미 Zod와 통신하는 방법을 알고 있으므로, 이를 사용하도록 하면 됩니다.

validateTree()validateStandardSchema()로 교체하기

모든 validateTree()‑관련 코드를 제거하고 다음으로 교체합니다:

import { form, validateStandardSchema } from '@angular/forms/signals';
import { signupSchema } from './form.schema';

protected readonly form = form(this.model, s => {
  validateStandardSchema(s, signupSchema);
});
  • 첫 번째 인수(s)는 폼 스코프입니다.
  • 두 번째 인수는 Zod 스키마(signupSchema)입니다.

그게 전부입니다! 이제 Angular가 스키마를 자동으로 실행하고, 오류를 올바른 필드에 매핑하며, 값이 유효해지면 즉시 오류를 지웁니다. 더 이상 수동 연결, 오류 번역, 혹은 필드‑조회 로직이 필요하지 않습니다.

스키마 파일 간소화

커스텀 검증 코드가 사라졌으므로 form.schema.ts에서 validateSignup() 함수와 ZodErrorMap 타입을 삭제할 수 있습니다. 이제 파일은 검증 규칙만 정의합니다:

import { z } from 'zod';

export type SignupModel = z.infer<typeof signupSchema>;

export const signupSchema = z.object({
  username: z
    .string()
    .min(3, 'Username must be at least 3 characters long')
    .regex(
      /^[a-zA-Z0-9_]+$/,
      'Only letters, numbers, and underscores are allowed'
    ),
  email: z
    .string()
    .email('Please enter a valid email address'),
});

스키마 파일은 이제 한 가지 작업만 수행합니다: 검증 규칙을 정의하는 것입니다. 완벽합니다!

최종 결과: Signal Forms를 활용한 깔끔한 Zod 검증

이러한 변경을 적용하고 커밋한 후, 프로젝트는:

  • 폼 상태를 위한 단일 진실의 원천 신호를 사용합니다.
  • Angular의 내장 validateStandardSchema()를 활용하여 Zod와 통합합니다.
  • 모든 수동 오류 매핑 보일러플레이트를 제거합니다.
  • 스키마 파일을 검증 규칙에만 집중하도록 유지합니다.

코드베이스가 이제 훨씬 더 깔끔해지고 유지보수가 쉬워졌으며, Angular Signal Forms의 네이티브 기능을 완전히 활용합니다.

검증 데모

폼을 다시 제출하면, 검증 오류가 자동으로 계속 표시됩니다:

사용자 이름과 이메일 필드가 흐릿하게 표시된 회원가입 양식, 검증 오류가 보임

이제 올바른 데이터를 입력해 보겠습니다:

사용자 이름과 이메일 필드에 올바른 값이 입력된 회원가입 양식, 검증 오류가 사라짐

완벽합니다. 오류가 사라집니다.

폼을 다시 제출하면:

제출되는 폼 데이터를 보여주는 콘솔 로그

멋지네요! 기대한 대로 성공적으로 제출됩니다.

이제 훨씬 적은 코드로 동일한 동작을 구현했습니다!

validateStandardSchema()validateTree()를 언제 사용해야 할까

validateStandardSchema()를 사용해야 할 경우:

  • Zod, Yup, Joi 등과 같은 표준 스키마 검증 라이브러리를 사용할 때.
  • 스키마가 표준 계약을 따르고(파싱 가능하고, 오류를 표준 형식으로 반환함)
  • 가능한 가장 간단한 통합을 원할 때.

validateTree()를 사용해야 할 경우:

  • 표준 스키마 패턴에 맞지 않는 맞춤형 검증 로직이 필요할 때.
  • 표준 계약을 따르지 않는 검증 라이브러리를 통합하고 있을 때.
  • 오류 매핑이나 검증 시점을 세밀하게 제어해야 할 때.

대부분의 Angular 개발자가 Zod를 사용할 경우, validateStandardSchema()가 올바른 선택입니다.

이 API는 검증 라이브러리를 통합할 때마다 스키마 어댑터를 새로 만들 필요가 없도록 하기 위해 존재합니다.

Angular Signal Forms에서 Zod 검증을 위한 모범 사례

이것은 놓치기 쉬운 Angular API 중 하나이지만, 한 번 보면 다시는 돌아가고 싶지 않을 겁니다.

Signal Forms와 함께 Zod를 사용한다면, 검증을 수동으로 연결하지 마세요 제가 그랬던 것처럼. 대신 validateStandardSchema()를 사용하세요.

장점

  • 코드 감소 – 수동 오류 매핑이나 필드 조회가 필요 없습니다.
  • 버그 감소 – Angular가 통합을 올바르게 처리합니다.
  • 유지 보수 용이 – 스키마 파일이 검증 규칙에 집중합니다.
  • 확장성 향상 – 폼이 복잡해져도 원활하게 작동합니다.
  • 타입 안전성 – 전체 TypeScript 지원을 제공합니다.

큰 감사를 pkgmain에게 드립니다!
그리고 다음 번엔 문서를 더 꼼꼼히 읽고 게시하겠습니다.

추가 리소스

Back to Blog

관련 글

더 보기 »

Angular 21 — 새로운 점, 변경된 점

Angular 21은 단순화, 성능, 그리고 현대적인 reactive patterns에 중점을 둡니다. 화려한 APIs를 추가하기보다는 Angular 개발자들이 이미 사용하고 있는 것을 강화합니다.