C.W.K.
Stream
Lesson 06 of 06 · published

Zod — Action 안 타입 안전 유효성

~12 min · zod, validation, schemas

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
FormData 는 느슨함: 모든 필드가 문자열 또는 File, TypeScript 가 어떤 필드 있는지 모름. Zod 가 캐논 답 — schema 선언, 들어오는 데이터 파싱, 타입 잡힌 객체 (또는 타입 잡힌 에러) 받음.

모양

Zod schema 가 유효한 데이터 모양 묘사: z.object({ email: z.string().email(), age: z.coerce.number().min(18) }). .parse() (fail 시 throw) 또는 .safeParse() (success/failure 의 discriminated union 반환) 호출. Action 안에선 .safeParse 가 유효성 에러를 state 의 일부로 반환 가능하게.

FormData → 타입 잡힌 객체

Zod 가 FormData 네이티브로 모름, 다만 필드를 먼저 plain object 로 추출 가능: Object.fromEntries(formData). 그 후 safeParse 돌림. 결과: 모두 타입 잡힌 { success: true, data } 또는 필드 레벨 에러 메시지 가진 { success: false, error }.

에러 모양

Zod 에러가 필드별 그룹: error.flatten().fieldErrors{ email: ['invalid email'], age: ['must be a number'] } 줌. 이 객체를 state 의 일부로 반환하면 form 이 각 필드 개별 하이라이트 가능.

Actions 스토리와의 composition

Action 이 됨: 파싱 → invalid 면 에러 반환; valid 면 작업 수행, success 반환. 컴포넌트가 state 에서 에러 메시지 직접 렌더. 별도 유효성 라이브러리 없음, 수동 필드 반복 없음 — Zod 가 호출 하나로 타입 잡힌 입력 + 타입 잡힌 에러.

유효성이 계약. Zod schema 가 '이메일 필수, 소문자, 최대 255 chars' 말하는 유일한 자리. 서버와 클라이언트가 같은 schema 공유 (단일 출처). TypeScript 가 schema 에서 파싱된 타입 추론 → 나머지 코드가 자동완성과 타입 안전 공짜로.

Code

Zod 로 유효성 검증된 subscribe action·tsx
import { z } from "zod";
import { useActionState } from "react";

const SubscribeSchema = z.object({
  email: z.string().email("Enter a valid email"),
  name: z.string().min(1, "Name is required"),
});

type SubscribeState = {
  email: string;
  name: string;
  errors: {
    email?: string[];
    name?: string[];
    _form?: string[];
  };
  submitted: boolean;
};

const initial: SubscribeState = { email: "", name: "", errors: {}, submitted: false };

async function subscribe(_prev: SubscribeState, fd: FormData): Promise<SubscribeState> {
  const raw = Object.fromEntries(fd) as { email?: string; name?: string };
  const result = SubscribeSchema.safeParse(raw);
  if (!result.success) {
    return {
      email: raw.email ?? "",
      name: raw.name ?? "",
      errors: result.error.flatten().fieldErrors,
      submitted: false,
    };
  }
  // result.data 가 타입 잡힘: { email: string; name: string }
  try {
    await fetch("/api/subscribe", { method: "POST", body: JSON.stringify(result.data) });
    return { ...result.data, errors: {}, submitted: true };
  } catch (e) {
    return {
      ...result.data,
      errors: { _form: [(e as Error).message] },
      submitted: false,
    };
  }
}

export function SubscribeForm() {
  const [state, formAction] = useActionState(subscribe, initial);
  return (
    <form action={formAction} className="space-y-3 max-w-md">
      <Field name="name" label="Name" defaultValue={state.name} errors={state.errors.name} />
      <Field name="email" label="Email" defaultValue={state.email} errors={state.errors.email} />
      {state.errors._form && <p className="text-danger text-sm">{state.errors._form[0]}</p>}
      {state.submitted && <p className="text-success text-sm">Subscribed!</p>}
      <SubmitButton>Subscribe</SubmitButton>
    </form>
  );
}

function Field({ name, label, defaultValue, errors }: {
  name: string;
  label: string;
  defaultValue: string;
  errors?: string[];
}) {
  return (
    <label className="block">
      <span className="text-sm text-muted">{label}</span>
      <input name={name} defaultValue={defaultValue} className="w-full mt-1 px-3 py-2 border" />
      {errors && <p className="text-danger text-xs mt-1">{errors[0]}</p>}
    </label>
  );
}

External links

Exercise

Subscribe-form 연습의 수동 이메일 regex 를 Zod schema 로 교체. name 필드 (필수, 최소 길이 2) + referralCode (선택, 형식 [A-Z]{4}-\d{4}) 추가. flatten().fieldErrors 모양 사용해 필드별 에러 렌더. 확인: 빈 제출이 필드 에러 둘 보임, 잘못된 이메일이 이메일 특정 에러 보임, 유효 제출 성공.
Hint
Referral code: z.string().regex(/^[A-Z]{4}-\d{4}$/).optional(). .optional() 이 비어/없어도 OK 면서 제공 시 형식 검증.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.