외부 메일 서비스인 Resend를 이용해 이메일 발송 기능을 구현하고 배포 환경에서 동작을 검토하는 방법을 확인합니다
| 구분 | 학습 내용 |
|---|---|
| 메일 어댑터 | 실제 메일 발송 구현을 감싸는 교체 가능한 구조 |
| 실제 발송 어댑터 | Resend API를 이용해 메일을 발송하는 구현 |
| 실패 어댑터 | local 또는 설정 누락 상황에서 발송하지 않고 실패 결과를 반환하는 구현 |
| 환경 표식 | preview 메일 제목 앞에 [preview]를 붙이는 정책 |
| 결과 분리 | 데이터 저장 성공과 메일 발송 성공을 분리해서 처리하는 정책 |
| 동작 검토 | local, preview, production에서 각각 기대 동작을 확인하는 방법 |
<aside> 💡
resend.com 가입 → 대시보드 API Keys → Create API Key로 키 발급(한 번만 보이므로 복사).
항상 모든 키 발급 시 권한은 필요한 만큼만 최소로!

Permission - Sending만
Domain - 특정 도메인만
*아직 인증된 도메인이 없어서 전체 옵션밖에 없을 수 있음 ⇒ 도메인 인증 후 돌아와서 변경하기
발신 주소(EMAIL_FROM_ADDRESS) 선택:
우리 권장 가이드라인 → 발신주소는 Vercel 환경변수에!
(빠름, 개발 검증용) 도메인 인증 없이:
발신자를 [email protected]로 둔다.
단, 이 테스트 발신자는 Resend 가입에 사용한 본인 이메일로만 발신 가능
도메인 인증 후 모든 주소로 발신:
Resend Domains에서 도메인을 인증하고
EMAIL_FROM_ADDRESS={noreply}@{your.domain} 사용
Resend 에 도메인 인증하기 (기존 Vercel 도메인 연결 방식도 수정 필요 → ~~Vercel 버그 때문~~)
</aside>
이메일 기능 구현 간단 프롬프팅
Resend 서비스 무료 티어를 사용해서 이메일 기능을 구현하려고 한다.
RESEND_API_KEY를 환경변수에서 읽어들여 매직링크 이메일을 발송하도록 구현해줘.
이후 리팩토링용 프롬프트
[# 메일 발송 구현 검토 & 개선 및 테스트 프롬프트
메일 발송 기능이 여기저기 흩어진 코드가 아니라 환경변수와 발송 케이스를 감지하는 방식으로 체계적으로 관리되는 형태인지 검토하고, 필요한 경우 개선해줘.
그리고 메일 발송 정책을 자동 점검할 수 있게 테스트를 만들어줘.
다음 세 가지 규칙이 반드시 지켜져야 해.
실제 Resend API를 호출하지 말고, 가짜 메일 발송 장치를 사용해서 제목과 발송 여부만 확인해줘. 테스트 이름은 비전공자도 이해할 수 있게 작성해줘.](https://wildmental.notion.site/1-API-2-preview-preview-3-157d03212bd482129c1f019979c70d88)
메일 발송은 보통 다음 흐름에서 사용됩니다.
| 상황 | 메일 예시 |
|---|---|
| 로그인 | 매직링크 발송 |
| 결제 | 결제 완료 안내 |
| 강의 | 수강 신청 완료 안내 |
| 관리자 | 공지 메일 발송 |
| 알림 | 과제 제출 확인, 승인 안내 |
하지만 메일 발송은 외부 서비스에 의존합니다. 네트워크 문제, API 키 누락, 도메인 설정 문제, 수신자 주소 문제로 실패할 수 있습니다.
따라서 중요한 원칙은 다음과 같습니다.
데이터 저장과 메일 발송 결과를 분리한다.
예를 들어 수강 신청 기능을 생각해 보겠습니다.
| 작업 | 실패 시 영향 |
|---|---|
| 수강 신청 데이터 저장 | 핵심 작업이므로 실패하면 요청 실패 |
| 수강 신청 완료 메일 발송 | 알림 작업이므로 실패해도 저장 결과를 되돌리지 않음 |
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#ECFDF5",
"primaryBorderColor": "#10B981",
"primaryTextColor": "#064E3B",
"lineColor": "#64748B",
"secondaryColor": "#E0F2FE",
"tertiaryColor": "#FEF3C7",
"fontFamily": "Pretendard, Inter, Arial"
}
}}%%
flowchart TD
A["사용자 요청<br/>예: 수강 신청"] --> B["핵심 작업 수행<br/>DB에 신청 데이터 저장"]
B --> C{"저장 성공?"}
C -- "아니오" --> X["요청 실패<br/>핵심 데이터가 저장되지 않음"]
C -- "예" --> D["알림 메일 발송 시도<br/>외부 메일 서비스 호출"]
D --> E{"메일 발송 성공?"}
E -- "성공" --> F["저장 성공 + 알림 성공<br/>정상 완료"]
E -- "실패" --> G["저장 성공 + 알림 실패<br/>실패 로그만 남기고 본 작업은 유지"]
classDef request fill:#E0F2FE,stroke:#0284C7,stroke-width:1.5px,color:#0C4A6E;
classDef db fill:#DCFCE7,stroke:#16A34A,stroke-width:1.5px,color:#14532D;
classDef mail fill:#FEF3C7,stroke:#F59E0B,stroke-width:1.5px,color:#78350F;
classDef fail fill:#FEE2E2,stroke:#DC2626,stroke-width:1.5px,color:#7F1D1D;
class A request;
class B,C db;
class D,E,F,G mail;
class X fail;