Next.js에서 Nodemailer로 메일 전송하기

포트폴리오에 Contact Me 페이지를 만들어서 Nodemailer로 메일을 전송할 수 있는 기능을 추가해보자

|4분 읽기
ReactTypeScriptNext.jsTailwind CSSReact-Hook-FormNodemailerGmail이메일 전송EmailFormForm ValidationAPI RoutesSMTP프론트엔드

시작

포트폴리오에는 메일, 카카오톡 오픈 채팅 혹은 다양한 SNS 플랫폼을 통해 포트폴리오를 읽는 사람이 연락을 할 수 있는 기능이 존재해야 한다고 생각한다. 현재 포트폴리오에는 기존에 만들어 둔 카카오톡 오픈 채팅을 연결하고자 했으나 다크모드 이슈로 인해 잠정적으로 비활성화해둔 상태였다. 따라서 연락을 할 수 있는 페이지, Contact 페이지를 제작하게 되었다.

react-hook-form을 활용한 form 개발

라이브러리를 활용하지 않고도 form을 개발할 수 있지만, 최근 많은 form을 직접 구현하면서 change 이벤트를 처리하기 위해 디바운싱, 쓰로틀링 처리를 추가적으로 해주어야 하거나, 각 input에 대한 상태를 관리해주어야 하거나, [colocation](https://epicreact.dev/improve-the-performance-of-your-react-forms/) 등의 추가적인 최적화를 해주어야 하기 때문에 비효율과 불편함이 존재했다.

따라서 비제어 컴포넌트를 활용하는 react-hook-form 라이브러리를 통해 보다 개발 경험을 높이고자 했다. 결론적으로 react-hook-form이 다소 러닝커브가 존재한다고 해도, 굉장히 만족스럽고 효율적으로 개발할 수 있었다.

import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";
 
type Inputs = {
  name: string;
  email: string;
  phone: string;
  category: string;
  title: string;
  content: string;
};
 
const Contact = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Inputs>();
 
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
 
  return (
    <main className="relative w-11/12 h-[calc(90vh-50px)] mx-auto md:w-4/5">
      <h2>Contact Me</h2>
      <form
        className="flex flex-col w-1/2 gap-2 mx-auto"
        onSubmit={handleSubmit(onSubmit)}
      >
        <div className="flex justify-between w-full gap-5">
          <input
            className="w-full px-2 rounded"
            type="text"
            required={true}
            placeholder="이름"
            {...register("name", {
              required: "반드시 이름을 입력해주세요.",
              minLength: {
                value: 2,
                message: "2글자 이상을 입력해주세요",
              },
            })}
          />
        </div>
        {errors.name && (
          <p className="w-full text-sm text-right text-red-500">
            {errors.name.message}
          </p>
        )}
        <div className="flex justify-between w-full gap-5">
          <input
            className="w-full px-2 rounded"
            type="email"
            required={true}
            placeholder="이메일"
            {...register("email", {
              required: "반드시 이메일을 입력해주세요.",
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: "이메일 형식에 맞게 입력해주세요.",
              },
            })}
          />
        </div>
        {errors.email && (
          <p className="w-full text-sm text-right text-red-500">
            {errors.email.message}
          </p>
        )}
        <div className="flex justify-between w-full gap-5">
          <input
            className="w-full px-2 rounded"
            type="text"
            placeholder="휴대폰 번호"
            {...register("phone", {
              pattern: {
                value: /^\d{3}-\d{3,4}-\d{4}$/,
                message: "000-0000-0000 형식으로 입력해주세요.",
              },
            })}
          />
        </div>
        {errors.phone && (
          <p className="w-full text-sm text-right text-red-500">
            {errors.phone.message}
          </p>
        )}
        <div className="flex justify-between w-full gap-5">
          <select
            className="w-full px-2 rounded"
            {...register("category", { required: "주제를 선택해주세요." })}
          >
            <option value="">주제</option>
            <option value="채용">채용</option>
            <option value="프로젝트">프로젝트</option>
            <option value="경험">경험</option>
            <option value="기타">기타</option>
          </select>
        </div>
        {errors.category && (
          <p className="w-full text-sm text-right text-red-500">
            {errors.category.message}
          </p>
        )}
        <input
          className="w-full h-6 px-2 mt-4 rounded"
          type="text"
          placeholder="제목"
          {...register("title", {
            minLength: {
              value: 3,
              message: "3글자 이상의 제목을 입력해주세요",
            },
          })}
        />
        {errors.title && (
          <p className="w-full text-sm text-right text-red-500">
            {errors.title.message}
          </p>
        )}
        <textarea
          className="w-full h-32 p-2 rounded"
          placeholder="어떤 사항이 궁금하신가요?"
          {...register("content", {
            minLength: {
              value: 10,
              message: "10글자 이상, 300글자 이하의 내용을 입력해주세요.",
            },
            maxLength: {
              value: 300,
              message: "10글자 이상, 300글자 이하의 내용을 입력해주세요.",
            },
          })}
        ></textarea>
        {errors.content && (
          <p className="w-full text-sm text-right text-red-500">
            {errors.content.message}
          </p>
        )}
        <button className="w-full h-8 font-bold rounded bg-sky-500 hover:bg-sky-500/40 hover:text-sky-500">
          문의하기
        </button>
      </form>
    </main>
  );
};
 
export default Contact;

validation과 에러를 유연하게 설정할 수 있고, textarea, input, select 모두 쉽게 작성할 수 있으며, form data를 정말 손쉽게 받아올 수 있다는 점이 아주 인상깊었다. Typescript를 활용했기 때문에 타입 설정 부분에서 약간 문제가 있었지만, 굉장히 친절하고 깔끔한 공식 문서가 존재하기 때문에 이를 잘 참고하면 쉽게 배울 수 있다고 생각한다. 보다 많은 기능이 있으나, 필요한 부분만 속성으로 학습하고 넘어갔기 때문에 추가적으로 공식 문서를 보며 학습해봐야 할 것 같다.

Next.js와 Nodemailer를 활용한 메일 전송 기능 추가하기

Next.js에 존재하는 pages/api/*에 API를 구축할 수 있다. 따라서, 손쉽게 Nodemailer 라이브러리를 활용해 메일 전송 기능을 추가할 수 있었다.

먼저 Nodemailer를 활용해 메일을 전달받을 새로운 gmail 계정을 만들어주고, App Access 관련 보안을 낮춰주어야 한다.

Untitled

다만, 2022년 5월 30일부로 해당 설정을 수정할 수 없게 되었기에, 다른 방법을 활용해야 해서 검색을 해봤더니 친절히 나와있었다. 역시 잘 모르면 GitHub의 Issue를 확인해보면 된다.

google security setting · Issue #1424 · nodemailer/nodemailer

즉, 2단계 인증을 활성화하고, 앱 비밀번호를 기타 항목으로 새로 설정한 후, 해당 비밀번호를 통해 메일을 전송하면 된다.

import type { NextApiRequest, NextApiResponse } from "next";
 
export default function (req: NextApiRequest, res: NextApiResponse) {
  let nodemailer = require("nodemailer");
 
  const transporter = nodemailer.createTransport({
    port: process.env.SMTP_PORT,
    host: process.env.SMTP_HOST,
    auth: {
      user: process.env.SMTP_USERNAME,
      pass: process.env.SMTP_PASSWORD,
    },
    secure: true,
  });
 
  const mailData = {
    from: req.body.email,
    to: "contact.portfolio23@gmail.com",
    subject: `Message From ${req.body.name}`,
    text: req.body.content + " | Sent from: " + req.body.email,
    html: `<div>${req.body.content}</div><p>Sent from:
    ${req.body.email}</p>`,
  };
 
  transporter.sendMail(mailData, (err: any, info: any) => {
    if (err) console.log("error", err);
    else console.log(info);
  });
 
  res.status(200);
}

이 또한 공식 문서에 정말 자세히 잘 나와있기 때문에 공식 문서를 참고하면서 작성했다.

마지막

Untitled

이렇게 메일이 도착하는 것을 확인했기 때문에 HTML을 추가하고, 받는 form 데이터를 잘 적용해봐야겠다.