일단 기본적인 이메일 전송 기능의 골격을 만들어 놓았습니다. 현재는 비즈니스 로직을 추가하지 않아 단순히 콘솔에 출력하는 형태로 구현되어 있습니다.

// app/contact/page.tsx
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(form);
setBanner({ message: "Success!!", state: "success" });
setTimeout(() => {
setBanner(null);
}, 3000);
};
이제 본격적으로 개발을 시작하기에 앞서, 우리는 Next.js를 사용하고 있으므로 서버에서 이메일을 전송하기 위해 nodemailer 라이브러리와 간단한 유효성 검사를 위한 yup 라이브러리를 활용할 계획입니다.
우선 두 라이브러리를 설치합니다.
yarn add yup nodemailer
계획은 이렇습니다:
nodemailer를 이용해 이메일을 전송합니다.
우선 그러면 1번에 해당하는 클라이언트 측에서 API 요청을 처리하는 부분을 만들려고 합니다. 유지보수와 책임을 분산시키기 위해서 비즈니스 로직을 별도로 추출해야 합니다.
// service/contact.ts
import { EmailData } from "./email";
export async function sendContactEmail(email: EmailData) {
const response = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(email),
headers: { "Content-Type": "application/json" },
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed Server Request 🥲");
}
return data;
}
이제 서버에 POST 요청을 하게 되는데, yup을 사용하여 유효성 검사를 진행하기로 했습니다. yup을 사용하면 유효성 검사를 간단하고 엄격하게 구현할 수 있습니다.
// app/api/contact/route.ts
import { NextResponse } from "next/server";
import * as yup from "yup";
const bodySchema = yup.object().shape({
from: yup.string().email().required(),
subject: yup.string().required(),
message: yup.string().required(),
});
export async function POST(request: Request) {
const body = await request.json();
if (!bodySchema.isValidSync(body)) {
return new Response(JSON.stringify({ message: "Invalid format" }), {
status: 400,
});
}
return NextResponse.json("");
}
이제 기존 브라우저 페이지의 이메일 전송 로직을 아래와 같이 수정할 수 있게 되었습니다.
// app/contact/page.tsx
const DEFAULT_DATA = {
from: "",
subject: "",
message: "",
};
// ...
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
sendContactEmail(form)
.then(() => {
setBanner({
message: "Email has been sent successfully",
state: "success",
});
setForm(DEFAULT_DATA);
})
.catch(() => {
setBanner({
message: "Failed to send the email. Please try again",
state: "error",
});
})
.finally(() => {
setTimeout(() => {
setBanner(null);
}, 3000);
});
};
이제 nodemailer를 사용하는 작업만 남았다. 공식 문서에 따르면 사용법은 간단한 것 같다. 다만, 인증 정보에 비밀번호를 입력해야 하므로 보안 측면에서 조금 걱정이 된다. 그래서 Gmail을 사용하고 앱 비밀번호를 설정하기로 했다.

참고로 2단계 인증을 설정하고 앱 비밀번호를 추가하면 보안을 한층 강화할 수 있습니다. 해당 과정에 대한 자세한 내용은 구글링을 통해 해결했습니다.
구글 앱 비밀번호 설정
보안 설정에서 '앱 비밀번호'를 검색하면 관련 정보가 더욱 상세하게 나옵니다
환경 변수를 설정한 후, nodemailer를 사용하는 비즈니스 로직을 작성합니다
// service/email.ts
import nodemailer from "nodemailer";
export type EmailData = {
from: string;
subject: string;
message: string;
};
const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: process.env.AUTH_USER,
pass: process.env.AUTH_PASS,
},
});
export async function sendEmail({ subject, from, message }: EmailData) {
const mailData = {
to: process.env.AUTH_USER,
subject: `[BLOG] ${subject}`,
from,
html: `
<h1>${subject}</h1>
<div>${message}</div>
<br/>
<p>보낸사람 : ${from}</p>
`,
};
return transporter.sendMail(mailData);
}
이제 서버에서 이 함수를 호출하여 이메일을 전송하는 작업만 남았습니다. 클라이언트와 서버 간의 소통을 위해 암묵적으로 사용되는 JSON.stringify를 이용해야 합니다
// api/contact/route.ts
export async function POST(request: Request) {
const body = await request.json();
if (!bodySchema.isValidSync(body)) {
return new Response(JSON.stringify({ message: "Invalid format" }), {
status: 400,
});
}
return sendEmail(body)
.then(() =>
new Response(JSON.stringify({ message: "Email sent successfully" }), {
status: 200,
})
)
.catch((err) => {
console.error(err);
return new Response(JSON.stringify({ message: "Fail to send Email" }), {
status: 500,
});
});
}
이제 모든 설정이 완료되었고 정상적으로 잘 오네요. 이 글이 도움이 되길 바랍니다 :)

