본문 바로가기
FrontEnd

Zod를 활용한 공통화 및 유효성 검증 로직 설계: 회원가입 폼 예제

by E_van 2024. 11. 20.

프론트엔드에서 폼 유효성 검증은 사용자 경험(UX) 향상과 데이터 품질 보장을 위해 필수적입니다. 특히 대규모 서비스에서 폼 필드가 반복적으로 사용되거나, 검증 로직이 복잡한 경우, 코드의 재사용성과 유지보수성을 높이는 방식으로 설계해야 합니다. 본 글에서는 Zod를 활용하여 자주 사용하는 필드의 검증 로직을 공통화하고, 회원가입 폼의 유효성 검증 로직을 설계하는 방법에 대해서 정리해보겠습니다.

자주 사용하는 필드 공통화

반복적으로 사용되는 필드는 zod를 통해 간결하고 일관된 방식으로 처리할 수 있습니다. 예를 들어, 이름, 이메일, 사용자 아이디 등의 필드는 자주 사용되며, 아래와 같이 공통화된 유틸리티 함수를 정의할 수 있습니다.

1. 공통 필드 유틸리티 함수

import { z } from "zod";

const stringField = (message: string) => 
  z.string().min(1, { message }); // 최소 한 글자 이상 입력 필수

const stringRequiredField = (message: string) => 
  z.string({ required_error: message }); // 필수 입력 필드

이 유틸리티 함수는 특정 필드에 대한 기본적인 검증 로직을 설정합니다. 여기서 메시지는 사용자 경험 향상을 위해 명시적인 에러 메시지를 포함하도록 합니다.

회원가입 정보 스키마 정의

회원가입에서는 이름, 이메일, 아이디 등 다양한 입력 필드가 존재합니다. 이를 위해 스키마를 정의합니다.

2. 회원가입 정보 스키마

const infoSchema = z.object({
  name: stringField("이름을 입력해 주세요."),
  email: stringField("이메일을 입력해 주세요.")
    .email("올바른 이메일 형식이어야 합니다."),
  identity: stringRequiredField("아이디를 입력해 주세요.")
    .min(4, "아이디는 최소 4자 이상이어야 합니다.")
    .regex(/^[a-zA-Z0-9]+$/, "아이디는 영문과 숫자만 포함할 수 있습니다."),
});

여기서 identity 필드는 아이디에 대해 추가적인 제약 조건(최소 길이, 정규식)을 포함하며, 이를 통해 필드별 유효성 검증을 더욱 세부적으로 조정할 수 있습니다.

비밀번호와 비밀번호 확인 검증

비밀번호 필드는 특히 중요한 데이터이므로 추가적인 검증 로직이 필요합니다. 다음은 복잡한 검증 로직이 필요한 비밀번호 필드에 대해 superRefine을 활용한 설계 방법입니다.

3. 비밀번호 검증 로직 분리

비밀번호 유효성 검사는 보통 길이, 패턴, 연속된 키 입력 여부, 비밀번호 확인 일치 여부 등을 포함합니다. 이를 분리하여 재사용 가능한 함수로 정의합니다.

const passwordRefinement = (
  data: { password?: string; password_confirmation?: string },
  ctx: z.RefinementCtx
) => {
  const { password, password_confirmation } = data;

  if (password && /^(.)\1+$/.test(password)) { // 연속된 문자 검사
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: ["password"],
      message: "비밀번호에 연속된 문자를 사용할 수 없습니다.",
    });
  }

  if (
    password &&
    (password.length < 10 ||
      password.length > 20 ||
      !/[A-Za-z]/.test(password) || // 영문 포함
      !/\d/.test(password) || // 숫자 포함
      !/[@$!%*?&#]/.test(password)) // 특수문자 포함
  ) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: ["password"],
      message: "비밀번호는 10~20자 길이여야 하며, 영문, 숫자, 특수문자를 포함해야 합니다.",
    });
  }

  if (password !== password_confirmation) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: ["password_confirmation"],
      message: "비밀번호가 일치하지 않습니다.",
    });
  }
};

4. 회원가입 스키마에 통합

위에서 정의한 infoSchemapasswordRefinement를 결합하여 최종적으로 회원가입 스키마를 정의합니다.

export const registerFormSchema = z
  .object({
    ...infoSchema.shape,
    password: z.string().min(1, "비밀번호를 입력해 주세요."),
    password_confirmation: z.string().min(1, "비밀번호 확인을 입력해 주세요."),
  })
  .superRefine(passwordRefinement);​

 

초기화 데이터 분리

스키마에서 기본값을 설정할 수도 있지만, 폼 초기화 데이터를 별도로 관리하면 더 유연한 설계가 가능합니다.

const initRegisterInfo: z.infer<typeof registerFormSchema> = {
  name: "",
  identity: "",
  email: "",
  password: "",
  password_confirmation: "",
};

이 초기화 데이터는 상태 관리 라이브러리(Zustand, Recoil 등)와 결합하여 폼의 초기 상태로 활용할 수 있습니다.


결론

위와 같은 방식으로 Zod를 활용하면 자주 사용하는 필드의 유효성 검증 로직을 공통화하고, 복잡한 검증이 필요한 필드에 대해 분리된 로직을 설계할 수 있습니다. 이를 통해 코드의 재사용성과 유지보수성을 높일 수 있습니다. 이러한 패턴은 회원가입 뿐만 아니라, 다양한 폼 검증 로직에도 확장 가능하며, 서비스 전반에 걸쳐 일관성을 제공합니다.