우당탕탕 우리네 개발생활

[Nestjs] 결국 @nest-modules/mailer 대신 nodemailer를 사용했습니다 본문

tech

[Nestjs] 결국 @nest-modules/mailer 대신 nodemailer를 사용했습니다

미스터카멜레온 2024. 3. 30. 23:10

nestjs 프로젝트에 급하게 이메일 전송기능을 구현할 일이 생겼습니다. 이 기능에는 전송할 이메일에 파일을 첨부할 수 있는 필수 요청사항이 있었습니다.

이미 기존 프로젝트 코드 중 AWS SES를 사용할 수 있도록 aws-sdk 라이브러리를 이용하여 유틸성 이메일 서비스를 구현해 놓은 것을 확인했었기에 해당 코드의 재사용을 계획했습니다. 하지만 이미 구현되어 있는 유틸성 이메일 서비스는 파일 첨부를 전혀 염두에 두지 않은 채로 개발되어 있었습니다. 일부 옵션들을 추가하며 파일 첨부를 구현할 수 있을까 가능성을 살펴봤지만 불가능했습니다. 

 

aws-sdk를 사용하면서 이메일에 파일 첨부를 하는 방법은 있었습니다. 

Multipurpose Internet Mail Extensions (MIME) type email body에 첨부하려는 파일을 직접 기입하여 전송하는 방법이었습니다. 아래와 같은 좋은 글을 참고하여 rawEmailBody를 만들수도 있었지만 시간이 촉박한 상황에서 선택을 하기엔 제게 익숙지 않아 다른 방법들을 더 찾아보기로 했습니다.

https://plainenglish.io/blog/aws-ses-how-to-send-a-mail-with-an-attachment-nodejs

 

AWS SES: How to send a mail with an attachment in Node.js

 

plainenglish.io

 

그렇게 서칭을 하다보니 꽤 유명한 라이브러리인 nodemailer가 눈에 들어왔습니다.

파일 첨부를 담당하는 옵션인 attachment parameter를 이용하여 손쉽게 기능을 구현(raw 하게 MIME type의 email body를 구현하지 않아도 됨)할 수 있었기에 마다할 이유가 없었습니다. 

https://www.npmjs.com/package/@nestjs-modules/mailer?activeTab=readme

 

@nestjs-modules/mailer

NestJS - a mailer module (@mailer). Latest version: 1.11.2, last published: a month ago. Start using @nestjs-modules/mailer in your project by running `npm i @nestjs-modules/mailer`. There are 63 other projects in the npm registry using @nestjs-modules/mai

www.npmjs.com

nodemailer를 염두에 둔 이후에도 여러 레퍼런스들을 찾아보던 도중 @nest-modules/mailer라이브러리 역시 발견했습니다.

nodemailer를 nestjs의 컨셉에 맞게 래핑 하여  MailerModule을 제공해 주는 부분이 흥미로웠고 모듈 내에서 SES 인스턴스의 라이프사이클이 함께 관리되는 것이 좋았습니다. 어차피 nodemailer의 장점들은 그대로 유지해 가면서 모듈을 사용할 수 있는 @nestjs-modules/mailer를 채택하여 개발을 시작했습니다.

// email.module.ts

imports:[
    MailerModule.forRootAsync({
        useFactory: (configService: ConfigService) => ({
            transport: {
                SES: new SES({
                    apiVersion: /**/,
                    region: /**/,
                    credentials: {
                        accessKeyId: configService.get('accessKeyId') as string,
                        secretAccessKey: configService.get('secretAccessKey') as string,
                    },
                }),
            },
        }),
        inject: [ConfigService],
    }),
],

SES를 적용하여 email기능을 담당할 모듈을 위와 같이 구현해볼 수 있었습니다.

 

yarn환경에서 @nest-modules/mailer 라이브러리를 설치한 후 yarn build 시 에러가 발생

과정이 너무 수월했던 걸까요.. 개발을 후딱 끝내고 로컬에선 분명 기능테스트까지 완료한 상태에서 배포를 하던 중 github action에서 에러가 발생했습니다.

dockerfile을 기반으로 ECR을 만들던 중 yarn install을 마친 후 yarn build 시 발생한 에러

 

에러를 파악하기 위해 다음과 같이 진행했습니다.

1. 해당 에러 stacktrace를 통해 source code를 확인했습니다.

-> error source code와 @nestjs-modules/mailer 라이브러리와의 연관성을 찾기가 어려웠습니다.

2. google을 통해 위 에러 발생 사례들을 검색했습니다.

-> 일치하는 사례가 많지 않아 참조하여 문제를 해결하기엔 어려움이 있었습니다.

3. dockerfile 내 스크립트를 로컬에서 순서대로 수행해봤습니다.

-> 이렇게 했더니 기존에 로컬에서 문제가 없었던 게 똑같이 재현이 됐습니다.

4. mailer를 사용하기 위해 설치했던 @nestjs-modules/mailer, nodemailer, @types/nodemailer를 전부 삭제한 후 nodemailer, @types/nodemailer만 설치한 후 dockerfile 내 스크립트를 로컬에서 수행했습니다.

-> 스크립트가 정상적으로 동작했습니다.

 

결론적으로 nodemailer와 @types/nodemailer의 조합은 문제가 없었다는 것을 발견하였고 원인을 정확히 알진 못했지만 yarn으로 @nestjs-modules/mailer를 설치하게 되면 버그가 발생할 수 있다는 사실을 알 수 있었습니다.(ps. stackoverflow 등 여러 레퍼런스에서 npm을 통해 설치한 경우엔 정상적으로 동작을 한다는 사례들을 발견하긴 했습니다.)

 

@nestjs-modules/mailer를 nodemailer기반으로 마이그레이션

결국 @nestjs-modules/mailer기반으로 작성했던 코드들을 nodemailer기반으로 마이그레이션 하기 시작했습니다.

다행히 공수는 많지 않았고 아래와 같이 할 수 있었습니다.

1. MailerModule의 transport를 createTransport라는 메서드를 사용하여 서비스 내 설정했습니다.

2. 이후 생성된 transport를 기반으로 코드를 재구성했습니다.

결과적으로 아래와 같이 코드를 작성할 수 있었고 이를 기반으로 이메일 파일 전송 기능을 구현할 수 있었습니다.

@Injectable()
export class EmailService {
  private readonly transporter: nodemailer.Transporter;
  constructor(
    private readonly configService: ConfigService,
  ) {
    const transport = {
      SES: new AWS.SES({
        apiVersion: /**/,
        region: /**/,
        credentials: {
          accessKeyId: this.configService.get('accessKeyId') as string,
          secretAccessKey: this.configService.get('secretAccessKey') as string,
        },
      }),
    };
    this.transporter = nodemailer.createTransport(transport);
  }

 

마치며

위 사례를 겪으면서 얻은 교훈이 있습니다. nestjs 가 붙어있는 라이브러리라고 맹신하진 말자(적어도 official docs에서 언급하는 라이브러리를 사용하자)입니다. 운 좋게도 마이그레이션 공수가 크지 않았고 기능 구현의 난이도가 높지 않았기에 기한 내 요구사항을 처리할 수 있었지만 상황을 마주했을 땐 아찔했습니다. 곱씹어보면 그래도 라이브러리의 선택에 대해선 나름의 기준으로 올바르게 선택했었다고 생각이 들었습니다. 그러나 앞으로 좀 더 세밀하게 사전 조사를 할 수 있도록 수준을 높이는 데 지속적으로 노력해야겠습니다. 많은 동료분들에게 제

가 겪은 사례가 라이브러리를 선택하는 데 있어 시행착오를 줄일 수 있는 데 도움이 되었으면 좋겠습니다.

 

(원래는 그래서 파일을 어떻게 쉽게 담아서 이메일 기능을 구현할 수 있었는지에 대한 구체적인 코드 공유를 해보고 싶었으나 전달하고자 내용의 목적이 바뀌어서 위와 같은 내용으로 작성하게 되었습니다. 추후엔 구체적인 방법에 대해 공유해 보도록 하겠습니다.)

참조

https://plainenglish.io/blog/aws-ses-how-to-send-a-mail-with-an-attachment-nodejs

https://github.com/nest-modules/mailer/issues/1071

https://nodemailer.com/message/attachments/

https://medium.com/@boladebode/exploring-the-new-release-of-nest-js-version-10-and-the-migration-from-nest-modules-mailer-b80c574f89e6