최근 클라이언트를 위해 간단한 랜딩 페이지 웹사이트를 제작하던 중, 방문자가 자신의 이메일 주소를 공개하지 않고도 사이트를 통해 메일을 받을 수 있는 기능이 필요했습니다.
솔직히 말씀드리면, 저는 그전까지 이 기능을 직접 구현해 본 적이 없었습니다. 늘 그렇듯 앵커 태그와 mailto 속성만 넣은 단순한 '문의하기' 버튼으로 충분하다고 여겨왔죠.
<button>
<a href="mailto:myemail@example.com">Contact Me</a>
</button>
하지만 이 방식에는 두 가지 불편함이 있습니다.
- 메시지를 보내는 사용자와 이를 받는 사이트 소유자 모두 서로에게 이메일 주소를 노출해야 합니다. 일부 사람들에게는 괜찮지만, 개인정보 보호를 중시하는 사용자에게는 바람직하지 않습니다.
- 방문자가 링크를 클릭하면 기기에 설정된 기본 메일 프로그램이 강제로 열립니다. 공용 컴퓨터를 사용 중이라면 어떨까요? 로그인이 되어 있지 않다면? 아니면 단순히 메일 프로그램을 쓰고 싶지 않다면?
물론 받는 사람의 이메일 주소를 복사해서 브라우저 등 다른 경로로 보낼 수는 있습니다. 하지만 이런 추가 절차와 장애물은 사용자가 메시지를 보내는 것을 망설이게 만들고, 결과적으로 비즈니스는 소중한 피드백이나 기회를 잃을 수 있습니다.
이러한 이유로 우리는 사용자가 웹사이트를 벗어나지 않고도 메시지를 작성하고 전송 버튼만 누르면 사이트 소유자에게 이메일이 발송되는 문의 폼 방식을 선택했습니다.
구글에서 간단히 검색해 보면 웹사이트에 삽입할 수 있는 서드파티 도구나 위젯이 많이 나옵니다. 하지만 대부분 브랜드 로고가 표시되고, 완전한 커스터마이징을 위해서는 유료 구독이 필요합니다.
WordPress처럼 해당 기능을 제공하는 플러그인이 내장된 CMS를 사용하는 경우가 아니라면, 반복적으로 발생하는 비용이 부담될 수밖에 없습니다.
그래서 저는 모든 것을 직접 통제할 수 있도록 이 기능을 직접 코딩하기로 했습니다.
이 가이드에서는 HTML과 AWS 서비스를 활용해 해당 기능을 구현했던 과정을 그대로 재현해 보겠습니다.
HTML 폼 만들기
여기서는 최대한 단순하게, CSS 없이 기본적인 HTML 폼만 사용해 원하는 기능을 테스트해 보겠습니다.
<h2>Contact Us</h2>
<form>
<label for="name">Name:</label>
<input name="name" type="text"/><br/><br/>
<label for="email">Email:</label>
<input name="email" type="email"/><br/><br/>
<label for="name">Message:</label>
<textarea name="message"></textarea><br/><br/>
<input type="submit"/>
<div>
<p id="result-text"></p>
</div>
</form>

이제 JavaScript로 제출(submit) 기능을 처리해 보겠습니다.
const form = document.querySelector('form')
form.addEventListener('submit', event => {
// prevent the form submit from refreshing the page
event.preventDefault()
const { name, email, message } = event.target
console.log('Name: ', name.value)
console.log('email: ', email.value)
console.log('Message: ', message.value)
})
이 시점에서 우리는 사용자 입력을 받는 폼과, 입력값을 콘솔에 출력하기만 하는 JavaScript 코드를 갖추었습니다.
일단 여기까지 해두고, 폼 데이터를 받아 이메일을 발송할 백엔드 서비스 작업으로 넘어가겠습니다.
백엔드 아키텍처 개요
AWS에서 어떤 서비스를 어떻게 사용할지 살펴보겠습니다.
제목에서 언급했듯이 AWS Lambda와 Simple Email Service(SES)를 사용합니다. SES는 호출 시 이메일 메시지를 발송할 수 있는 서버리스 메시징 서비스입니다. AWS Lambda는 이벤트에 반응해 실행되는 서버 측 코드를 작성할 수 있게 해줍니다.
또한 HTTP를 통해 Lambda 함수를 호출할 수 있게 해주는 API Gateway도 함께 사용합니다.

폼이 제출되면 다음과 같은 워크플로우가 진행됩니다.
- 브라우저(JavaScript)가 폼 데이터를 요청 본문(request body)에 담아 AWS API Gateway가 지정한 엔드포인트 URL로 POST 요청을 보냅니다.
- API Gateway가 요청을 검증한 후, event 파라미터를 받는 Lambda 함수를 트리거합니다. API Gateway는 폼 데이터를 event 파라미터의 body 속성에 담아 전달합니다.
- Lambda 함수가 event body에서 데이터를 추출하고, 이 데이터를 사용해 발송할 이메일의 본문과 수신자를 구성합니다. 그런 다음 함수는 AWS SDK를 통해 SES를 이메일 데이터와 함께 호출합니다.
- SES가 sendMail 요청을 받으면 이메일 데이터를 실제 텍스트 이메일로 변환하고, AWS 자체 메일 서버를 통해 수신자에게 발송합니다.
이메일이 발송되면 브라우저는 상태 코드 200과 성공 메시지가 담긴 응답을 받습니다. AWS 클라우드 내 어느 단계든 실패하면 응답은 500 상태 코드를 반환합니다.
1단계: SES 설정 방법
각 단계는 역순으로 설정해 나가겠습니다. 가장 간단한 SES부터 시작합니다.
AWS 콘솔에서 SES 서비스로 이동 → 사이드 메뉴에서 Email Addresses 클릭 → "Verify a New Email Address" 버튼을 클릭합니다.

열리는 대화상자에 SES가 이메일을 보낼 때 발신자로 지정할 이메일 주소를 입력합니다.

그러면 입력한 이메일 주소로 인증 링크가 포함된 메일이 발송됩니다. AWS는 이를 통해 해당 이메일 소유자가 자신의 주소를 발신자 주소로 사용하는 것에 동의했음을 확인합니다.
이메일 인증이 완료될 때까지 SES 대시보드에는 인증 상태가 'pending'으로 표시됩니다.

이메일 소유자가 AWS에서 받은 메일을 열고 인증 링크를 클릭하면, 인증 상태가 'verified'로 변경됩니다(변경 사항을 확인하려면 페이지를 새로고침하세요).

SES 설정은 이것으로 끝입니다. 선택적으로 목록에서 인증된 이메일을 선택하고 "Send a Test Email" 버튼을 클릭해 서비스를 테스트할 수 있습니다. 수신자 이메일 주소, 제목, 메시지를 입력해 발송해 볼 수 있습니다.
발송된 이메일은 AWS 서버에서 서명되며, 인증된 주소가 발신자로 표시됩니다. 다음과 같은 형태입니다.

2단계: Lambda 설정 방법
이제 가장 재미있는 부분입니다. 폼 데이터를 받아 SES를 호출하는 함수를 만들어 보겠습니다.
Lambda 함수의 매력은 백엔드 코드를 24시간 내내 서버에서 실행하고 그 서버를 관리할 걱정이 없다는 점입니다. 바로 서버리스(serverless)이기 때문입니다.
물론 서버가 아예 없다는 뜻은 아닙니다. AWS가 내부적으로 서버를 관리해주기 때문에 우리는 코드 작성에만 집중하면 됩니다. 게다가 함수가 호출된 횟수와 실행 시간에 대해서만 과금되며, 놀랄 만큼 저렴합니다!
IAM 역할 생성 및 구성
Lambda 함수 작성에 앞서, 함수에 연결할 IAM 역할(role)을 생성하고 SES 서비스를 호출할 수 있는 권한(AWS에서는 정책(policy)이라 부릅니다)을 부여해야 합니다.

AWS 콘솔에서 IAM 서비스로 이동 → 사이드 메뉴에서 Policies 클릭 → "Create Policy" 버튼을 클릭합니다.

정책 생성 페이지에서 JSON 탭으로 이동해 다음 권한을 붙여넣고 Next를 클릭합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
세 번째 화면에서 정책 이름을 지정하고 "Create Policy" 버튼을 클릭합니다.

이제 Lambda에 연결될 IAM 역할을 생성하고, 방금 만든 권한 정책과 연결합니다.

IAM 사이드 메뉴에서 Roles를 클릭한 후 "Create role" 버튼을 클릭합니다.

역할 생성 화면에서 선택된 유형이 "AWS service"인지 확인하고, Lambda 항목을 선택한 뒤 "Next:Permissions" 버튼을 클릭합니다.

다음 화면에서 앞서 생성한 정책을 이름으로 검색해 선택하고 next를 클릭합니다.

검토 화면에서 기억하기 쉬운 역할 이름을 지정한 후 "Create role"을 클릭합니다.
이제 새 Lambda 함수를 생성할 수 있습니다. Lambda 서비스 대시보드로 이동해 "Create Function" 버튼을 클릭하세요.

함수 생성 화면에서 함수 이름을 지정하고, "Author from scratch" 옵션을 선택한 후 런타임으로 Node.js를 선택합니다.
"Change default execution role" 섹션에서 "Use an existing role" 옵션을 선택하고, "Existing role" 드롭다운에서 이전 단계에서 생성한 역할 이름을 고릅니다.
마지막으로 "Create function" 버튼을 클릭해 함수를 생성합니다.
코드 작성 및 테스트

에디터에서 index.js 파일(Lambda가 호출될 때 실행되는 파일)을 열고, 내용을 다음 코드로 교체합니다.
const aws = require("aws-sdk");
const ses = new aws.SES({ region: "us-east-1" });
exports.handler = async function (event) {
console.log('EVENT: ', event)
const params = {
Destination: {
ToAddresses: ["your@email.com"],
},
Message: {
Body: {
Text: {
Data: `Hello from Lambda!`
},
},
Subject: { Data: `Message from AWS Lambda` },
},
Source: "your@email.com",
};
return ses.sendEmail(params).promise()
};
2번째 줄에서 AWS SDK를 사용해 SES 인스턴스를 생성하는 점에 주목하세요. us-east-1 리전을 선택한 이유는 제가 이메일을 등록하고 인증한 리전이기 때문입니다. 반드시 이메일 주소를 교체하고, 본인이 이메일을 등록한 AWS 리전을 사용하세요.
이제 함수를 테스트해 보겠습니다. "Deploy" 버튼을 클릭한 후 Test 버튼 → Configure test event를 클릭하면 새 테스트 이벤트를 만들 수 있는 설정 대화상자가 열립니다.
테스트 이벤트 본문 에디터에 최종적으로 브라우저 요청에서 넘어올 데이터를 흉내 내는 다음 JSON을 입력하고 create를 클릭합니다.
{
"body": {
"senderName": "Namo",
"senderEmail": "namo@trains.com",
"message": "I love trains!"
}
}
이제 Test 버튼을 클릭하면 방금 만든 테스트가 실행됩니다. 에디터에 새 탭이 열리며 함수 실행 로그가 표시되는데, 다음과 같은 형태입니다.

로그아웃한 event 객체가 Function logs 아래에 표시되며, 테스트 이벤트에서 사용한 body 데이터가 함께 보입니다.
이 테스트로 제 받은 편지함에도 이메일이 발송되었을 텐데요, 실제로 도착했는지 확인해 보겠습니다.

네, 예상대로입니다. 그리고 테스트 실행 직후 거의 즉시 도착했습니다.
이제 테스트 데이터로 더 의미 있는 메시지를 보낼 수 있도록 함수 코드를 수정해 보겠습니다.
const aws = require("aws-sdk");
const ses = new aws.SES({ region: "us-east-1" });
exports.handler = async function (event) {
console.log('EVENT: ', event)
// Extract the properties from the event body
const { senderEmail, senderName, message } = JSON.parse(event.body)
const params = {
Destination: {
ToAddresses: ["the.benhawy@gmail.com"],
},
// Interpolate the data in the strings to send
Message: {
Body: {
Text: {
Data: `You just got a message from ${senderName} - ${senderEmail}:
${message}`
},
},
Subject: { Data: `Message from ${senderName}` },
},
Source: "the.benhawy@gmail.com",
};
return ses.sendEmail(params).promise();
};
중요한 점 하나! API Gateway가 우리 함수를 호출할 때 event body에 문자열(string)을 전달합니다. 그래서 JSON.parse를 사용해 event.body를 JSON으로 변환한 후 발신자의 이메일, 이름, 메시지를 추출하는 것입니다. 그런 다음 문자열 보간(string interpolation)을 통해 이 변수들을 이메일 본문과 제목에 활용합니다.
하지만 이 상태로 테스트를 실행하면 오류가 발생합니다. 테스트는 JSON 객체를 event.body에 전달하는데, 우리는 JSON에 다시 JSON.parse를 적용하기 때문에 JavaScript에서 오류가 나는 것입니다.
아쉽게도 테스트 에디터에서는 event에 문자열을 전달할 수 없으므로, 이 부분은 나중에 다른 곳에서 테스트해야 합니다.
3단계: API Gateway 설정 방법
마지막으로 사용할 AWS 서비스는 API Gateway입니다. 이를 통해 브라우저가 생성한 Lambda 함수로 HTTP 요청을 보낼 수 있습니다.

Lambda 함수 페이지를 벗어나지 않은 상태에서 "Function overview" 섹션을 펼치고 "Add trigger"를 클릭합니다.

다음으로 드롭다운에서 API Gateway를 선택하고, API 유형은 HTTP API, 보안 방식은 "Open"을 선택한 후 CORS 체크박스에 체크합니다. 그리고 "Add"를 클릭합니다.

함수의 "Configuration" 탭으로 리디렉션되며, 방금 생성한 API Gateway 트리거가 표시됩니다. 여기서 API endpoint URL을 메모해 두세요. 이 URL이 브라우저에서 폼 데이터와 함께 호출할 주소입니다.
HTML로 돌아가기
드디어 폼이 실제로 이메일을 보내는지 테스트할 차례입니다.
폼이 제출될 때 요청을 전송하도록 JavaScript를 수정해 보겠습니다.
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
// prevent the form submit from refreshing the page
event.preventDefault();
const { name, email, message } = event.target;
// Use your API endpoint URL you copied from the previous step
const endpoint =
"<https://5ntvcwwmec.execute-api.us-east-1.amazonaws.com/default/sendContactEmail>";
// We use JSON.stringify here so the data can be sent as a string via HTTP
const body = JSON.stringify({
senderName: name.value,
senderEmail: email.value,
message: message.value
});
const requestOptions = {
method: "POST",
body
};
fetch(endpoint, requestOptions)
.then((response) => {
if (!response.ok) throw new Error("Error in fetch");
return response.json();
})
.then((response) => {
document.getElementById("result-text").innerText =
"Email sent successfully!";
})
.catch((error) => {
document.getElementById("result-text").innerText =
"An unkown error occured.";
});
});
자, 결정적 순간입니다. 폼을 작성하고 submit 버튼을 클릭해 보세요. 성공 메시지가 표시되면 이메일이 발송된 것입니다.

메시지가 전송된 이메일의 소유자인 제가 받은 편지함을 살펴보니, 폼에 입력한 내용이 담긴 이메일이 제게 도착해 있었습니다!

여기까지 따라오셨다면, 이제 어떤 웹사이트에든 붙여 넣을 수 있는 완전히 작동하는 '문의하기' 폼을 갖게 된 것입니다. 그리고 실제로 사용될 때만 과금됩니다.
어떠신가요? 저는 이게 정말 멋지고 거의 마법 같다고 생각합니다. 클라우드 컴퓨팅/서비스를 업무 흐름에 활용하는 아주 실용적인 방법이기도 하죠.
물론 프런트엔드에서 React나 Vue 같은 프레임워크를 사용하거나, Lambda를 Python이나 Go 같은 다른 프로그래밍 언어로 작성하는 등 이 흐름을 자유롭게 커스터마이징할 수 있습니다.
마치며
끝까지 읽어주셔서 감사합니다! 저는 독학 개발자로서 JavaScript, 클라우드 개발, 그리고 개인적인 학습 및 직업 경험에 관한 글을 쓰고 있습니다. 트위터 @adham_benhawy에서도 관련 내용을 공유하고 있으니 팔로우해 주세요!
참고 자료
- https://aws.amazon.com/premiumsupport/knowledge-center/lambda-send-email-ses/
- https://docs.aws.amazon.com/lambda/latest/dg/lambda-invocation.html
- https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html?icmpid=docs_lambda_console