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

Zod 로 Validation

~22 min · validation, Zod, security

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Server 에서 validate, 항상

Client validation 은 UX nicety. Server validation 이 security boundary. 누구든 action endpoint 에 POST 가능; client 가 check 했을 거라고 절대 못 믿어.

Zod 가 깔끔하게 fit

Schema 정의, form input 에 safeParse 호출, success/failure 로 분기. Failure 시 useActionState 통해 field-level error 반환; success 시 mutation 돌고 revalidate.

State 모양 type

Action 의 State type 명시적으로. Front-end 와 back-end 가 TypeScript 통해 모양 합의.

Code

Zod validation 의 Server Action·ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

const CreatePost = z.object({
  title: z.string().min(1, 'Title is required').max(100),
  content: z.string().min(10, 'Content must be at least 10 characters'),
  email: z.string().email('Invalid email address'),
});

export type State = {
  errors?: { title?: string[]; content?: string[]; email?: string[] };
  message?: string;
};

export async function createPost(prev: State, formData: FormData): Promise<State> {
  const parsed = CreatePost.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    email: formData.get('email'),
  });

  if (!parsed.success) {
    return {
      errors: parsed.error.flatten().fieldErrors,
      message: 'Validation failed',
    };
  }

  await db.post.create({ data: parsed.data });
  revalidatePath('/posts');
  return { message: 'Post created' };
}
State 에서 field error render·tsx
'use client';
import { useActionState } from 'react';
import { createPost, type State } from '@/app/actions';

export function PostForm() {
  const [state, action, pending] = useActionState<State, FormData>(createPost, {});
  return (
    <form action={action} className="space-y-3">
      <div>
        <input name="title" />
        {state.errors?.title?.map(e => <p key={e} className="text-red-500 text-sm">{e}</p>)}
      </div>
      <div>
        <textarea name="content" />
        {state.errors?.content?.map(e => <p key={e} className="text-red-500 text-sm">{e}</p>)}
      </div>
      <button disabled={pending}>{pending ? 'Saving&hellip;' : 'Create'}</button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}

External links

Exercise

기존 Server Action 한 개에 Zod validation 추가. useActionState 통해 적어도 셋의 field-level error render. 잘못된 input submit 후 action 이 절대 database 안 만지는 거 확인.

Progress

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

댓글 0

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

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