Next.js : 14.1.0
nodemailer : 6.9.8
tailwindcss : 3.3.0
이 글의 목표는 다음과 같이 설정했습니다.
- Next.js API Route를 이해하기
- 프론트엔드와 백엔드 작업 방식 이해하기
- 이 글만 보고도 이메일 전송 기능 구현 가능하게 하기
// page.tsx
import Contact from './contact/page';
export default function HomePage() {
return (
<div>
<Contact />
</div>
);
}
// contact/page.tsx
import EmailForm from '../components/EmailForm';
export default function Contact() {
return (
<section>
<h1>1:1 문의하기</h1>
<EmailForm />
</section>
);
}
// components/EmailForm.tsx
'use client';
import { useState } from 'react';
import Image from 'next/image';
const initialContact = {
from: '',
title: '',
content: '',
};
export default function EmailForm() {
const [contact, setContact] = useState(initialContact);
const [file, setFile] = useState<string | undefined>();
const onChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { value, name } = e.target;
setContact((prev) => ({
...prev,
[name]: value,
}));
};
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(contact, file); // 수정 예정 코드
};
return (
<form onSubmit={onSubmit}>
<label>
제목
<input
required
type="text"
name="title"
value={contact.title}
onChange={onChange}
/>
</label>
<label>
문의 내용
<textarea
required
name="content"
rows={10}
value={contact.content}
onChange={onChange}
/>
</label>
<label>
이메일
<input
required
type="text"
name="from"
value={contact.from}
onChange={onChange}
/>
</label>
<label>
첨부 파일
<div>
<div>
<input
type="file"
name="file"
accept="image/*"
onChange={} // 수정 예정 코드
/>
{file && (
<Image
src={file}
alt="local file"
width="70"
height="70"
/>
)}
</div>
</div>
</label>
<button>작성 완료</button>
</form>
);
}
코드의 간결성을 위해 tailwindcss 스타일 코드는 다 제거했습니다.
현재 Form 데이터를 전송하는 onSubmit 함수에는 별다른 코드없이 state를 콘솔에 출력하는걸로 해두었습니다. 이 코드는 app 폴더 내 api로 post 요청하는 함수를 호출하는 코드로 수정될 예정입니다.
또한, 첨부파일인 이미지 파일도 state 정의만 해두었을 뿐 onChange함수를 정의하고 있지 않습니다.
아래에서 설명할 예정입니다.
app/api폴더 내 email.ts 파일을 만들어서 nodemailer 관련 함수를 설정해주었습니다.
nodemailer 공식문서에 따라 설치해줍니다.
npm i nodemailer
현재 공식문서에 나온 transporter 위 예시를 토대로 우리에게 맞는 코드로 바꿔보겠습니다.
// app/api/email.ts
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: process.env.NEXT_APP_EMAIL,
pass: process.env.NEXT_APP_PWD,
},
});
.env파일을 통해 구글계정과 앱 비밀번호를 환경 변수로 설정해봅시다.
// .env
NEXT_APP_EMAIL=자신의구글이메일계정
NEXT_APP_PWD=구글앱비밀번호(16자리)
구글 계정에 들어가서 앱 비밀번호라고 치거나 왼 쪽에 보안 탭으로 갑니다.
해당 화면이 나옵니다. 간단하게 앱 이름으로 프로젝트 이름을 써줍니다.
만들기 버튼을 누르면 앱 비밀번호를 확인할 수 있습니다. 16자리의 앱 비밀번호가 생성되는데 한 번 발급받으면 다시 알 수 없으므로 어딘가에 잘 기록해 두시는게 좋습니다.
transporter을 생성했으니 transporter의 sendMail을 return해야 합니다.
mailOptions을 설정하여 sendMail 매개변수로 넣어줍니다.
공식문서에 나온 mailOpions입니다.
이제 이 코드를 우리에게 맞는 코드로 바꿔봅시다.
(더 많은 mailOptions은 해당 공식문서 페이지를 확인하면 됩니다.)
// app/api/email.ts
// ...
export type ContactType = {
from: string;
title: string;
content: string;
file?: string;
};
type MailOptionType = {
to: string;
from: string;
subject: string;
attachments?: Attachment[];
html: string;
};
export function sendEmail({ from, title, content, file }: ContactType) {
const mailOptions: MailOptionType = {
to: process.env.NEXT_APP_EMAIL || '',
from,
subject: `[1:1 문의] ${title}`,
attachments: [
{
path: file,
},
],
html: `
<h1>${title}</h1>
<div>${content}</div>
</br>
<p>보낸사람 : ${from}</p>
`,
};
return transporter.sendMail(mailOptions);
}
저희는 첨부파일도 이미지로 추가할 예정이라, attachmets도 넣어주었습니다.
nodemailer 관련 함수를 email.ts
에서 잘 설정해두었으니, 클라이언트단에서 API Route에 이메일 전송을 위한 요청을 보내야 합니다.
클라이언트에서 서버로 api 요청하는 함수이기 때문에 클라이언트단에서 onSubmit함수 내에서 바로 실행해도 되지만, 비즈니스 로직과 ui 로직을 분리하기 위해 별개의 파일로 만들어줍니다.
// api/contact.ts
import { ContactType } from './email';
export async function sendContactEmail(emailForm: ContactType) {
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(emailForm),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '서버 요청에 실패함');
}
return data;
}
'use client';
...
import { sendContactEmail } from '../api/contact';
...
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
sendContactEmail({ ...contact, file }); // 호출
};
...
클라이언트 컴포넌트에서 api 요청 함수인 sendContactEmail
에 props로 from, title, content, file을 넘겨줍니다.
// EmailForm.tsx
...
const [file, setFile] = useState<string | undefined>();
...
const onSelectFile = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) {
return;
}
if (e.target.files[0].type) {
let reader = new FileReader();
reader.readAsDataURL(e.target.files[0]);
reader.onloadend = () => {
const base64 = reader.result;
if (base64) {
let base64Sub = base64.toString();
setFile(base64Sub);
}
};
} else {
alert('파일 형식이 올바르지 않습니다.');
}
}, []);
위 file
state을 콘솔에 찍어보면 다음과 같이 나타납니다.
nodemailer함수에 첨부파일을 넣기 위해 해당 file
state을 url(타입: string)로 만들어주었습니다. 보통 input type이 file인 경우에 input value를 File타입으로 지정하지만, nodemailer의 mailOptions에서 attachments의 path는 url경로가 들어가기 때문에 타입을 string으로 지정했습니다.
파일을 Base64 인코딩한 데이터 URL입니다. 이 데이터 URL은 이미지를 나타내는 URL 형식의 문자열입니다. 이 문자열은 Base64로 인코딩된 이미지 데이터를 포함하며, 브라우저에서 직접 사용할 수 있습니다.
(path에 url이나 파일 경로가 들어가야 하지만, 저희는 사용자가 추가한 첨부파일 이미지 url값을 얻어와야 해서 삽질을 하다가 해결했습니다. 😅)
// email.ts
...
const mailOptions: MailOptionType = {
to: process.env.NEXT_APP_EMAIL || '',
from,
subject: `[1:1 문의] ${title}`,
attachments: [
{
path: file, // 파일 경로나 url
},
],
html: `
<h1>${title}</h1>
<div>${content}</div>
</br>
<p>보낸사람 : ${from}</p>
`,
};
...
이 API Route 는 next.js의 자체 서버를 사용해서 다른 서버가 필요없이 서버리스하게 API를 간단하게 만들 수 있다는 장점이 있습니다. 이를 잘 활용하면 개발을 하다가 Backend API 가 필요한 시점에서 백엔드에게 API 요청을 하거나, 따로 API 서버를 생성할 필요가 없이 프론트단에서 처리를 할 수가 있게 됩니다.
현재 Next.js 14버전을 사용하고 있기 때문에 pages/api/*
가 아니라 app/api/*
에 api를 만들어보겠습니다. 아직 공식문서에서는 여전히 pages 경로를 이용하여 설명하고 있어서 루트 설정하는데 삽질 좀 하다가 올바른 경로를 찾았습니다.
라우팅하듯이 원하는 이름의 폴더를 만들고 그 안에 route.ts를 설정하면 됩니다.
// app/api/contact/route.ts
import { sendEmail } from '@/app/api/email';
export async function POST(req: Request) {
const body = await req.json();
return sendEmail(body)
.then(
() =>
new Response(JSON.stringify({ message: '메일을 성공적으로 보냈음' }), {
status: 200,
})
)
.catch(() => {
return new Response(
JSON.stringify({ message: '메일 전송에 실패함' }),
{
status: 500,
}
);
});
}
nodemailer는 node서버에서 메일을 보낼 수 있는 메일 전송 모듈이라 서버에서만 이용할 수 있습니다.
서버단에서 nodemailer 함수인 sendEmail을 이용해서 요청받은대로 이메일을 전송하면 됩니다!
화면이 잘 나오는 것을 확인할 수 있습니다! !
깃허브에 오시면 velog 폴더에서 전체 소스 코드를 확인할 수 있습니다.