Dev Diary

[Question Cloud] 회원가입 만들기 (1) 본문

Projects

[Question Cloud] 회원가입 만들기 (1)

sik9252 2024. 10. 6. 01:14
SMALL

드디어 길었던 디자인 시스템 작업이 끝났다. 자기소개서도 써보고 이력서랑 포트폴리오도 다듬느라 더 오래 걸렸다. 이제 슬슬 프로젝트를 완성하는데 집중해보려고 한다.

 

회원가입부터 만들어볼건데 진행중인 프로젝트에는 크게 2종류의 회원가입을 구현할 것이다.

1. 자체 회원가입: 사용자의 이메일을 직접 기입해 회원가입한다.

2. 소셜 회원가입: 네이버, 카카오, 구글 계정을 이용해 회원가입한다.

 

이번 게시글에서는 자체 회원가입을 구현해 볼 것이고, 핵심 기술로는 zodreact-hook-form을 사용할 것이다.


zod 왜 써?

기존에는 입력 값의 검증을 하나하나 했었다. 만약 비밀번호를 입력받는데 영어, 숫자, 특수문자가 섞여있어야 한다는 조건이 있으면 useState로 password 상태를 하나 만들고 useEffect를 통해 상태가 변경될때마다 지속적으로 검증하는... 그런 방식이었다.

 

이 방식은 뭐 입력 받아야 할 데이터가 적으면 괜찮은데, 이전에 10개나 되는 대량의 입력 데이터를 한번에 받아야했던 적이 있었는데 10개의 입력에 대한 state와 검증을 위한 useEffect를 하나하나 생성하니까 코드가 엄청 길어지고 유지보수가 힘들어졌었다. 그래서 편하게 검증을 수행하는 법이 없을까 찾던 도중 zod에 대해 알게 되었고, 재밌어 보이기도 했고 새로운 방법으로 검증을 시도해보고 싶어서 써보게 되었다.

 

폴더 구조는 이렇게 했다

 

register 화면에 대한 api와 component 등을 넣을 곳이고, 이 곳에 schemas 폴더를 생성해 zod를 위한 Schema를 작성했다.

 

// registerSchema.ts

import { z } from "zod";

// 비밀번호 Schema
const passwordSchema = z
  .string()
  .min(8, "비밀번호는 최소 8자 이상이어야 합니다.")
  .regex(
    /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*()_+{}\[\]:;<>,.?~\\-])/,
    "비밀번호는 영문, 숫자, 특수문자를 포함해야 합니다."
  );

// 전화번호 Schema
const phoneSchema = z
  .string()
  .min(10, "전화번호는 최소 10자 이상이어야 합니다.")
  .regex(/^\d+$/, "전화번호는 숫자만 입력해야 합니다.");

// 회원가입 데이터 Schema
const registerSchema = z
  .object({
    name: z.string().min(1, "닉네임을 입력하세요."),
    email: z.string().email("올바른 이메일 형식을 입력해주세요."),
    password: passwordSchema,
    passwordConfirm: z.string(),
    phone: phoneSchema,
  })
  .refine((data) => data.password === data.passwordConfirm, {
    path: ["passwordConfirm"],
    message: "비밀번호가 일치하지 않습니다.",
  });

export { registerSchema };

 


react-hook-form으로 회원가입 Form 만들기

// RegisterContainer.tsx

"use client";

import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, Input } from "@/components/_shared/ui";
import { registerSchema } from "./schemas";

type RegisterFormValues = z.infer<typeof registerSchema>;

const RegisterContainer = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm<RegisterFormValues>({
    resolver: zodResolver(registerSchema),
    mode: "onChange",
    reValidateMode: "onChange",
  });

  const onSubmit = (data: RegisterFormValues) => {
  // 나중에 API 요청 할 것임
    console.log(data);
  };

  return (
    <div className="space-y-[32px] w-[620px] m-auto">
      <div className="flex justify-center heading1">회원가입</div>
      <form onSubmit={handleSubmit(onSubmit)} className="space-y-[24px]">
        <Input
          label="닉네임"
          isRequired
          type="text"
          placeholder="닉네임을 입력해주세요"
          error={!!errors.name}
          errorMessage={errors.name && errors.name.message}
          {...register("name")}
        />

        <Input
          label="이메일"
          isRequired
          type="email"
          placeholder="이메일을 입력해주세요"
          error={!!errors.email}
          errorMessage={errors.email && errors.email.message}
          {...register("email")}
        />

        <Input
          label="전화번호"
          isRequired
          type="tel"
          placeholder="전화번호를 입력해주세요"
          error={!!errors.phone}
          errorMessage={errors.phone && errors.phone.message}
          {...register("phone")}
        />

        <Input
          label="비밀번호"
          isRequired
          type="password"
          placeholder="비밀번호는 영문, 숫자, 특수문자를 포함하여 8~12 문자로 작성해주세요"
          error={!!errors.password}
          errorMessage={errors.password && errors.password.message}
          {...register("password")}
        />

        <Input
          label="비밀번호 확인"
          isRequired
          type="password"
          placeholder="비밀번호를 한번 더 입력해주세요"
          error={!!errors.passwordConfirm}
          errorMessage={errors.passwordConfirm && errors.passwordConfirm.message}
          {...register("passwordConfirm")}
        />

        <Button variant="navy" size="large" type="submit" disabled={!isValid}>
          회원가입
        </Button>
      </form>
    </div>
  );
};

export { RegisterContainer };

 

이전에 만들어둔 디자인 시스템의 Input 컴포넌트와 Button 컴포넌트를 사용하고, Input에 react-hook-form을 연결해 최종적인 회원가입 Form을 완성했다.

 

isRequired를 가진 Input들에 값이 입력되지 않았다면 회원가입 버튼을 disabled 처리할 것이므로 입력값을 실시간으로 검증하고, 오류 메시지를 실시간으로 출력하기 위해 mode: "onChange", revalidateMode: "onChange"를 추가해주었다.

 

그리고 이 부분은 처음 본 문법(?)인데 사실 얼마 전 시험에서 infer라는게 나왔던거 같은데 몰라서 못풀었다.ㅎㅎ 다시 보게되다니 반갑군

type RegisterFormValues = z.infer<typeof registerSchema>;

 

여튼 이 김에 찾아봤는데, infer를 사용하면 지정한 필드(여기선 registerSchema)에 기반한 타입을 자동으로 추출할 수 있게 해준다고 한다.

type ElementType<T> = T extends (infer U)[] ? U : never;

type StringArray = string[];
type ElementOfStringArray = ElementType<StringArray>;// string

 

T extends (infer U)[]: T가 배열 타입이라면, 그 배열의 요소 타입을 U로 추론한다. 결과적으로, ElementOfStringArray는 string 타입이 된다는 것이다. infer는 복잡한 타입을 자동으로 추론하여 개발자가 더 쉽게 타입을 관리하고 유지할 수 있도록 도와준다는데 사실 잘 모르겠다 나는 아직 저런걸 보면 더 혼란스럽다^^.

 

여튼 중요한건 아래 코드처럼 RegisterFormValues는 registerSchema에 기반한 타입을 자동으로 추론할 수 있게 되는 것이다. 이렇게 하면 좋은 점은 타입을 스키마와 따로 또 작성해 줄 필요도 없고, 스키마가 변경될 때마다 타입을 직접 수정해야 하는 번거로운 일도 없어지게 된다는 것이다.

type RegisterFormValues = {
  name: string;
  email: string;
  password: string;
  passwordConfirm: string;
  phone: string;
};

최종 구현 화면

 

 

다 만들고 보니 콘솔에 이런 Advice가 출력되어 있었다.

 

브라우저가 자동완성 기능을 개선하기 위해 autocomplete 속성을 Input 요소에 추가하도록 권장하는 메시지라고 한다. 크롬 기준 한번 로그인하면 다음부터 해당 페이지 진입 시 입력 값을 채워주는 기능인듯 하다.

 

근데 로그인 페이지도 아니고 회원가입 페이지인데 자동완성?(의문) 여튼 그래서 new-password 속성을 함께 추천해주는 것 같다. new-password 속성을 주면 Input에 "새로운 값 쓸거다? 아무것도 적지마"라고 이야기해주는 것이라고 한다. 사실 그냥 autocomplete 같은거 안쓰면 그만이지만 콘솔에 저렇게 뜨는거 보기도 싫어서 추가해주었다.


다음 게시글에서는 아래 Figma에 구현되어있는 부분인 회원가입 정보를 작성하고 회원가입 버튼을 눌렀을때 인증 이메일이 보내지고, 이메일 인증을 통해 회원가입을 완료하는 부분까지 해보겠다!

LIST