node-cron 스케쥴러 사용기

DaeChan Jo·2023년 10월 13일
3

node.js

목록 보기
2/2
post-thumbnail

현재 프로젝트를 진행하면서 단순하고 반복되는 CRUD보단, 좀 더 생소하고 재밌는 기능들이 없을까 하다 생각해낸 몇가지 중 하나가 특정 시간에 데이터베이스를 조회하고, 조건에 맞는 유저에게 메일을 전송해보면 어떨가 생각했다.

하지만 내가 알고있는 서버는, 클라이언트의 요청이 없으면 움직이지 않는 아주 수동적인 자세를 취하는데 어떻게 하면 클라이언트의 간섭 없이 서버 내에서 이를 처리할 수 있을까 찾아보다 node-cron이라는 아주 간단하고 재밌는 패키지를 찾았다

그래서 그게 뭔데?

node-cron은 이름에서 알 수 있듯이 Node.js환경에서 cron(유닉스 계열 컴퓨터 운영 체제의 시간 기반 잡 스케줄러) 작업을 구현하기 위한 패키지이다.특정 시간에 주기적으로 실행되어야 하는 작업들을 관리한다. 즉 Node.js 애플리케이션 내에서 타이머 또는 주기적인 작업을 스케줄링하는 기능을 제공한다.

주요기능은 다음과 같다.

  • 시간 기반 스케줄링
    표준 cron 구문을 사용하여 분, 시, 일, 월, 요일 등에 따라 주기적인 작업 실행 시간을 정의할 수 있다.
  • 시작 및 중지
    생성된 각 cron 작업은 start와 stop 메서드를 이용해서 언제든 시작하거나 중지할 수 있다
  • 시간대 지원
    v1.7.0 부터는 특정 시간대를 지정하여 작업이 실행될 시각을 제어할 수 있다
  • Promise와 async/await 지원
    비동기 함수를 스케줄링하는 것도 가능하다

사용법

사용법이 따로 있다고 말하기 민망할정도로 엄청 간단하다.
일단 npm을(또는 yarn) 사용해 node-cron을 설치해준다.
npm istall --save node-cron

그리고 작성해준다.

import cron from "node-cron";

cron.schedule('* * * * *', () => {
  console.log('running a task every minute');
}, {
    scheduled: false,
    timezone: "Asia/Seoul"
});

놀랍게도 끝이다.

schedule 함수의 첫 번째 인자로 크론 식(crontab syntax)을, 두 번째 인자로 크론작업이 시잘될 때 호출될 콜백 함수를 넘어주면 된다. 세 번째 인자로는 옵션 객체를 넣어줄 수 있는데 scheduled로 작업을 즉시 시작할지, 아니면 호출될 때. 시작되도록 할지 설정할 수 있고 timezone 옵션으로 작업이 실행될 시간대(도시)를 지정할 수 있다.

첫 번째 인자로 받는 크론식은 다음을 참고하면 된다.

 # ┌────────────── second (optional)
 # │ ┌──────────── minute
 # │ │ ┌────────── hour
 # │ │ │ ┌──────── day of month
 # │ │ │ │ ┌────── month
 # │ │ │ │ │ ┌──── day of week
 # │ │ │ │ │ │
 # │ │ │ │ │ │
 # * * * * * *

각각의 값에는 콤마 등 여러방법을 지원하기 때문에 공식문서를 참조하면 좋다
node-cron README

cron.schedule('1,2,4,5 * * * *', () => { ...
cron.schedule('1-5 * * * *', () => { ...
cron.schedule('*/2 * * * *', () => { ...
cron.schedule('* * * January,September Sunday', () => { ...

적용하기

진행중인 프로젝트는 영단어 학습과 관련된 웹 서비스 프로젝트였고, 사용자가 특정 시간동안 학습을 하지 않으면 리마인드할 수 있게 메일링하는 기능을 만들고자 했다.

사용 방법 자체는 간단하지만 물론 잘못된 정보를 보고 약간의 삽질을 하긴 했지만 두 번째 인자로 넣어줄 콜백함수는 상당히 지저분하기 때문에 따로 모듈화를 해주었다. 어디에 작성하는게 좋을까 고민하다가 (nest.js 공부 해야돼..?) 데이터베이스를 조작하기에 services 경로에 작성했다.
참고로 아주많이 참고한 모 앱의 기능중 하나

import cron from "node-cron";
import nodemailer from "nodemailer";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const logo: string | undefined = process.env.LOGO;

// 노드메일러 설정
let transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: process.env.NODE_MAILER_USER,
    pass: process.env.NODE_MAILER_PASS,
  },
});

// cron 정의
export const startScheduler = () =>
// 테스트를 위해 매 초마다 스케줄링이 실행되도록 설정
  cron.schedule("* * * * * *", async (): Promise<void> => {
    console.log("⏰ :: 스케줄링 작업 실행...");

    const today: Date = new Date();

    const daysInKorean: string[] = ["일", "월", "화", "수", "목", "금", "토"];

    let studyDays: any[] = [];

// 과거 일주일 동안 각 날짜가 미학습 상태인지 정장할 배열을 초기화
    for (let i = 6; i >= 0; i--) {
      let d: Date = new Date();
      d.setDate(today.getDate() - i);
      studyDays.push({
        day: daysInKorean[d.getDay()],
        studied: false,
      });
    }

// 당일 학습 여부를 확인할 변수 초기화
    let hasStudiedToday: boolean = false;

// 데이터베이스에서 유저들의 학습 진행 상황 조회
    const users = await prisma.user.findMany({
      select: {
        name: true,
        email: true,
        id: true,
        wordProgress: {
          select: {
            studiedAt: true,
          },
          orderBy: {
            studiedAt: "desc",
          },
        },
      },
    });

// 각 유저에 대해 학습 진행 상황을 확인하고, 그에 따라 제목과 본문내용을 선택하여 이메일 전송 
//(여기서부턴 지극히 개인적인 복잡한 코드라 안보셔도 됩니다.)
    for (let user of users) {
      if (user.email && user.wordProgress.length > 0) {
        const lastStudiedAt: Date = new Date(user.wordProgress[0].studiedAt);
        const daysSinceLastStudy: number = Math.ceil(
          (today.getTime() - lastStudiedAt.getTime()) / (1000 * 60 * 60 * 24),
        );

        for (let progress of user.wordProgress) {
          let progressDayIndex: number =
            (today.getDate() - new Date(progress.studiedAt).getDate() + 7) % 7;
          if (progressDayIndex >= 0 && progressDayIndex < 7) {
            studyDays[progressDayIndex].studied = true;
            if (new Date(progress.studiedAt).getDate() == today.getDate()) {
              hasStudiedToday = true;
            }
          }
        }
        let subject;
        if (daysSinceLastStudy === 1) {
          subject = "[Wordy] 오늘이 끝나기 전에 보러 와주실거죠..?🥺";
        } else {
          subject = `[Wordy] ${daysSinceLastStudy}일 동안 못봤네요🥺`;
        }
        if (!hasStudiedToday) {
          let mailOptions = {
            from: process.env.EMAIL_USERNAME,
            to: user.email,
            subject: subject,
            
            // 본문은 HTML 형식의 문자열로 작성하며, 유저의 이름과 과거 일주일 동안의 학습 진행 상황, 오늘 학습한 여부 등을 포함
            html: `<div style="text-align:center;">
<img src=${logo} alt="Wordy Logo" />
            <h1>안녕하세요, ${user.name}님!</h1>
            <hr />
            <h3>학습 진행 상황을 알려드립니다</h3><br />
            <p>${studyDays.map((day) => `${day.day}: ${day.studied ? "😎" : "🫥"}`).join(" | ")}</p>
            ${
              hasStudiedToday
                ? "<p>오늘도 이미 학습을 완료하셨군요! 멋져요 👍</p><br />"
                : "<p>아직 오늘의 학습을 하지 않으셨다면, 지금 바로 시작해보세요!</p><br />"
            }
            <p>🙌🏻노력은 배신하지 않습니다🙌🏻</p>
            <br />
             <a href="${process.env.SERVER_URL}" style="
                display: inline-block;
                margin-top: 20px;
                padding: 10px 20px;
                background-color: #007BFF;
                color: white;
                text-decoration: none;
                border-radius: 5px;">학습하러 가기</a>
        </div>`,
          };

          transporter.sendMail(mailOptions, function (error: Error | null): void {
            if (error) {
              console.log(error);
            } else {
              console.log(`메일 전송 : ${user.email}`);
            }
          });
        }
      }
    }
  });

이렇게 작성하고 앞서 말한 사소한 삽질이 있었는데, 이 상태로 서버를 킨다고 해당 스케쥴러가 자동으로 작동하진 않는다.누가 된다 했다고 생각해보면 아주 당연하고 간단한건데 app.ts에서 모듈화시킨 cron함수를 호출해줘야 한다. 위 코드를 예로 들자면 코드상 서버 포트가 열리고 listen 하기 전 startScheduler() 를 작성해주면 된다.

그런 다음 서버를 실행하면 스케쥴러가 작동하는 로그를 확인할 수 있다.

메일을 확인해보면 다음과 같이 귀염뽀짝 정상적으로 스케쥴러가 작동한걸 확인할 수 있다


클라이언트 요청 없이 서버사이드 내에서 무언가를 처리하니깐 엄청 재밌었다.
이 기능을 이용해서 서버사이드로 클라이언트의 간섭 없이 무언가를 주기적으로 확인하거나 다른 재밌는 기능을 만들 수 있을것 같다.

profile
BackEnd Developer

0개의 댓글