7/26 이메일 중복검사 및 회원가입 구현 마무리

낄낄박사·2024년 7월 25일

Gotcha

목록 보기
9/22

중복 검사 버튼을 누르면 handleSubmit이 실행되는 문제

이는 기본적으로 폼 내의 버튼을 클릭하면 해당 버튼이 submit 타입으로 간주되기 때문임. 이로 인해 폼이 제출되고 handleSubmit 함수가 호출됨. 이를 방지하려면 중복 검사 버튼의 타입을 button으로 명시적으로 지정해야 함.

해결

이메일 유효성 검사 상태 관리 개선

기존에는 이메일 중복 검사 결과를 setEmailError 상태로만 관리하고, "사용가능한 이메일입니다"와 같은 사용 가능 여부를 동일한 상태에서 관리했음.

이메일 사용 가능 여부를 더 명확하게 관리하기 위해 emailAvailable이라는 새로운 상태를 추가함.

UI 업데이트

이메일 사용 가능 여부에 따라 UI에서 적절한 메시지를 표시하도록 span 태그를 추가하여, emailAvailable 상태가 ture인 경우에만 메시지를 렌더링하도록 변경함.

이메일 입력 변경 시 상태 초기화

기존에는 이메일 값이 변경될 때 setEmail만 호출하던 것을 수정.
이메일 값이 변경될 때마다 setEmailError와 setEmailAvailable을 null로 초기화하여, 중복 검사 결과를 무효화.
이로 인해, 사용자가 이메일을 변경할 때마다 에러 메시지와 이메일 사용 가능 여부가 초기화되어, 회원가입 버튼은 이메일 중복 검사를 하지 않으면 비활성화되도록 설정.

회원가입 버튼 활성화 조건

회원가입 버튼은 emailAvailable이 반드시 true일 때만 활성화되도록 변경.
다른 모든 필드 값이 유효해도 이메일 중복 검사를 하지 않은 경우 버튼이 비활성화되도록 로직 수정.

유효성 검사 로직 개선

기존에 useErrorMessage 훅에서 setTimeout을 사용하여 키 입력 후 일정 시간 후에 유효성 검사를 수행하던 로직을 제거.
키 입력 후 즉시 유효성 검사 결과를 업데이트하도록 변경.

  • 이유: setTimeout을 사용하는 경우, 비밀번호가 유효한 상태에서 키 입력으로 하나의 문자를 삭제한 후, 유효성 검사 메시지가 업데이트되기 전에 사용자가 버튼을 클릭하여 폼이 제출되는 문제가 발생함. 이를 방지하기 위해 입력 즉시 유효성 검사가 이루어지도록 수정하여 실시간으로 유효성 검사 결과를 반영하도록 함.

의존성배열

: useEffect, useCallback, useMemo 훅에서 함수나 값의 변경 여부를 감지하여 해당 훅의 로직을 실행할지를 결정하는 것임. SignupForm 컴포넌트에서 의존성 배열로 사용할 수 있는 값은 주로 상태 값(state)이나 props임.

전체코드

"use client";
import { useErrorMessage } from "@/hooks/errorMessage";
import { useRouter } from "next/navigation";
import { FormEvent, MouseEventHandler, useState } from "react";

type Props = {
  csrfToken: string;
};

export default function SignupForm({ csrfToken }: Props) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [emailAvailable, setEmailAvailable] = useState<boolean>(false);

  const router = useRouter();

  const {
    nameError,
    emailError,
    passwordError,
    confirmPasswordError,
    setEmailError,
  } = useErrorMessage({ name, email, password, confirmPassword });

  const isButtonDisabled =
    nameError !== null ||
    emailError !== null ||
    passwordError !== null ||
    confirmPasswordError !== null ||
    emailAvailable == false;

  const handleEmailCheck = async () => {
    if (emailError || email == "") return;

    await fetch("/api/check-email", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(email),
    })
      .then((res) => res.json())
      .then((res) => {
        if (res) {
          setEmailError("이미 사용중인 이메일 입니다.");
          setEmailAvailable(false);
        }
        if (!res) {
          setEmailError(null);
          setEmailAvailable(true);
        }
      });
  };

  const handleSubmit = async () => {
    if (!emailAvailable) return;

    const formData = new FormData();
    formData.append("name", name ?? "");
    formData.append("email", email ?? "");
    formData.append("password", password ?? "");

    fetch("/api/auth/signup", {
      method: "POST",
      body: formData,
    }).then((res) => {
      if (!res.ok) {
        return console.log(`${res.status}${res.statusText}`);
      }
      router.push("/auth/signin");
    });
  };

  return (
    <form method="post" className="flex flex-col" onSubmit={handleSubmit}>
      <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
      <div>
        <label>
          이름
          <input
            name="name"
            value={name}
            type="text"
            className="border"
            onChange={(e) => setName(e.target.value)}
          />
        </label>
        {nameError && <span>{nameError}</span>}
      </div>
      <div>
        <label>
          이메일
          <input
            name="email"
            value={email}
            type="email"
            className="border"
            onChange={(e) => {
              setEmailAvailable(false);
              setEmailError(null);
              setEmail(e.target.value);
            }}
          />
        </label>
        <button className="border" onClick={handleEmailCheck} type="button">
          중복검사
        </button>
        {emailError && <span>{emailError}</span>}
        {emailAvailable && <span>사용 가능한 이메일입니다.</span>}
      </div>
      <div>
        <label>
          비밀번호
          <input
            name="password"
            value={password}
            type="password"
            className="border"
            onChange={(e) => setPassword(e.target.value)}
          />
          {passwordError && <span>{passwordError}</span>}
        </label>
      </div>
      <div>
        <label>
          비밀번호 확인
          <input
            name="passwordCheck"
            value={confirmPassword}
            type="password"
            className="border"
            onChange={(e) => setConfirmPassword(e.target.value)}
          />
        </label>
        {confirmPasswordError && <span>{confirmPasswordError}</span>}
      </div>
      <button
        className={`border  ${isButtonDisabled && "bg-gray-200 text-white"}`}
        type="submit"
        disabled={isButtonDisabled}
      >
        회원가입
      </button>
    </form>
  );
}
import { validate } from "@/utils/validate";
import { useEffect, useState } from "react";

type Props = {
  name: string;
  email: string;
  password: string;
  confirmPassword: string;
};

export function useErrorMessage({
  name,
  email,
  password,
  confirmPassword,
}: Props) {
  const [nameError, setNameError] = useState<string | null>(null);
  const [emailError, setEmailError] = useState<string | null>(null);
  const [passwordError, setPasswordError] = useState<string | null>(null);
  const [confirmPasswordError, setConfirmPasswordError] = useState<
    string | null
  >(null);

  useEffect(() => {
    setNameError(name ? validate.name(name) : null);
    setEmailError(email ? validate.email(email) : null);
    setPasswordError(password ? validate.password(password) : null);
    setConfirmPasswordError(
      confirmPassword
        ? validate.confirmPassword(password, confirmPassword)
        : null
    );
  }, [name, email, password, confirmPassword]);

  return {
    nameError,
    emailError,
    passwordError,
    confirmPasswordError,
    setEmailError,
  };
}

0개의 댓글