약 6개월만에 블로그 글을 작성해봅니다. 역대급으로 힘든 프로젝트를 진행을 하면서 최근 6개월간 블로그 글을 작성을 할 수 없었다고 하면 변명이겠지만, 해당 프로젝트를 하면서 약간의 번아웃이 오면서 작성을 못했는데 "싫은 사람에게도 배울점이 있다" 라는 말처럼 싫은 프로젝트에도 배울점은 많았습니다. 해당 프로젝트에서 사용했던 AWS SES 서비스를 이용해서 첨부파일을 포함한 이메일 보내기 관련하여 글을 작성해보겠습니다. 다른 라이브러리는 사용하지않고 오직 aws-sdk 라이브러리를 이용해서 작업을 하겠습니다. 프론트엔드 개발자라 리액트를 이용하여 작업을 할 예정이지만 이런 작업들은 서버에서 이루어지는게 좋습니다. 이메일을 전송하는 로직이므로 nodejs(서버) 환경에서 사용해도 큰 이상은 없을거 같습니다.
AWS 관련 된 내용은 블로그글은 상대적으로 다른 글에 비해 많이 보는듯하고 해당 내용은 잘 없는거 같아서 작성을 해봅니다.
(1) IAM 접속 하여 사용자 생성 버튼 클릭
(2) 사용자 이름을 생성
(3) 필요한 권한 선택하여 생성 (AmazonSESFullAccess)
(4) 생성 후 해당 사용자에 들어가서 보안 자격 증명 탭에서 액세스 키 만들키 버튼을 클릭 후 AWS 외부에서 실행되는 애플리케이션 카드(?)를 클릭해서 다음 버튼을 누르면 키가 생성이 됩니다.
// 사용사례 예시 - 사용자 비밀번호 재설정
Use case description: 1. How do you plan to create or compile your mailing list?
Our mailing list is constructed based on our internal user database. Email addresses of users who request password resets are added to the mailing list. In other words, only registered users of our service, when needing to reset their passwords, will receive authentication codes via email.
2. How do you plan to handle bounce-backs and opt-outs?
Since authentication code emails are sent exclusively in response to user-initiated password reset requests and are essential for account security, we do not offer an opt-out mechanism for this specific type of communication. Users have the option to reset their passwords or not request authentication codes if they wish to discontinue receiving such emails.
3. How can recipients opt-out of your emails?
Recipients cannot opt-out of receiving authentication code emails since these emails are specifically sent in response to user-initiated password reset requests. The emails are essential for security purposes, and we do not provide an opt-out mechanism for this type of communication.
4. How did you choose the sending rate or allocation you specified in this request?
In this response, we have not specified a particular sending rate or allocation. Instead, we request AWS Support to increase our limits based on our anticipated email frequency and volume for sending authentication codes during password resets.
Mail Type: TRANSACTIONAL
Website URL:
정상적으로 승인이 떨어져서 프로덕션 모드가 되면 아래 이미지 처럼 나옵니다.
내용이 많아보이지만, 빠르면 10분안에 사전준비 작업은 할 수 있습니다.
사전준비작업은 끝났기 때문에 이제 코드를 작성해서 정상적으로 메일이 전송이 되는지 테스트만 해보면 됩니다.
// 필요한 라이브러리 및 버전정보
"aws-sdk": "^2.1494.0",
// aws.ts
import AWS from 'aws-sdk';
import { PromiseResult } from 'aws-sdk/lib/request';
class AWSService {
private ses: AWS.SES;
constructor() {
this.ses = new AWS.SES({ apiVersion: '2010-12-01' });
}
private initializeAWS() {
AWS.config.update({
accessKeyId: process.env.REACT_APP_AWS_ACCESS_KEY,
secretAccessKey: process.env.REACT_APP_AWS_SECRET_KEY,
region: 'ap-northeast-2', // 서울 리전
});
}
public async sendEmail(params: {
Source: string;
Destinations: string[];
RawMessage: { Data: string };
}): Promise<PromiseResult<AWS.SES.SendRawEmailResponse, AWS.AWSError>> {
return await this.ses.sendRawEmail(params).promise();
}
const awsService = new AWSService();
export default awsService;
해당 코드를 먼저 보면 우선 AWS.config.update를 이용해서 먼저 accessKeyId, secretAccessKey 이전에 발급받은 키를 이용해서 초기화를 먼저 진행을 해야됩니다. 이후 메일을 전송할때는 첨부파일을 첨부해서 메일을 전송할꺼기 때문에 sendEmail이 아닌 ses에서 제공하는 sendRawEmail 메서드를 이용해서 메일을 전송을 해야됩니다. 재사용성을 위해서 class를 이용해서 작성을 하였고 위에처럼 꼭 작성은 안해도 됩니다.
예를들어 pdf 파일이 s3에 저장이 되서 url로 해당 파일을 가지고 있다면 (해당 프로젝트 로컬에 pdf 파일이 있는경우도 비슷합니다.) 우선 데이터를 변환을 해줘야 합니다. PDF 파일 같은경우는 텍스트 파일이 아니기 떄문에 바이너리 데이터로 변경을 해줘야 합니다. 이후 uint8Array는 ArrayBuffer는 데이터의 원시적인 바이너리 형태만을 제공하며, 이를 직접 조작하기 어렵습니다. Uint8Array는 이 바이너리 데이터를 8비트 단위의 배열로 만들어, 각 바이트에 쉽게 접근하고 조작할 수 있는 인터페이스를 제공합니다. 그 다음 base64로 인코딩 하기 위해 Buffer를 이용하여 Uint8Array를 Buffer객체로 생성 후 base64로 인코딩을 합니다. 여기서는 메일 전송을 위해 base64로 인코딩을 하였습니다.
const pdfFileUrl = "https://pdf-bucket.s3.ap-northeast-2.amazonaws.com/pdf/1709462261081-969cd273fb8f6e2323" // 해당 링크는 참고값입니다 유효하지 않은 url입니다.
const pdfArrayBuffer = await fetch(pdfFileUrl).then((resp) => resp.arrayBuffer()); // 바이너리 데이터로 변경
const uint8Array = new Uint8Array(pdfArrayBuffer);
cosnt pdfBuffers = Buffer.from(uint8Array).toString('base64');
아래 코드를 보면 MIME이라는 단어가 나오는데 MIME(Multipurpose Internet Mail Extensions) 표준을 사용하여 이메일 본문을 구성하는 방법을 보여줍니다. MIME는 이메일에서 텍스트 이외의 다양한 형태의 데이터(예: HTML, 이미지, 오디오, 비디오 파일 등)를 전송할 수 있게 하는 인터넷 표준입니다.
boundary 같은 경우는 이메일 본문 내의 다양한 섹션(예: HTML 본문, 첨부 파일)을 명확하게 구분하여, 이메일 클라이언트가 각 부분을 올바르게 해석하고 표시할 수 있게 합니다.
인코딩 방식 (HTML 내용 및 텍스트 내용이므로) 그리고 Content-Transfer-Encoding: quoted-printable 이것은 어떻게 인코딩되어 전송되는지를 나타내고 quoted-printable 인코딩은 주로 ASCII 문자가 아닌 텍스트(예: 특수 문자가 포함된 비영어권 텍스트)를 위해 사용됩니다. 아래 내용은 HTML 이메일 본문 및 제목에 관한 내용만 포함이 되어 있습니다.
const boundary = 'boundary-example';
const emailSubject = '메일 제목 PDF 보고서 ';
const additionalContent = `
안녕하세요. 센터입니다.<br/><br/>
본문 내용 ~~~~
<br/><br/>
본문 내용 ~~~~
<br/><br/>
감사합니다.<br/>
센터 드림.<br/>
`;
const emailContent =
`Subject: ${emailSubject}\n` +
'MIME-Version: 1.0\n' +
`Content-type: multipart/mixed; boundary=${boundary}\n\n` +
`--${boundary}\n` +
`Content-Type: text/html; charset=UTF-8\n` + // HTML로 변경
`Content-Transfer-Encoding: quoted-printable\n\n` +
`<html><body>${additionalContent}</body></html>\n\n`; // HTML 코드 포함
이제 첨부파일 관련된 내용을 첨부하겠습니다. 아래 코드를 위에 코드에 한번에 작성을 해도 되지만, 여기서는 지금 프로젝트에 적용된 코드이기때문에 아래처럼 작성을 하게 되었습니다.
Content-Disposition 헤더는 데이터의 처리 방식을 제어하는 데 사용되며, 특히 파일 전송에 있어서 다운로드할 파일의 이름을 지정하고, 데이터가 첨부 파일로 처리되어야 함을 명시하는 데 사용됩니다.
마지막에 --${boundary}-- 하는 이유는 멀티파트 메시지의 종료를 나타내며, 이는 메시지의 마지막 섹션이 끝났음을 의미합니다.
const pdfFileName = '결과보고서'
emailContent +=
`--${boundary}\n` +
`Content-Type: application/pdf;\n` +
`Content-Disposition: attachment; filename="${pdfFileName}.pdf"\n` +
`Content-Transfer-Encoding: base64\n\n` +
`${pdfBuffer}\n`;
`--${boundary}\n`; // 각 PDF 파일 첨부 후에 boundary 추가
emailContent += `--${boundary}--`;
이후 해당 emailContent를 이용해서 메일을 전송하면 됩니다. 발신자 이메일 주소는 AWS SES에 등록된 이메일만 메일 전송이 가능합니다.
import awsService from '@app/utils/aws';
.
.
.
.
const params = {
Source: '이메일주소', // 발신자 이메일 주소 이 주소는 ses에 등록된 이메일만 메일 전송이 가능합니다.
Destinations: [email], // 수신자 이메일 주소
RawMessage: {
Data: emailContent,
},
};
await awsService.sendEmail(params);
지금은 프로젝트에 있는 코드들을 복붙을 해서 코드조각들로만 이루어져 있는데 시간날떄 전체적인 코드도 첨부해서 올리도록 하겠습니다. 순서대로 위 코드조각들을 합치면 정상적으로 동작을 할껍니다.
이후에 블로깅 내용은 NextJS 14 관련해서 적어볼 예정입니다. (몇달전에 13버전을 작성했는데 14버전 나왔네 ㅎ)