I T H

[MySchedule project] 4. frontend - 회원가입 로직 본문

React + Node.js

[MySchedule project] 4. frontend - 회원가입 로직

thdev 2024. 1. 25. 10:58
frontend의 회원가입의 흐름과, 어떤함수들을 사용하고, 타입스크립트를 어떻게 적용하는지에 대해 작성할 것임.
들어가기에 앞서 이번챕터의 사용할 훅들과 함수들을 미리 알아보자!

- useForm: form을 관리하는 커스텀훅으로  state와 onChange함수의 많은 양을 줄이고 반복적인 코드의 양을 줄일 수 있다. 회원가입과 같이 입력받을 컬럼들이 많을 경우 사용하면 유용할 훅이라고 볼수있다.
 useFrom은 여러가지의 리턴 props을 가지고 있다. 필자는 register, handleSubmit, reset, watch 등을 사용했다.
- 입력받을 변수 타입들에 대한 ts파일을 만들어서 import해서 사용함.
- 지난 챕터에서 미리 만들어둔 useAddDispatch를 사용해 리듀서를 호출할 것이다.

 

[proxy 설정]

- 먼저 package.json에 백앤드 url을 호출하는 proxy를 넣어주자.

 

 

[src / types폴더 / register.ts]

 

- useForm에 넣을 타입과 onSubmit함수 호출시 전달할 파라미터의 타입에 대해 interface로 만들어놨다.

export interface RegisterValue {
  userId: string;
  userPwd: string;
  userPwdCheck: string;
  userName: string;
  userPhone: string;
  userEmail: string;
}

export interface RegisterParams {
  userId: string;
  userPassword: string;
  userName: string;
  userPhone: string;
  userEmail: string;
  userImage: string;
}

 

[src / pages / Register폴더 / index.tsx]

1) useForm 사용

- register: validation check(유효성검사)를 하였음. 

  이메일양식 체크와 같은 패턴과 자리수 체크와 같은 기능을 넣을수있다.

- watch : input태그의 값들을 가져올수있다. useRef함수와 같이 사용하였음.
- reset: 값들을 초기화 한다.

- handleSubmit: form 안의 데이터들을 받는 함수이다. from태그의 onsubmit안에 지정된 함수로 이동함.

- mode는 onChange를 사용해 인풋 태그의 값을 입력받을때마다 validation check를 하게 하였음.

- 타입스크립트 사용시 useForm의 타입을 넣어주어야함. 위에서 미리 만들어논 타입을 import함. java의 제네릭문법임.

   useForm<RegisterValue>({ mode: "onChange" });

 

2) Email 자동완성 함수

- email의 @까지 입력하면 @뒤에 도메인주소가 자동으로 뜨게 설정하였음.

  사용자가 쉽게 이메일주소를 입력할수있도록 구현함.

 

3) id 중복확인 함수

- 똑같은 id가 db에 있는지 체크하는 함수임.

- 이함수는 단순히 체크만 하면되서 리덕스의 dispatch를 사용하진 않고, 바로 axios를 사용해 백앤드로 id값을 보냄.

- id값이 있다면 toast로 창을 띄운 후  값을 재입력 받게 하였고, 없다면 사용가능한 아이디로 중복확인버튼을 사라지게 하였음.

 

4) onSubmit 함수

- dispatch함수를 사용해 리덕스의 thunkFunction.tsx로 전달하였다. 타입을 지정해놔야하므로, 미리 등록해둔 타입을 지정한 메서드를 가져와서 사용하였음. ( redux.ts에 만들어놓은 useAddDispatch )

- 회원가입이 완료되었다면 (백앤드의 데이터에 잘 저장되었다면)  reset()으로 초기화시키고, navigate함수로 메인페이지로 이동하게 하였음.

- 전달하는 파라미터의 타입과 받는쪽의 타입을 잘 넣도록 하자!

import React, { ChangeEvent, useRef, useState } from "react";
import { RegisterParams, RegisterValue } from "../../types/register";
import { useForm } from "react-hook-form";
import axiosInstance from "../../utils/axios";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { userRegister } from "../../store/thunkFunction";
import { useNavigate } from "react-router-dom";
import { useAddDispatch } from "../../store/redux";
const Register = () => {
  const {
    register, //등록함수
    handleSubmit,
    formState: { errors },
    reset,
    watch,
  } = useForm<RegisterValue>({ mode: "onChange" });
  //} = useForm<RegisterValue>({ mode: "onSubmit" });
  const dispatch = useAddDispatch();
  const navigate = useNavigate();
  const [userEmail, setUserEmail] = useState("");
  const [emailList, setEmailList] = useState<string[]>([]);
  const [idcheck, setIdCheck] = useState(false);
  const domainEmails = [
    "@naver.com",
    "@gmail.com",
    "@daum.net",
    "@nate.com",
    "@kakao.com",
  ];
  const emailForm =
    /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;

  //이메일 자동완성 함수
  const emailHandleChange = (e: ChangeEvent<HTMLInputElement>) => {
    e.preventDefault();
    const userEmails = domainEmails.map((email) =>
      e.target.value.includes("@")
        ? e.target.value.split("@")[0] + email
        : e.target.value + email
    );
    setUserEmail(e.target.value);
    setEmailList(userEmails);
  };

  // VALIDATION CHECK START
  // register등록함수와 함께 사용한다. {...register("userId", userId)}
  const userId = {
    required: "필수입력 요소입니다.",
    minLength: {
      value: 3,
      message: "최소 3글자 이상으로 입력해 주세요.",
    },
  };
  const userPwd = {
    required: "필수입력 요소입니다.",
    pattern: {
      value:
        /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{11,20}$/,
      message: "영문, 숫자, 특수문자 포함 11자 이상 20 자 이하로 입력해 주세요",
    },
  };
  const userName = {
    required: "필수입력 요소입니다.",
  };
  const userPhone = {
    required: "필수입력 요소입니다.",
    minLength: {
      value: 11,
      message: "숫자 11자리 이상으로 입력해 주세요.",
    },
  };
  //비밀번호 일치여부 체크 - watch사용해서 input의 userPwd의 값을 가져옴
  const passwordRef = useRef<string | null>(null);
  passwordRef.current = watch("userPwd");
  //VALIDATION CHECK END

  //아이디 체크
  const handleIdCheck = async (
    e: React.MouseEvent<HTMLButtonElement, MouseEvent>
  ) => {
    e.preventDefault();
    let id = watch("userId");
    if (id === "" || id === null) {
      toast.info("아이디를 입력해주세요.");
      return;
    }
    const body = {
      userId: id,
    };
    try {
      const idCheckValue = await axiosInstance.post("/register/idCheck", body);
      console.log(idCheckValue);
      if (idCheckValue.data !== "") {
        //값이 있으면
        toast.info("이미 등록된 아이디 입니다.");
      } else {
        toast.info("사용가능한 아이디 입니다.");
        setIdCheck(true);
      }
    } catch (error) {
      console.error(error);
    }
  };

  //Submit 버튼 이벤트
  const onSubmit = (
    // event: React.FormEvent<HTMLFormElement>,
    { userId, userPwd, userName, userPhone }: RegisterValue
  ) => {
    // event.preventDefault();
    const body: RegisterParams = {
      userId,
      userName,
      userPhone,
      userEmail,
      userPassword: userPwd,
      userImage: "",
    };
    console.log(body);
    if (idcheck) {
      dispatch(userRegister(body));
      reset();
      navigate("/");
    } else {
      toast.info("아이디 중복확인을 먼저 해주세요.");
    }
  };

  return (
    <section className="sm:max-w-[900px] max-w-[1100px] m-auto mt-16">
      <h2 className="text-2xl font-bold text-center my-3">회원가입</h2>
      {/* <hr className="h-3 my-2" /> */}
      {/* <div className="p-3 bg-white rounded-md shadow-md"> */}
      <div className="p-3 bg-white rounded-md my-2">
        <form className="w-full" onSubmit={handleSubmit(onSubmit)}>
          <table className="sm:w-8/12 w-10/12 m-auto text-left ">
            <thead></thead>
            <tbody>
              <tr className="border-t-[1px] h-[60px] w-full">
                <th className="p-3 bg-neutral-100 sm:text-[14px] text-[11px] font-semibold sm:w-4/12 w-5/12">
                  아이디
                </th>
                <td className="py-3 w-8/12">
                  <input
                    className="my-2 mx-2 sm:w-[250px] w-[150px] rounded-md border-[1px] border-gray-200  border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
                    type="text"
                    id="userId"
                    disabled={idcheck}
                    {...register("userId", userId)}
                  />

                  {idcheck ? (
                    <button disabled={idcheck}></button>
                  ) : (
                    <button
                      onClick={handleIdCheck}
                      className="sm:w-15 w-15 mx-2 m-auto bg-blue-500 h-[45px] text-white font-semibold sm:text-[14px] text-xs py-2 px-2 rounded-md hover:bg-blue-600"
                    >
                      중복확인
                    </button>
                  )}
                  {errors?.userId && (
                    <div className="py-1">
                      <span className="text-red-500 pl-2 text-sm">
                        {errors.userId.message}
                      </span>
                    </div>
                  )}
                </td>
              </tr>

              <tr className=" border-t-[1px] h-[60px] w-full">
                <th className="p-3 bg-neutral-100 sm:text-[14px] text-[11px] font-semibold ">
                  비밀번호
                </th>
                <td className="py-3 w-8/12">
                  <input
                    className="my-2 mx-2 sm:w-[250px] w-[150px] rounded-md border-[1px] border-gray-200  border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
                    type="password"
                    id="userPwd"
                    {...register("userPwd", userPwd)}
                  />
                  {errors?.userPwd && (
                    <div className="py-1">
                      <span className="text-red-500 pl-2 text-sm">
                        {errors.userPwd.message}
                      </span>
                    </div>
                  )}
                </td>
              </tr>

              <tr className=" border-t-[1px] h-[60px] w-full">
                <th className="p-3 bg-neutral-100 sm:text-[14px] text-[12px] font-semibold">
                  비밀번호확인
                </th>
                <td className="py-3 w-8/12">
                  <input
                    className="my-2 mx-2 sm:w-[250px] w-[150px] rounded-md border-[1px] border-gray-200  border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
                    type="password"
                    id="userPwdCheck"
                    minLength={11}
                    {...register("userPwdCheck", {
                      required: "필수입력 요소입니다.",
                      validate: (value) => value === passwordRef.current,
                    })}
                  />
                  {errors?.userPwdCheck?.type === "required" && (
                    <div className="py-1">
                      <span className="text-red-500 pl-2 text-sm">
                        {errors.userPwdCheck.message}
                      </span>
                    </div>
                  )}
                  {errors?.userPwdCheck?.type === "validate" && (
                    <div className="py-1">
                      <span className="text-red-500 pl-2 text-sm">
                        비밀번호가 맞지 않습니다. 한번더 확인해 주세요.
                      </span>
                    </div>
                  )}
                </td>
              </tr>

              <tr className=" border-t-[1px] h-[60px] w-full">
                <th className="p-3 bg-neutral-100 sm:text-[14px] text-[11px] font-semibold ">
                  사용자명
                </th>
                <td className="py-3 w-8/12">
                  <input
                    className="my-2 mx-2 sm:w-[250px] w-[150px] rounded-md border-[1px] border-gray-200  border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
                    type="text"
                    id="userName"
                    {...register("userName", userName)}
                  />
                  {errors?.userName && (
                    <div className="py-1">
                      <span className="text-red-500 pl-2 text-sm">
                        {errors.userName.message}
                      </span>
                    </div>
                  )}
                </td>
              </tr>

              <tr className=" border-t-[1px] h-[60px] w-full">
                <th className="p-3 bg-neutral-100 sm:text-[14px] text-[11px] font-semibold ">
                  휴대폰번호
                </th>
                <td className="py-3 w-8/12">
                  <input
                    className="my-2 mx-2 sm:w-[250px] w-[150px] rounded-md border-[1px] border-gray-200  border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
                    type="number"
                    id="userPhone"
                    {...register("userPhone", userPhone)}
                  />
                  {errors?.userPhone && (
                    <div className="py-1">
                      <span className="text-red-500 pl-2 text-sm">
                        {errors.userPhone.message}
                      </span>
                    </div>
                  )}
                </td>
              </tr>

              <tr className="border-t-[1px] h-[60px] w-full">
                <th className="p-3 bg-neutral-100 sm:text-[14px] text-[11px] font-semibold ">
                  이메일
                </th>
                <td className="py-3 w-8/12">
                  <input
                    id="userEmail"
                    className="my-2 mx-2 sm:w-[250px] w-[150px] rounded-md border-[1px] border-gray-200  border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
                    list="email"
                    value={userEmail}
                    onChange={emailHandleChange}
                  />
                  <datalist id="email">
                    {emailList &&
                      emailList.map((email, idx) => (
                        <option value={email} key={idx} />
                      ))}
                  </datalist>

                  {userEmail && !emailForm.test(userEmail) && (
                    <p className="p-2 text-red-500 text-sm">
                      이메일 형식을 확인해주세요.
                    </p>
                  )}
                </td>
              </tr>
              <tr className="border-t-[1px] h-[60px] w-full">
                <td colSpan={2} className="text-center pl-0">
                  <button
                    type="submit"
                    className="m-auto w-2/12 bg-blue-500 h-[45px] text-white font-semibold sm:text-[16px] text-sm py-2 my-4 px-2 rounded-md hover:bg-blue-600"
                  >
                    가입
                  </button>
                </td>
              </tr>
            </tbody>
          </table>
        </form>
      </div>
    </section>
  );
};

export default Register;

 

[ src / store / thunkFunction.tsx ]

 

- 파마리터(body) 타입 : ' types폴더 / register.ts' 에서 지정한 타입을 넣어주었음.

- request url : 'http://localhost:OOOO/register', parameter: body에 id값을 넣어서 백앤드로 호출하였고,

   백앤드에서 전달받은 값을 받아 response에 넣었다.

   return response.data는 redux의 payload임. 'store / userSlice.tsx'의 reducer에  payload로 넣으면 store에 저장된다.

import { createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../utils/axios";
import { RegisterParams } from "../types/register";

export const userRegister = createAsyncThunk(
  "userRegister",
  async (body: RegisterParams, thunkAPI) => {
    try {
      const response = await axiosInstance.post(`/register`, body);

      return response.data; //payload
    } catch (error: any) {
      console.log(error);
      return thunkAPI.rejectWithValue(error.response.data || error.message);
    }
  }
);

 

[ src / store / userSlice.tsx ]

- return response.data 는 payload이다. 백앤드에서 받아오는 데이터가 있을 경우엔 action.payload 데이터를 미리 만들어둔 (initialState) -> state에 넣어준다. 그렇게되면 redux - store에 저장된 데이터가 뜨게 됨.

const userSlice = createSlice({
  name: "user",
  initialState: initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(userRegister.pending, (state: any) => {
        state.isLoading = true;
      })
      .addCase(userRegister.fulfilled, (state) => {
        state.isLoading = false;
        toast.info("회원가입이 완료되었습니다.");
      })
      .addCase(userRegister.rejected, (state, action: any) => {
        state.isLoading = false;
        state.error = action.payload;
        toast.error(action.payload);
      });
  },
});

 

[결과화면]

- 회원가입 /  id 중복확인

 

 

- 회원가입 / 이메일 자동완성기능