1. 스케줄러
매일 아침 같은 시간에 알람이 울리는 것처럼 서버에서도 정해진 날짜, 요일, 시간에 특정 기능을 해야 할 경우가 있습니다.
개발 서버를 예로 들면, 정기적인 백업, 주문 재고 알림, 가격 변동, 구독 계정 활성화/비활성화 등 특정 시간에 한 번만 실행이 되거나 주기적으로 실행이 될 필요가 있는 기능들이 있습니다.
이를 위해 필요한 것이 서버 스케줄러인데, 오늘은 제목에서 알 수 있듯이 JavaScript 파일을 Linux에 기본 내장된 기능인 cron 스케줄러에 등록해 보겠습니다.
2. cron
https://help.ubuntu.com/community/CronHowto
CronHowto - Community Help Wiki
Introduction Cron is a system daemon used to execute desired tasks (in the background) at designated times. A crontab file is a simple text file containing a list of commands meant to be run at specified times. It is edited using the crontab command. The c
help.ubuntu.com
AWS EC2 인스턴스를 Linux의 Ubuntu로 생성하면 기본적으로 cron이 설치되어있습니다.
만약 설치가 되어있는지 확인하고 싶다면 아래 명령어 중 하나로 확인 가능합니다.
2-1) 서비스 확인
systemctl status cron
정상적으로 설치되었고, 백그라운드에서 실행 중 일 때
● cron.service - Regular background program processing daemon
Loaded: loaded (/lib/systemd/system/cron.service; enabled; vendor preset: enabled)
Active: active (running) since ddd yyyy-mm-dd hh:mm:ss KST; mm months d days ago
Docs: man:cron(8)
Main PID: 443 (cron)
Tasks: 1
Memory: 39.5M
CGroup: /system.slice/cron.service
└─443 /usr/sbin/cron -f
2-2) 스케줄러 목록 확인
crontab -l
정상적으로 설치된 경우
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
.
.
.
2-3) cron 패키지 설치 확인
dpkg -l | grep cron
cron 패키지가 존재할 경우
ii cron 3.0pl1-136ubuntu1 amd64 process scheduling daemon
만약 위의 예시처럼 나오지 않고 'not found', 'command not found' 가 출력이 된다면 cron을 설치해야 합니다.
- Ubuntu 예시
// 1. 패키지 업데이트
sudo apt update
// 2. cron 설치
sudo apt install cron
// 3. cron 실행
sudo systemctl start cron
// 4. cron 자동 실행
sudo systemctl enable cron
3. JavaScript (Node.js)
HTML 안에서만 사용 가능한 JavaScript를 서버에서 사용하기 위해선 Node가 필수적입니다. 그러나 AWS EC2 Ubuntu 인스턴스 생성 시 기본적으로 Node가 설치되어 있지만, 버전이 상당히 낮습니다.
포스팅 날짜 기준으로 20 버전이 LTS인데 Ubuntu의 기본 Node는 12 또는 14 버전이 설치되어 있을 겁니다.
node -v 로 직접 확인해 보면 알 수 있습니다.
ubuntu@ip-000-00-00-000:~$ node -v
v20.13.1
(생략 가능)
Ubuntu 서버의 Node 버전 업데이트 방법은 검색하면 많이 나오니 쉽게 따라 할 수 있습니다.
개인적으로 여러 Node 버전을 쉽게 설치하고 필요할 때마다 버전을 변경할 수 있는 NVM (node version manager) 방식을 추천합니다.
https://github.com/nvm-sh/nvm
GitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions
Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions - nvm-sh/nvm
github.com
아래는 네이버 증권에서 KOSPI 지수를 스크래핑하는 간단한 코드 예시입니다.
// scrapingKOSPI.js
const axios = require('axios');
const cheerio = require('cheerio');
const getKOSPI = async () => {
const result = await axios.get('https://finance.naver.com/sise/sise_index.nhn?code=KOSPI');
const $ = cheerio.load(result.data);
const kospiValue = $('#now_value').text();
console.log(`KOSPI value: ${kospiValue}`);
};
getKOSPI();
만약 위 코드를 실행하려면 커멘드에 node scrapingKOSPI.js 를 입력해서 해당 함수를 호출할 텐데 해당 파일을 실행시키는 방법은 서버에서도 똑같고 스케줄러에 등록 후 실행되는 것 또한 node scrapingKOSPI.js가 됩니다.
> node scrapingKOSPI.js
> KOSPI value: 2,578.52
만약 10분마다 해당 함수가 실행되도록 스케줄러에 등록하면, 서버는 10분마다 같은 명령어를 내게 됩니다.
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
// 10분
node scrapingKOSPI.js
.
.
.
4. crontab
cron을 사용할 때 언제, 어떤 파일을 실행시킬지 스케줄러에 등록하는 것은 crontab에서 진행합니다.
이제 cron 스케줄러에 scrapingKOSPI.js 파일을 등록할 텐데 등록에 앞서 cron은 날짜, 요일, 날짜 기반으로 실행되기 때문에 작성법을 간단하게 알아야 합니다.
Linux Ubuntu의 경우 총 5자리를 입력하게 되는데, 앞에서부터 분-시-일-월-요일이 됩니다.
* * * * *
| | | | |
| | | | +---- 요일 (0 - 7) (0과 7은 일요일)
| | | +------ 월 (1 - 12)
| | +-------- 일 (1 - 31)
| +---------- 시 (0 - 23)
+------------ 분 (0 - 59)
# 매일 자정에 명령 실행
0 0 * * * /path/to/
# 매시간 15분마다 명령 실행
*/15 * * * * /path/to/
# 매주 월요일 오전 9시에 명령 실행
0 9 * * 1 /path/to/
# 매달 1일 오전 6시에 명령 실행
0 6 1 * * /path/to/
# 매일 오후 5시 30분에 명령 실행
30 17 * * * /path/to/
시간을 정했으면 어떤 실행 파일로 어떤 경로에 있는 파일을 실행시킬지 등록해 주면 됩니다.
서버 시간 기준으로 평일 08시부터 18시까지 1시간마다 node scrapingKOSPI.js 를 하도록 등록해 보겠습니다.
4-1) 스케줄러 목록 수정
crontab -e
0 : 매시 정각
8-18 : 09시-18시
* : 매일
* : 매달
1-5 : 월-금
# 은 주석입니다.
ubuntu@ip-000-00-00-000:~$ crontab -e
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
# KOSPI
0 8-18 * * 1-5 node /home/ubuntu/kospi/scrapingKOSPI.js
등록 완료 시 CTRL + X 로 저장 후 종료
4-2) 스케줄러 목록 확인
crontab -l
ubuntu@ip-000-00-00-000:~$ crontab -l
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
# KOSPI
0 8-18 * * 1-5 node /home/ubuntu/kospi/scrapingKOSPI.js
5. 로그 기록
현재 코드는 코스피 지수를 console.log로만 출력하기 때문에 직접 실행하지 않는 이상 확인이 불가능하고, 기록되지 않기 때문에 의미가 없습니다.
따라서 Node에 기본 내장된 모듈인 fs을 활용해 로그를 기록하도록 하겠습니다.
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
// 로그 기록 함수
// 성공 시 - logs/kospi.log 파일에 기록
// 실패 시 - logs/error.log 파일에 기록
const logger = (isError, log) => {
const logPath = isError ? `${dir}/error.log` : `${dir}/kospi.log`;
const currentTime = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
fs.appendFile(logPath, `${currentTime} - ${log}\n`, (error) => {
if (error) console.error(error);
});
}
const getKOSPI = async () => {
try {
const result = await axios.get('https://finance.naver.com/sise/sise_index.nhn?code=KOSPI');
const $ = cheerio.load(result.data);
const kospiValue = $('#now_value').text();
console.log(`KOSPI value: ${kospiValue}`);
logger(false, `KOSPI value: ${kospiValue}`);
} catch (error) {
console.error(error);
logger(true, `Failed : ${error}`);
}
};
getKOSPI();
로그 기록 경로를 'logs/error.log', 'logs/kospi.log'로 지정했기 때문에 실제 logs 폴더 경로와 log 파일 경로에 주의해야 합니다.
📦kospi
┣ 📂logs
┃ ┣ 📜error.log
┃ ┗ 📜kospi.log
┣ 📂node_modules
┣ 📜package-lock.json
┣ 📜package.json
┗ 📜scrapingKOSPI.js
fs.appendFile은 log 파일이 없다면 생성하지만, 경로 폴더가 존재하지 않는다면 에러가 발생합니다.
이를 방지하기 위해 폴더 확인 함수 existsSync와 폴더 생성 함수 mkdirSync 함수를 사용해도 좋습니다.
const logger = (isError, log) => {
const dir = 'logs';
// 폴더 존재 확인
if (!fs.existsSync(dir)) {
// 폴더 생성
fs.mkdirSync(dir);
}
const logPath = isError ? `${dir}/error.log` : `${dir}/kospi.log`;
const currentTime = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
fs.appendFile(logPath, `${currentTime} - ${log}\n`, (error) => {
if (error) console.error(error);
});
}
기록된 log 예시
// kospi.log
2024. 10. 1. 오후 3:00:00 - KOSPI value: 2,565.22
2024. 10. 1. 오후 4:00:00 - KOSPI value: 2,565.22
2024. 10. 1. 오후 5:00:00 - KOSPI value: 2,565.22
6. 스케줄러에 등록한 파일이 실행되지 않을 때
우선 직접 터미널에서 실행해 본다.
node scrapingKOSPI.js
6-1) 직접 터미널에서 실행 시 오류
6-1-1) 서버에 node 설치 확인
node -v
6-1-2) 올바른 경로에서 명령어 입력했는지 확인
ubuntu@ip-000-00-00-000:~/kospi node scrapingKOSPI.js
6-1-3) 실행하려는 파일의 라이브러리가 서버에 설치되었는지 확인
node_modules 폴더
6-1- 4) 실행하려는 파일의 라이브러리의 경로가 맞게 설정되었는지 확인
예) const axios = require('axios');
6-1- 5) 파일을 실행했을 때 함수를 호출하는지 확인
const getKOSPI = async () => {
(생략)
};
// 함수 호출
getKOSPI();
6-2) 직접 터미널에서 실행 시 성공
터미널에서 직접 실행 시 성공했으나 스케줄러에 등록했을 때 되지 않는다면 한 가지 확인해야 할 것이 있습니다.
로컬이나 서버 터미널에서 node scrapingKOSPI.js 명령어 실행 시 Node가 설치되어 있다면, PATH 변수가 자동으로 설정됩니다.
우리가 자주 사용하는 npm, npx, yarn, node, git 등 명령어 입력 시 가장 앞에 들어가는 파일명은 알아서 찾아주는 게 아닌 직접 등록해주어야 하거나 설치 시 등록된 것들입니다.
따라서 서버의 쉘 터미널과 독립적인 환경에서 cron 작업이 이루어지기 때문에 PATH 변수가 적용되지 않을 수 있습니다.
한마디로 cron은 node가 어디에 있는지 모르기 때문에 절대 경로를 직접 입력해주어야 합니다.
6-2-1) Node 절대 경로 찾기
which node
Unix 계열인 Linux 서버의 경우 which 명령어로 찾을 수 있습니다.
ubuntu@ip-000-00-00-000:~$ which node
// *사용자 마다 다를 수 있음*
/usr/local/bin/node
6-2-2) cronteb 스케줄러 수정
crontab -e
# KOSPI
0 8-18 * * 1-5 node /home/ubuntu/kospi/scrapingKOSPI.js
->
# KOSPI
0 8-18 * * 1-5 /usr/local/bin/node /home/ubuntu/kospi/scrapingKOSPI.js
7. 추가
이번 예시에선 서버에 간단하게 log만 남기도록 구현했지만 nodemailer로 이메일을 보낸다던가, DB에 값을 저장하거나, 문자 혹은 카카오톡 알림 등 여러 방면으로 로직을 추가할 수 있습니다.