Zod

2023-08-14
ReactZodTypeScript

Table of Contents

1. 프로젝트 세팅 React + TS + Vite

$ npm init vite@latest React-Zod -- --template react-ts
$ cd React-Zod
$ npm i
$ npm i zod

2. 사용해보기

2-1. 회원가입 폼 만들어보기

  • 기본적인 zod 사용 방법을 알아봅니다.

zod 스키마 만들기

  • 스키마란, 데이터의 구조와 규칙을 정의하는 것을 말합니다.

src/userType.ts

import { z } from "zod";

// validate 함수로 스키마 설정
const User = z.object({
  address: z.string(),
  password: z.string(),
  email: z.string().email().optional(), // optional option으로 값이 있든지 없든지 상관없이, email 형식이 아니면 error
  birth: z.date().optional(),
});

// 타입 설정
type UserType = z.infer<typeof User>;

export { User, type UserType };
  • z.object로 설정된 User 객체는 zod의 속성을 상속받아 zod의 메소드를 사용할 수 있습니다. 즉 zod 스키마 설정을 한 것입니다.
  • infer 메서드로 타입으로 변환이 가능합니다.

입력값 만들기

  • 유저에게 받을 Input Element를 만들어 보겠습니다.

src/App.tsx

import "./App.css";
import { useReducer } from "react";

function reducer() {
}

function App() {
  const [state, dispatch] = useReducer(reducer, {
    address: "",
    password: "",
    email: "",
    birth: undefined,
  });

  return (
    <>
      <h1>Zod APP</h1>
      <div>
        <label htmlFor='address'>Address</label>
        <input type='text' id='address' onChange={dispatchAddress} />
      </div>
      <div>
        <label htmlFor='password'>Password</label>
        <input type='text' id='password' onChange={dispatchPassword} />
      </div>
      <div>
        <label htmlFor='email'>Email</label>
        <input type='text' id='email' onChange={dispatchEmail} />
      </div>
      <div>
        <label htmlFor='birth'>Birth</label>
        <input type='date' id='birth' onChange={dispatchBirth} />
      </div>
    </>
  );
}

export default App;
  • state는 useReducer로 관리하고, initial state와 reducer를 할당합니다.

리듀서 함수 만들기

src/App.tsx

function reducer(state, action) {
  console.log(action);
  switch (action.type) {
    case "ADDRESS":
      return { ...state, address: action.payload };
    case "PASSWORD":
      return { ...state, password: action.payload };
    case "EMAIL":
      return { ...state, email: action.payload };
    case "BIRTH":
      return { ...state, birth: new Date(action.payload) };
    default:
      return state;
  }
}
// app.tsx에 추가
  • 리듀서 함수는 두가지 parameter를 받습니다, state와 action
  • 초기의 state는 initial state로 할당한 값들이 들어가 있습니다.
  • action 파라미터는 type과 payload라는 객체를 받습니다.

리듀서 함수 만들기-2

src/App.tsx

import { UserType } from "./userType";

function reducer(state: UserType, action): UserType {
  switch (action.type) {
    case "ADDRESS":
      return { ...state, address: action.payload };
    case "PASSWORD":
      return { ...state, password: action.payload };
    case "EMAIL":
      return { ...state, email: action.payload };
    case "BIRTH":
      return { ...state, birth: new Date(action.payload) };
    default:
      return state;
  }
}
  • state와 return에 타입을 추가해 봅시다. import로 정의했던 type을 정의합니다.

action type 만들기

type ActionType = {
  type: "ADDRESS" | "PASSWORD" | "EMAIL" | "BIRTH";
  payload: string;
};

function reducer(state: UserType, action: ActionType): UserType {
  switch (action.type) {
    case "ADDRESS":
      return { ...state, address: action.payload };
    case "PASSWORD":
      return { ...state, password: action.payload };
    case "EMAIL":
      return { ...state, email: action.payload };
    case "BIRTH":
      return { ...state, birth: new Date(action.payload) };
    default:
      return state;
  }
}
  • action 타입도 적용해봅니다.

dispatch 함수 추가

function App() {
  const [state, dispatch] = useReducer(reducer, {
    address: "",
    password: "",
    email: "",
    birth: undefined,
  });

  const dispatchAddress = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault();
    dispatch({ type: "ADDRESS", payload: event.target.value });
  };
  const dispatchPassword = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault();
    dispatch({ type: "PASSWORD", payload: event.target.value });
  };
  const dispatchEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault();
    dispatch({ type: "EMAIL", payload: event.target.value });
  };
  const dispatchBirth = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.preventDefault();
    dispatch({
      type: "BIRTH",
      payload: new Date(event.target.value).toISOString(),
    });
  };

  return (
    <>
      ...
    </>
  );
}

export default App;
  • 값을 업데이트 할 dispatch 함수를 작성합니다.

dispatch input에 연결하기

function App() {
  const [state, dispatch] = useReducer(reducer, {
    address: "",
    password: "",
    email: "",
    birth: undefined,
  });

...dispatch 함수들

  return (
    <>
      <h1>Zod APP</h1>
      <div>
        <label htmlFor='address'>Address</label>
        <input type='text' id='address' onChange={dispatchAddress} />
      </div>
      <div>
        <label htmlFor='password'>Password</label>
        <input type='text' id='password' onChange={dispatchPassword} />
      </div>
      <div>
        <label htmlFor='email'>Email</label>
        <input type='text' id='email' onChange={dispatchEmail} />
      </div>
      <div>
        <label htmlFor='birth'>Birth</label>
        <input type='date' id='birth' onChange={dispatchBirth} />
      </div>
      <div>
    </>
  );
}
  • onChange시 작동할 dispatch 함수를 연결합니다.

zodError

  • 리듀서 함수에 console.log(action)을 호출하면 이런 모습일 겁니다.

전송버튼 누를 시 유효성 검사 실행

src/userType.ts

import { z } from "zod";

// validate 함수로 유효성 설정
const User = z.object({
  address: z.string(),
  password: z.string(),
  email: z.string().email().optional(), // optional option으로 값이 있든지 없든지 상관없이
  birth: z.date().optional(),
});

// 타입 설정
type UserType = z.infer<typeof User>;

function processUser(user: UserType) {
  return User.parse(user); // 유효성 검증
}

export { User, type UserType, processUser };
  • processUser 함수 하나를 추가합니다. zod 속성을 상속받은 객체는 parse라는 메서드로 정의했던 User의 타입을 상속받은 객체에 대해 유효성 검사를 할 수 있습니다.
  • parse는 error 메세지와 성공 시 성공한 객체를 리턴합니다.
  • safeParse라는 메서드는 동일한 기능을 수행하지만 error를 리턴하지 않고 성공과 실패여부를 담은 success 객체를 리턴합니다.

전송버튼 만들고 유효성검사 연동하기

src/App.tsx

import { UserType, processUser } from "./zodTypes/userType";


function App() {
  const [state, dispatch] = useReducer(reducer, {
    address: "",
    password: "",
    email: "",
    birth: undefined,
  });

  dispatchs...

  const onClickHandler = (event: React.MouseEvent) => {
    event.preventDefault();
    try {
      // 유효성검사
      const result = processUser(state);
      console.log(result);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <>
      ...
      <div>
        <input type='button' value='눌러' onClick={onClickHandler} />
      </div>
    </>
  );
}

export default App;
  • import로 processUser를 가져옵니다. onClick시 onClickHandler가 동작하고 그 안에 있는 processUser 함수가 관리하고 있는 지역변수 state를 검사합니다.

zodError2

zodError3

  • 이메일 형식에 맞지 않는다는 에러가 발생합니다. zodError4 zodError5

  • 유효성 검사 통과 시 검사에 통과한 객체를 리턴합니다.

2-2. 요구사항 정의

예시로 간단한 폼을 만들었지만, 실제 회원가입에서는 요구하는 유효성 검사가 훨씬 까다롭겠죠. 그 요구사항대로 로직을 zod로 작성해보겠습니다.

  1. 요구사항 목록아이디 (Username):길이가 3자 이상 20자 이하여야 합니다.알파벳 소문자, 숫자, 밑줄(_)만 사용할 수 있습니다.
  2. 비밀번호 (Password):길이가 8자 이상이어야 합니다.최소한 하나 이상의 대문자, 소문자, 숫자, 특수문자를 포함해야 합니다.
  3. 이메일 (Email):올바른 이메일 형식을 가져야 합니다 (예: example@example.com).이름 (Name):공백을 제외하고 최소 2자 이상이어야 합니다.
  4. 알파벳 대소문자만 사용할 수 있습니다.생년월일 (Birth):date 형식 이어야 합니다.

zod 스키마 만들기

src/joinUserType.ts

import { z } from "zod";

// 입력값 유효성 처리
const JoinUser = z.object({
  id: z
    .string()
    .trim()
    .min(4)
    .max(20)
    .regex(/^[a-z0-9_]+$/),
  password: z
    .string()
    .trim()
    .min(8)
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/),
  email: z.string().trim().email(),
  name: z
    .string()
    .trim()
    .min(2)
    .regex(/^[A-Za-z]+$/),
  birth: z.date(),
});

// type으로 만들기
type JoinUserType = z.infer<typeof JoinUser>;

function processJoinUser(user: JoinUserType) {
  // 유효성 검사
  return JoinUser.safeParse(user);
}

export { type JoinUserType, processJoinUser };
  • zod의 z 객체를 받은 JoinUser의 프로퍼티는 zod 가 가지고 있는 많은 유효성 검사 메서드들을 사용하는 것을 확인할 수 있습니다.
  • 요구사항 목록의 요구사항과 같이 구현한 것입니다. regex로 정규식 유효성 검사를 가능하게 합니다.

입력값 만들기

src/JoinUser.tsx

import { useRef } from "react";
import { JoinUserType, processJoinUser } from "../joinUserType";

const JoinUser = () => {
  const inputIdRef = useRef<HTMLInputElement>(null);
  const inputPasswordRef = useRef<HTMLInputElement>(null);
  const inputEmailRef = useRef<HTMLInputElement>(null);
  const inputNameRef = useRef<HTMLInputElement>(null);
  const inputBirthRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <div>
        <label htmlFor='id'>ID</label>
        <input type='text' id='id' ref={inputIdRef} />
      </div>
      <div>
        <label htmlFor='password'>Password</label>
        <input
          type='password'
          id='password'
          ref={inputPasswordRef}
        />
      </div>
      <div>
        <label htmlFor='email'>Email</label>
        <input type='email' id='email' ref={inputEmailRef} />
      </div>
      <div>
        <label htmlFor='name'>Name</label>
        <input type='text' id='name' ref={inputNameRef} />
      </div>
      <div>
        <label htmlFor='birth'>Birth</label>
        <input type='date' id='birth' ref={inputBirthRef} />
      </div>
      <div>
        <input type='button' value='눌러' onClick={onClickHandler} />
      </div>
    </>
  );
};

export default JoinUser;
  • Reducer보다 간편한 useRef로 연결해 input값을 추출했습니다.

전송버튼 누를 시 유효성 검사 실행

import { useRef } from "react";
import { JoinUserType, processJoinUser } from "../joinUserType";

const JoinUser = () => {
  const inputIdRef = useRef<HTMLInputElement>(null);
  ...

	function onClickHandler(event: React.MouseEvent) {
    event.preventDefault();
    const value: JoinUserType = {
      id: inputIdRef.current!.value,
      password: inputPasswordRef.current!.value,
      email: inputEmailRef.current!.value,
      name: inputNameRef.current!.value,
      birth: new Date(inputBirthRef.current!.value),
    };
			// 유효성 검사 실행
			const result = processJoinUser(value);
  }

  return (
    <>
      <div>
        <label htmlFor='id'>ID</label>
        <input type='text' id='id' ref={inputIdRef} defaultValue={""}/>
      </div>
      ...
			<div>
       <button type='button' onClick={onClickHandler}>
          눌러
       </button>
      </div>
    </>
  );
};

export default JoinUser;
  • ref의 current.value를 추출하였습니다. 그리고 null 타입은 JoinUserType에서 허용하지 않기 때문에 Definite Assignment Assertions기법을 이용해 ? 대신 !을 넣어주었습니다.
  • 모든 input에서는 defaultValue 옵션값을 “”로 설정했습니다.

zodError5

zodError5

  • 이런 입력값을 검사한다면 값이 이렇게 나옵니다.

예외처리

import { useRef } from "react";
import { JoinUserType, processJoinUser } from "../joinUserType";

const JoinUser = () => {
  const inputIdRef = useRef<HTMLInputElement>(null);
  ...

  function onClickHandler(event: React.MouseEvent) {
    ...
    const result = processJoinUser(value);
    console.log(result);
    if (!result.success) {
      // handle error then return
      console.log(result.error.errors)
    } else {
      console.log(result.data);
    }
  }

  return (
    <>
        ...
    </>
  );
};

export default JoinUser;
  • result객체는 safeParse 메서드가 리턴한 값을 저장하고 있습니다.
  • safeParse는 success 객체를 리턴하는데요. false시 error정보를 저장하고 있는 객체를 리턴합니다. true시는 성공한 객체를 리턴받습니다.

에러 시 메세지 추가하기

src/joinUserType.ts

// 입력값 유효성 처리
const JoinUser = z.object({
  id: z
    .string()
    .trim()
    .min(4, { message: "4글자 이상 적어주세요" })
    .max(20, { message: "20글자 이하로 적어주세요" })
    .regex(/^[a-z0-9_]+$/, {
      message: "알파벳 소문자, 숫자, 밑줄(**`_`**)만 사용할 수 있습니다.",
    }),
  password: z
    .string()
    .trim()
    .min(8)
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/),
  email: z.string().trim().email(),
  name: z
    .string()
    .trim()
    .min(2)
    .regex(/^[A-Za-z]+$/),
  birth: z.string().datetime(),
});
  • 검사하는 메소드에 객체로 message 객체를 추가합니다. zodError5

zodError5

  • 이런식으로 메세지 객체가 추가되어 리턴됩니다.

src/JoinUser.tsx

import { useRef } from "react";
import { JoinUserType, processJoinUser } from "../joinUserType";

const JoinUser = () => {
  const inputIdRef = useRef<HTMLInputElement>(null);
  ...

  function onClickHandler(event: React.MouseEvent) {
    ...
    const result = processJoinUser(value);
    console.log(result);
    if (!result.success) {
      // handle error then return
		console.log(result.error.errors[0].message);
    } else {
      console.log(result.data);
    }
  }

  return (
    <>
        ...
    </>
  );
};

export default JoinUser;
  • message를 출력해봅시다. alert이나 modal로 사용자에게 표시할 수 있습니다.

자세한 코드는

https://github.com/gogleset/react_begin /React-Zod 디렉토리에 있습니다.

참고 글

https://www.daleseo.com/?tag=Zod