현재 구현한 회원가입은 email, username, password를 입력해서 회원가입을 진행한다. 하지만 이메일 검증을 하지 않기 때문에 이를 구현해보려 한다.
간단하게 form에서 입력한 username이 DB에 있는지 boolean 값을 return 해주었다.
export async function checkValidUsername(
username: string
): Promise<{ success: boolean }> {
try {
const db = (await connectDB()).db("guam");
const usersCollection = db.collection<User>("users");
const existingUser = await usersCollection.findOne({ username });
console.log(existingUser)
if (existingUser) {
return { success: false };
} else {
return { success: true };
}
} catch (error) {
console.error("Error checking username:", error);
throw new Error("Failed to check username");
}
}
이 함수를 form에 버튼을 생성해서 유효성 확인을 하면 된다.
이메일 중복 검사는 username과 마찬가지로 코드를 작성했기 때문에 생략한다. 중복 검사를 확인한 후 이메일을 전송해서 인증 코드를 확인할 예정이다. 그 전에 이메일을 전송하는 것부터 구현하기로 했다.
이메일을 전송하기 위해 nodemailer를 사용하기로 했다.
nodemailer
Nodemailer는 Node.js 응용 프로그램을 위한 모듈로 이메일을 쉽게 전송할 수 있다.
공식 문서에 자세하게 나와 있어서 그대로 커스텀해서 사용했다.
"use server";
import nodemailer from "nodemailer";
export type EmailType = {
from: string;
to: string;
subject: string;
message: string;
};
const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: process.env.NODEMAILER_EMAIL,
pass: process.env.NODEMAILER_EMAIL_APP_PASS,
},
});
export async function sendEmail({ from, to, subject, message }: EmailType) {
const mailData = {
to: to,
subject: `[이메일 인증] 회원가입 인증번호`,
from: from,
html: `
<h1>${subject}</h1>
<div>${message}</div>
</br>
<p>보낸사람 : ${from}</p>
`,
};
return transporter.sendMail(mailData);
}
이메일을 전송하기 위해서 Gmail을 사용했는데 원래는 사용할 이메일 보안수준을 낮추어 사용했지만, 2024년 가을부터 이를 더 이상 지원하지 않는다고 구글에서 발표했다. 따라서 앱 비밀번호를 만들어서 사용했다.
앱을 생성하면 16자리 비밀번호를 알려준다. 이메일과 비밀번호를 env 파일에 등록하여 사용할 수 있다.
// .env
NODEMAILER_EMAIL="____@gmail.com"
NODEMAILER_EMAIL_APP_PASS="____ ____ ____ ____"
// SignupForm.tsx
const mailOptions = {
from: "FROM에서 보냄",
to: "이메일 보낼 곳",
subject: "[이메일 인증] 회원가입 인증번호입니다.",
message: "message",
};
async function verifyEmail() {
return sendEmail(mailOptions)
.then((res) => console.log(res))
.catch((err) => console.log(err));
}
아래와 같이 이메일이 전송된다.

이제 이메일을 보낼 때 인증번호를 보내고, 이를 확인하는 코드를 구현해야 한다.
먼저 verifyEmail을 수정했다. 누구에게 보낼지 파라미터로 받아서 6자리의 랜덤 숫자를 생성해서 같은지 인증을 확인한다.
async function verifyEmail(to: string) {
const number = Math.floor(100000 + Math.random() * 900000);
console.log("number : " + number);
setAuthNumber(number);
const mailOptions = {
from: "FROM에서 보냄",
to: to,
subject: "[이메일 인증] 회원가입 인증번호입니다.",
message: `<h3>인증번호 : ${number}</h3>`,
};
return sendEmail(mailOptions)
.then((res) => console.log(res))
.catch((err) => console.log(err));
}
const verifyNumber = () => {
const inputNumber = numberRef.current?.value;
if (!inputNumber || !authNumber) return;
console.log(inputNumber);
if (inputNumber === authNumber.toString()) {
setIsVerifiedEmail(true);
} else {
setIsVerifiedEmail(false);
}
};
아래는 form 에서 이메일 인증 부분의 코드이다. 상태에 따라 값을 다르게 나타내었다.
// SignupForm.tsx
...
<div className="flex justify-between">
<label className="text-xl self-start" htmlFor="email">
이메일
</label>
{errors.email?.message && <span>{errors.email?.message}</span>}
<div className="flex gap-4 items-center">
{emailValid !== null && (
<p className={`text-${emailValid ? "green" : "red"}-500`}>
{emailValid
? "사용 가능한 이메일입니다."
: "이메일이 이미 사용 중입니다."}
</p>
)}
<button
type="button"
className="px-4 py-2 border border-black rounded-lg"
onClick={validEmailHandler}
disabled={checkingEmail}
>
{checkingEmail ? "확인 중..." : "중복 확인"}
</button>
</div>
</div>
<input
className="flex mb-4 border-gray-600 border py-2 px-4"
type="email"
id="email"
placeholder="이메일"
required
{...register("email")}
/>
{emailValid && (
<button
className={`px-4 py-2 bg-gray-800 rounded-2xl mb-4 w-[240px] self-center text-white tracking-widest bg-opacity-${isVerifiedEmail ? "40" : "100"}`}
onClick={() => {
setIsGettingAuth(true);
verifyEmail(watch("email"));
}}
disabled={isVerifiedEmail ? true : false}
>
{isGettingAuth
? isVerifiedEmail
? "인증 완료"
: "인증코드 다시 받기"
: "인증코드 받기"}
</button>
)}
{isGettingAuth && (
<div className="mb-2">
<input
className="px-6 py-2 border border-gray-500 mr-4 rounded-xl"
name="number"
ref={numberRef}
placeholder="인증코드"
/>
<button
className={`px-4 py-2 border border-black rounded-md bg-green-500 text-white bg-opacity-${isVerifiedEmail ? "40" : "100"} mb-2`}
onClick={verifyNumber}
disabled={isVerifiedEmail ? true : false}
>
확인
</button>
{isVerifiedEmail !== null && isVerifiedEmail ? (
<p>인증 완료되었습니다.</p>
) : (
<p>인증 번호가 다릅니다.</p>
)}
</div>
)}
...