Velog와 비슷한 블로그 프로젝트: 이메일 보내기

derek·2024년 10월 16일
0

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

// 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 라이브러리를 활용할 계획입니다.

Nodemailer 공식 문서
Yup GitHub 페이지

우선 두 라이브러리를 설치합니다.

yarn add yup nodemailer

계획은 이렇습니다:

  1. 브라우저(클라이언트)에서 이벤트가 발생하면, Vercel에 배포된 API 라우트에 이메일 전송 요청을 보냅니다
  2. 서버의 함수에서 nodemailer를 이용해 이메일을 전송합니다.

Plan

우선 그러면 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,
      });
    });
}

이제 모든 설정이 완료되었고 정상적으로 잘 오네요. 이 글이 도움이 되길 바랍니다 :)

profile
derek

0개의 댓글