Nestjs, AWS SES templated 이메일 보내기

·2024년 3월 10일
0

nestjs

목록 보기
7/10

1. AWS SES 세팅

모든 진행과 캡쳐는 2024-03-10에 이루어졌습니다. 이후 UI나 기능이 달라졌을 수 있습니다.
가장 많은 도움 얻음: https://coding-groot.tistory.com/172

1-1. SES 권한만 있는 IAM user 생성, 소스 코드에서 사용하기 위한 Access key 획득

1-1-1. AWS > IAM > 사용자 > 사용자 생성 클릭 (캡쳐에 제가 이미 생성해놓은 이름이 하나 보이고 있는데, '사용자 생성'을 누르시면 됩니다.)


1-1-2. 사용자 이름만 입력한 뒤 '다음' 클릭


1-1-3. 저는 미리 생성해놓은 그룹이나 권한이 없어서, '직접 정책 연결'을 선택했습니다.
ses로 검색해서 AWS에서 미리 설정된 정책 중 AmazonSESFullAccess만 선택한 뒤 '다음' 클릭


1-1-4. 태그는 선택사항입니다. 필요하시면 자유롭게 입력하시고 '사용자 생성' 클릭


1-1-5. 애플리케이션에서 사용하기 위해 액세스키(Access Key)를 만들어야 합니다.
방금 만든 'ses-client'를 클릭


1-1-6. '액세스 키 만들기' 클릭


1-1-7.
저는 AWS LightSail instance에 올린 nestjs application에서 사용할 예정이므로 아래와 같이 선택했습니다. (AWS를 고르고 액세스키를 생성한다고해서 로컬에서 앱 테스팅할 때 access key로 접속이 안되지는 않네요.)


1-1-8.
이 액세스키가 어디서 무슨 용도로 쓰는 액세스키인지 적당히 태그를 달아줍니다.


1-1-9.

위에서 액세스 키 만들기를 클릭하면 바로 Access key와 Secret Access key(비밀액세스키)가 나오는데, 이때 복사 또는 다운로드해서 다른 사람들이 볼 수 없는 곳에 잘 저장해둡니다.

  • 주의: private 레포라도 소스코드에 넣지 않으시는 걸 권장 드리고, public 레포일 경우에는 소스코드에 절대 넣지 마세요. 최소한 환경변수를 사용하시거나 다른 방법을 이용하길 권장드립니다.

저는 gitignore로 깃으로 관리하지 않는 .env 파일에 저장했습니다.


1-2. Amazon SES 사용 설정

1-2-1. AWS > Amazon SES(Simple Email Service) 접속
'설정 시작 페이지 보기' 클릭


1-2-2. 내 이메일 주소를 인증하기 위해 '자격 증명 생성' 클릭


1-2-3. 자신의 이메일 주소를 적고 '자격 증명 생성' 클릭
저는 Gmail을 사용했습니다.


1-2-4. (메일을 확인하기 전)


1-2-5. Gmail 메일보관함에 들어가서 AWS에서 온 이메일을 확인하고 링크를 클릭


1-2-6. AWS에서 온 이메일 링크를 클릭해서 인증 후

--
1-2-7. 이 상태에서는 테스트 용도로 제한된 수(하루 200개)의 이메일을 제한된 메일주소(자기가 인증한 이메일)로밖에 못 보냅니다.
더 본격적인 사용을 위해서는 Sandbox 탈출 신청서를 AWS에 제출해야 합니다. 이 부분은 저도 이 글을 쓰는 시점에 신청 후 아직 승인과정 중에 있습니다. 적당히 우리 서비스에서는 회원들의 가입을 통해 이메일주소를 수집할 거고, 회원이 자기가 신청한 이벤트에 대해 신청완료이메일같은 걸 보내주기 위해 이 서비스를 이용할 거야라고 적어서 보냈습니다.
단, 샌드박스 상태에서도 기능은 다 동작하므로 신청서만 보내놓고 샌드박스 상태로 Nestjs에 코드 작성을 진행했습니다.

2. Nestjs에 SES 메일 발송 코드 작성

2-1. sdk client 설치, MailModule 생성

우선 aws-sdk로 ses client를 프로젝트에 추가해줍니다.

pnpm add @aws-sdk/client-ses

자신이 사용하는 패키지 매니저로 설치하면 되는데, 저는 이번 프로젝트에서 pnpm을 사용 중입니다.
(참고로, 이렇게 해서 설치한 패키지의 버전은 현시점에서 v3입니다.
저는 "@aws-sdk/client-ses": "^3.529.1", 가 설치되었습니다.)

nest g mo mail, nest g s mail을 이용해 MailModule을 만듭니다.

2-2. MailService에서 템플릿 생성, 이메일 발송

.env 파일에 SES_ACCESS_KEY, SES_SECRET_KEY 라는 이름으로 Access key와 Secret Access key를 저장해주었습니다.

import {
  CreateTemplateCommand,
  SESClient,
  SendTemplatedEmailCommand,
} from '@aws-sdk/client-ses';

@Injectable()
export class MailService {
  private sesClient: SESClient;

  constructor() {
    // AWS SES 클라이언트 생성 Region은 서울로 설정
    this.sesClient = new SESClient({
      region: 'ap-northeast-2',
      credentials: {
        accessKeyId: process.env.SES_ACCESS_KEY ?? '',
        secretAccessKey: process.env.SES_SECRET_KEY ?? '',
      },
    });
  }
  
  /**
   * TEST용 메서드!
   * nestjs 앱이 켜질 때 자동으로 실행되는 메서드로, 정의해놓으면 앱이 켜질 때 자동으로 한번 실행되어
   * 코드 조금씩 바꿔가면서 테스트할 때 편리합니다. 
   * start:dev 로 실행해둘 경우 코드 한 줄 고치고 저장할 때마다 테스트메일이 계속 날아갈 수 있으므로 주의!
   */
  async onApplicationBootstrap() {
    console.log('메일 서비스 시작');
    await this.run();
    await this.sendTemplatedMail();
  }

  /**
   * 템플릿 객체를 만들어 반환합니다.
   */
  createCreateTemplateCommand() {
    return new CreateTemplateCommand({
      /**
       * The template feature in Amazon SES is based on the Handlebars template system.
       * Handlebars 템플릿 문법은 기본적으로 {{}}로 감싸면 변수라는 의미로, 
       * 추후 TemplateData로 JSON 형태로 넣어준 변수의 값이 이름에 맞게 들어가게 됩니다.
       * 참고: 이메일템플릿에 CSS 넣을 때는 태그 안에 style을 넣는 인라인 방식으로 넣어야 잘 적용됩니다. 
       * 위 내용 참고: https://ojava.tistory.com/111
       * 또, button onclick을 써보니 안됩니다. css로 버튼처럼 만든 a태그를 많이 쓰는 것 같습니다.
       */
      Template: {
        TemplateName: 'testTemplate',
        HtmlPart: `
          <h1>Hello, {{contact.firstName}}!</h1>
          <div>
			It is a SES templated mail.
          	<a href="https://google.co.kr">googleLinkTest</a>
          </div>
        `,
        SubjectPart: 'Amazon SES Templated Mail Test to {{contact.firstName}}',
      },
    });
  };

  /**
   * 템플릿 객체를 SES에 등록합니다.
   */
  async run() {
    const createTemplateCommand = this.createCreateTemplateCommand();

    try {
      return await this.sesClient.send(createTemplateCommand);
    } catch (err) {
      console.log('Failed to create template.', err);
      return err;
    }
  };

  private async sendTemplatedMail() {
    const templatedEmail = new SendTemplatedEmailCommand({
      Destination: {
        CcAddresses: [], // 참조
        BccAddresses: [], // 숨은 참조
        ToAddresses: ['your_gmail_id@gmail.com'], // 받는 사람 이메일 (샌드박스 상태에서는 인증받은 자기 자신의 메일로 테스트)
      },
      Template: 'testTemplate', // 여기에 TemplateName으로 등록했던 이름을 넣어줍니다.
      // * 주의: TemplateData에 Template에 들어있는 모든 변수가 정의되어야합니다. 오타 주의!
      TemplateData: JSON.stringify({
        contact: {
          firstName: 'TestJohn',
        },
      }),
      Source: 'your_gmail_id@gmail.com', // 보내는 사람 이메일 (인증받은 이메일)
      ReplyToAddresses: [],
    });

    try {
      const response = await this.sesClient.send(templatedEmail);
      console.log('메일 전송 완료\n', response.$metadata);
    } catch (error) {
      console.log(error);
    }
  }
}

2-3 받은 메일 확인

위 코드가 잘 동작했다면 템플릿도 등록되었을 뿐 아니라 바로 테스트메일이 발송되었을 것입니다. 메일함에서 확인해보세요!

  • 단, 여기 코드에서는 템플릿 등록과 이메일 발송을 한번에 다 처리했지만 템플릿은 최초 1번만 등록해두면, SES에 등록돼있으니 그다음부터는 템플릿 name만 가지고 SendTemplatedEmailCommand 만들어서 메일 발송만 하시면 됩니다! 이메일 발송할 때마다 템플릿 등록해야하는 거 아님 주의!

3. SES 템플릿 관리

3-1 AWS SES 화면에서 등록된 템플릿 확인하기


그냥 내가 templateName으로 작성했던 이름으로 코드를 통해 등록요청했을 때 잘 등록이 됐는지만 확인할 수 있습니다. 템플릿의 내용은 이 화면에서 GUI로는 볼 수 없고, 따로 API를 통해 요청해서 보거나 아래에서처럼 터미널 명령어로 봐야합니다.

3-2 AWS Cloud Shell 터미널 명령어로 템플릿 내용확인 및 삭제하기

캡쳐이미지의 제일 오른쪽 위에 '서울'을 찾은 뒤 그 왼쪽에 있는 아이콘 목록 중에서 제일 왼쪽 터미널 아이콘을 눌러 AWS에서 제공해주는 CloudShell 터미널을 켭니다.
aws ses delete-template --template-name testTemplate 명령어를 통해 testTemplate 템플릿을 삭제했습니다.
aws ses get-template --template-name testTemplate5 명령어를 통해 testTemplate5 템플릿의 내용을 확인했습니다.
템플릿 생성도 아까처럼 코드를 통해 api로 전송하지 않고 이렇게 터미널 명령어를 통해 생성할 수 있으니 참고하세요!

4. 한번에 보내기 SendBulkTemplatedEmail

템플릿에 변수로 메일 수신자의 이름 등을 치환해서 개인화된 메일을 보내지만, 예를 들어 모임 참여자 100명에게 이름만 바꿔서 보낸다고 메일주소, 이름(templateData) 파라미터만 바꿔가면서 sendTemplatedMail 메서드를 100번 호출하긴 싫으니 bulk 메서드를 알아봅시다. 거의 똑같습니다.


/**
* 주의: 위에서 언급했던 테스트용 메서드! 실제 서비스 들어갈 땐 반드시 삭제해주셔야 합니다.
*/
  async onApplicationBootstrap() {
    console.log('메일 서비스 시작');
    // await this.run();
    // await this.sendSesSimpleMail();
    await this.sendBulkTemplatedMail([
      {
        email: 'abc@gmail.com',
        firstName: 'David',
        bookTitle: 'Clean Code',
      },
      {
        email: 'abc2@gmail.com',
        firstName: 'Hello',
        bookTitle: 'Refactoring',
      }
    ]);
  }
  

  private async sendBulkTemplatedMail(dataList: any[]) {
    const destinations = dataList.map((data) => {
      return {
        Destination: {
          ToAddresses: [data.email],
        },
        ReplacementTemplateData: JSON.stringify({
          contact: {
            firstName: data.firstName,
          },
          book: {
            title: data.bookTitle,
          },
        }),
      };
    });

    const input: SendBulkTemplatedEmailCommandInput = {
      Template: 'testTemplate5',
      DefaultTemplateData: JSON.stringify({
        contact: {
          firstName: 'DefaultName',
        },
        book: {
          title: 'DefaultBookTitle',
        },
      }),
      Destinations: destinations,
      Source: 'abc@gmail.com',
    };


    const bulkTemplatedCommand = new SendBulkTemplatedEmailCommand(input);
    try {
      const response = await this.sesClient.send(bulkTemplatedCommand);
      console.log('메일 전송 완료\n', response.$metadata);
    } catch (error) {
      console.log(error);
    }
  }
  • 주의: Destinations 내에 ReplacementTemplateData가 제대로 정의되어있지 않을 때 대신 들어가는 DefaultTemplateData는 optional이라고 표시돼있지만 넣지 않으면 에러가 나면서 메일이 발송되지 않습니다.

    도움: https://blog.naver.com/couponpapa/221256819248

profile
백엔드 개발자. 공동의 목표를 함께 이해한 상태에서 솔직하게 소통하며 일하는 게 가장 즐겁고 효율적이라고 믿는 사람.

0개의 댓글