티스토리 뷰

기록

zod의 refine과 superRefine

als982001 2024. 10. 6. 13:42

1. refine

 

 refine은 단일 필드나 스키마의 특정 값에 대한 검증을 할 때 사용되며, 아래의 코드처럼 이용할 수 있다.

 

const formSchema = z
  .object({
    username: z
      .string({
        invalid_type_error: "Username must be a string!",
        required_error: "No username",
      })
      .min(5, "Way too short")
      .max(10, "That is too long")
      .trim()
      .toLowerCase(),
    email: z.string().email().toLowerCase(),
    password: z
      .string()
      .min(PASSWORD_MIN_LENGTH)
      .regex(PASSWORD_REGEX, PASSWORD_REGEX_ERROR),
    confirmPassword: z.string().min(PASSWORD_MIN_LENGTH),
  })
  .refine(
    async ({ username }) => {
      const user = await db.user.findUnique({
        where: {
          username,
        },
        // select: user에서 가져오고 싶은 것들 선택
        select: {
          id: true, // user에서 id만 가져옴 (email 등은 안 가져옴)
        },
      });

      return !Boolean(user);
    },
    { message: "이미 해당 username이 존재합니다.", path: ["username"] }
  )
  .refine(
    async ({ email }) => {
      const user = await db.user.findUnique({
        where: {
          email,
        },
        select: {
          id: true,
        },
      });

      return !Boolean(user);
    },
    { message: "이미 해당 email이 존재합니다.", path: ["email"] }
  )
  .refine(({ password, confirmPassword }) => password === confirmPassword, {
    message: "Two passwords should be equal",
    path: ["confirmPassword"],
  });

 

refine의 첫 번째 인자는 검증을 위한 함수를, 두 번째 인자는 검증에 실패했을 때 필요한 메시지이다. 그리고 위 코드는 3개의 refine을 통해 이미 존재하는 username, email이 있는지, 비밀번호를 제대로 입력했는지를 검증한다. 하지만 refine은 검증에 실패해도 다음 refine을 수행한다.

 

 

 예를 들어, 이미 test01이라는 username과 test01@email.com 이라는 이메일을 이용하는 계정이 있고, 이 username과 email을 이용해 계정을 생성하는 경우, 3개의 refine을 모두 실행해 3개의 에러 메시지를 확인할 수 있다.

 

 

 

이는 별로 바람직하지 않을 수도 있다. 왜냐하면 username과 email을 검증할 때 db에서 user를 확인하는데, 이 경우에는 이미 username이 중복되는 것을 확인했음에도 email도 확인하기 때문이다. 그렇기에 지금처럼 규모가 작을 경우에는 문제가 없지만 규모가 커지면 좋지 않을 가능성이 있다. 

 

 

2. superRefine

 

 이런 경우 refine 대신 이용할 수 있는 것이 superRefine이다. 기존 refine과 다르게 superRefine은 스키마 전체 혹은 여러 필드 간의 상호 관계에 따라 검증할 때 이용할 수 있으며 addIssue를 통해 원하는 필드에 오류를 수동으로 지정할 수 있다. 그리고 검증에 실패할 경우, 다음 refine을 실행하지 않게 할 수도 있다.

 

const formSchema = z
  .object({
    username: z
      .string({
        invalid_type_error: "Username must be a string!",
        required_error: "No username",
      })
      .min(5, "Way too short")
      .max(10, "That is too long")
      .trim()
      .toLowerCase(),
    email: z.string().email().toLowerCase(),
    /* .refine(
        checkUniqueEmail,
        "There is an account already registered with that email"
      ),*/
    password: z
      .string()
      .min(PASSWORD_MIN_LENGTH)
      .regex(PASSWORD_REGEX, PASSWORD_REGEX_ERROR),
    confirmPassword: z.string().min(PASSWORD_MIN_LENGTH),
  })
  .superRefine(async ({ username }, ctx) => {
    const user = await db.user.findUnique({
      where: {
        username,
      },
      select: {
        id: true,
      },
    });

    if (user) {
      ctx.addIssue({
        code: "custom",
        message: "This username is already taken",
        path: ["username"], // path가 없으면 이 에러 메시지는 formErrors로 들어감
        fatal: true,
      });

      return z.NEVER; // z.NEVER를 return하는데 fatal이 true라면 이후의 refine을 실행하지 않고 끝냄
    }
  })
  .superRefine(async ({ email }, context) => {
    const user = await db.user.findUnique({
      where: { email },
      select: { id: true },
    });

    if (user) {
      context.addIssue({
        code: "custom",
        message: "This email is already taken",
        path: ["email"],
        fatal: true,
      });

      return z.NEVER;
    }
  })
  .superRefine(({ password, confirmPassword }, ctx) => {
    if (password !== confirmPassword) {
      // ctx로 특정 필드에 에러 메시지 추가
      ctx.addIssue({
        code: "custom",
        message: "Two passwords should be equal",
        path: ["confirmPassword"],
      });
    }
  });

 

superRefine의 첫 번째 인자는 z.object로 설정한 객체, 두 번째 인자는 ctx(컨텍스트 객체)이다. 그리고 ctx.addIssue를 통해 특정 필드에 오류를 지정할 수 있다. 그리고 (위 코드처럼) 조건에 따라 fatal: true일 때 z.NEVER를 return할 경우, 다음 refine은 실행하지 않는다.

 

 

 

첫 번째 경우랑 입력한 값은 같지만, username의 유효성 검증이 실패했기 때문에 email 검증은 하지 않은 것을 확인할 수 있다.

 

 

3. 정리

  • refine과 달리 superRefine은 여러 필드 혹은 스키마 전체에 대한 종합적인 검증을 할 수 있음
  • 복잡한 논리 추가 혹은 수동으로 오류 제어할 때 유용
  • 효성 검증 실패 시 다음 refine을 실행하지 않게 설정할 수 있음
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함