ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Next.js] Zod + React Hook Form 사용해 회원가입 페이지 만들기
    개발공부/Next.js 2024. 4. 3. 15:58

    본 글은 Next.js에서 Zod + React Hook Form을 사용해 회원가입 페이지를 만든 방법을 다룬 내용입니다.

     

    다양한 프로젝트 진행하면서 항상 회원가입 페이지는 직접 구현했었는데,

    주변에서 간편한데 성능 좋은 라이브러리 조합이 있다고 추천해줘서 사용해봤다.

     

    사용해보니 정말 간편하게 회원가입 기능을 만들 수 있어서 다른 사람들과 함께 공유하면 좋을 거 같아서 글을 쓰게 되었다.

     

    목차

    Zod

    React Hook Form

    실제로 적용해보기

     

    Zod

    TypeScript를 우선으로 하는 스키마 선언 및 유효성 검증 라이브러리이다.

     

    스키마 검증에 사용하며, 사용자 등록 양식을 캡슐화하여 관리한다. 

     

    장점으로는 

    1. 강력한 유효성 검사 → 유효성 검증 후 값을 리턴, 타입을 자연스럽게 추론 가능
    2. 쉬운 사용 → 직관적이고 간단한 API
    3. 의존성 없음

    단점은

    1. 유연성 부족 → 개발자가 원하는 유연성 제공이 안될 수도..
    2. 성능 → 대규모 데이터나 복잡한 스키마의 경우 유효성 검사 규칙이 많으면 런타임 성능에 부담

     

    사용 방식을 간단하게 정리하면 아래와 같다.

    // 스키마 정의
    const schema = z.object({
      name: z.string().min(2, { message: "이름은 2글자 이상이어야 합니다." }),
      // 그 외 사용할 수 있는 메서드로는
      // string(): 문자열 타입을 나타내며, 문자열에 대한 유효성 검사를 수행
      // number(): 숫자 타입을 나타내며, 숫자에 대한 유효성 검사를 수행
      // boolean(): 부울 타입을 나타내며, 참/거짓 값에 대한 유효성 검사를 수행
      // array(): 배열 타입을 나타내며, 배열에 대한 유효성 검사를 수행
      // object(): 중첩된 객체를 정의할 수 있으며, 객체에 대한 유효성 검사를 수행
      // nullable(): 값이 null일 수 있는지 여부를 나타냄
      // optional(): 값을 생략할 수 있는지 여부를 나타냄
      // refine(): 사용자 정의 유효성 검사 함수를 정의
    });
    
    // 스키마 사용해 입력 데이터의 유효성 검사
    const { register, handleSubmit, formState: { errors } } = useForm({
      resolver: zodResolver(schema),
      defaultValues: {
          name: "",
      },
    });

     

    React Hook Form

    폼을 쉽게 관리하기 위한 라이브러리이다. 

     

    비제어 컴포넌트 방식 사용으로 불필요한 리렌더링을 최소화 시켜준다.

    제어 컴포넌트: React에 의해 값이 제어됨
    비제어 컴포넌트: React에 의해 값이 제어되지 않음

     

    장점으로는

    1. 높은 성능 → 불필요한 리렌더링을 최소화하여 성능 향상
    2. TypeScript 지원 → TS와 함께 사용할 수 있어 타입 안정성 보장

     

    단점은

    1. 커스터마이징 어려움 → 고급 기능 구현시에는 추가적인 작업이 필요

     

    사용 방식을 간단하게 정리하면 아래와 같다.

    // 1) import useForm 
    import { useForm } from "react-hook-form";
    
    type newUserType = {
        name: string;
    }
    
    const SignUp = () => {
        // 2) React Hook Form을 사용하여 폼을 초기화
    		// register: 폼 필드를 React Hook Form에 등록하는 역할 (등록된 필드를 추적, 폼 데이터를 수집)
    		// handleSubmit: 폼이 제출될 때 호출될 함수를 정의
    		// formState: { errors }: 폼 상태를 나타내며, 주로 입력 필드의 유효성 검사 에러를 포함
        const { register, handleSubmit, formState: { errors } } = useForm({
            resolver: zodResolver(schema), // 폼 유효성 검사에 Zod 스키마를 사용
            defaultValues: { // 기본값 설정
                name: "",
            },
        });
    
    	// 3) 폼 제출할 때 호출될 onSubmit 함수 정의 (실제 입력된 값 확인 가능)
        const onSubmit = (data: newUserType) => {
            alert(JSON.stringify(data, null, 4));
        };
    
        return (
    	    // 4) 폼 구성하기
            <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col space-y-10">
                <label htmlFor="name">이름</label>
    
    			{/* 5) 제어된 컴포넌트 사용, register을 사용하여 폼 필드를 등록 */}
                <input type="text" id="name" placeholder="홍길동" {...register("name")}/>
                
    			{/* 6) 양식 유효성 검사: 필요한 경우 폼 필드에 대한 유효성 검사 추가 */}
    			{errors.name && <p>{errors.name.message}</p>}
            </form>
        )
    }
    
    export default SignUp;

     

    실제로 적용해보기

    1. 설치하기

    // zod
    npm install zod
    yarn add zod

    // React Hook Form
    npm install react-hook-form
    yarn add react-hook-form

     

    2. 스키마 설정

    zod의 object() 함수 사용해 스키마 및 메시지 생성

    const SignUp = () => {
        // 스키마
        const schema = z.object({
            name: z.string().min(2, { message: "이름은 2글자 이상이어야 합니다." }),
            email: z.string().email({ message: "올바른 이메일을 입력해주세요." }),
            tel: z.string().refine(value => value.startsWith('010'), { message: "010으로 시작하는 11자리 숫자를 입력해주세요." })
                            .refine(value => value.length >= 11, { message: "연락처는 11자리여야 합니다." }),
            role: z.string().nonempty({ message: "역할을 선택해주세요." }),
            password: z.string().min(6, { message: "비밀번호는 최소 6자 이상이어야 합니다." })
                             .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/, { message: "비밀번호는 영문, 숫자, 특수문자를 포함해야 합니다." }),
            passwordConfirm: z.string()
                            .min(6, { message: "비밀번호는 최소 6자 이상이어야 합니다." })
                            .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/, { message: "비밀번호는 영문, 숫자, 특수문자를 포함해야 합니다." })
        }).refine((data) => data.password === data.passwordConfirm, {
            path: ["passwordConfirm"],
            message: "비밀번호가 일치하지 않습니다.",
        });

     

    3. 스키마 적용 및 유효성 검사

    사용자로부터 입력 받은 데이터 관리를 위해 React Hook Form 사용, 위에서 생성한 스키마 적용

    const { register, handleSubmit, formState: { errors } } = useForm({
        resolver: zodResolver(schema),
        defaultValues: {
            name: "",
            email: "",
            tel: "",
            role: "",
            password: "",
            passwordConfirm: "",
        },
    });

     

    4. 실제로 <form> 태그에 적용해서 사용

    <form> 태그의 onSubmit에 useForm에서 준 handleSubmit을 적용

    return (
            <article>
                <section>
                    <h1>계정을 생성합니다.</h1>
                    <span>필수 정보를 입력해볼게요.</span>
                </section>
                <section>
                    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col space-y-10">
                        <div>
                            <label htmlFor="name">이름</label>
                            <input type="text" id="name" placeholder="홍길동" {...register("name")}/>
                            {errors.name && <p>{errors.name.message}</p>}
                        </div>
    
                        <div>
                            <label htmlFor="email">이메일</label>
                            <input type="email" id="email" placeholder="hello@sparta-devcamp.com" {...register("email")}/>
                            {errors.email && <p>{errors.email.message}</p>}
                        </div>
    
                        <div>
                            <label htmlFor="tel">연락처</label>
                            <input type="tel" id="tel" placeholder="01000000000" {...register("tel")}/>
                            {errors.tel && <p>{errors.tel.message}</p>}
                        </div>
    
                        <div>
                            <label htmlFor="role">역할</label>
                            <select id="role" {...register("role")}>
                                <option value="" disabled selected hidden>역할을 선택해주세요</option>
                                <option value="관리자">관리자</option>
                                <option value="일반 사용자">일반 사용자</option>
                            </select>
                            {errors.role && <p>{errors.role.message}</p>}
                        </div>
                        <div>
                            <label htmlFor="password">비밀번호</label>
                            <input type="password" id="password" {...register("password")}/>
                            {errors.password && <p>{errors.password.message}</p>}
                        </div>
    
                        <div>
                            <label htmlFor="passwordConfirm">바말번호 확인</label>
                            <input type="password" id="passwordConfirm" {...register("passwordConfirm")}/>
                            {errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}
                        </div>
    
                        <button type="submit">다음 단계로 → </button>
                        <button>이전 단계로</button>
                    </form>
                </section>
            </article>
        )
    }

     

    전체 코드

    더보기

    "use client"

    import React from "react";
    import { useForm } from "react-hook-form";
    import { z } from "zod";
    import { zodResolver } from '@hookform/resolvers/zod';

    type newUserType = {
        name: string;
        email: string;
        tel: string;
        role: string;
        password: string;
        passwordConfirm: string;
    }

    const SignUp = () => {
        // 스키마
        const schema = z.object({
            name: z.string().min(2, { message: "이름은 2글자 이상이어야 합니다." }),
            email: z.string().email({ message: "올바른 이메일을 입력해주세요." }),
            tel: z.string().refine(value => value.startsWith('010'), { message: "010으로 시작하는 11자리 숫자를 입력해주세요." })
                            .refine(value => value.length >= 11, { message: "연락처는 11자리여야 합니다." }),
            role: z.string().nonempty({ message: "역할을 선택해주세요." }),
            password: z.string().min(6, { message: "비밀번호는 최소 6자 이상이어야 합니다." })
                             .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/, { message: "비밀번호는 영문, 숫자, 특수문자를 포함해야 합니다." }),
            passwordConfirm: z.string()
                            .min(6, { message: "비밀번호는 최소 6자 이상이어야 합니다." })
                            .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/, { message: "비밀번호는 영문, 숫자, 특수문자를 포함해야 합니다." })
        }).refine((data) => data.password === data.passwordConfirm, {
            path: ["passwordConfirm"],
            message: "비밀번호가 일치하지 않습니다.",
        });

        const { register, handleSubmit, formState: { errors } } = useForm({
            resolver: zodResolver(schema),
            defaultValues: {
                name: "",
                email: "",
                tel: "",
                role: "",
                password: "",
                passwordConfirm: "",
            },
        });

        const onSubmit = (data: newUserType) => {
            alert(JSON.stringify(data, null, 4));
        };

        return (
            <article>
                <section>
                    <h1>계정을 생성합니다.</h1>
                    <span>필수 정보를 입력해볼게요.</span>
                </section>
                <section>
                    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col space-y-10">
                        <div>
                            <label htmlFor="name">이름</label>
                            <input type="text" id="name" placeholder="홍길동" {...register("name")}/>
                            {errors.name && <p>{errors.name.message}</p>}
                        </div>

                        <div>
                            <label htmlFor="email">이메일</label>
                            <input type="email" id="email" placeholder="hello@sparta-devcamp.com" {...register("email")}/>
                            {errors.email && <p>{errors.email.message}</p>}
                        </div>

                        <div>
                            <label htmlFor="tel">연락처</label>
                            <input type="tel" id="tel" placeholder="01000000000" {...register("tel")}/>
                            {errors.tel && <p>{errors.tel.message}</p>}
                        </div>

                        <div>
                            <label htmlFor="role">역할</label>
                            <select id="role" {...register("role")}>
                                <option value="" disabled selected hidden>역할을 선택해주세요</option>
                                <option value="관리자">관리자</option>
                                <option value="일반 사용자">일반 사용자</option>
                            </select>
                            {errors.role && <p>{errors.role.message}</p>}
                        </div>
                        <div>
                            <label htmlFor="password">비밀번호</label>
                            <input type="password" id="password" {...register("password")}/>
                            {errors.password && <p>{errors.password.message}</p>}
                        </div>

                        <div>
                            <label htmlFor="passwordConfirm">바말번호 확인</label>
                            <input type="password" id="passwordConfirm" {...register("passwordConfirm")}/>
                            {errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}
                        </div>

                        <button type="submit">다음 단계로 → </button>
                        <button>이전 단계로</button>
                    </form>
                </section>
            </article>
        )
    }

    export default SignUp;

    728x90
Designed by Tistory.