React, Apollo GraphQL 및 Hasura를 사용하여 주가 알리미 앱 구축

게시 됨: 2022-03-10
빠른 요약 ↬ 이 기사에서는 이벤트 기반 애플리케이션을 구축하고 특정 이벤트가 트리거될 때 웹 푸시 알림을 보내는 방법을 배웁니다. Hasura GraphQL 엔진에서 데이터베이스 테이블, 이벤트 및 예약된 트리거를 설정하고 GraphQL 끝점을 프런트 엔드 애플리케이션에 연결하여 사용자의 주가 선호도를 기록합니다.

선택한 이벤트가 발생했을 때 알림을 받는 개념은 특정 발생을 스스로 찾기 위해 연속적인 데이터 스트림에 연결하는 것과 비교하여 대중적입니다. 사람들은 자신이 선호하는 이벤트가 발생했을 때 관련 이메일/메시지를 받는 것을 선호합니다. 이벤트 기반 용어는 소프트웨어 세계에서도 매우 일반적입니다.

좋아하는 주식의 가격 업데이트를 휴대전화로 받을 수 있다면 얼마나 좋을까요?

이 기사에서는 React, Apollo GraphQL 및 Hasura GraphQL 엔진을 사용하여 주가 알리미 애플리케이션을 구축할 것입니다. 우리는 create-react-app 상용구 코드에서 프로젝트를 시작하고 모든 것을 기초부터 구축할 것입니다. Hasura 콘솔에서 데이터베이스 테이블과 이벤트를 설정하는 방법을 배웁니다. 또한 웹 푸시 알림을 사용하여 주가 업데이트를 받기 위해 Hasura의 이벤트를 연결하는 방법을 배웁니다.

다음은 우리가 구축할 내용을 간략하게 살펴보겠습니다.

주가 알리미 애플리케이션 개요
주가 알리미 애플리케이션

가자!

점프 후 더! 아래에서 계속 읽기 ↓

이 프로젝트에 대한 개요

주식 데이터( high , low , open , close , volume 과 같은 메트릭 포함)는 Hasura 지원 Postgres 데이터베이스에 저장됩니다. 사용자는 특정 가치를 기반으로 특정 주식을 구독하거나 매시간 알림을 받도록 선택할 수 있습니다. 사용자는 구독 기준이 충족되면 웹 푸시 알림을 받게 됩니다.

이것은 많은 것 같으며 우리가 이 조각들을 어떻게 구축할 것인지에 대한 몇 가지 미해결 질문이 분명히 있을 것입니다.

다음은 이 프로젝트를 4단계로 수행하는 방법에 대한 계획입니다.

  1. NodeJs 스크립트를 사용하여 주식 데이터 가져오기
    주식 API 제공자 중 하나인 Alpha Vantage에서 간단한 NodeJs 스크립트를 사용하여 주식 데이터를 가져오는 것부터 시작하겠습니다. 이 스크립트는 5분 간격으로 특정 주식에 대한 데이터를 가져옵니다. API의 응답에는 high , low , open , closevolume이 포함됩니다. 이 데이터는 Hasura 백엔드와 통합된 Postgres 데이터베이스에 삽입됩니다.
  2. Hasura GraphQL 엔진 설정
    그런 다음 데이터 포인트를 기록하기 위해 Postgres 데이터베이스에 일부 테이블을 설정합니다. Hasura는 이러한 테이블에 대한 GraphQL 스키마, 쿼리 및 변형을 자동으로 생성합니다.
  3. React와 Apollo Client를 사용하는 프론트엔드
    다음 단계는 Apollo 클라이언트와 Apollo Provider(Hasura에서 제공하는 GraphQL 끝점)를 사용하여 GraphQL 레이어를 통합하는 것입니다. 데이터 포인트는 프런트 엔드에 차트로 표시됩니다. 또한 구독 옵션을 빌드하고 GraphQL 계층에서 해당 변형을 실행합니다.
  4. 이벤트/예약 트리거 설정
    Hasura는 방아쇠에 대한 훌륭한 도구를 제공합니다. 주식 데이터 테이블에 이벤트 및 예약된 트리거를 추가합니다. 사용자가 주가가 특정 값에 도달할 때 알림을 받는 데 관심이 있는 경우 이러한 트리거가 설정됩니다(이벤트 트리거). 사용자는 매시간 특정 주식에 대한 알림을 받도록 선택할 수도 있습니다(예약된 트리거).

이제 계획이 준비되었으니 실행에 옮기자!

다음은 이 프로젝트의 GitHub 리포지토리입니다. 아래 코드에서 길을 잃으면 이 리포지토리를 참조하고 빠르게 돌아가십시오!

NodeJs 스크립트를 사용하여 주식 데이터 가져오기

이것은 들리는 것처럼 그렇게 복잡하지 않습니다! Alpha Vantage 끝점을 사용하여 데이터를 가져오는 함수를 작성해야 하며 이 가져오기 호출은 5분 간격으로 시작되어야 합니다(맞췄습니다. 이 함수 호출을 setInterval 에 넣어야 합니다).

Alpha Vantage가 무엇인지 여전히 궁금하고 코딩 부분으로 넘어가기 전에 이것을 머리에서 지우고 싶다면 다음과 같습니다.

Alpha Vantage Inc.는 주식, 외환(FX) 및 디지털/암호화폐에 대한 실시간 및 과거 데이터를 위한 무료 API를 제공하는 선두 업체입니다.

우리는 이 끝점을 사용하여 특정 주식의 필수 메트릭을 얻을 것입니다. 이 API는 API 키를 매개변수 중 하나로 예상합니다. 여기에서 무료 API 키를 얻을 수 있습니다. 이제 흥미로운 부분을 다룰 수 있습니다. 코드 작성을 시작해 보겠습니다!

종속성 설치

stocks-app 디렉터리를 만들고 그 안에 server 디렉터리를 만듭니다. npm init 를 사용하여 노드 프로젝트로 초기화하고 다음 종속성을 설치합니다.

 npm i isomorphic-fetch pg nodemon --save

이것들은 주식 가격을 가져와 Postgres 데이터베이스에 저장하는 스크립트를 작성하는 데 필요한 유일한 세 가지 종속성입니다.

다음은 이러한 종속성에 대한 간략한 설명입니다.

  • isomorphic-fetch
    클라이언트와 서버 모두에서 동형적으로(동일한 형식으로) fetch 를 쉽게 사용할 수 있습니다.
  • pg
    NodeJ용 비차단 PostgreSQL 클라이언트입니다.
  • nodemon
    디렉터리의 모든 파일 변경 사항에 대해 서버를 자동으로 다시 시작합니다.

구성 설정

루트 수준에서 config.js 파일을 추가합니다. 지금은 해당 파일에 아래 코드 스니펫을 추가하세요.

 const config = { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: '<IS_SSL>', apiHost: 'https://www.alphavantage.co/', }; module.exports = config;

user , password , host , port , database , ssl 은 Postgres 구성과 관련이 있습니다. 우리는 Hasura 엔진 부분을 설정하는 동안 이것을 편집하기 위해 돌아올 것입니다!

데이터베이스 쿼리를 위한 Postgres 연결 풀 초기화

connection pool 은 컴퓨터 과학에서 흔히 사용되는 용어이며 데이터베이스를 다룰 때 이 용어를 자주 듣게 될 것입니다.

데이터베이스의 데이터를 쿼리하는 동안 먼저 데이터베이스에 대한 연결을 설정해야 합니다. 이 연결은 데이터베이스 자격 증명을 가져오고 데이터베이스의 모든 테이블을 쿼리할 수 있는 후크를 제공합니다.

참고 : 데이터베이스 연결을 설정하는 것은 비용이 많이 들고 상당한 자원을 낭비합니다. 연결 풀은 데이터베이스 연결을 캐시하고 후속 쿼리에서 다시 사용합니다. 열려 있는 모든 연결이 사용 중인 경우 새 연결이 설정되고 풀에 추가됩니다.

이제 연결 풀이 무엇이며 무엇에 사용되는지 명확해 졌으므로 이 응용 프로그램에 대한 pg 연결 풀의 인스턴스를 만드는 것으로 시작하겠습니다.

루트 수준에서 pool.js 파일을 추가하고 다음과 같이 풀 인스턴스를 만듭니다.

 const { Pool } = require('pg'); const config = require('./config'); const pool = new Pool({ user: config.user, password: config.password, host: config.host, port: config.port, database: config.database, ssl: config.ssl, }); module.exports = pool;

위의 코드 줄은 구성 파일에 설정된 구성 옵션을 사용하여 Pool 의 인스턴스를 만듭니다. 아직 구성 파일을 완료하지 않았지만 구성 옵션과 관련된 변경 사항은 없습니다.

이제 기반을 마련했으며 Alpha Vantage 엔드포인트에 대한 일부 API 호출을 시작할 준비가 되었습니다.

흥미로운 부분으로 들어가 봅시다!

주식 데이터 가져오기

이 섹션에서는 Alpha Vantage 끝점에서 주식 데이터를 가져올 것입니다. 다음은 index.js 파일입니다.

 const fetch = require('isomorphic-fetch'); const getConfig = require('./config'); const { insertStocksData } = require('./queries'); const symbols = [ 'NFLX', 'MSFT', 'AMZN', 'W', 'FB' ]; (function getStocksData () { const apiConfig = getConfig('apiHostOptions'); const { host, timeSeriesFunction, interval, key } = apiConfig; symbols.forEach((symbol) => { fetch(`${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key}`) .then((res) => res.json()) .then((data) => { const timeSeries = data['Time Series (5min)']; Object.keys(timeSeries).map((key) => { const dataPoint = timeSeries[key]; const payload = [ symbol, dataPoint['2. high'], dataPoint['3. low'], dataPoint['1. open'], dataPoint['4. close'], dataPoint['5. volume'], key, ]; insertStocksData(payload); }); }); }) })()

이 프로젝트의 목적을 위해 NFLX(Netflix), MSFT(Microsoft), AMZN(Amazon), W(Wayfair), FB(Facebook)와 같은 주식에 대해서만 가격을 쿼리할 것입니다.

구성 옵션은 이 파일을 참조하십시오. IIFE getStocksData 함수는 많은 일을 하지 않습니다! 이 기호를 반복하고 Alpha Vantage 끝점 ${host}query/?function=${timeSeriesFunction}&symbol=${symbol}&interval=${interval}&apikey=${key} 를 쿼리하여 이러한 주식에 대한 메트릭을 가져옵니다.

insertStocksData 함수는 이러한 데이터 포인트를 Postgres 데이터베이스에 넣습니다. 다음은 insertStocksData 함수입니다.

 const insertStocksData = async (payload) => { const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)'; pool.query(query, payload, (err, result) => { console.log('result here', err); }); };

이거 야! Alpha Vantage API에서 주식의 데이터 포인트를 가져와서 이를 stock_data 테이블의 Postgres 데이터베이스에 넣는 함수를 작성했습니다. 이 모든 작업을 수행하기 위해 누락된 부분이 하나뿐입니다! 구성 파일에 올바른 값을 채워야 합니다. Hasura 엔진을 설정한 후 이 값을 얻습니다. 바로 가봅시다!

Alpha Vantage 엔드포인트에서 데이터 포인트를 가져와 Hasura Postgres 데이터베이스에 채우는 전체 코드는 server 디렉토리를 참조하십시오.

연결 설정, 구성 옵션 및 원시 쿼리를 사용하여 데이터를 삽입하는 이러한 접근 방식이 다소 어려워 보이더라도 걱정하지 마십시오! Hasura 엔진이 설정되면 GraphQL 변형을 사용하여 이 모든 작업을 쉽게 수행하는 방법을 배울 것입니다!

Hasura GraphQL 엔진 설정

Hasura 엔진을 설정하고 GraphQL 스키마, 쿼리, 변형, 구독, 이벤트 트리거 등을 시작하고 실행하는 것은 정말 간단합니다!

Hasura 시도를 클릭하고 프로젝트 이름을 입력하십시오.

Hasura 프로젝트 만들기
Hasura 프로젝트 만들기. (큰 미리보기)

Heroku에서 호스팅되는 Postgres 데이터베이스를 사용하고 있습니다. Heroku에서 데이터베이스를 만들고 이 프로젝트에 연결합니다. 그런 다음 쿼리가 풍부한 Hasura 콘솔의 기능을 경험할 준비가 모두 되어 있어야 합니다.

프로젝트 생성 후 받을 Postgres DB URL을 복사해 주세요. 이것을 설정 파일에 넣어야 합니다.

Launch Console을 클릭하면 다음 보기로 리디렉션됩니다.

하수라 콘솔
하수라 콘솔. (큰 미리보기)

이 프로젝트에 필요한 테이블 스키마 구축을 시작하겠습니다.

Postgres 데이터베이스에서 테이블 스키마 생성

데이터 탭으로 이동하여 테이블 추가를 클릭하십시오! 몇 가지 테이블 생성을 시작해 보겠습니다.

symbol 테이블

이 테이블은 기호 정보를 저장하는 데 사용됩니다. 지금은 idcompany 라는 두 개의 필드를 유지했습니다. 필드 id 는 기본 키이고 companyvarchar 유형입니다. 이 표에 몇 가지 기호를 추가해 보겠습니다.

심볼 테이블
symbol 테이블. (큰 미리보기)

stock_data 테이블

stock_data 테이블은 id , symbol , timehigh , low , open , close , volume 과 같은 메트릭을 저장합니다. 이 섹션의 앞부분에서 작성한 NodeJs 스크립트는 이 특정 테이블을 채우는 데 사용됩니다.

표는 다음과 같습니다.

stock_data 테이블
stock_data 테이블. (큰 미리보기)

정돈 된! 데이터베이스 스키마의 다른 테이블로 이동합시다!

user_subscription 테이블

user_subscription 테이블은 사용자 ID에 대한 구독 개체를 저장합니다. 이 구독 개체는 사용자에게 웹 푸시 알림을 보내는 데 사용됩니다. 이 구독 개체를 생성하는 방법은 이 문서의 뒷부분에서 배울 것입니다.

이 테이블에는 두 개의 필드가 있습니다. iduuid 유형의 기본 키이고 구독 필드는 jsonb 유형입니다.

events 테이블

이것은 중요하며 알림 이벤트 옵션을 저장하는 데 사용됩니다. 사용자가 특정 주식의 가격 업데이트를 선택하면 해당 이벤트 정보가 이 테이블에 저장됩니다. 이 테이블에는 다음 열이 포함되어 있습니다.

  • id : 자동 증가 속성이 있는 기본 키입니다.
  • symbol : 텍스트 필드입니다.
  • user_id : uuid 유형입니다.
  • trigger_type : 이벤트 트리거 유형인 time/event 를 저장하는 데 사용됩니다.
  • trigger_value : 트리거 값을 저장하는 데 사용됩니다. 예를 들어 사용자가 가격 기반 이벤트 트리거를 선택한 경우 주식 가격이 1000에 도달하면 업데이트를 원하고 trigger_value 는 1000이고 trigger_typeevent 가 됩니다.

이것이 이 프로젝트에 필요한 모든 테이블입니다. 또한 원활한 데이터 흐름과 연결을 위해 이러한 테이블 간의 관계를 설정해야 합니다. 그걸하자!

테이블 간의 관계 설정

events 테이블은 이벤트 값을 기반으로 웹 푸시 알림을 보내는 데 사용됩니다. 따라서 이 테이블에 저장된 구독에 대한 푸시 알림을 보낼 수 있도록 이 테이블을 user_subscription 테이블과 연결하는 것이 좋습니다.

 events.user_id → user_subscription.id

stock_data 테이블은 다음과 같이 기호 테이블과 관련됩니다.

 stock_data.symbol → symbol.id

또한 다음과 같이 symbol 테이블에 몇 가지 관계를 구성해야 합니다.

 stock_data.symbol → symbol.id events.symbol → symbol.id

이제 필요한 테이블을 만들고 테이블 간의 관계도 설정했습니다! 콘솔의 GRAPHIQL 탭으로 전환하여 마법을 살펴보겠습니다!

Hasura는 이미 다음 테이블을 기반으로 GraphQL 쿼리를 설정했습니다.

Hasura 콘솔의 GraphQL 쿼리/변이
Hasura 콘솔의 GraphQL 쿼리/변이. (큰 미리보기)

이러한 테이블에 대해 쿼리하는 것은 매우 간단하며 원하는 데이터를 얻기 위해 이러한 필터/속성( distinct_on , limit , offset , order_by , where ) 중 하나를 적용할 수도 있습니다.

이 모든 것이 좋아 보이지만 아직 서버 측 코드를 Hasura 콘솔에 연결하지 않았습니다. 그 부분을 완료합시다!

Postgres 데이터베이스에 NodeJs 스크립트 연결하기

다음과 같이 server 디렉토리의 config.js 파일에 필요한 옵션을 넣으십시오.

 const config = { databaseOptions: { user: '<DATABASE_USER>', password: '<DATABASE_PASSWORD>', host: '<DATABASE_HOST>', port: '<DATABASE_PORT>', database: '<DATABASE_NAME>', ssl: true, }, apiHostOptions: { host: 'https://www.alphavantage.co/', key: '<API_KEY>', timeSeriesFunction: 'TIME_SERIES_INTRADAY', interval: '5min' }, graphqlURL: '<GRAPHQL_URL>' }; const getConfig = (key) => { return config[key]; }; module.exports = getConfig;

Heroku에서 Postgres 데이터베이스를 생성할 때 생성된 데이터베이스 문자열에서 이러한 옵션을 입력하십시오.

apiHostOptionshost , key , timeSeriesFunctioninterval 과 같은 API 관련 옵션으로 구성됩니다.

Hasura 콘솔의 GRAPHIQL 탭에 graphqlURL 필드가 표시됩니다.

getConfig 함수는 구성 개체에서 요청된 값을 반환하는 데 사용됩니다. 우리는 이미 server 디렉토리의 index.js 에서 이것을 사용했습니다.

이제 서버를 실행하고 데이터베이스의 일부 데이터를 채울 차례입니다. 다음과 같이 package.json 에 하나의 스크립트를 추가했습니다.

 "scripts": { "start": "nodemon index.js" }

터미널에서 npm start 를 실행하면 index.js 에 있는 기호 배열의 데이터 포인트가 테이블에 채워져야 합니다.

NodeJs 스크립트의 원시 쿼리를 GraphQL 돌연변이로 리팩토링

이제 Hasura 엔진이 설정 stock_data 테이블에서 돌연변이를 호출하는 것이 얼마나 쉬운지 봅시다.

queries.jsinsertStocksData 함수는 원시 쿼리를 사용합니다.

 const query = 'INSERT INTO stock_data (symbol, high, low, open, close, volume, time) VALUES ($1, $2, $3, $4, $5, $6, $7)';

이 쿼리를 리팩토링하고 Hasura 엔진에서 제공하는 변형을 사용하겠습니다. 다음은 서버 디렉터리에서 리팩터링된 queries.js 입니다.

 const { createApolloFetch } = require('apollo-fetch'); const getConfig = require('./config'); const GRAPHQL_URL = getConfig('graphqlURL'); const fetch = createApolloFetch({ uri: GRAPHQL_URL, }); const insertStocksData = async (payload) => { const insertStockMutation = await fetch({ query: `mutation insertStockData($objects: [stock_data_insert_input!]!) { insert_stock_data (objects: $objects) { returning { id } } }`, variables: { objects: payload, }, }); console.log('insertStockMutation', insertStockMutation); }; module.exports = { insertStocksData }

참고: config.js 파일에 graphqlURL 을 추가해야 합니다.

apollo-fetch 모듈은 GraphQL 끝점에서 날짜를 쿼리/변경하는 데 사용할 수 있는 가져오기 함수를 반환합니다. 충분히 쉽죠?

index.js 에서 해야 할 유일한 변경 사항은 insertStocksData 함수에서 요구하는 형식으로 stock 객체를 반환하는 것입니다. 이 접근 방식의 전체 코드는 index2.jsqueries2.js 를 확인하세요.

이제 프로젝트의 데이터 측면을 완료했으므로 프런트 엔드 비트로 이동하여 흥미로운 구성 요소를 빌드해 보겠습니다.

참고 : 이 접근 방식에서는 데이터베이스 구성 옵션을 유지할 필요가 없습니다!

React 및 Apollo 클라이언트를 사용하는 프론트엔드

프런트 엔드 프로젝트는 동일한 저장소에 있으며 create-react-app 패키지를 사용하여 생성됩니다. 이 패키지를 사용하여 생성된 서비스 워커는 자산 캐싱을 지원하지만 서비스 워커 파일에 더 많은 사용자 지정을 추가할 수 없습니다. 사용자 지정 서비스 작업자 옵션에 대한 지원을 추가하기 위한 몇 가지 미해결 문제가 이미 있습니다. 이 문제를 해결하고 사용자 지정 서비스 작업자에 대한 지원을 추가하는 방법이 있습니다.

프런트 엔드 프로젝트의 구조부터 살펴보겠습니다.

프로젝트 디렉토리
프로젝트 디렉토리. (큰 미리보기)

src 디렉토리를 확인하십시오! 지금은 서비스 워커 관련 파일에 대해 걱정하지 마십시오. 이 섹션의 뒷부분에서 이러한 파일에 대해 자세히 알아볼 것입니다. 나머지 프로젝트 구조는 단순해 보입니다. components 폴더에는 구성 요소(로더, 차트)가 있습니다. services 폴더에는 필요한 구조에서 개체를 변환하는 데 사용되는 일부 도우미 기능/서비스가 포함되어 있습니다. styles 은 이름에서 알 수 있듯이 프로젝트 스타일 지정에 사용되는 sass 파일을 포함합니다. views 는 기본 디렉토리이며 뷰 레이어 구성 요소를 포함합니다.

이 프로젝트에는 Symbol List와 Symbol Timeseries의 두 가지 보기 구성 요소만 필요합니다. highcharts 라이브러리의 Chart 구성 요소를 사용하여 시계열을 작성합니다. 이 파일에 코드를 추가하여 프론트 엔드에서 조각을 구축해 봅시다!

종속성 설치

필요한 종속성 목록은 다음과 같습니다.

  • apollo-boost
    Apollo Boost는 Apollo Client 사용을 시작하는 제로 구성 방법입니다. 기본 구성 옵션과 함께 번들로 제공됩니다.
  • reactstrap 스트랩과 bootstrap
    구성 요소는 이 두 패키지를 사용하여 빌드됩니다.
  • graphqlgraphql-type-json
    graphqlapollo-boost 를 사용하기 위한 필수 종속성이며 graphql-type-json 은 GraphQL 스키마에서 사용되는 json 데이터 유형을 지원하는 데 사용됩니다.
  • highchartshighcharts-react-official
    그리고 이 두 패키지는 차트를 작성하는 데 사용됩니다.

  • node-sass
    이것은 스타일 지정을 위한 sass 파일을 지원하기 위해 추가되었습니다.

  • uuid
    이 패키지는 강력한 임의 값을 생성하는 데 사용됩니다.

이러한 모든 종속성은 프로젝트에서 사용하기 시작하면 이해가 될 것입니다. 다음 비트로 가자!

Apollo 클라이언트 설정

다음과 같이 src 폴더 안에 apolloClient.js 를 만듭니다.

 import ApolloClient from 'apollo-boost'; const apolloClient = new ApolloClient({ uri: '<HASURA_CONSOLE_URL>' }); export default apolloClient;

위의 코드는 ApolloClient를 인스턴스화하고 구성 옵션에서 uri 를 사용합니다. uri 는 Hasura 콘솔의 URL입니다. GraphQL 끝점 섹션의 GRAPHIQL 탭에서 이 uri 필드를 얻을 수 있습니다.

위의 코드는 단순해 보이지만 프로젝트의 주요 부분을 처리합니다! Hasura에 구축된 GraphQL 스키마를 현재 프로젝트와 연결합니다.

또한 이 apollo 클라이언트 개체를 ApolloProvider 에 전달하고 ApolloProvider 내부에 루트 구성 요소를 래핑해야 합니다. 이렇게 하면 기본 구성 요소 내부의 모든 중첩 구성 요소가 client 소품을 사용하고 이 클라이언트 개체에 대한 쿼리를 실행할 수 있습니다.

index.js 파일을 다음과 같이 수정해 보겠습니다.

 const Wrapper = () => { /* some service worker logic - ignore for now */ const [insertSubscription] = useMutation(subscriptionMutation); useEffect(() => { serviceWorker.register(insertSubscription); }, []) /* ignore the above snippet */ return <App />; } ReactDOM.render( <ApolloProvider client={apolloClient}> <Wrapper /> </ApolloProvider>, document.getElementById('root') );

insertSubscription 관련 코드는 무시하세요. 나중에 자세히 이해하겠습니다. 나머지 코드는 돌아다니기 쉬워야 합니다. render 함수는 루트 구성 요소와 elementId를 매개 변수로 사용합니다. client ( ApolloProvider 인스턴스)가 ApolloProvider 에 소품으로 전달되고 있습니다. 여기에서 전체 index.js 파일을 확인할 수 있습니다.

사용자 정의 서비스 작업자 설정

서비스 작업자는 네트워크 요청을 가로챌 수 있는 기능이 있는 JavaScript 파일입니다. 요청된 자산이 서버로 이동하는 대신 캐시에 이미 있는지 확인하기 위해 캐시를 쿼리하는 데 사용됩니다. 서비스 워커는 웹 푸시 알림을 구독 장치로 보내는 데에도 사용됩니다.

구독한 사용자에게 주가 업데이트에 대한 웹 푸시 알림을 보내야 합니다. 기초를 다지고 이 서비스 워커 파일을 빌드해봅시다!

index.js 파일에 있는 insertSubscription 관련 snipped는 subscriptionMutation을 사용하여 서비스 워커를 등록하고 subscriptionMutation 객체를 데이터베이스에 넣는 작업을 하고 있습니다.

프로젝트에서 사용되는 모든 쿼리 및 변형은 query.js를 참조하십시오.

serviceWorker.register(insertSubscription); serviceWorker.js 파일에 작성된 register 기능을 호출합니다. 여기있어:

 export const register = (insertSubscription) => { if ('serviceWorker' in navigator) { const swUrl = `${process.env.PUBLIC_URL}/serviceWorker.js` navigator.serviceWorker.register(swUrl) .then(() => { console.log('Service Worker registered'); return navigator.serviceWorker.ready; }) .then((serviceWorkerRegistration) => { getSubscription(serviceWorkerRegistration, insertSubscription); Notification.requestPermission(); }) } }

위의 함수는 먼저 브라우저에서 serviceWorker 를 지원하는지 확인한 다음 URL swUrl 에서 호스팅되는 서비스 워커 파일을 등록합니다. 잠시 후 이 파일을 확인하겠습니다!

getSubscription 함수는 pushManager 개체의 subscribe 메서드를 사용하여 구독 개체를 가져오는 작업을 수행합니다. 그런 다음 이 구독 개체는 userId에 대해 user_subscription 테이블에 저장됩니다. userId는 uuid 함수를 사용하여 생성된다는 점에 유의하십시오. getSubscription 함수를 확인해 보겠습니다.

 const getSubscription = (serviceWorkerRegistration, insertSubscription) => { serviceWorkerRegistration.pushManager.getSubscription() .then ((subscription) => { const userId = uuidv4(); if (!subscription) { const applicationServerKey = urlB64ToUint8Array('<APPLICATION_SERVER_KEY>') serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }).then (subscription => { insertSubscription({ variables: { userId, subscription } }); localStorage.setItem('serviceWorkerRegistration', JSON.stringify({ userId, subscription })); }) } }) }

전체 코드는 serviceWorker.js 파일에서 확인할 수 있습니다!

알림 팝업
알림 팝업. (큰 미리보기)

Notification.requestPermission() 은 사용자에게 알림 전송 권한을 요청하는 이 팝업을 호출했습니다. 사용자가 허용을 클릭하면 푸시 서비스에 의해 구독 개체가 생성됩니다. 해당 객체를 다음과 같이 localStorage에 저장합니다.

Webpush 구독 개체
Webpush 구독 개체. (큰 미리보기)

위 개체의 필드 endpoint 은 장치를 식별하는 데 사용되며 서버는 이 끝점을 사용하여 사용자에게 웹 푸시 알림을 보냅니다.

서비스 워커를 초기화하고 등록하는 작업을 완료했습니다. 사용자의 구독 객체도 있습니다! 이것은 public 폴더에 있는 serviceWorker.js 파일 때문에 잘 작동합니다. 이제 서비스 워커를 설정하여 준비를 마치겠습니다!

조금 어려운 주제지만 제대로 이해합시다! 앞서 언급했듯이 create-react-app 유틸리티는 기본적으로 서비스 작업자에 대한 사용자 지정을 지원하지 않습니다. workbox-build 모듈을 사용하여 고객 서비스 작업자 구현을 달성할 수 있습니다.

또한 사전 캐싱 파일의 기본 동작이 손상되지 않았는지 확인해야 합니다. 프로젝트에서 서비스 워커가 빌드되는 부분을 수정하겠습니다. 그리고 workbox-build는 정확히 그것을 달성하는 데 도움이 됩니다! 깔끔한 물건! 간단하게 유지하고 사용자 지정 서비스 작업자가 작동하도록 하기 위해 해야 할 모든 것을 나열해 보겠습니다.

  • workboxBuild 를 사용하여 자산의 사전 캐싱을 처리합니다.
  • 자산 캐싱을 위한 서비스 작업자 템플릿을 만듭니다.
  • sw-precache-config.js 파일을 만들어 사용자 지정 구성 옵션을 제공합니다.
  • package.json 의 빌드 단계에서 빌드 서비스 작업자 스크립트를 추가합니다.

이 모든 것이 혼란스럽게 들리더라도 걱정하지 마십시오! 이 기사는 이러한 각 요점 뒤에 있는 의미를 설명하는 데 중점을 두지 않습니다. 지금은 구현 부분에 집중해야 합니다! 나는 다른 기사에서 커스텀 서비스 워커를 만들기 위해 모든 작업을 하는 이유를 다루려고 노력할 것이다.

src 디렉토리에 sw sw-build.jssw-custom.js custom.js 파일 두 개를 생성해 보겠습니다. 이 파일에 대한 링크를 참조하고 프로젝트에 코드를 추가하십시오.

이제 루트 수준에서 sw-precache-config.js 파일을 만들고 해당 파일에 다음 코드를 추가해 보겠습니다.

 module.exports = { staticFileGlobs: [ 'build/static/css/**.css', 'build/static/js/**.js', 'build/index.html' ], swFilePath: './build/serviceWorker.js', stripPrefix: 'build/', handleFetch: false, runtimeCaching: [{ urlPattern: /this\\.is\\.a\\.regex/, handler: 'networkFirst' }] }

또한 package.json 파일을 수정하여 사용자 지정 서비스 작업자 파일을 빌드할 공간을 만들어 보겠습니다.

scripts 섹션에 다음 명령문을 추가하십시오.

 "build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",

그리고 build 스크립트를 다음과 같이 수정합니다.

 "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",

드디어 설정이 완료되었습니다! 이제 public 폴더 안에 사용자 지정 서비스 작업자 파일을 추가해야 합니다.

 function showNotification (event) { const eventData = event.data.json(); const { title, body } = eventData self.registration.showNotification(title, { body }); } self.addEventListener('push', (event) => { event.waitUntil(showNotification(event)); })

서버에서 보내는 푸시 알림을 수신하기 위해 push 리스너를 하나만 추가했습니다. showNotification 기능은 사용자에게 웹 푸시 알림을 표시하는 데 사용됩니다.

이거 야! 웹 푸시 알림을 처리하기 위해 사용자 지정 서비스 작업자를 설정하는 모든 힘든 작업을 마쳤습니다. 사용자 인터페이스를 구축하면 이러한 알림이 작동하는 것을 볼 수 있습니다!

우리는 주요 코드 조각을 구축하는 데 점점 더 가까워지고 있습니다. 이제 첫 번째 보기부터 시작하겠습니다!

기호 목록 보기

이전 섹션에서 사용된 App 구성 요소는 다음과 같습니다.

 import React from 'react'; import SymbolList from './views/symbolList'; const App = () => { return <SymbolList />; }; export default App;

SymbolList 보기를 반환하는 간단한 구성 요소이며 SymbolList 는 깔끔하게 묶인 사용자 인터페이스에서 기호를 표시하는 모든 무거운 작업을 수행합니다.

views 폴더 안에 있는 symbolList.js 를 살펴보겠습니다.

여기 파일을 참고해주세요!

구성 요소는 renderSymbols 함수의 결과를 반환합니다. 그리고 이 데이터는 다음과 같이 useQuery 후크를 사용하여 데이터베이스에서 가져옵니다.

 const { loading, error, data } = useQuery(symbolsQuery, {variables: { userId }});

symbolsQuery 는 다음과 같이 정의됩니다.

 export const symbolsQuery = gql` query getSymbols($userId: uuid) { symbol { id company symbol_events(where: {user_id: {_eq: $userId}}) { id symbol trigger_type trigger_value user_id } stock_symbol_aggregate { aggregate { max { high volume } min { low volume } } } } } `;

userId 를 받아 해당 특정 사용자의 구독 이벤트를 가져와 알림 아이콘(제목과 함께 표시되는 종 모양 아이콘)의 올바른 상태를 표시합니다. 쿼리는 주식의 최대값과 최소값도 가져옵니다. 위 쿼리에서 aggregate 사용을 확인하십시오. Hasura의 집계 쿼리는 count , sum , avg , max , min 등과 같은 집계 값을 가져오기 위해 배후에서 작업을 수행합니다.

위의 GraphQL 호출의 응답에 따라 프런트 엔드에 표시되는 카드 목록은 다음과 같습니다.

스톡 카드
주식 카드. (큰 미리보기)

카드 HTML 구조는 다음과 같습니다.

 <div key={id}> <div className="card-container"> <Card> <CardBody> <CardTitle className="card-title"> <span className="company-name">{company} </span> <Badge color="dark" pill>{id}</Badge> <div className={classNames({'bell': true, 'disabled': isSubscribed})} id={`subscribePopover-${id}`}> <FontAwesomeIcon icon={faBell} title="Subscribe" /> </div> </CardTitle> <div className="metrics"> <div className="metrics-row"> <span className="metrics-row--label">High:</span> <span className="metrics-row--value">{max.high}</span> <span className="metrics-row--label">{' '}(Volume: </span> <span className="metrics-row--value">{max.volume}</span>) </div> <div className="metrics-row"> <span className="metrics-row--label">Low: </span> <span className="metrics-row--value">{min.low}</span> <span className="metrics-row--label">{' '}(Volume: </span> <span className="metrics-row--value">{min.volume}</span>) </div> </div> <Button className="timeseries-btn" outline onClick={() => toggleTimeseries(id)}>Timeseries</Button>{' '} </CardBody> </Card> <Popover className="popover-custom" placement="bottom" target={`subscribePopover-${id}`} isOpen={isSubscribePopoverOpen === id} toggle={() => setSubscribeValues(id, symbolTriggerData)} > <PopoverHeader> Notification Options <span className="popover-close"> <FontAwesomeIcon icon={faTimes} onClick={() => handlePopoverToggle(null)} /> </span> </PopoverHeader> {renderSubscribeOptions(id, isSubscribed, symbolTriggerData)} </Popover> </div> <Collapse isOpen={expandedStockId === id}> { isOpen(id) ? <StockTimeseries symbol={id}/> : null } </Collapse> </div>

이 카드를 렌더링하기 위해 ReactStrap의 Card 구성 요소를 사용하고 있습니다. Popover 구성 요소는 구독 기반 옵션을 표시하는 데 사용됩니다.

알림 옵션
알림 옵션. (큰 미리보기)

사용자가 특정 주식의 bell 아이콘을 클릭하면 매시간 또는 주식 가격이 입력된 값에 도달했을 때 알림을 받도록 선택할 수 있습니다. 이벤트/시간 트리거 섹션에서 작동하는 것을 볼 수 있습니다.

참고 : 다음 섹션에서 StockTimeseries 구성 요소에 대해 알아보겠습니다!

주식 목록 구성 요소와 관련된 전체 코드는 symbolList.js 를 참조하십시오.

주식 시계열 보기

StockTimeseries 구성 요소는 stocksDataQuery 쿼리를 사용합니다.

 export const stocksDataQuery = gql` query getStocksData($symbol: String) { stock_data(order_by: {time: desc}, where: {symbol: {_eq: $symbol}}, limit: 25) { high low open close volume time } } `;

위의 쿼리는 선택한 주식의 최근 25개 데이터 포인트를 가져옵니다. 예를 들어 다음은 Facebook 주식 시가 지표에 대한 차트입니다.

주가 타임라인
주가 타임라인. (큰 미리보기)

이것은 [ HighchartsReact ] 구성 요소에 일부 차트 옵션을 전달하는 간단한 구성 요소입니다. 차트 옵션은 다음과 같습니다.

 const chartOptions = { title: { text: `${symbol} Timeseries` }, subtitle: { text: 'Intraday (5min) open, high, low, close prices & volume' }, yAxis: { title: { text: '#' } }, xAxis: { title: { text: 'Time' }, categories: getDataPoints('time') }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle' }, series: [ { name: 'high', data: getDataPoints('high') }, { name: 'low', data: getDataPoints('low') }, { name: 'open', data: getDataPoints('open') }, { name: 'close', data: getDataPoints('close') }, { name: 'volume', data: getDataPoints('volume') } ] }

X축에는 시간이 표시되고 Y축에는 해당 시점의 메트릭 값이 표시됩니다. getDataPoints 함수는 각 계열에 대한 일련의 점을 생성하는 데 사용됩니다.

 const getDataPoints = (type) => { const values = []; data.stock_data.map((dataPoint) => { let value = dataPoint[type]; if (type === 'time') { value = new Date(dataPoint['time']).toLocaleString('en-US'); } values.push(value); }); return values; }

단순한! 이것이 Chart 컴포넌트가 생성되는 방법입니다! 주식 시계열에 대한 전체 코드는 Chart.js 및 stockTimeseries.js 파일을 참조하십시오.

이제 프로젝트의 데이터 및 사용자 인터페이스 부분을 사용할 준비가 되었습니다. 이제 흥미로운 부분으로 넘어가 보겠습니다. 사용자 입력을 기반으로 이벤트/시간 트리거를 설정합니다.

이벤트/예약된 트리거 설정

이 섹션에서는 Hasura 콘솔에서 트리거를 설정하는 방법과 선택한 사용자에게 웹 푸시 알림을 보내는 방법을 배웁니다. 시작하자!

Hasura 콘솔에서 이벤트 트리거

stock_data 테이블에 이벤트 트리거 stock_value 를 만들고 트리거 작업으로 insertstock_data . 웹훅은 stock_data 테이블에 삽입이 있을 때마다 실행됩니다.

이벤트 트리거 설정
이벤트 트리거 설정. (큰 미리보기)

웹훅 URL에 대한 글리치 프로젝트를 만들 것입니다. 이해하기 쉽도록 웹훅에 대해 간략히 설명하겠습니다.

Webhook은 특정 이벤트 발생 시 한 애플리케이션에서 다른 애플리케이션으로 데이터를 전송하는 데 사용됩니다. 이벤트가 트리거되면 이벤트 데이터를 페이로드로 사용하여 웹훅 URL에 대한 HTTP POST 호출이 수행됩니다.

이 경우 stock_data 테이블에 삽입 작업이 있을 때 구성된 웹훅 URL에 대한 HTTP 사후 호출이 만들어집니다(글리치 프로젝트의 사후 호출).

웹 푸시 알림을 보내기 위한 글리치 프로젝트

위의 이벤트 트리거 인터페이스에 넣을 웹훅 URL을 가져와야 합니다. Go to glitch.com and create a new project. In this project, we'll set up an express listener and there will be an HTTP post listener. The HTTP POST payload will have all the details of the stock datapoint including open , close , high , low , volume , time . We'll have to fetch the list of users subscribed to this stock with the value equal to the close metric.

These users will then be notified of the stock price via web-push notifications.

That's all we've to do to achieve the desired target of notifying users when the stock price reaches the expected value!

Let's break this down into smaller steps and implement them!

종속성 설치

We would need the following dependencies:

  • express : is used for creating an express server.
  • apollo-fetch : is used for creating a fetch function for getting data from the GraphQL endpoint.
  • web-push : is used for sending web push notifications.

Please write this script in package.json to run index.js on npm start command:

 "scripts": { "start": "node index.js" }

Setting Up Express Server

Let's create an index.js file as:

 const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); const handleStockValueTrigger = (eventData, res) => { /* Code for handling this trigger */ } app.post('/', (req, res) => { const { body } = req const eventType = body.trigger.name const eventData = body.event switch (eventType) { case 'stock-value-trigger': return handleStockValueTrigger(eventData, res); } }); app.get('/', function (req, res) { res.send('Hello World - For Event Triggers, try a POST request?'); }); var server = app.listen(process.env.PORT, function () { console.log(`server listening on port ${process.env.PORT}`); });

In the above code, we've created post and get listeners on the route / . get is simple to get around! We're mainly interested in the post call. If the eventType is stock-value-trigger , we'll have to handle this trigger by notifying the subscribed users. Let's add that bit and complete this function!

구독 사용자 가져오기

 const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); }

위의 handleStockValueTrigger 함수에서 먼저 handleStockValueTrigger 함수를 사용하여 구독한 사용자를 가져 getSubscribedUsers . 그런 다음 이러한 각 사용자에게 웹 푸시 알림을 보냅니다. sendWebpush 는 알림을 보내는 데 사용됩니다. 잠시 후에 웹 푸시 구현을 살펴보겠습니다.

getSubscribedUsers 함수는 다음 쿼리를 사용합니다.

 query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }

이 쿼리는 주식 기호와 값을 가져오고 다음 조건과 일치하는 user-iduser_subscription 을 포함한 사용자 세부 정보를 가져옵니다.

  • 페이로드에서 전달되는 것과 동일한 symbol 입니다.
  • trigger_typeevent 와 같습니다.
  • trigger_value 는 이 함수에 전달되는 값보다 크거나 같습니다(이 경우 close ).

사용자 목록을 받으면 웹 푸시 알림을 보내는 일만 남습니다! 바로 해보자!

등록된 사용자에게 웹 푸시 알림 보내기

웹 푸시 알림을 보내려면 먼저 공개 및 비공개 VAPID 키를 가져와야 합니다. 이 키를 .env 파일에 저장하고 index.js 에서 이러한 세부 정보를 다음과 같이 설정하세요.

 webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) }

sendNotification 함수는 첫 번째 매개 변수로 제공되는 구독 끝점에서 웹 푸시를 보내는 데 사용됩니다.

구독한 사용자에게 웹 푸시 알림을 성공적으로 보내기 위해서는 이것이 전부입니다. 다음은 index.js 에 정의된 전체 코드입니다.

 const express = require('express'); const bodyParser = require('body-parser'); const { createApolloFetch } = require('apollo-fetch'); const webPush = require('web-push'); webPush.setVapidDetails( 'mailto:<YOUR_MAIL_ID>', process.env.PUBLIC_VAPID_KEY, process.env.PRIVATE_VAPID_KEY ); const app = express(); app.use(bodyParser.json()); const fetch = createApolloFetch({ uri: process.env.GRAPHQL_URL }); const getSubscribedUsers = (symbol, triggerValue) => { return fetch({ query: `query getSubscribedUsers($symbol: String, $triggerValue: numeric) { events(where: {symbol: {_eq: $symbol}, trigger_type: {_eq: "event"}, trigger_value: {_gte: $triggerValue}}) { user_id user_subscription { subscription } } }`, variables: { symbol, triggerValue } }).then(response => response.data.events) } const sendWebpush = (subscription, webpushPayload) => { webPush.sendNotification(subscription, webpushPayload).catch(err => console.log('error while sending webpush', err)) } const handleStockValueTrigger = async (eventData, res) => { const symbol = eventData.data.new.symbol; const triggerValue = eventData.data.new.close; const subscribedUsers = await getSubscribedUsers(symbol, triggerValue); const webpushPayload = { title: `${symbol} - Stock Update`, body: `The price of this stock is ${triggerValue}` } subscribedUsers.map((data) => { sendWebpush(data.user_subscription.subscription, JSON.stringify(webpushPayload)); }) res.json(eventData.toString()); } app.post('/', (req, res) => { const { body } = req const eventType = body.trigger.name const eventData = body.event switch (eventType) { case 'stock-value-trigger': return handleStockValueTrigger(eventData, res); } }); app.get('/', function (req, res) { res.send('Hello World - For Event Triggers, try a POST request?'); }); var server = app.listen(process.env.PORT, function () { console.log("server listening"); });

일부 값으로 주식을 구독하고 테이블에 수동으로 해당 값을 삽입하여 이 흐름을 테스트해 보겠습니다(테스트용)!

값이 2000AMZN 에 가입한 다음 이 값으로 테이블에 데이터 포인트를 삽입했습니다. 주식 알리미 앱이 삽입 직후에 나에게 알림을 보낸 방법은 다음과 같습니다.

테스트를 위해 stock_data 테이블에 행 삽입
테스트를 위해 stock_data 테이블에 행을 삽입합니다. (큰 미리보기)

정돈 된! 여기에서 이벤트 호출 로그를 확인할 수도 있습니다.

이벤트 로그
이벤트 로그. (큰 미리보기)

Webhook이 예상대로 작동하고 있습니다! 이제 이벤트 트리거를 위한 모든 준비가 되었습니다!

예약된/Cron 트리거

다음과 같이 Cron 이벤트 트리거를 사용하여 매시간 구독자 사용자에게 알리는 시간 기반 트리거를 달성할 수 있습니다.

Cron/Scheduled Trigger 설정
Cron/Scheduled Trigger 설정. (큰 미리보기)

동일한 웹훅 URL을 사용하고 트리거 이벤트 유형을 기반으로 구독한 사용자를 stock_price_time_based_trigger 로 처리할 수 있습니다. 구현은 이벤트 기반 트리거와 유사합니다.

결론

이 기사에서는 주가 알림 애플리케이션을 구축했습니다. Alpha Vantage API를 사용하여 가격을 가져오고 Hasura가 지원하는 Postgres 데이터베이스에 데이터 포인트를 저장하는 방법을 배웠습니다. 또한 Hasura GraphQL 엔진을 설정하고 이벤트 기반 및 예약된 트리거를 생성하는 방법도 배웠습니다. 구독한 사용자에게 웹 푸시 알림을 보내기 위한 글리치 프로젝트를 구축했습니다.