회원가입 이메일 인증코드 + SMTP 완전 정리
회원가입 이메일 인증 흐름을 짜면서 SMTP를 처음부터 정리한 글. SMTP가 실제로 주고받는 명령, 포트 25/587/465가 나뉘는 이유, nodemailer·Resend와의 관계까지.
NOTE
백엔드를 거의 처음 보는 프론트엔드 개발자가 회원가입 이메일 인증을 붙이며 정리한 글입니다. "인증 메일을 보낸다"의 실제 배달 경로, 즉 SMTP가 어떻게 생겼는지를 개념 위주로 풀었습니다.
1. 회원가입 이메일 인증코드, 어떻게 만들까?
목표는 단순합니다. "이 이메일이 진짜 이 사람 것인지"를 확인하는 것. 방법은 "그 이메일로만 받을 수 있는 코드를 보내고, 그 코드를 입력하게 한다"입니다.
전체 흐름은 3단계로 나뉩니다.
① 이메일 입력 → "인증코드 받기" (서버가 6자리 코드 생성 → 이메일로 발송)
② 받은 코드 입력 → "확인" (서버가 코드 일치 확인 → verified 표시)
③ 나머지 정보 채우고 → "회원가입" (서버가 "이 이메일 인증됐나?" 재확인 후 계정 생성)왜 "가입 전 인증"인가?
계정을 먼저 만들고 나중에 인증하는 방식도 있지만, 인증을 먼저 하고 계정을 만드는 방식이 깔끔합니다. 미인증 유령 계정이 DB에 안 쌓입니다. 그래서 인증코드는 계정과 분리된 임시 테이블에 저장했다가, 가입이 완료되면 지웁니다.
보안을 위해 챙긴 것들
- 코드는 해시로 저장 — DB가 털려도 코드 원문을 알 수 없게 (비밀번호와 똑같은 원리)
- 5분 만료 — 오래된 코드 재사용 방지
- 5회 시도 제한 —
000000부터999999까지 무차별 대입(brute force) 차단 - 60초 재발송 쿨다운 — 스팸/메일 과금 폭탄 방지
- 서버에서 재확인 — 프론트를 우회해 API를 직접 찔러도 "인증 안 된 이메일이면 가입 거부"
여기까지가 "로직"입니다. 그런데 "이메일로 발송" 이 부분, 이걸 실제로 어떻게 보내느냐가 바로 SMTP 이야기입니다.
2. SMTP란?
SMTP = Simple Mail Transfer Protocol (간이 메일 전송 규약).
한 문장으로: 이메일을 "보내는" 표준 약속.
우리가 평소에 이메일을 볼 때는 여러 프로토콜이 섞여 있습니다. 역할을 나눠보면:
| 프로토콜 | 역할 | 비유 |
|---|---|---|
| SMTP | 메일을 보낼 때 | 편지를 부치는 우체국 |
| IMAP/POP3 | 메일을 받아서 볼 때 | 우편함에서 편지 꺼내 읽기 |
즉 시스템이 자동으로 인증 메일을 보내려면 반드시 SMTP를 거쳐야 합니다. "SMTP 서버"란 결국 "메일을 대신 부쳐주는 우체국 서버"입니다.
왜 내 서버가 직접 못 보내고 SMTP가 필요할까?
내 백엔드 서버가 "이 메일 보내!" 한다고 그냥 나가지 않습니다. 받는 쪽(gmail, naver 등)은 아무 서버나 보낸 메일을 스팸으로 막습니다. 그래서 우리 코드는 "메일 내용"만 만들고, 실제 배달은 SMTP 서버에게 위탁하는 구조가 됩니다.
2-1. SMTP는 실제로 뭘 주고받나?
SMTP를 "규약"이라고 부르는데, 정작 그 규약이 뭔지는 안 보이면 추상적으로 남습니다. SMTP는 사실 평문 텍스트 명령 몇 줄을 정해진 순서로 주고받는 것뿐입니다. 서버와의 대화를 그대로 옮기면 이렇게 생겼습니다.
EHLO myserver.com 나 이런 서버야 (인사)
MAIL FROM: <[email protected]> 봉투 발신자
RCPT TO: <[email protected]> 봉투 수신자
DATA 이제 본문 보낼게
Subject: [Example] 이메일 인증코드
인증코드: 483920
. 마침표 한 줄 = 본문 끝한 줄 보내면 서버가 250 OK 같은 상태 코드로 답하고, 다음 줄로 넘어갑니다. SMTP가 "규약"이라는 건 곧 이 명령의 이름과 순서가 표준으로 정해져 있다는 뜻입니다.
여기서 하나 짚어둘 개념. 발신자가 두 번 나옵니다. MAIL FROM(봉투에 적힌 주소)과 본문 안의 From:(편지지에 적힌 서명). 이 둘은 다를 수 있습니다. 받는 사람 화면에 뜨는 건 후자입니다. 이게 발신자 위조가 가능한 이유이고, 그걸 검증하려고 별도 장치(SPF/DKIM 등)가 붙습니다 — 이건 도달률 이야기라 다음 글로 미룹니다.
지금 기억할 것은 하나. 이 대화를 손으로 짜는 건 번거롭다. 그래서 nodemailer 같은 도구가 이 과정을 대신 처리해줍니다(§4).
3. SMTP엔 뭐가 들어갈까? (접속 정보 5~6개)
SMTP 서버에 "나 이 계정으로 로그인해서 메일 보낼게"라고 하려면 아래 정보가 필요합니다. 아웃룩이나 메일 앱을 처음 설정할 때 넣는 그 값들과 정확히 같습니다.
| 항목 | 예시 | 설명 |
|---|---|---|
| HOST (서버 주소) | smtp.gmail.com / mail.example.com | 어느 우체국으로 보낼지 |
| PORT (포트) | 587 또는 465 | 우체국의 어느 창구로 들어갈지 |
| SECURE (보안 방식) | TLS / SSL | 편지를 암호화해서 보낼지 |
| USER (계정) | [email protected] | 우체국 로그인 아이디 |
| PASS (비밀번호) | ******** | 로그인 비밀번호 (또는 앱 비밀번호) |
| FROM (발신자 표시) | Example <[email protected]> | 받는 사람 화면에 뜨는 이름 |
포트가 왜 465, 587, 그리고 25로 나뉘나
이 부분이 제일 헷갈리는 부분이였습니다. 포트를 이해하는 핵심은 메일이 한 번에 안 가고 여러 서버를 거친다는 사실입니다. 내 코드는 첫 서버에 "제출"만 하고, 그다음 서버끼리의 배달은 알아서 흘러갑니다. 이 두 구간이 서로 다른 포트를 씁니다.
| 포트 | 용도 | 누가 씀 |
|---|---|---|
| 25 | 서버 ↔ 서버 배달 (relay) | 우체국 사이 |
| 587 / 465 | 앱 → 첫 서버 제출 (submission) | 우리 코드 |
우리 코드가 다루는 건 아래쪽, 즉 587 / 465(제출용) 입니다. 포트 25는 서버끼리 알아서 쓰는 구간이라, 요즘은 스팸 봇 차단 때문에 일반 회선에선 아예 막아둡니다. "내 서버에서 포트 25로 직접 쏘면 안 되나?"의 답이 여기 있습니다 — 막혀 있어서 안 됩니다.
그럼 587과 465는 뭐가 다른가. 암호화를 켜는 방식이 다릅니다.
- 포트 465 = SSL — 처음부터 암호화된 통로로 연결 →
secure: true - 포트 587 = TLS(STARTTLS) — 평문으로 연결한 뒤 암호화로 전환 →
secure: false
둘 다 암호화되지만 방식이 다릅니다. 요즘은 587 + TLS가 표준입니다. 서버가 알려주는 대로 맞추면 됩니다.
"앱 비밀번호"란?
Gmail 같은 곳은 보안 때문에 일반 로그인 비밀번호로 SMTP 접속을 막아뒀습니다. 대신 "이 프로그램 전용 비밀번호"를 따로 발급받아 쓰는데, 이게 앱 비밀번호입니다. (2단계 인증을 켜야 발급 가능) 회사 자체 메일 서버는 보통 그냥 계정 비밀번호를 씁니다.
4. nodemailer + SMTP
여기서 또 하나 헷갈리는 게 nodemailer와 SMTP의 관계입니다. 이 둘은 층위가 다릅니다.
| nodemailer | SMTP | |
|---|---|---|
| 정체 | Node.js에서 메일 보내는 라이브러리 | 메일 전송 규약/서버 |
| 위치 | 내 백엔드 코드 안 | 외부(우체국) |
| 비유 | 전화기 | 통신망 |
nodemailer는 "SMTP 서버와의 대화(§2-1)를 대신 처리해주는 도구" 입니다. EHLO → MAIL FROM → RCPT TO → DATA 같은 명령을 순서대로 주고받고, 연결·인증·암호화·메일 포맷팅까지 다 감싸줘서, 우리는 이렇게 짧게 쓸 수 있습니다.
// 1) SMTP 서버 접속 정보로 트랜스포터(전화기) 준비
const transporter = nodemailer.createTransport({
host: "mail.example.com",
port: 587,
secure: false,
auth: { user: "[email protected]", pass: "****" },
});
// 2) 메일 보내기
await transporter.sendMail({
from: "Example <[email protected]>",
to: "[email protected]",
subject: "[Example] 이메일 인증코드",
text: "인증코드: 483920",
});정리하면 nodemailer는 도구, SMTP는 그 도구가 연결하는 대상입니다. nodemailer 없이 SMTP를 직접 다룰 수도 있지만(§2-1의 명령을 직접 짜는 것) 훨씬 번거로워서, Node 생태계에선 거의 nodemailer를 씁니다.
5. Resend는 뭐고, 왜 안 썼나?
Resend는 "메일을 대신 쏴주는 클라우드 서비스" 입니다. SMTP 서버를 직접 준비하기 번거로우니, 가입하고 API 키만 받으면 발송을 대행해줍니다. (Brevo, SendGrid, Amazon SES 등도 같은 부류)
nodemailer/SMTP와 비교하면:
| nodemailer + SMTP | Resend | |
|---|---|---|
| 방식 | 내가 SMTP 서버에 직접 연결 | Resend가 발송 대행 |
| 필요한 것 | SMTP 접속정보 5개 | API 키 1개 |
| 발신 서버 | 내가 지정(회사 서버 등) | Resend 인프라 |
| 도달률 | 서버 설정에 따라 | 대체로 좋음(전문 서비스) |
| 종속성 | 없음(표준 SMTP) | Resend에 묶임 |
왜 우리는 nodemailer + SMTP를 골랐나
- 회사 도메인이 있고 자체 메일 서버(
mail.example.com) 를 쓰는 게 확인됐습니다. - 그러면 회사 SMTP 정보만 받으면 추가 서비스 가입/비용 없이 바로 발송 가능합니다.
- 게다가 Resend도 SMTP 방식으로 붙일 수 있어서, nodemailer 구조로 짜두면 나중에 Resend로 바꾸고 싶어도 접속 정보만 교체하면 됩니다.
즉 nodemailer + SMTP는 "특정 서비스에 안 묶이는 표준 방식"이라, 어떤 발신 수단이 오든 코드를 안 바꿔도 됩니다.
NOTE
다만 하나 정직하게 짚을 것 자체 SMTP를 고르는 순간 도달률(스팸함에 안 빠지게 하는 일)은 내 책임이 됩니다. Resend 같은 서비스가 존재하는 이유가 바로 이 도달률 대행입니다. 위 표의 "도달률: 서버 설정에 따라"가 그 비용입니다. 이 부분(SPF/DKIM/DMARC)은 SMTP 개념을 넘어서는 주제라 다음 글로 미룹니다.
6. console 모드 — 개발할 땐 메일 없이
실무 팁 하나. SMTP 정보가 아직 없어도 기능 개발은 시작할 수 있습니다. 발송부를 이렇게 추상화했습니다.
MAIL_TRANSPORT=console → 실제로 안 보내고 서버 터미널에 코드를 출력
MAIL_TRANSPORT=smtp → 위의 SMTP 정보로 진짜 발송console 모드는 이렇게 로그에 찍습니다.
──────── 이메일 인증코드(개발용) ────────
받는사람: [email protected]
인증코드: 483920
(5분 후 만료)
──────────────────────────────────────개발자가 이 숫자를 눈으로 보고 테스트합니다. 실제 메일 계정 하나 없이도 전체 회원가입 흐름을 다 검증할 수 있습니다. 그리고 SMTP 정보가 도착하면 .env 한 줄만 smtp로 바꾸면 실제 발송으로 전환 — 코드는 그대로입니다.
이 "환경변수로 발송 수단을 갈아끼우는" 패턴이 핵심입니다. 개발/운영을 코드 변경 없이 오갈 수 있게 해줍니다.
7. 최종 요약 — 우리가 실제로 한 것
백엔드 (NestJS)
- 인증코드 테이블 분리 저장 (해시 + 5분 만료 + 5회 제한 + 60초 쿨다운)
- 엔드포인트 3개: 코드 발송 / 코드 확인 / 가입(인증 재확인)
MailService로 발송부 추상화 → console(개발) ↔ smtp(실제) env로 전환- nodemailer로 SMTP 연결, Resend 미사용
프론트엔드 (React)
- "인증코드 받기" 누르면 코드 입력칸이 등장하는 단계적 UI
- 인증 완료 전엔 회원가입 버튼 잠금
남은 것
- 회사에서 SMTP 5개(HOST/PORT/SECURE/USER/PASS) 받아
.env에 넣고MAIL_TRANSPORT=smtp로 → 실제 발송 완료
한 줄 개념 정리
SMTP는 메일 보내는 표준 규약이고, 실제로는 EHLO/MAIL FROM/RCPT TO/DATA 같은 명령을 순서대로 주고받는 대화입니다. 이 대화를 대신 처리해주는 도구가 nodemailer, 우체국 자체를 대신 운영해주는 대행 서비스가 Resend입니다. 우리는 회사 자체 서버(사내 SMTP)에 nodemailer로 직접 연결하는 방식을 택했고, 정보가 오기 전까진 console 모드로 개발했습니다.
Comments