본문 바로가기

DevOps/Git

<Github Actions> 깃헙 액션을 이용한 자동화 봇 만들기

1. 계획

 - 최근 깃헙 액션을 통한 웹훅 셋팅을 하는 작업을 했었다. 깃헙액션을 써보긴 했지만 CI용으로만 써왔어서 다양하게 활용해본 것은 이번이 처음이었다. 실제로 트리거를 걸어서 이슈가 생기면 디스코드로 알린다던가 하는 등의 작업은 흔히 하는 것 같았다. 작업을 마치고 깃헙 액션으로 뭔가 더 할 수 없을까 생각했고, 크론잡을 돌릴 수 있다는 것을 알게 되었다. 

 

 - 크론잡은 특정 기간마다 작업을 수행되게 하거나 특정 시간마다 작업을 반복 시키는 등의 스케줄링 작업을 의미한다. 

 

 - 크론잡을 돌릴 수 있다는 것을 알게 되어 재밌는 프로젝트가 떠올랐다. 필자는 매일 여러개의 기술블로그를 둘러보는데 이 포스팅들이 한 곳에 모아져있다면 좋겠다는 생각을 종종 했다. 그래서 한 공간에 이 글들을 크롤링해서 매일 업데이트 하도록 해보자는 계획을 세웠다.

 

 - 계획은 이렇다. 크롤링 코드를 짠다. => 매일 아침마다 깃헙 액션이 동작한다. => 크롤링 코드가 동작한다. => 리드미를 최신화한다. => 레포지토리에 푸시한다.

 

2. 코드

 - 필자는 타입스크립트로 크롤링 코드를 작성하고 노드환경에서 돌렸다.(자바스크립트로 작성해도 무관) 해당 코드에 크게 관심이 없고 액션을 짜는 부분만 보고싶다면 아래의 "* 액션 코드" 부터 봐도 좋다. 모든 코드는 필자의 깃허브 레포지토리(fe-news-bot)에 공개되어 있다.

 

* 크롤링 코드

// package.json
  "scripts": {
    "build": "tsc && tsc-alias",
    "start": "npm run build && node dist/app"
  },
  "dependencies": {
    "axios": "^0.26.1",
    "cheerio": "^1.0.0-rc.10"
  },

 

 - 타입스크립트 파일이므로 변환 후 컴파일된 파일을 실행하도록 start명령어를 구성하였다. (build 부분은 아래 3.문제 에서 따로 설명하겠다.)

 

 - html요청을 편하게 하기위해 axios를 설치하였고, 크롤링을 편하게 하기위해 cheerio를 설치하였다. 각 패키지의 자세한 사용법은 여기서 설명하지 않는다.

 

// app.ts
import { ObjType } from '@/type';
import { makeMarkDown } from '@/utils/lib';
import { getYozmList } from '@/yozm';
import { getKFAList } from '@/korean-fe-article';

const main = async () => {
  const result: ObjType[] = [];
  result.push(...(await (await getKFAList()).slice(0, 3)));
  result.push(...(await (await getYozmList()).slice(0, 3)));
  makeMarkDown(result);
};

main();

 

 - app.ts는 위와 같다. 각 블로그의 포스팅 리스트를 가져와서 일정한 형식(Objtype)에 맞추도록 하였다. 이 형식은 제목, 설명, 링크를 프로퍼티로 갖는 객체이다.

 

 - 가져온 리스트(result)로 마크다운 문법에 따라 리드미를 작성하고 저장하도록 하였다. 각 함수를 살펴보자.

 

// yozm
import { ObjType } from '@/type';
import { getHtml } from '@/utils/lib';
import { MAX_DESC_LENGTH } from '@/utils/static';
import * as cheerio from 'cheerio';

export const getYozmList = async () => {
  const yozm = 'https://yozm.wishket.com';
  const result: ObjType[] = [];
  const html = await getHtml(yozm + '/magazine/list/develop');
  const $ = cheerio.load(html?.data);
  const $bodyList = $('div.list-cover ').children('div.list-item-link');

  $bodyList.each((i, elem) => {
    result.push({
      title: $(elem).find('.list-item .item-main a.item-title').text(),
      desc: $(elem).find('.list-item .item-description').text().slice(0, MAX_DESC_LENGTH) + '...',
      url: yozm + $(elem).find('.list-item .item-main a.item-title').attr('href'),
    });
  });
  return result;
};

 

 - 위는 요즘 IT 라는 플랫폼에서 크롤링을 진행한 코드이다. getHtml은 axios를 이용하여 만든 함수로써 return await axios.get(url) 의 결과를 반환하는 것이 전부이다.

 

 - cheerio를 사용하는 방법은 이 포스팅에서는 다루지 않겠다. 위에서 말했던대로 미리 정의해둔 ObjType에 맞게 title, desc, url을 파싱해서 담도록 하였다. 이 때 크롤링하는 곳에 따라 url을 path를 넣어 a태그를 구성하기도 하므로 origin을 빼먹지 않도록 주의하자.

 

// utils/lib.ts
import fs from 'fs';
// ...
export const makeMarkDown = (lst: ObjType[]) => {
  let result = '# 오늘의 포스팅 \n';
  result += getKoreaTime() + '기준 \n\n';
  for (const obj of lst) {
    result += `### ${obj.title} \n\n ${obj.desc} \n\n [바로가기](${obj.url}) \n\n`;
  }
  result += getReferenceText(DEV_BLOG_LIST);
  fs.writeFileSync('README.md', result, 'utf-8');
};
// ...

 

 - 다음은 마크다운을 작성하는 makeMarkDown 함수이다. 인자로 정해진 형식의 리스트를 받고 result에 작성할 텍스트들을 추가하며 진행된다. 제목과 작성시간을 입력하고 포스팅들을 작성하며, 마지막에는 참조했던 플랫폼들을 레퍼런스로 작성한다.

 

 - fs모듈을 사용하여 결과 텍스트를 README.md에 입력하여 저장한다.

 

* 액션 코드

 - 액션 코드를 작성하는 방법은 크게 두 가지이다. 깃허브 레포지토리에서 Actions탭을 클릭하여 new workflow를 클릭해 생성하거나 직접 .github/workflows 경로에 yaml 파일을 작성할 수 있다.

 

name: cron

on:
  schedule:
    - cron: '0 9 * * *'

jobs:
  cron:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]
    steps:
      - uses: actions/checkout@v1
      
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Run Cron
        run: |
          npm install
          npm start
      - name: Push Github
        run: |
          git config --global user.name "haesoo-y"
          git config --global user.email "haesoo9410@gmail.com"
          git pull origin main
          git add .
          git commit -m ":memo: 리드미 업데이트" || exit 0
          git push

 

 - 필자가 처음 작성한 main.yaml파일이다. (최종 코드는 포스팅 가장 하단에 있음)

 

 - 위에서 부터 차근차근 설명해 보자면 on에는 이벤트를 트리거링할 수 있는 조건을 정의한다. 이 때 schedule을 통해 크론잡을 수행할 수 있다. 크론은 리눅스에서 작성하는 형식과 동일하게 작성한다. 분, 시, 일, 월, 요일 순이며 위 코드는 9시 0분마다 수행하라는 의미이다.

 

 - on을 작성한 후에는 실제 jobs를 정의한다. job명칭을 임의로 필자는 cron이라고 명했다. runs-on은 어디에서 돌릴 지를 명시할 수 있다. 위 코드는 우분투에서 돌린다는 의미이다. 관련해서 자세한 정보는 github actions 공식 문서를 참고해도 좋다.

 

 - strategy의 matrix를 통해 복수의 선언을 대비할 수 있다. 현재 코드의 경우 노드 14버전에서만 돌릴 것이므로 해당 버전만 변수로 명시하였다. 배열내에 여러개를 쓴다면 해당 갯수만큼 아래의 step들이 동작할 것이다.

 

 - steps에는 실제 동작들을 순서대로 적는다. 하나의 동작당 name을 하나 명시할 수 있다. 또 하나의 동작당 run과 uses를 하나 작성한다. uses는 누군가 미리 만들어둔 작업을 수행한다. actions/checkout의 경우 깃헙 공식 액션으로써 현재 레포지토리에 체크아웃하여 깃헙 관련 수행들을 할 수 있도록 돕는다. 이 외에도 다른사람들이 만들어둔 작업을 쓰고 싶다면 Github Marketplace에서 찾아보길 바란다. 매우 좋은 액션들이 준비되어있다.

 

 - 다음 step에서는 노드 관련 액션을 추가하였다. 그 다음 step에서는 npm install로 필요한 모듈을 설치하고 미리 만들어둔 명령어인 npm start를 입력하여 위에서 미리 만들어둔 크롤링 코드를 실행시켰다.

 

 - 다음 step에서는 git config로 실제 내가 git push를 하는 것 처럼 동작하도록 등록된 name과 email을 설정시켰고, push하여 레포지토리에 리드미가 업데이트 되도록 구성하였다.

 

3. 문제

* fs 모듈 

 - 첫번째로 마주한 문제는 타입스크립트 관련 문제였다. fs모듈은 node에서 제공하는 모듈임에도 Import시 에러가 떴었다.

 

 - 찾아보니 기본 제공하는 모듈을 타입스크립트에서 사용하려면 별도로 타입을 받아야한다고 한다. 따라서 아래의 타입을 설치하여 해당 이슈를 해결하였다.

 

npm i -D @types/node

* alias

 - 위의 타입스크립트 코드를 보면 Import시 alias로 @를 사용하고 있다. 이는 src 경로를 나타낸 것이다.

 

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "downlevelIteration": true,
    "isolatedModules": true,
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "baseUrl": "./",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src", "app.ts"]
}

 

 - tsconfig에 paths를 작성하였기 때문에 코드 작성시에는 올바르게 인식되고 있었다. 하지만 컴파일을 하면 @문자가 그대로 남아있어 컴파일된 js코드가 동작하지 않는 문제가 발생하였다.

 

 - 컴파일 후 @도 함께 변환 되려면 추가적인 모듈이 필요하였다.

 

npm i -D tsc-alias

 

 - 위 모듈을 설치한 후 build 명령어를 tsc 에서 tsc && tsc-alias로 변경하였고, 정상적으로 컴파일되는 것을 확인하였다.

 

 

* 테스트

 - 깃헙액션을 셋팅하고 나서 마주한 문제점은 어떻게 테스트할 것이냐 였다. 크론에 정해둔 시간까지 기다리는 것은 무리라고 판단했다. 찾아보니 외부 툴을 사용하여 테스트가 가능하다고는 하나 굳이 그렇게까지 해야하나 싶었다.

 

 - 그래서 Yaml코드에 아래와 같은 트리거를 추가하였다.

 

on:
  issues:
    types: [opened, edited]
  schedule:
    - cron: '0 0 * * *'

 

 - 위는 이슈가 열리거나 수정될 때에도 깃헙 액션이 동작하도록 하는 코드이다. 이렇게 작성한 후 이슈를 하나열고 수정해가면서 테스트를 진행했다.

 

 - 더 좋은 방법은 workflow_dispatch를 추가하는 것이다.

 

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *"

 

 

 - workflow_dispatch가 있으면 깃허브의 액션 탭에서 위처럼 수동으로 워크플로우를 실행시킬 수 있다.

 

* 토큰 관련

 - 처음 액션을 돌렸을 때 Push가 제대로 되지 않았다. git config에서도 에러가 나고 있었다. 이 당시에 checkout v1을 쓰고 있었는데, 이 경우 전역으로 깃헙 토큰을 설정해줘야 했다. push 시에도 깃헙 토큰을 명시해야 권한 이슈가 생기지 않는 것이었다.

 

 - 다행히 v2부터는 자동으로 토큰이 설정된다고 하여 v2 로 버전을 변경해서 해결하였다.

 

uses: actions/checkout@v2

 

* 시간 관련

 - push가 정상적으로 된 후 리드미를 확인해봤더니 시간이 현재 시간과 다르게 표기되고 있었다. github action이 도는 서버는 한국이 아니기 때문에 UTC기준으로 뜨는 것이었다. 게다가 오전 9시마다 업데이트를 하려면 0 9 * * * 로 작성하면 안되는 것도 함께 깨달았다.

 

 - 먼저 오전 9시로 맞추기 위해 크론을 0 0 * * * 로 변경하였다. UTC와 한국 시간은 9시간 차이가 나기 때문이다. 그리고 시간 텍스트를 반환하는 getKoreanTime 함수를 아래와 같이 수정하였다.

 

const getKoreaTime = () => {
  const now = new Date();
  const diffConfig = now.getTimezoneOffset() * 60 * 1000;
  const diffKorea = 9 * 60 * 60 * 1000;
  const koreaTime = new Date(now.getTime() + diffConfig + diffKorea);
  return (
    koreaTime.getFullYear() +
    '년 ' +
    (koreaTime.getMonth() + 1) +
    '월 ' +
    koreaTime.getDate() +
    '일 ' +
    koreaTime.getHours() +
    '시 '
  );
};

 

 - GetTimezoneOffset 함수를 사용하면 UTC와 현재 코드를 돌리는 곳의 시차를 분 단위로 구할 수 있다. 이를 ms단위로 변환하였다. 이는 한국에서 실행시 -9h, 깃헙 액션이면 0h 일 것이다.

 

 - 한국과 UTC의 시차는 9시간이므로 diffKorean라는 변수에 이를 담았다. 그리고 서버 위치에 상관없이 동적으로 한국 시간을 구할 수 있도록 new Date의 인자로 위에서 만든 timestamp들을 계산하여 넣어주었다.

 

 - 위를 통해 리드미에 한국 시간 기준으로 작성할 수 있게 되었다. 최종 yaml 코드는 다음과 같다.

 

name: cron

on:
  issues:
    types: [opened, edited]
  schedule:
    - cron: '0 0 * * *'

jobs:
  cron:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Run Cron
        run: |
          npm install
          npm start
      - name: Push Github
        run: |
          git config --global user.name "haesoo-y"
          git config --global user.email "haesoo9410@gmail.com"
          git pull origin main
          git add .
          git commit -m ":memo: 리드미 업데이트" || exit 0
          git push

 

 


참고

 

 

Features • GitHub Actions

Easily build, package, release, update, and deploy your project in any language—on GitHub or any external system—without having to run code yourself.

github.com

 

GitHub Marketplace: actions to improve your workflow

Find the actions that help your team build better, together.

github.com

 

GitHub - haesoo-y/fe-news-bot: 프론트엔드 최신 포스트를 가져오는 봇 🤖

프론트엔드 최신 포스트를 가져오는 봇 🤖. Contribute to haesoo-y/fe-news-bot development by creating an account on GitHub.

github.com