엘리스 AI트랙 4기/프로젝트

[PWA] Push Notification 알림 자동 발송하기 (feat. node-schedule)

남쪽마을밤송이 2022. 7. 18. 15:14

 [ 01 개념 알아보기 ] 

 (1) cron이란? 

  • 유닉스 계열 컴퓨터 운영체제(Unix, Linux 등)의 잡 스케줄러이다. 잡 스케줄러란 어떤 작업을 특정 시간에 실행시키기 위한 데몬이다.
  • 일정한 시간 간격으로 수행되어야 할 작업이나 관리자가 그 시간에 작업을 할 수 없는 상황일 때에도 서버는 항상 돌아가고 있다는 점을 이용하는 아주 유용한 방법이다.
  • cron은 셸 명령어들이 주어진 일정에 주기적으로 실행하도록 규정해놓은 crontab (cron table) 파일에 의해 구동된다.
    • 셸 명령어들로 필요한 작업을 프로그래밍한 파일이 shell script(.sh)이다.
  • 윈도우에서는 작업 스케줄러(Task Scheduler)와 배치(.bat) 파일로 대체할 수 있다.

 (2) node-schedule를 선택한 배경 

  • 우리는 Ubuntu 서버를 사용하여 배포할 예정이었기 때문에 처음에는 crontab을 사용하려고 했으나, 찾아보니 이러한 스케줄링 기능을 운영체제 상관없이 + 코드로 제어하며 사용할 수 있도록 도와주는 패키지들이 많이 있었다.

  • 비교해보니 크게 DB를 사용하는 것, DB를 사용하지 않는 것으로 나뉘고 DB 종류도 redis와 MongoDB로 나뉘었다.
  • 그런데 redis는 스케줄러 작업의 DB로 사용하기에 단점이 있다고 했고(설정 필요, 데이터 무결성 보장 문제 등) 그래서 MongoDB를 이용하는 agenda와 같은 패키지가 등장한 것 같았다.
  • 하지만 우리 프로젝트에서는 메인 DB로 MySQL을 사용하고 있었고, 따라서 굳이 MongoDB를 추가해야 하는 agenda도 탈락, 남은 것 중에 가장 cron job을 구체적으로 프로그래밍 할 수 있다는 node-schedule을 선택했다.

참고 : https://blog.logrocket.com/comparing-best-node-js-schedulers/

 

 [ 02 node-schedule ] 

 (1) 패키지 설치 

  • 사용하기 위해서는 먼저 패키지 설치부터 진행한다. (공식문서)
npm i node-schedule

 (2) job 추가 

  • 나는 앞서 back end 서버와 push 서버를 분리했기 때문에, node-schedule은 자동으로 푸시 알림이 가도록 필요한 push 서버에서 작업했다.
  • 시간에 따라 영양제 일정 푸시 알림이 가도록 하는 api는 이미 작성한 상태이므로, 이걸 30분마다 자동으로 실행하도록 하는 job을 추가한다.
  • 먼저 api 요청을 보낼 함수를 정의해줬다.
// 배포하면 주소 변경해야 함
const serverUrl = "http://localhost:5004/";

const push = async (time: Date) => {
  try {
    await axios
      .get(serverUrl + "subscribe/push-supplements", {
        params: { time: time },
      })
      .then((res) => {
        const pushResultObj = {
          status: res.status,
          statusText: res.statusText,
          data: res.data,
        };
      });
  } catch (error: any) {
    console.log(error)
};
  • 그리고 이 함수를 실행하는 job을 설정하는 부분이다.
    • 직접 crontab 룰을 사용해서 추가도 가능하지만 RecurrenceRule( ) 메소드를 사용해야 timezone 설정이 가능했다.
    • 스케줄러가 오전 6시부터 밤 12시까지를 보여주기 때문에 그 시간 내에 30분 간격으로 동작하도록 설정해줬다.
    • 현재 시간을 인자로 보내줘야 해서 new Date()를 사용했다. DB 쿼리가 정상적으로 조회되기 위해서는 시간이 00초로 끝나야 하지만 작동 시간에 딜레이가 생길까봐 걱정했는데 문제없이 00초로 요청이 갔다.
const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = new schedule.Range(0, 6); // 매일
rule.hour = [0, new schedule.Range(6, 23)]; // 오전 6시부터 자정까지
rule.minute = [0, 30]; // 정각과 30분에
rule.tz = "Asia/Seoul"; // 한국 시간 기준

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const job = schedule.scheduleJob(rule, () => {
  const current = new Date();
  push(current);
});
  • 전체 코드는 다음과 같다.
더보기

 utils / webPush.ts 

  • 로그를 추가하는 부분은 다음 포스팅에서 설명하겠다.
import axios from "axios";
import webpush from "web-push";
import schedule from "node-schedule";
import moment from "moment";
import "moment-timezone";
import { logger } from "../utils/winston";
import { emailUtil } from "../utils/emailUtil";

moment.tz.setDefault("Asia/Seoul"); // 로그 시간대 한국 기준으로 변경

// VAPID keys should only be generated only once.
const vapidKeys = {
  publicKey: <any>process.env.PUBLIC_KEY,
  privateKey: <any>process.env.PRIVATE_KEY,
};

export default (): void => {
  webpush.setVapidDetails(
    "mailto:s0n9h2@gmail.com",
    vapidKeys.publicKey,
    vapidKeys.privateKey
  );
};

// 배포하면 주소 변경해야 함
// const serverUrl = "http://localhost:" + process.env.PORT + "/";
const serverUrl = "http://localhost:5004/";

const push = async (time: Date) => {
  try {
    await axios
      .get(serverUrl + "subscribe/push-supplements", {
        params: { time: time },
      })
      .then((res) => {
        const pushResultObj = {
          status: res.status,
          statusText: res.statusText,
          data: res.data,
        };

        // 푸시 알림 발송 성공하면 push.log에 기록
        if (res.status == 200) {
          logger.info(`${time}`, pushResultObj);
          console.log(`${time}에 설정된 푸시 알림을 전송했습니다.`);
        }
      });
  } catch (error: any) {
    const pushErrorObj = {
      syscall: error.syscall,
      code: error.code,
      errno: error.errno,
    };

    // 푸시 알림 발송 실패하면 error.log에 기록
    logger.error(`${time}`, pushErrorObj);
    console.log(`${time}에 설정된 푸시 알림 전송에 실패했습니다.`);

    const data = {
      developers: ["s0n9h2@gmail.com", "tbr06057@naver.com"],
      time: time,
      errorContent: pushErrorObj,
    };

    // 담당 개발자들에게 이메일 전송
    emailUtil.pushErrorEmail(data);
  }
};

const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = new schedule.Range(0, 6); // 매일
rule.hour = [0, new schedule.Range(6, 23)]; // 오전 6시부터 자정까지
rule.minute = [0, 30]; // 정각과 30분에
rule.tz = "Asia/Seoul"; // 한국 시간 기준

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const job = schedule.scheduleJob(rule, () => {
  const current = new Date();
  push(current);
});

 

 [ 03 동작하는 모습 ] 

  • 오전 11시 영양제 일정 추가

  • 11시가 되면 푸시 알림이 전송되는 모습