[nest.js] 이메일 인증 구현하기

채연·2023년 10월 2일
2

응애

목록 보기
7/9
post-thumbnail

이메일 인증 구현하기!

nest.js의 공식 문서에 MailerModule을 이용하여 메일을 보내는 방법이 나와있다.

공식문서를 따라가며 이메일 인증을 구현해보자!

1. 패키지 설치

npm install --save @nestjs-modules/mailer nodemailer

이메일을 보낼 때, 템플릿을 사용하면 더더욱 이쁘게 보일 수 있다

2. 템플릿 패키지 설치

필수적인 기능은 아니지만 퀄리티를 높여주는 아이기 때문에 같이 설치해본당

공식 문서에는 handlebars, pub, ejs가 나와있는데,

이 블로그 를 참고하여 고민해본 결과, Handlebars를 사용하기로 결정하였다!

새로운 라이브러리를 또 깔지 않아도 되고, 확장성이 좋아보였다 :D
-> 다른 언어에서도 사용할 수 있다는 점!

3. Module에 넣기

nest.js 공식 문서에서 알려준 비동기 코드를 적용하였다.

//app.module.ts
import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';

@Module({
  imports: [
    MailerModule.forRootAsync({
      useFactory: () => ({
        transport: 'smtps://user@domain.com:pass@smtp.domain.com',
        defaults: {
          from: '"nest-modules" <modules@nestjs.com>',
        },
        template: {
          dir: __dirname + '/templates',
          adapter: new HandlebarsAdapter(),
          options: {
            strict: true,
          },
        },
      }),
    }),
  ],
})
export class AppModule {}

4. Trasport 적용

보내는 이메일을 네이버 이메일로 보낼 거기 때문에 네이버 메일에서 설정을 해주어야 했다.

-> IMAP/SMTP를 사용 안 함 -> 사용 함으로 바꾸어주고 저장한다.

이 블로그 를 참고하여 진행하였다

5. AppModule -> MailModule로 변경

위의 imports 코드들은 AppModule에서 진행했었는데, MailModule을 파고 난 후, MailModule로 변경해주었다.

아직 템플릿 적용 전이다!

nest g controller Mail
nest g service Mail
nest g module Mail

mail.module.ts

import * as path from 'path';
import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { MailService } from './mail.service';
import { MailController } from './mail.controller';

@Module({
  imports: [
    MailerModule.forRootAsync({
      useFactory: () => ({
        transport: {
          host: 'smtp.naver.com',
          port: 587,
          auth: {
            user: process.env.EMAILADDRESS,
            pass: process.env.EMAILPASSWORD,
          },
        },
        defaults: {
          // 우리 서비스 이름 Waa와 함께, 답장을 해야할 이메일을 알려준다.
          from: `'Waa' <${process.env.EMAILADDRESS}>`,
        },
      }),
    }),
  ],
  controllers: [MailController],
  providers: [MailService],
})
export class MailModule {}

mail.service.ts

import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';

@Injectable()
export class MailService {
  constructor(private readonly mailerService: MailerService) {}

  async sendEmail() {
    await this.mailerService.sendMail({
      // 누구에게 보낼 것인가?
      to: 'myEmail',
      
      // 메일 제목 설정
      subject: 'Test',
      
      // 메일 내용 설정
      text: '테스트',
    });
  }
}

mail.controller.ts

import { Controller, Get } from '@nestjs/common';
import { MailService } from './mail.service';
import { ApiOperation, ApiTags } from '@nestjs/swagger';

@Controller('mail')
export class MailController {
  constructor(private readonly userService: MailService) {}

  @Get()
  @ApiTags('Auth')
  @ApiOperation({ summary: '이메일 보내기' })
  async createUser() {
    return this.userService.sendEmail();
  }
}

위와 같이 설정해주면, 메일 발송 준비 끝!

-> 스웨거에 잘 나타나는 모습도 볼 수 있고, 요청을 날리면 메일이 온다!

6. 템플릿 적용시키기

이 블로그 를 참고하여 진행하였다.

위에서 구현했던 mail.module.ts에 template를 추가한다

@Module({
  imports: [
    MailerModule.forRootAsync({
      useFactory: () => ({
        transport: {
          host: 'smtp.naver.com',
          port: 587,
          auth: {
            user: process.env.EMAILADDRESS,
            pass: process.env.EMAILPASSWORD,
          },
        },
        defaults: {
          from: `'Waa' <${process.env.EMAILADDRESS}>`,
        },
        // 이곳 추가
        template: {
          dir: path.join(__dirname, './templates'),
          adapter: new HandlebarsAdapter(),
          options: {
            strict: true,
          },
        },
      }),
    }),
  ],
  controllers: [MailController],
  providers: [MailService],
})
export class MailModule {}

./templates로 적어두었으니 mail의 하위폴더로 templates 폴더를 생성한다.

그리고, 템플릿을 적을 email.hbs를 생성!

// email.hbs
<p>Hi {{username}},</p>
<p>Hello from NestJS NodeMailer</p>

일단 임시 데이터로 이렇게 놔둔다.

그리고 service 로직을 변경해준다.

// mail.service.ts
@Injectable()
export class MailService {
  constructor(private readonly mailerService: MailerService) {}

  async sendEmail() {
    await this.mailerService
      .sendMail({
        to: 'myEmail.naver.com',
        subject: 'Test',
        text: '테스트',
      
      // email.hbs라서 ./email 적어줌. 확장자는 자동 생성
        template: './email',
      
      // 동적으로 들어갈 변수 정의.
        context: {
          code: 'cf1a3f828287',
          username: 'Chaeyeon',
        },
      })
      .then((response) => {
        console.log(response);
      })
      .catch((err) => {
        console.log(err);
      });
  }
}

email.hbs에서

<p>Hi {{username}},</p>

이렇게 적었었는데, 그걸 service 로직에서 username: 'Chaeyeon'과 같이 정의해주는 방식이다.

-> 나중엔 이것도 당연히 동적으로 변경시켜주어야 한다!

이슈사항

분명, 맞는 디렉토리 경로를 입력해도 불구하고 계속해서 디렉토리를 찾지 못한다는 이슈가 있었다.

Error: ENOENT: no such file or directory, open '/Users/Desktop/dist/mail/templates/email.hbs'

-> 조금 더 자세히 에러 코드를 봤어야 했는데 처음에는 뒤의 mail/templates/email.hbs만 보아 왜 안 되지.. 이러고 있었다.

계속 바꾸어도 안 되길래 조금 더 에러코드를 자세히 보니 /dist에서 난다는 것을 확인하였고 실제 dist 폴더를 가보니 /templates라는 폴더 자체가 생성이 안 되고 있었다.

npm run build 를 이용하여 빌드를 해보아도 생성이 안 됐다.

구글링 결과 nest-cli.json에 코드를 추가해줘야 한다는 것을 알았고,

  "compilerOptions": {
    "deleteOutDir": true,
    "assets": [
      {
        "include": "mail/templates",
        "outDir": "dist",
        "watchAssets": true
      }
    ]
  }

다음과 같이 입력하여 템플릿 폴더를 무사히 생성할 수 있었다!

결과

-> username에 Chaeyeon이 잘 들어가는 모습!

7. 템플릿 구현

<table
  width='100%'
  cellpadding='0'
  cellspacing='0'
  style="border: '0px'; background-color: #ffffff; width: 100%"
  class='container'
>
  <tr style='display: block; width: 100%; margin-bottom: 20px'>
    <td style='display: block; width: 100%; text-align: center'></td>
  </tr>
  <tr width='100%' style='display: block; margin-bottom: 50px'>
    <td
      style='
            display: block;
            width: 100%;
            font-weight: bold;
            font-size: 28px;
            color: #000000;
            text-align: center;
          '
    >
      자녀의
      <span style='color: red'>위험 의심 단어</span>가 5회를 초과하였습니다.
    </td>
    <td
      style='
            margin-top: 4px;
            display: block;
            width: 100%;
            font-size: 20px;
            color: #000000;
            text-align: center;
          '
    >
      아래는 5회 초과한 단어 리스트입니다.
    </td>
  </tr>
  <tr>
    <td
      style='
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
          '
    >
      <div
        style='
              background: #f3f3f3;
              padding: 20px 34px 12px 34px;
              width: 450px;
              border-radius: 8px;
              color: #4b5550;
              font-size: 15px;
            '
      >
      {{!-- 반복문 코드 --}}
        {{#each keywords}}
          <div style='margin-bottom: 8px'>
            {{value}}
            :
            {{count}}
          </div>
        {{/each}}
      </div>
    </td>
  </tr>
</table>

완성 !

profile
Hello Velog

1개의 댓글

comment-user-thumbnail
2024년 5월 1일

잘 보고 갑니다. 👍

답글 달기