Skip to content

개발일지

CT-1326 edited this page Dec 19, 2021 · 3 revisions

프로젝트 개요

새로운 공모전 개최를 알기 위해 관련 사이트를 일일이 방문하는 게 너무 귀찮다.

또한, 매 시간 핫딜 같은 게시판 확인도 귀찮다.

그래서 이러한 류의 게시판에 새 게시물이 올라올 때마다 자동으로 알려주는 알림 서비스를 개발해보자

알림 플랫폼에는 많이들 쓰고 있는 디스코드 메신저를 이용해본다.


프로젝트 구조

  1. Heroku 앱 스케줄러를 통해 사용자가 지정한 시간에 알림 서비스 소스코드가 실행되게 트리거 역할 수행
  2. 소스코드가 작업을 시도하여 알림을 보내기 위한 조건처리에 충족 한다면 본인의 디스코드 서버에 연동된 Webhook 을 통해 관련 메시지를 작성 및 전송
  3. webhook이 연동된 디스코드 서버에 알림 메시지가 도착 및 조회

구현에는 NodeJS를 사용


개발 내용

디스코드 WebHook 생성 그리고 연동

서버 채널의 설정을 통해 webhook 을 생성

const hook = new Webhook("YOUR WEBHOOK URL"); // 디스코드 webhook 주소 기재
const { Webhook, MessageBuilder } = require('discord-webhook-node'); // discord-webhook-node 라이브러리 연동

const embed = new MessageBuilder()
.setTitle('새로운 공모전이 올라오다!') // 타이틀 작성
.setAuthor(title, 'https://www.campuspick.com/favicon.ico') // 타이틀 아이콘 첨부
.setURL('https://www.campuspick.com/contest?category=108') // 클릭되면 해당 주소로 연결
.setColor('#00b0f4') // 메시지 색상 지정
.setFooter('올라온 시간', 'https://www.campuspick.com/favicon.ico') // 메시지 footer 내용 작성
.setTimestamp(); // 현재 메시지가 작성된 시간 연동

hook.send(embed); // 메시지 전송

webhook 의 URL 을 복사하여 discord-webhook-node 라이브러리에 지원하는 다양한 메시지 중 원하는 규격의 메시지를 작성하고 전송

샘플로 작성한 메시지가 디스코드 채널에 전송됨을 확인

추가로 소스코드에 공개하기 어려운 URL, 키 값은 heroku의 환경변수를 이용해서도 해당 값을 은닉한채 불러올 수 있다.

스케줄러 구현

heroku 앱을 생성하고 해당 앱에 스케줄러 애드온을 패치

해당 스케줄러 설정을 통해 원하는 시간 그리고 실행을 원하는 소스코드가 첨부된 구조 주소를 적어주면

해당 시간에 해당 소스코드가 실행되게 끔 트리거를 역할을 부여할 수 있다.

해당 heroku 앱에 본 프로젝트의 저장소를 연동

이를 통해 소스코드 내용이 변경되면 즉각 heroku 앱에 반영이 가능하며

DB, 스케줄러 같은 부가 애드온 연동 작업이 가능해진다.

공모전 알림 스크립트

1. 공모전 리스트 데이터 확보

공모전 페이지 화면

페이지 소스 분석 결과 공모전 관련 게시물이 작성된 DOM은 따로 모듈화 처리를 하여 Crawling 으론 해당 내용을 불러올 수 없었다.

let config = {
    method: 'post',
    url: 'https://api.campuspick.com/find/activity/list?target=1&limit=10&offset=0&categ' +
            'oryId=108',
    headers: {}
};

axios(config)
    .then(function (response) {

따라서 Axios를 통해 해당 모듈의 주소를 따로 POST 메소드 처리하여 공모전 게시물 관련 데이터들을 획득

2. Webhook 전송을 위한 조건 처리

해당 사이트에서 개발 관련 특별히 제공하는 API가 없어서

새 게시물이 업로드 될 때 마다 이벤트 감지 내용을 대신할 만한 방법을 모색하던 중

    "activities": [
        {
            "id": 11312,
            "title": "[대전] 2021 커뮤니티 지원사업 청년모락",
            "endDate": "2021-02-17",
            "company": "(주)인터플레이, 청춘두두두",
            "image": "https://cf-cpi.campuspick.com/activity/1611893136663907_thumb.jpg",

게시판 모듈의 POST 메소드 결과 값 내용 중에 게시물 하나 당 id 값이 존재

ID 값을 이용해 heroku 서버에 저장되지 않은 ID 값일 경우 새 게시물로 판단하여 알림을 전송 하기로 하였다.

값 저장 및 쓰기에는 Redis 를 이용해 DB 처리한다.

    let getData = response.data.result.activities;
    let check = false;
    let arr = [];

POST 메소드 응답 값 즉 크롤링 결과 값을 저장하는 getData

새로운 게시물이 포함되었는지를 구분하는 Flag 값의 check

크롤링으로 추출된 게시물 id 값을 저장하는 배열 arr

const redis = require('redis');
const client = redis.createClient('YOUR redis URL');

redis 라이브러리 로드

    client.get('idKey', (err, params) => {
        // console.log(params);
        client.quit();
      }

미리 테스트 배열 데이터를 input한 redis DB의 key값을 호출

        for (let index = 0; index < getData.length; index++) {
            // console.log(getData[index].id);
            arr.push(getData[index].id);
            if (params.indexOf(getData[index].id) == -1) {
            
            } else {
                console.log('Not today!');
            }
        }

저장 된 크롤링 결과 값 길이 만큼 반복문을 돌려 arr 배열에 게시물 id 값을 저장

또한, redis DB에서 해당 게시물 id 값이 존재하는지 안하는지 if문 처리

                check = true;
                const embed = new MessageBuilder()
                    .setTitle('새로운 공모전이 올라오다!')
                    .setAuthor("알림봇", 'https://www.campuspick.com/favicon.ico')
                    .setURL('https://www.campuspick.com/contest/view?id=' + getData[index].id)
                    .setColor('#00b0f4')
                    .setFooter('올라온 시간', 'https://www.campuspick.com/favicon.ico')
                    .setTimestamp();

                hook.send(embed);

값이 존재하지 않을 경우 새 게시물로 판단하여 Flag 값을 변경하고 해당 게시물의 제목과 주소를 추출하여 알림 메시지 내용으로 작성 및 전송

        if (check) {
            const redisValue = JSON.stringify(arr);
            client.set('idKey', redisValue);
        }

Flag 값이 True 일때만 redis DB 상태를 게시물 id 값을 저장처리 하였던 배열 값으로 set 처리

3. 결과

heroku logs --tail --ps scheduler 결과 화면

스케줄러로 지정한 시간에 해당 작업이 수행됨을 확인할 수 있다.

또한, 조건 처리가 맞아 떨어지는 경우 알림이 자동으로 전송됨을 확인할 수 있다!

핫딜 알림 스크립트

해당 프로젝트의 추가 작업으로

이번엔 10분 단위로 작업을 하여 매 시간 올라오는 핫딜 정보를 알림 받아본다.

마찬가지로 해당 사이트 관련 API가 없기에 공모전 알림과 같은 방식의 대체 방안을 이용한다.

1. 핫딜 데이터 확보

핫딜 페이지 모습

axios
    .get('https://bbs.ruliweb.com/market/board/1020')
    .then(function (html) {
        // console.log(html.data);
        const $ = cheerio.load(html.data);

해당 핫딜 게시판 Crawling 준비

    client.get('lastID', (err, reply) => {
        // console.log(reply);
        client.quit();
      }

Crawling 작업 전 마찬가지로 샘플 데이터를 추가한 핫딜 관련 redis DB key 값을 호출

        const tableLength = $(
            '#board_list > div > div.board_main.theme_default > table > tbody > tr'
        ).length;
        for (let index = 1; index <= tableLength; index++) {

핫딜 페이지의 Table DOM 구간을 Crawling 및 해당 table 의 tr 갯수만큼 반복문 작성

2. 핫딜 게시물의 유형 판별

페이지 소스 분석 결과 상단의 진한 파란색 부분이 업체, 유저들이 뽑은 추천수가 높은 오늘의 핫딜 게시물이고

하단의 연한 파란색 부분이 업로드 순으로 배치된 게시물임을 알 수 있었다.

여기서 우리는 업로드 순으로 배치되는 유형의 핫딜 게시물 데이터가 필요하기에

            let tableName = $(
                '#board_list > div > div.board_main.theme_default > table > tbody > tr:nth-chil' +
                'd(' + index + ')'
            )
                .attr()
                .class;
            if (tableName == 'table_body') {

tr 하나당 Class 명을 읽어 해당 Class 명이 업로드 순으로 배치된 게시물의 Class 명칭인지 추천수 순으로 뽑힌 오늘의 핫딜 게시물의 Class 명칭인지를 판별

                let checkID = $(
                    '#board_list > div > div.board_main.theme_default > table > tbody > tr:nth-chil' +
                    'd(' + index + ') > td.id'
                )
                    .text()
                    .replace(/\s/g, '');
                // console.log('Hotdeal ID value:', checkID);
                arr.push(checkID);

업로드 순으로 배치된 게시물로 판별 된 게시물의 id 값을 배열에 저장

3. Webhook 전송

                if (reply.indexOf(checkID) == -1) {
                    check = true;
                }

redis DB에 저장된 id 값이 아닐 경우 새 게시물로 판별하여 Flag 값을 변경

                    const title = $(
                        '#board_list > div > div.board_main.theme_default > table > tbody > tr:nth-chil' +
                        'd(' + index + ') > td.subject > div > a.deco'
                    ).text();
                    // console.log(title);
                    const embed = new MessageBuilder()
                        .setTitle(title)
                        .setAuthor(
                            "알림봇",
                            'https://img.ruliweb.com/img/2016/icon/ruliweb_icon_144_144.png'
                        )
                        .setURL('https://bbs.ruliweb.com/market/board/1020/read/' + checkID)
                        .setColor('#181696')
                        .setFooter(
                            '올라온 시간',
                            'https://img.ruliweb.com/img/2016/icon/ruliweb_icon_144_144.png'
                        )
                        .setTimestamp();
                    hook.send(embed);

또한, 해당 게시물의 제목과 주소를 추출하여 알림 메시지 내용으로 작성 및 전송

        if (check) {
            const redisValue = JSON.stringify(arr);
            client.set('lastID', redisValue);
        }

Flag 값이 True 이면 기존 핫딜 게시물의 id 값을 저장하였던 배열을 json 문자열로 변환 및 redis DB에 set 처리

4. 결과

스케줄러에 지정한 시간인 10분 단위로 새로운 핫딜 게시물이 올라올 때 마다 자동으로 알림 메시지가 오고 있다!


TMI

사실 초기 구조는

공부겸 Github Action 을 이용하여 스케줄러를 구현할 계획 이였는데

이런 문제가 있댄다. 내 삽질 시간...

그러나 본 프로젝트가 처음에는 시간 단위로 자동화가 이루어지는 작업이 아니고 하루 단위로 할 계획이기에 해당 이슈를 크게 문제 삼을건 없어 보였었다.

또한, WorkFlow 에 수동으로 트리거 하는 내용을 추가하면 해당 이슈가 완화되는 결과도 있다는 것을 확인 하였기에

1시간 간격으로 테스트 해본 결과

시간 테이블은 조금씩 엉키긴 하지만 1시간씩 메시지가 전송됨을 확인하였기에 그대로 작업을 진행하였다.

1. 하루 간격으로 스케줄러 설정

on:
  workflow_dispatch:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * *'

cronTab 값을 이용해 set

workflow_dispatch 가 수동으로 트리거 해주는 역할

2. WorkFlow 설정

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [10.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run build --if-present
    - run: node index

기존 workflow 내용에서 node 버젼과 run 내용을 수정

이렇게 작업을 진행하려 했는데 Github Action 을 이용한 스케줄러에는 큰 결함이 있었다.

공개되면 곤란한 APIkey 값 같은 내용이 해당 Repo에 Commit 되어야만 원하는 결과를 얻을 수 있다는 것

여러 방법으로 해당 문제를 해결해보려 했지만 결국 실패하여 Heroku 로 스케줄러를 하게 되었던 것이다.

초기 방식의 저장 값 읽고 쓰기

처음에 구현 당시 어떠한 알림 서비스 이던간에 서버에 값을 저장하고 읽고 쓰기를 어떤 방식으로 할지 고민되었다.

DBStorage 를 쓰기엔 저장 사이즈가 그렇게 크지 않기에 너무 오버스펙이라는 생각에 최대한 배재하기로 하였기에

FS 라이브러리를 이용해 Json 파일 읽고 쓰기 방식으로 시도해보았다.

const fs = require('fs');

라이브러리 로드

const readNumber = JSON.parse(fs.readFileSync('./lastNumber.json'));

해당 json 파일 값 읽기

        const postData = JSON.stringify({"lastNumber": number});
        fs.writeFileSync('./lastNumber.json', postData);

json 형태로 해당 파일에 새로운 값 저장

로컬로 테스트 하였을 땐 읽고 쓰기가 잘 되어 이대로 Heroku 서버에 적용해보았는데

이런 문제가 발생한다.

값은 읽히지만 새로 쓴 값은 적용되질 않는 문제인데

서버리스라 그런지 이러한 이유로 인해 불가능한가보다...

이렇게 되면 새 핫딜 게시물의 ID 값을 저장하질 못해 다음 작업 자체가 불가능하기에

In-memory DB 에서 가장 유명한 Redis 가 속도나 사이즈가 본 프로젝트에 적합한 백엔드로 생각되어 이용하게 되었던 것이다.

(참고로 Dotenv 라이브러리를 이용한 환경변수 방식도 이용해보았는데 위와 같은 사유로 읽기는 되지만 쓰기는 Heroku 상에선 불가능하다.)

중요한건

어디까지나 새 게시물 확인을 대신한 방법이라 좀 더 확실하거나

해당 사이트에서 API를 제공한다면 해당 내용으로 변경은 불가피 하다.

그러나 해당 방식을 활용하여 추가적으로 본인이 원하는 사이트의 내용을 추출 및 자신이 이용하는 메신저 혹은 플랫폼에 적용해 자동화 작업을 수행하는 관련 툴이나 SW를 제작하는데 도움이 될 것으로 기대한다.