백엔드: 정적 사이트 업데이트에 Gatsby.js 및 Node.js 사용

게시 됨: 2022-03-11

이 기사 시리즈에서는 정적 콘텐츠 웹 사이트 프로토타입을 개발할 것입니다. 최신 릴리스를 추적하기 위해 인기 있는 GitHub 리포지토리에 대해 매일 업데이트되는 간단한 정적 HTML 페이지를 생성합니다. 정적 웹 페이지 생성 프레임워크에는 이를 달성하기 위한 훌륭한 기능이 있습니다. 우리는 가장 인기 있는 것 중 하나인 Gatsby.js를 사용할 것입니다.

개츠비에는 백엔드(서버리스), 헤드리스 CMS 플랫폼, 개츠비 소스 플러그인 없이 프론트엔드에 대한 데이터를 수집하는 방법이 많다. 그러나 GitHub 리포지토리 및 최신 릴리스에 대한 기본 정보를 저장하기 위해 백엔드를 구현합니다. 따라서 우리는 백엔드와 프론트 엔드 모두를 완전히 제어할 수 있습니다.

또한 응용 프로그램의 매일 업데이트를 트리거하는 도구 집합을 다룰 것입니다. 수동으로 또는 특정 이벤트가 발생할 때마다 트리거할 수도 있습니다.

프론트엔드 애플리케이션은 Netlify에서 실행되고 백엔드 애플리케이션은 무료 플랜을 사용하여 Heroku에서 작동합니다. 주기적으로 잠자기 상태가 됩니다. "누군가 앱에 액세스하면 dyno 관리자가 자동으로 웹 dyno를 깨워 웹 프로세스 유형을 실행합니다." 따라서 AWS Lambda 및 AWS CloudWatch를 통해 깨울 수 있습니다. 이 글을 쓰는 시점에서 이것은 프로토타입을 연중무휴로 온라인으로 유지하는 가장 비용 효율적인 방법입니다.

노드 정적 웹사이트의 예: 예상되는 사항

이 기사를 한 주제에 집중하기 위해 인증, 유효성 검사, 확장성 또는 기타 일반 주제는 다루지 않겠습니다. 이 기사의 코딩 부분은 최대한 간단합니다. 프로젝트의 구조와 올바른 도구 세트의 사용이 더 중요합니다.

시리즈의 첫 번째 파트에서는 ​​백엔드 애플리케이션을 개발하고 배포합니다. 두 번째 부분에서는 프런트 엔드 애플리케이션을 개발 및 배포하고 일일 빌드를 트리거합니다.

Node.js 백엔드

백엔드 애플리케이션은 Node.js로 작성되며(필수는 아니지만 단순성을 위해) 모든 통신은 REST API를 통해 이루어집니다. 이 프로젝트의 프런트 엔드에서 데이터를 수집하지 않습니다. (그렇게 하는 데 관심이 있다면 Gatsby Forms를 살펴보십시오.)

먼저 MongoDB에 있는 리포지토리 컬렉션의 CRUD 작업을 노출하는 간단한 REST API 백엔드를 구현하는 것으로 시작합니다. 그런 다음 이 컬렉션의 문서를 업데이트하기 위해 GitHub API v4(GraphQL)를 사용하는 크론 작업을 예약합니다. 그런 다음 이 모든 것을 Heroku 클라우드에 배포합니다. 마지막으로 cron 작업이 끝날 때 프런트 엔드의 재구축을 트리거합니다.

Gatsby.js 프론트 엔드

두 번째 기사에서는 createPages API의 구현에 중점을 둘 것입니다. 백엔드에서 모든 리포지토리를 수집하고 모든 리포지토리 목록과 반환된 각 리포지토리 문서에 대한 페이지가 포함된 단일 홈 페이지를 생성합니다. 그런 다음 Netlify에 프런트 엔드를 배포합니다.

AWS Lambda 및 AWS CloudWatch에서

애플리케이션이 절전 모드가 아닌 경우 이 부분은 필수 항목이 아닙니다. 그렇지 않으면 리포지토리를 업데이트할 때 백엔드가 작동하고 실행 중인지 확인해야 합니다. 솔루션으로 매일 업데이트하기 10분 전에 AWS CloudWatch에서 cron 일정을 생성하고 이를 AWS Lambda의 GET 메서드에 대한 트리거로 바인딩할 수 있습니다. 백엔드 애플리케이션에 액세스하면 Heroku 인스턴스가 깨어납니다. 자세한 내용은 두 번째 기사 말미에 있습니다.

구현할 아키텍처는 다음과 같습니다.

AWS Lambda 및 CloudWatch가 GitHub API를 사용하여 매일 업데이트를 받은 다음 백엔드 API를 사용하여 정적 페이지를 업데이트하고 Netlify에 배포하는 Gatsby 기반 프런트 엔드를 빌드하는 Node.js 백엔드를 ping하는 것을 보여주는 아키텍처 다이어그램. 백엔드는 무료 요금제로 Heroku에도 배포됩니다.

가정

이 기사의 독자는 다음 영역에 대한 지식이 있다고 가정합니다.

  • HTML
  • CSS
  • 자바스크립트
  • REST API
  • 몽고DB
  • 힘내
  • 노드.js

다음 사항을 알고 있는 경우에도 좋습니다.

  • 익스프레스.js
  • 몽구스
  • GitHub API v4(GraphQL)
  • Heroku, AWS 또는 기타 클라우드 플랫폼
  • 반응

백엔드 구현에 대해 알아보겠습니다. 우리는 그것을 두 가지 작업으로 나눌 것입니다. 첫 번째는 REST API 끝점을 준비하고 저장소 컬렉션에 바인딩하는 것입니다. 두 번째는 GitHub API를 사용하고 컬렉션을 업데이트하는 cron 작업을 구현하는 것입니다.

Node.js 정적 사이트 생성기 백엔드 개발, 1단계: 간단한 REST API

웹 애플리케이션 프레임워크에는 Express를 사용하고 MongoDB 연결에는 Mongoose를 사용합니다. Express와 Mongoose에 익숙하다면 2단계로 건너뛸 수도 있습니다.

(반면, Express에 대해 좀 더 알고 싶다면 공식 Express 스타터 가이드를 확인할 수 있고, Mongoose가 아직 익숙하지 않다면 공식 Mongoose 스타터 가이드가 도움이 될 것입니다.)

프로젝트 구조

프로젝트의 파일/폴더 계층 구조는 간단합니다.

config, controller, model 및 node_modules 폴더와 index.js 및 package.json과 같은 몇 가지 표준 루트 파일을 보여주는 프로젝트 루트의 폴더 목록입니다. 처음 세 폴더의 파일은 지정된 폴더 내의 각 파일 이름에서 폴더 이름을 반복하는 명명 규칙을 따릅니다.

더 자세하게:

  • env.config.js 는 환경 변수 구성 파일입니다.
  • routes.config.js 는 나머지 끝점을 매핑하기 위한 것입니다.
  • repository.controller.js 에는 리포지토리 모델에서 작업하는 메서드가 포함되어 있습니다.
  • repository.model.js 는 리포지토리 및 CRUD 작업의 MongoDB 스키마를 포함합니다.
  • index.js 는 초기화 클래스입니다.
  • package.json 에는 종속성 및 프로젝트 속성이 포함됩니다.

구현

다음 종속성을 package.json 에 추가한 후 npm install (또는 Yarn이 설치된 경우 yarn )을 실행합니다.

 { // ... "dependencies": { "body-parser": "1.7.0", "express": "^4.8.7", "moment": "^2.17.1", "moment-timezone": "^0.5.13", "mongoose": "^5.1.1", "node-uuid": "^1.4.8", "sync-request": "^4.0.2" } // ... }

env.config.js 파일에는 현재 port , environment ( dev 또는 prod ) 및 mongoDbUri 속성만 있습니다.

 module.exports = { "port": process.env.PORT || 3000, "environment": "dev", "mongoDbUri": process.env.MONGODB_URI || "mongodb://localhost/github-consumer" };

routes.config.js 에는 요청 매핑이 포함되어 있으며 컨트롤러의 해당 메서드를 호출합니다.

 const RepositoryController = require('../controller/repository.controller'); exports.routesConfig = function(app) { app.post('/repositories', [ RepositoryController.insert ]); app.get('/repositories', [ RepositoryController.list ]); app.get('/repositories/:id', [ RepositoryController.findById ]); app.patch('/repositories/:id', [ RepositoryController.patchById ]); app.delete('/repositories/:id', [ RepositoryController.deleteById ]); };

repository.controller.js 파일은 우리의 서비스 계층입니다. 그 책임은 저장소 모델의 해당 메서드를 호출하는 것입니다.

 const RepositoryModel = require('../model/repository.model'); exports.insert = (req, res) => { RepositoryModel.create(req.body) .then((result) => { res.status(201).send({ id: result._id }); }); }; exports.findById = (req, res) => { RepositoryModel.findById(req.params.id) .then((result) => { res.status(200).send(result); }); }; exports.list = (req, res) => { RepositoryModel.list() .then((result) => { res.status(200).send(result); }) }; exports.patchById = (req, res) => { RepositoryModel.patchById(req.params.id, req.body) .then(() => { res.status(204).send({}); }); }; exports.deleteById = (req, res) => { RepositoryModel.deleteById(req.params.id, req.body) .then(() => { res.status(204).send({}); }); };

repository.model.js 는 리포지토리 모델에 대한 MongoDb 연결 및 CRUD 작업을 처리합니다. 모델의 필드는 다음과 같습니다.

  • owner : 저장소 소유자(회사 또는 사용자)
  • name : 저장소 이름
  • createdAt : 마지막 릴리스 생성 날짜
  • resourcePath : 마지막 릴리스 경로
  • tagName : 마지막 릴리스 태그
  • releaseDescription : 릴리스 정보
  • homepageUrl Url : 프로젝트의 홈 URL
  • repositoryDescription : 저장소 설명
  • avatarUrl : 프로젝트 소유자의 아바타 URL
 const Mongoose = require('mongoose'); const Config = require('../config/env.config'); const MONGODB_URI = Config.mongoDbUri; Mongoose.connect(MONGODB_URI, { useNewUrlParser: true }); const Schema = Mongoose.Schema; const repositorySchema = new Schema({ owner: String, name: String, createdAt: String, resourcePath: String, tagName: String, releaseDescription: String, homepageUrl: String, repositoryDescription: String, avatarUrl: String }); repositorySchema.virtual('id').get(function() { return this._id.toHexString(); }); // Ensure virtual fields are serialised. repositorySchema.set('toJSON', { virtuals: true }); repositorySchema.findById = function(cb) { return this.model('Repository').find({ id: this.id }, cb); }; const Repository = Mongoose.model('repository', repositorySchema); exports.findById = (id) => { return Repository.findById(id) .then((result) => { if (result) { result = result.toJSON(); delete result._id; delete result.__v; return result; } }); }; exports.create = (repositoryData) => { const repository = new Repository(repositoryData); return repository.save(); }; exports.list = () => { return new Promise((resolve, reject) => { Repository.find() .exec(function(err, users) { if (err) { reject(err); } else { resolve(users); } }) }); }; exports.patchById = (id, repositoryData) => { return new Promise((resolve, reject) => { Repository.findById(id, function(err, repository) { if (err) reject(err); for (let i in repositoryData) { repository[i] = repositoryData[i]; } repository.save(function(err, updatedRepository) { if (err) return reject(err); resolve(updatedRepository); }); }); }) }; exports.deleteById = (id) => { return new Promise((resolve, reject) => { Repository.deleteOne({ _id: id }, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); }; exports.findByOwnerAndName = (owner, name) => { return Repository.find({ owner: owner, name: name }); };

이것이 첫 번째 커밋 후의 것입니다: MongoDB 연결 및 REST 작업.

다음 명령으로 애플리케이션을 실행할 수 있습니다.

 node index.js

테스트

테스트를 위해 localhost:3000 에 요청을 보냅니다(예: Postman 또는 cURL 사용):

리포지토리 삽입(필수 필드만)

게시물: http://localhost:3000/repositories

신체:

 { "owner" : "facebook", "name" : "react" }

저장소 가져오기

가져오기: http://localhost:3000/repositories

아이디로 받기

가져오기: http://localhost:3000/repositories/:id

ID별 패치

패치: http://localhost:3000/repositories/:id

신체:

 { "owner" : "facebook", "name" : "facebook-android-sdk" }

이제 업데이트를 자동화할 차례입니다.

Node.js 정적 사이트 생성기 백엔드 개발, 2단계: 리포지토리 릴리스 업데이트를 위한 크론 작업

이 부분에서는 데이터베이스에 삽입한 GitHub 리포지토리를 업데이트하기 위해 간단한 cron 작업(UTC 자정에 시작됨)을 구성합니다. 위의 예에서는 ownername 매개변수만 추가했지만 이 두 필드는 주어진 저장소에 대한 일반 정보에 액세스하기에 충분합니다.

데이터를 업데이트하려면 GitHub API를 사용해야 합니다. 이 부분에서는 GitHub API의 GraphQL 및 v4에 익숙해지는 것이 가장 좋습니다.

또한 GitHub 액세스 토큰을 생성해야 합니다. 이에 대한 최소 필수 범위는 다음과 같습니다.

필요한 GitHub 토큰 범위는 repo:status, repo_deployment, public_repo, read:org 및 read:user입니다.

그러면 토큰이 생성되고 이를 사용하여 GitHub에 요청을 보낼 수 있습니다.

이제 코드로 돌아가 보겠습니다.

package.json 에는 두 가지 새로운 종속성이 있습니다.

  • "axios": "^0.18.0" 은 HTTP 클라이언트이므로 GitHub API에 요청할 수 있습니다.
  • "cron": "^1.7.0" 은 cron 작업 스케줄러입니다.

평소와 같이 종속성을 추가한 후 npm install 또는 yarn 을 실행합니다.

config.js 에도 두 개의 새로운 속성이 필요합니다.

  • "githubEndpoint": "https://api.github.com/graphql"
  • "githubAccessToken": process.env.GITHUB_ACCESS_TOKEN (자신의 개인 액세스 토큰으로 GITHUB_ACCESS_TOKEN 환경 변수를 설정해야 함)

이름이 cron.controller.js controller 폴더 아래에 새 파일을 만듭니다. 예약된 시간에 repository.controller.jsupdateResositories 메서드를 호출하기만 하면 됩니다.

 const RepositoryController = require('../controller/repository.controller'); const CronJob = require('cron').CronJob; function updateDaily() { RepositoryController.updateRepositories(); } exports.startCronJobs = function () { new CronJob('0 0 * * *', function () {updateDaily()}, null, true, 'UTC'); };

이 부분의 최종 변경 사항은 repository.controller.js 에 있습니다. 간결함을 위해 모든 리포지토리를 한 번에 업데이트하도록 설계합니다. 하지만 리포지토리가 많으면 GitHub API의 리소스 제한을 초과할 수 있습니다. 이 경우 제한된 배치로 실행되도록 수정해야 하며 시간이 지남에 따라 분산되어야 합니다.

업데이트 기능의 일괄 구현은 다음과 같습니다.

 async function asyncUpdate() { await RepositoryModel.list().then((array) => { const promises = array.map(getLatestRelease); return Promise.all(promises); }); } exports.updateRepositories = async function update() { console.log('GitHub Repositories Update Started'); await asyncUpdate().then(() => { console.log('GitHub Repositories Update Finished'); }); };

마지막으로 끝점을 호출하고 저장소 모델을 업데이트합니다.

getLatestRelease 함수는 GraphQL 쿼리를 생성하고 GitHub API를 호출합니다. 해당 요청의 응답은 updateDatabase 함수에서 처리됩니다.

 async function updateDatabase(responseData, owner, name) { let createdAt = ''; let resourcePath = ''; let tagName = ''; let releaseDescription = ''; let homepageUrl = ''; let repositoryDescription = ''; let avatarUrl = ''; if (responseData.repository.releases) { createdAt = responseData.repository.releases.nodes[0].createdAt; resourcePath = responseData.repository.releases.nodes[0].resourcePath; tagName = responseData.repository.releases.nodes[0].tagName; releaseDescription = responseData.repository.releases.nodes[0].description; homepageUrl = responseData.repository.homepageUrl; repositoryDescription = responseData.repository.description; if (responseData.organization && responseData.organization.avatarUrl) { avatarUrl = responseData.organization.avatarUrl; } else if (responseData.user && responseData.user.avatarUrl) { avatarUrl = responseData.user.avatarUrl; } const repositoryData = { owner: owner, name: name, createdAt: createdAt, resourcePath: resourcePath, tagName: tagName, releaseDescription: releaseDescription, homepageUrl: homepageUrl, repositoryDescription: repositoryDescription, avatarUrl: avatarUrl }; await RepositoryModel.findByOwnerAndName(owner, name) .then((oldGitHubRelease) => { if (!oldGitHubRelease[0]) { RepositoryModel.create(repositoryData); } else { RepositoryModel.patchById(oldGitHubRelease[0].id, repositoryData); } console.log(`Updated latest release: http://github.com${repositoryData.resourcePath}`); }); } } async function getLatestRelease(repository) { const owner = repository.owner; const name = repository.name; console.log(`Getting latest release for: http://github.com/${owner}/${name}`); const query = ` query { organization(login: "${owner}") { avatarUrl } user(login: "${owner}") { avatarUrl } repository(owner: "${owner}", name: "${name}") { homepageUrl description releases(first: 1, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { createdAt resourcePath tagName description } } } }`; const jsonQuery = JSON.stringify({ query }); const headers = { 'User-Agent': 'Release Tracker', 'Authorization': `Bearer ${GITHUB_ACCESS_TOKEN}` }; await Axios.post(GITHUB_API_URL, jsonQuery, { headers: headers }).then((response) => { return updateDatabase(response.data.data, owner, name); }); }

두 번째 커밋 후에는 GitHub 리포지토리에서 매일 업데이트를 가져오기 위해 cron 스케줄러를 구현합니다.

백엔드 작업이 거의 완료되었습니다. 그러나 마지막 단계는 프런트 엔드를 구현한 후에 수행해야 하므로 다음 기사에서 다루겠습니다.

노드 정적 사이트 생성기 백엔드를 Heroku에 배포

이 단계에서는 애플리케이션을 Heroku에 배포하므로 아직 계정이 없는 경우 계정을 설정해야 합니다. Heroku 계정을 GitHub에 바인딩하면 지속적인 배포가 훨씬 쉬워집니다. 이를 위해 GitHub에서 프로젝트를 호스팅하고 있습니다.

Heroku 계정에 로그인한 후 대시보드에서 새 앱을 추가합니다.

Heroku 대시보드의 새 메뉴에서 "새 앱 만들기"를 선택합니다.

고유한 이름을 지정하십시오.

Heroku에서 앱 이름 지정

배포 섹션으로 리디렉션됩니다. 배포 방법으로 GitHub를 선택하고 저장소를 검색한 다음 "연결" 버튼을 클릭합니다.

새 GitHub 리포지토리를 Heroku 앱에 연결합니다.

단순성을 위해 자동 배포를 활성화할 수 있습니다. GitHub 리포지토리에 커밋을 푸시할 때마다 배포됩니다.

Heroku에서 자동 배포 활성화.

이제 MongoDB를 리소스로 추가해야 합니다. 리소스 탭으로 이동하여 "추가 기능 찾기"를 클릭하십시오. (저는 개인적으로 mLab mongoDB를 사용합니다.)

Heroku 앱에 MongoDB 리소스 추가

설치하고 "프로비저닝할 앱" 입력 상자에 앱 이름을 입력합니다.

Heroku의 mLab MongoDB 추가 기능 프로비저닝 페이지.

마지막으로 프로젝트의 루트 수준에 Procfile 이라는 파일을 만들어야 합니다. 이 파일은 Heroku가 앱을 시작할 때 앱에서 실행하는 명령을 지정합니다.

Procfile 은 다음과 같이 간단합니다.

 web: node index.js

파일을 생성하고 커밋합니다. 커밋을 푸시하면 Heroku는 https://[YOUR_UNIQUE_APP_NAME].herokuapp.com/ 으로 액세스할 수 있는 애플리케이션을 자동으로 배포합니다.

작동하는지 확인하기 위해 localhost 에 보낸 것과 동일한 요청을 보낼 수 있습니다.

Node.js, Express, MongoDB, Cron 및 Heroku: 절반 정도 왔습니다!

세 번째 커밋 이후의 저장소는 다음과 같습니다.

지금까지 백엔드에 Node.js/Express 기반 REST API, GitHub의 API를 사용하는 업데이터, 활성화하는 cron 작업을 구현했습니다. 그런 다음 지속적 통합을 위한 후크와 함께 Heroku를 사용하여 나중에 정적 웹 콘텐츠 생성기에 데이터를 제공할 백엔드를 배포했습니다. 이제 프론트 엔드를 구현하고 앱을 완성하는 두 번째 부분에 대한 준비가 되었습니다!

관련 항목: Node.js 개발자가 저지르는 가장 흔한 10가지 실수