SW/Typescript

[Typescript] Nodemailer에 Gmail 연동, HTML content 및 첨부파일 발송 방법

BetaMan 2021. 12. 3. 19:11
728x90
반응형

Nodemailer

본 포스팅은 Typescript를 기반으로 작성되었다.
Javascript와 문법이 다르지 않지만 차이점이 존재할 수도 있다는 부분을 염두하고 읽기 바란다.

2021.11.21 - [SW/Typescript] - [Typescript] 1. 소개 및 초기 설치

 

[Typescript] 1. 소개 및 초기 설치

사실 많은 주니어 개발자들이 기본적인 WEB 3 Stack을 통해 간단한 웹앱을 개발하면서 Typescript의 도입 필요성을 느끼진 못했을 것이다. 굳이 그딴거 복잡하게 써봤자 오히려 타입같은거 신경써야

betaman-workshop.tistory.com


1. 서두

이번엔 Node.js로 메일 발송을 구현하도록 도와주는 Nodemailer 패키지에 GMail을 연동하여 사용하는 방법에 대해 써보았다.

2. Nodemailer 란?

Nodemailer is a module for Node.js applications to allow easy as cake email sending. - nodemailer.com

Nodemailer 메인 렌딩페이지에 떡하니 박아놓은걸 보면 알 수 있듯이 Node.js 애플리케이션에서 이용할 수 있는 이메일 발송 모듈이라고 소개되고 있다.

Nodemailer의 특징은 다음과 같이 소개되고있다.

  • 종속성이 없는 단일 모듈
  • 보안에 집중. (RCE 취약점 같은거)
  • 유니코드 지원
  • Windows 지원
  • HTML content를 사용할 수 있지만, plain text로 대체하여 사용할 수도 있음
  • 메세지에 첨부파일 추가 가능
  • HTML content에 포함된 이미지 첨부 가능
  • TLS/STARTTLS 전송방식
  • SMTP 지원
  • DKIM을 통한 메세지 서명
  • 정상적인 OAuth2 인증
  • ES6 표준을 따름 (hoisted var로 인해 의도하지 않은 메모리 릭이 발생하지 않는다고 한다)

요구사항으로는 Node.js v6.0.0 이후 버전을 요구한다.

3. 개발환경 준비

우선 간단하게 아래 명령어로 Typescript를 사용할 수 있는 환경을 만들자.
이미 개발환경이 준비되었다면 이 단계는 건너뛰어도 좋다.

(리눅스 기준의 명령어이다. 윈도우 사용자들도 크게 무리 없이 입력할 수 있는 명령어이긴 하지만, 앞으로의 미래를 위해 윈도우 사용자들은 명령프롬프트, 파워쉘 대신 WSL2를 사용하도록 하자. 그게 정신 건강에 이롭다. WSL2 환경 기반의 개발환경 세팅에 관한 내용은 다른 포스팅에서 자세히 남겨두겠다.)

1
2
3
4
5
6
mkdir test
cd test
npm init   <= (npm 초기설정값은 아무렇게나 넣어도 상관없다.)
npm install -D typescript    <= (필자는 전역 패키지가 의존성을 망쳐놓는걸 선호하지 않기 때문이다.)
npx tsc --init
mkdir src
cs

위 명령어로 생성된 package.json과 tsconfig.json을 아래와 같이 수정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "name""test",
  "version""1.0.0",
  "description""",
  "main""dist/index.js",    <= dist/index.js 로 수정
  "scripts": {
    "start""npx tsc && node .",   <= "start"를 추가 하고 "npx tsc && node ." 로 지정
    "test""echo \"Error: no test specified\" && exit 1"
  },
  "author""",
  "license""ISC",
  "devDependencies": {
    "typescript""^4.5.2"
  }
}
cs

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "compilerOptions": {
    "target""ES6",
    "lib": ["ES5""ES6""ESNext""DOM"],
    "experimentalDecorators"true,
    "emitDecoratorMetadata"true,
    "module""commonjs",
    "moduleResolution""node",
    "resolveJsonModule"true,
    "sourceMap"true,
    "outDir""./dist",
    "allowSyntheticDefaultImports"true,
    "esModuleInterop"true,
    "forceConsistentCasingInFileNames"true,
    "strict"true,
    "skipLibCheck"true
  },
  "exclude": [],
  "include": ["src/**/*.ts"]
}
cs

tsconfig.json

4. Nodemailer 설치

우리는 Nodemailer를 Gmail과 연동해서 사용할 것이기 때문에 Nodemailer 발송자를 선언할때 연동할 GMail의 이메일 주소와 비밀번호를 인증정보로 제공해야한다.
그러다보니 소스코드 안에 개인정보가 들어가게 될 수 있는데 그래서 우리는 dotenv 패키지를 사용하여 환경변수 파일로 개인정보를 따로 분리하여 관리할 계획이다.
(dotenv는 환경변수를 사용할 수 있게 해주는 패키지이다. 개인정보를 따로 파일로 분리 후 해당 파일을 버전관리시스템에 올리지 않도록 주의해야한다. ".gitignore" 파일에 .env 파일을 스테이징 하지 않도록 설정하면 된다.)

아래 명령어를 통해 설치한다.
(typescript를 사용하기에 개발용으로 타입도 같이 설치해준다.)

1
2
npm install nodemailer dotenv
npm install -D @types/nodemailer @types/dotenv
cs

그리고 프로젝트 최상위 경로에 ".env"라는 이름의 파일을 생성하고 그 안에 연동할 GMail 이메일 주소와 비밀번호를 기입한다 (이때 비밀번호는 해당 GMail로 구글을 로그인할때 사용하는 비밀번호를 말한다.)

1
2
3
4
5
6
MAILER_EMAIL=<연동할 GMail 이메일 주소>
MAILER_PASSWORD=<연동할 GMail 비밀번호>
 
예)
MAILER_EMAIL=testuser@gmail.com
MAILER_PASSWORD=test1234!!
cs

.env

현재까지의 프로젝트 디렉토리 구조는 아래와 같다.

프로젝트 디렉토리 구조

그리고 연동할 GMail 계정에 로그인하여 "보안 수준이 낮은 앱의 액세스"를 허용 설정해주어야한다.

https://myaccount.google.com/lesssecureapps

 

로그인 - Google 계정

하나의 계정으로 모든 Google 서비스를 Google 계정으로 로그인

accounts.google.com

위 링크로 접속한 뒤 로그인 하여 "보안 수준이 낮은 앱 허용"을 체크해주어야 한다.

보안 수준이 낮은 앱의 액세스 허용

5. Nodemailer 발송 구현

드디어 본격적으로 Gmail 연동 메일 발송을 구현하기 위해 src 폴더 안에 index.ts 파일을 생성하고 코드 작성을 시작해보자.

우선 모듈을 import 해준다
(위 개발환경 구성 과정에서 "npm install -D @types/nodemailer @types/dotenv"를 통해 타입정의를 설치하지 않았다면 오류가 발생할 수 있다.)

1
2
3
import nodemailer from "nodemailer";
import dotenv from "dotenv";
import path from "path";
cs

path 모듈을 Node.js 기본 모듈 중 파일 경로에 관한 모듈인데, .env 파일의 경로를 지정해줄 때 문자열로 직접 입력하게 되면 나중에 빌드하는 환경(window냐 리눅스냐 맥이냐)에 따라 경로 지정 방법이 달라질 수 있는데 그로 인해 오류가 발 생할 수 있다. 이런 magic string 혹은 magic number의 사용은 지양해야 하므로 우리는 path.join( )을 이용해 경로를 표현해줄 것이다.

다음으로 dotenv에 .env 파일의 경로를 넘겨주면서 우리가 설정한 환경변수를 불러온다.
path.join( )을 이용해 현재 파일(index.ts)의 위치를 기준으로 .env 파일의 경로를 표현하여 dotenv.config( )에 path로 넘겨준다.
이때 dotenv.config( )는 환경변수 파일의 parsing 성공 여부를 객체로 반환하는데 반환된 객체 안에 parsed가 포함되어있으면 성공, error가 포함되어있으면 실패이다. 이 성공 여부를 가지고 예외처리를 진행한다.

1
2
3
4
5
6
// 일회성 변수가 전역적으로 남는 것을 방지하기 위해 익명함수로 스코프를 제한함
(() => {
  const result = dotenv.config({ path: path.join(__dirname, ".."".env") }); // .env 파일의 경로를 dotenv.config에 넘겨주고 성공여부를 저장함
  if (result.parsed == undefined// .env 파일 parsing 성공 여부 확인
    throw new Error("Cannot loaded environment variables file."); // parsing 실패 시 Throwing
})();
cs

이제 메일 발송 함수를 선언해주자. 우리가 만든 함수는 Nodemailer의 SendMailOptions 라는 타입의 객체(메일의 수신자 이메일 주소와 제목과 내용을 포함한 객체)를 인자로 받아서 메일을 발송한 뒤 성공 여부를 boolean 타입으로 리턴하는 함수가 될 것이다.

1
2
3
const sendGMail = (param: nodemailer.SendMailOptions): boolean => {
    // 메일 발송 함수 구현 내용이 들어갈 자리
};
cs

메일 발송을 위에선 두 가지 사전 작업이 필요한데, 첫 번째는 Nodemailer 발송자를 선언하는 것이다.
발송자 선언에 필요한 인자로 객체를 넘겨줘야 하는데 이 객체에는 연동할 메일 서비스의 이름, 포트번호, 메일 서버 도메인 또는 IP, TLS 사용 여부, TLS 연결 시도 여부, 인증정보를 포함해야한다.
우리가 선언한 sendMail 함수 내부에 발송자를 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// sendGMail 함수 내부
 
const transporter = nodemailer.createTransport({
  service: "GMail"// 메일 서비스 이름
  port: 587// 메일 서버 포트 (보안을 위해 TLS를 지원하는 587 포트 사용을 권장한다)
  host: "smtp.gmail.com"// 메일 서버 도메인 또는 IP
  securetrue// TLS 사용 여부
  requireTLS: true// TLS 연결 시도 여부
  auth: { // 인증정보 (여기에 연동하고자 하는 GMail의 이메일 주소와 비밀번호를 넣는다)
    user: process.env.MAILER_EMAIL, // 환경변수에 정의한 이메일 주소와 비밀번호를 가져온다.
    pass: process.env.MAILER_PASSWORD,
  },
});
cs

두 번째 사전 작업은 메일을 발송하기 위해 발송 메일에 대한 발송자, 수신자, 제목, 내용 등을 정의하는 것이다. 이부분은 sendMail의 인자인 param 변수를 통해 받은 내용으로 구성할 수 있도록 한다.
마찬가지로 sendMail 함수 내부의 const transport 아래에 선언한다.

1
2
3
4
5
6
7
8
9
10
// sendGMail 함수 내부, cosnt transporter 아래
 
const mailOptions: nodemailer.SendMailOptions = {
    from: process.env.MAILER_EMAIL, // 발송자 이메일 주소
    to: param.to, // 수신자 이메일 주소
    subject: param.subject, // 발송 이메일 제목
    text: param.text// 발송 이메일 내용(plain text)
    html: param.html, // 발송 이메일 내용(HTML content)
    attachments: param.attachments // 첨부파일 정보
};
cs

이제 모든 사전 준비가 완료됐으니 실질적인 메일 발송을 해보겠다.
transporter의 sendMail 함수에 우리가 선언해놓은 mailOptions를 메일 옵션 인자로 전달하고 콜백 함수를 전달한다.
콜백 함수의 인자로 err오 info를 받게 되는데, 메일 발송이 성공하게 되면 err가 조건 검사시 false가 되는데 이를 이용해 성공여부를 판단해 우리가 선언한 sendGMail 함수의 리턴 값을 정의해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
// sendGMail 함수 내부, const mailOptions 아래
 
transporter.sendMail(mailOptions, (err, info) => { // 메일 발송 함수, mailOptions을 메일옵션 인자로 넘겨주고 콜백 함수를 넘겨준다.
    // 콜백 함수의 인자로 err와 info가 있는데 메일 발송이 성공할 경우 err가 undefined가 된다.
    if(err) { // 메일 발송 성공 여부 확인
        console.error(`Failed to send mail - [${err.name}] ${err.message}`); // 메일 발송 실패 시, 오류 로그 출력하고 false 리턴
        return false;
    } 
    else console.info(`Successed to send mail - [${info.messageId}] ${info.response}`); // 메일 발송 성공 시, 성공 로그 출력하고 true 리
});
 
return true;
cs

이제 우리가 만든 sendGMail 함수를 사용하여 테스트 메일을 발송해보자.
sendGMail 함수 밖으로 나와서 sendGMail에 메일 옵션(수신자 이메일 주소, 메일 제목, 메일 내용)을 전달하여 호출하면 된다.

1
2
3
4
5
sendGMail({
  to: "skymin0417@gmail.com"// 수신자의 이메일 주소
  subject: "test 메일"// 메일 제목
  text"안녕세상! hello world!"// 메일 
});
cs

이제 "npm start" 명령으로 실행하면 된다.
만약 invaild login 에러가 뜨면서 메일 발송이 정상적으로 이루어지지 않았다면, .env 파일에 정의해놓은 이메일 주소와 비밀번호가 잘못됐거나 "보안 수준이 낮은 앱 허용"을 체크해주지 않아 생긴 문제일 것이다.

메일 발송 테스트 결과

아래는 우리가 지금까지 작성한 코드의 전문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/index.ts
 
import nodemailer from "nodemailer";
import dotenv from "dotenv";
import path from "path";
 
(() => {
  const result = dotenv.config({ path: path.join(__dirname, ".."".env") });
  if (result.parsed == undefined)
    throw new Error("Cannot loaded environment variables file.");
})();
 
const sendGMail = (param: nodemailer.SendMailOptions): boolean => {
  const transporter = nodemailer.createTransport({
    service: "gmail",
    port: 587,
    host: "smtp.gmail.com",
    securetrue,
    requireTLS: true,
    auth: {
      user: process.env.MAILER_EMAIL,
      pass: process.env.MAILER_PASSWORD,
    },
  });
 
  const mailOptions: nodemailer.SendMailOptions = {
    from: process.env.MAILER_EMAIL,
    to: param.to,
    subject: param.subject,
    text: param.text,
  };
 
  transporter.sendMail(mailOptions, (err, info) => {
    if (err) {
      console.error(`Failed to send mail - [${err.name}] ${err.message}`);
      return false;
    } else
      console.info(
        `Successed to send mail - [${info.messageId}] ${info.response}`
      );
  });
 
  return true;
};
 
sendGMail({
  to: "testuser@test.com",
  subject: "test 메일",
  text"안녕세상! hello world!",
});
 
cs

6. Nodemailer HTML content 발송

HTML content를 발송하는건 어렵지 않다. 메일옵션 객체의 인자중 "html"에 HTML content string을 넣어주면 된다.
HTML content는 ` `(백틱)으로 감싸서 정의하는 것을 추천한다. 파라미터 삽입과 멀티라인 작업에 효과적이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const htmlContent = `
  <h1>Hello World</h1>
  <p>
    <h2>메일 발송 테스트</h2>
    <img src="https://nodemailer.com/nm_logo_200x136.png" alt="로고 이미지">
  </p>
`;
 
sendGMail({
  to: "testuser@test.com",
  subject: "html test 메일",
  html: htmlContent,
});
cs

html content 포함 메일 발송 테스트 결과

아니면 아예 HTML 파일을 읽어와서 문자열로 저장 후 이를 전달하는 방법도 있다.
우선 테스트용 HTML 파일을 최상위 폴더에 만들었다.

테스트용 HTML 파일 생성

Node.js의 기본 모듈 중 "fs" 모듈을 이용해 파일을 읽어오면 된다.
index.ts 최상단 import 부분에 "fs" 모듈을 추가하고 fs.readFileSync( ) 를 이용해 파일을 읽어온 뒤, 그것을 메일옵션의 "html"로 전달해주면 된다.

1
2
3
4
5
6
7
8
9
10
const htmlContent = fs.readFileSync( // 파일 내용 읽기 및 변수에 저장
  path.join(__dirname, "..""test.html"),
  "utf-8"
);
 
sendGMail({
  to: "testuser@test.com",
  subject: "html 파일 read test 메일",
  html: htmlContent, // 읽어들인 파일 내용을 전
});
cs

html 파일 내용 발송 테스트 결과

7. Nodemailer 첨부파일 발송

첨부파일을 포함하여 발송하는 것 또한 그리 어려운 일은 아니다. 우선 프로젝트 최상위 경로에 테스트로 첨부하여 보낼 파일을 하나 만들었다. Typescript는 빌드하면 outDir로 설정한 dist폴더에 컴파일 결과물이 저장되고 Node로 그것을 실행하기 때문에 src 폴더 안에 넣어버리면 찾기가 귀찮아지니 최상위 폴더에 저장해둔 것이다.

테스트 첨부파일 생성

Nodemailer에서 파일 첨부 방법은 크게 6 가지가 있다.

  • 파일명을 지정하고 UTF-8 문자열을 컨텐츠로 넣는 방법
  • 파일명을 지정하고 바이너리 Buffer를 컨텐츠로 넣는 방법
  • 파일명을 지정하고 stream 할 파일의 경로를 지정하는 방법
  • 파일명 지정 없이 stream 할 파일의 경로만 지정하는 방법
  • 파일명을 지정하고 파일의 HTTP/HTTPS URL을 지정하는 방법
  • 파일명을 지정하고 인코딩한 문자열을 컨텐츠로 넣는 방법

이를 다음과 같이 구현하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sendGMail({
  to: "testuser@test.com",
  subject: "파일 첨부 test 메일",
  text'파일 첨부 test',
  attachments: [
    { // 파일명을 지정하고 UTF-8 문자열을 컨텐츠로 넣는 방법
      filename: "test1.txt",
      content: "hello world!",
    },
    { // 파일명을 지정하고 바이너리 Buffer를 컨텐츠로 넣는 방법
      filename: "test2.txt",
      content: Buffer.from("hello world!""utf-8"),
    },
    { // 파일명을 지정하고 stream 할 파일의 경로를 지정하는 방법
      filename: "test3.txt",
      path: path.join(__dirname, "..""test.txt"),
    },
    { // 파일명 지정 없이 stream할 파일의 경로만 지정하는 방법
      path: path.join(__dirname, ".."'test.txt')
    },
    { // 파일명을 지정하고 파일의 HTTP/HTTPS URL을 지정하는 방법
      filename: 'license.txt',
      path: 'https://raw.github.com/nodemailer/nodemailer/master/LICENSE'
    },
    { // 파일명을 지정하고 인코딩한 문자열을 컨텐츠로 넣는 방법
      filename: 'text4.txt',
      content: 'aGVsbG8gd29ybGQh',
      encoding: 'base64'
    },
  ],
});
cs

파일 첨부 테스트 결과

728x90
반응형