GraphQL 및 Postgres로 실시간 차트 작성

게시 됨: 2022-03-10
빠른 요약 ↬ 데이터를 차트와 다이어그램으로 시각화하는 것만큼 데이터를 이해하는 더 좋은 방법은 없습니다. JS 커뮤니티에는 데이터 시각화를 더 쉽게 만드는 훌륭한 오픈 소스 프로젝트가 몇 가지 있지만 이러한 차트를 뒷받침하고 실시간으로 만들 수 있는 실시간 백엔드를 구축하기 위한 솔루션은 없었습니다. GraphQL(실시간 구독에 대해 잘 정의된 사양이 있음)을 사용하면 몇 초 안에 실시간 백엔드를 실행하고 실시간 차트를 구동하는 데 사용할 수 있습니다.

차트는 데이터를 다루는 모든 산업에서 없어서는 안될 부분입니다. 차트는 투표 및 투표 업계에서 유용하며 우리와 함께 일하는 사용자 및 클라이언트의 다양한 행동과 특성을 더 잘 이해하는 데에도 유용합니다.

실시간 차트가 왜 중요한가요? 음, 새로운 데이터가 지속적으로 생성되는 경우에 유용합니다. 예를 들어, 주가를 시각화하기 위해 실시간 시계열을 사용할 때 실시간 차트에 매우 유용합니다. 이 튜토리얼에서는 정확히 이 특정 작업에 적합한 오픈 소스 기술로 실시간 차트를 작성하는 방법을 설명합니다.

참고 : 이 튜토리얼은 React 및 GraphQL에 대한 기본 지식이 필요합니다.

스택

  1. PostgreSQL
    차트 사용의 핵심은 "거대한" 볼륨 데이터를 시각화하는 것입니다. 따라서 대용량 데이터를 효율적으로 처리하고 재구성할 수 있는 직관적인 API를 제공하는 데이터베이스가 필요합니다. SQL 데이터베이스를 사용하면 데이터를 추상화하고 집계하는 보기를 만들 수 있습니다. 우리는 오랜 시간 검증된 고효율 데이터베이스인 Postgres를 사용할 것입니다. 또한 Timescale 및 PostGIS와 같은 멋진 오픈 소스 확장 기능을 통해 지리적 위치 기반 및 시계열 기반 차트를 각각 작성할 수 있습니다. 시계열 차트를 작성하기 위해 Timescale을 사용할 것입니다.
  2. GraphQL 엔진
    이 게시물은 실시간 차트 작성에 관한 것이며 GraphQL에는 실시간 구독에 대한 잘 정의된 사양이 함께 제공됩니다. Hasura GraphQL 엔진은 Postgres 연결을 사용하고 실시간 GraphQL을 통해 Postgres 데이터를 쿼리할 수 있는 오픈 소스 GraphQL 서버입니다. 또한 사용자 지정 액세스 제어 규칙에 따라 데이터를 제한하는 데 도움이 되는 액세스 제어 레이어가 함께 제공됩니다.
  3. 차트JS
    ChartJS는 JavaScript로 차트를 작성하기 위해 잘 유지되고 있는 인기 있는 오픈 소스 라이브러리입니다. 우리는 chart.js 를 ReactJS 추상화 react-chartjs-2 와 함께 사용할 것입니다. React를 사용하는 이유는 React가 개발자에게 직관적인 이벤트 기반 API를 제공하기 때문입니다. 또한 React의 단방향 데이터 흐름은 데이터 기반 차트를 작성하는 데 이상적입니다.
점프 후 더! 아래에서 계속 읽기 ↓

요구 사항

이 자습서의 경우 시스템에 다음이 필요합니다.

  1. 도커 CE
    Docker는 애플리케이션을 컨테이너화할 수 있는 소프트웨어입니다. 도커 이미지는 종속성 및 최소한의 운영 체제와 함께 소프트웨어를 포함하는 독립적인 패킷입니다. 이러한 도커 이미지는 기술적으로 도커가 설치된 모든 시스템에서 실행할 수 있습니다. 이 튜토리얼에는 도커가 필요합니다.
    • 도커에 대해 더 읽어보기
    • 도커 설치
  2. npm: npm은 JavaScript용 패키지 관리입니다.

데모

현재 시점에서 지난 20분 동안 5초 간격으로 위치의 최고 온도를 보여주는 다음 라이브 시계열 차트를 작성합니다.

실시간 차트의 GIF 데모
실시간 차트의 GIF 데모

백엔드 설정

서비스 실행

백엔드는 Postgres 데이터베이스, 타임스케일 확장 및 Hasura GraphQL 엔진으로 구성됩니다. 각각의 도커 이미지를 실행하여 데이터베이스와 GraphQL 서버를 실행하도록 합시다. docker-compose.yaml 이라는 파일을 만들고 이 내용을 붙여넣습니다.

참고 : docker-compose 는 여러 도커 이미지를 선언적으로 실행하는 유틸리티입니다.

 version: '2' services: timescale: image: timescale/timescaledb:latest-pg10 restart: always environment: POSTGRES_PASSWORD: postgrespassword volumes: - db_data:/var/lib/postgresql/data graphql-engine: image: hasura/graphql-engine:v1.0.0-alpha38 ports: - "8080:8080" depends_on: - "timescale" restart: always environment: HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@timescale:5432/postgres HASURA_GRAPHQL_ACCESS_KEY: mylongsecretkey command: - graphql-engine - serve - --enable-console volumes: db_data:

docker-compose.yaml 에는 두 가지 서비스에 대한 사양이 포함되어 있습니다.

  1. timescale
    이것은 Timescale 확장이 설치된 Postgres 데이터베이스입니다. 포트 5432에서 실행되도록 구성됩니다.
  2. graphql-engine
    이것은 Hasura GraphQL Engine 인스턴스, 즉 데이터베이스를 가리키고 GraphQL API를 제공하는 GraphQL 서버입니다. 포트 8080에서 실행되도록 구성되며 포트 8080은 이 도커 컨테이너가 실행되는 시스템의 포트 8080에 매핑됩니다. 이것은 컴퓨터의 localhost:8080 을 통해 이 GraphQL 서버에 액세스할 수 있음을 의미합니다.

docker-compose.yaml 을 배치한 곳마다 다음 명령을 실행하여 이러한 도커 컨테이너를 실행해 보겠습니다.

 docker-compose up -d

이 명령은 클라우드에서 도커 이미지를 가져와서 지정된 순서대로 실행합니다. 인터넷 속도에 따라 몇 초 정도 걸릴 수 있습니다. 완료되면 https://localhost:8080/console 에서 GraphQL 엔진 콘솔에 액세스할 수 있습니다.

Hasura GraphQL 엔진 콘솔
Hasura GraphQL 엔진 콘솔(큰 미리보기)

데이터베이스 설정

다음으로 서로 다른 시간의 온도 값을 저장하는 온도라는 테이블을 만들어 보겠습니다. 콘솔에서 데이터 탭으로 이동하고 SQL 섹션으로 이동합니다. 다음 SQL 블록을 실행하여 temperature 테이블을 생성합니다.

 CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );

이렇게 하면 데이터베이스에 간단한 Postgres 테이블이 생성됩니다. 그러나 우리는 Timescale 확장의 시간 간격 분할을 활용하고자 합니다. 이렇게 하려면 SQL 명령을 실행하여 이 테이블을 타임스케일의 하이퍼테이블로 변환해야 합니다.

 SELECT create_hypertable('temperature', 'recorded_at');

이 명령은 recorded_at 된_at 필드에서 시간으로 분할된 하이퍼테이블을 생성합니다.

이제 이 테이블이 생성되었으므로 이 테이블에 대해 GraphQL 쿼리를 직접 시작할 수 있습니다. 상단의 GraphiQL 탭을 클릭하여 사용해 볼 수 있습니다. 먼저 돌연변이를 만들어 보세요:

 mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }

위의 GraphQL 돌연변이는 temperature 테이블에 행을 삽입합니다. 이제 GraphQL 쿼리를 만들어 데이터가 삽입되었는지 확인합니다.

그런 다음 쿼리를 만들어 보세요.

 query { temperature { recorded_at temperature location } }

효과가 있기를 바랍니다 :)

이제 우리가 해야 할 일은 현재 순간부터 지난 20분 동안 5초 간격으로 위치의 최고 온도를 보여주는 실시간 시계열 차트를 만드는 것입니다. 정확히 이 데이터를 제공하는 뷰를 생성해 보겠습니다.

 CREATE VIEW last_20_min_temp AS ( SELECT time_bucket('5 seconds', recorded_at) AS five_sec_interval, location, MAX(temperature) AS max_temp FROM temperature WHERE recorded_at > NOW() - interval '20 minutes' GROUP BY five_sec_interval, location ORDER BY five_sec_interval ASC );

이 보기는 temperature 테이블의 데이터를 최대 온도( max_temp) 와 함께 5초 창으로 그룹화합니다. 2차 그룹화는 location 필드를 사용하여 수행됩니다. 이 모든 데이터는 현재 시점에서 지난 20분의 데이터입니다.

그게 다야 백엔드가 설정되었습니다. 이제 멋진 실시간 차트를 만들어 보겠습니다.

프론트엔드

Hello GraphQL 구독

GraphQL 구독은 본질적으로 "라이브" GraphQL 쿼리입니다. WebSocket을 통해 작동하며 GraphQL 쿼리와 동일한 응답 구조를 갖습니다. https://localhost:8080/console 로 돌아가서 우리가 만든 뷰에 GraphQL 구독을 시도합니다.

 subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }

이 구독은 위치가 London 인 뷰의 데이터를 구독하고 five_second_intervals 오름차순으로 정렬됩니다.

당연히 뷰의 응답은 지난 20분 동안 데이터베이스에 아무 것도 삽입하지 않았기 때문에 빈 배열이 됩니다. (이 섹션에 20분 이내에 도달하면 언젠가 다시 삽입한 항목을 볼 수 있습니다.)

 { "data": { "last_20_min_temp": [] } }

이 구독을 유지하고 다른 탭을 열고 이전에 수행한 것과 동일한 변형을 사용하여 temperatures 테이블에 다른 값을 삽입해 보십시오. 삽입 후 구독이 있었던 탭으로 돌아가면 응답이 자동으로 업데이트된 것을 볼 수 있습니다. 이것이 GraphQL Engine이 제공하는 실시간 마술입니다. 이 구독을 사용하여 실시간 차트를 강화해 보겠습니다.

Create-React-App 시작하기

create react 앱을 사용하여 React 앱 스타터를 빠르게 시작하겠습니다. 다음 명령을 실행합니다.

 npx create-react-app time-series-chart

그러면 빈 시작 프로젝트가 생성됩니다. cd 하고 GraphQL 및 차트 라이브러리를 설치합니다. 또한 타임스탬프를 사람이 읽을 수 있는 형식으로 변환하기 위한 순간을 설치합니다.

 cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment

마지막으로 npm start 로 앱을 실행하면 기본 React 앱이 https://localhost:3000 에서 열립니다.

원시 생성 반응 앱
원시 creat-react-app(큰 미리보기)

클라이언트 측 GraphQL용 Apollo 클라이언트 설정

Apollo 클라이언트는 현재 모든 GraphQL 호환 서버에서 작동하는 최고의 GraphQL 클라이언트입니다. 릴레이 모던도 좋지만 서버는 릴레이 모던의 모든 이점을 활용하기 위해 릴레이 사양을 지원해야 합니다. 이 튜토리얼에서는 클라이언트 측 GraphQL에 Apollo 클라이언트를 사용할 것입니다. 앱에 Apollo 클라이언트를 제공하기 위한 설정을 수행해 보겠습니다.

다음 코드 조각은 문서에서 직접 가져오기 때문에 이 설정의 미묘함은 다루지 않습니다. React 앱 디렉토리의 src/index.js 로 이동하여 Apollo 클라이언트를 인스턴스화하고 이 코드 조각을 ReactDOM.render 위에 추가합니다.

 import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache });

마지막으로 AppApolloProvider 안에 래핑하여 자식 구성 요소에서 Apollo 클라이언트를 사용할 수 있도록 합니다. App.js 는 최종적으로 다음과 같아야 합니다.

 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache }); ReactDOM.render( ( <ApolloProvider client={client}> <App /> </ApolloProvider> ), document.getElementById('root') );

Apollo 클라이언트가 설정되었습니다. 이제 앱에서 실시간 GraphQL을 쉽게 사용할 수 있습니다. src/App.js 로 이동합니다.

차트 작성

ChartJS는 차트 작성을 위한 꽤 깔끔한 API를 제공합니다. 우리는 꺾은 선형 차트를 만들 것입니다. 따라서 꺾은선형 차트는 다음 형식의 데이터를 예상합니다.

 { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }

위의 데이터 세트가 꺾은선형 차트를 렌더링하는 데 사용되는 경우 다음과 같이 보일 것입니다.

샘플 라인 차트
샘플 라인 차트(큰 미리보기)

먼저 이 샘플 차트를 작성해 보겠습니다. react-chartjs-2 에서 Line 을 임포트하고 위의 객체를 data prop으로 전달하여 렌더링합니다. render 메소드는 다음과 같을 것입니다:

 render() { const data = { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], } return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Line data={data} /> </div> ); }

다음으로 보기의 데이터를 구독하고 선형 차트에 공급합니다. 그러나 클라이언트에서 구독을 어떻게 수행합니까?

Apollo의 <Subscription> 구성 요소는 구성 요소의 자식이 구독 데이터의 컨텍스트로 렌더링되는 렌더링 소품 패턴을 사용하여 작동합니다.

 <Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>

Subscription 구성 요소 중 하나를 사용하여 뷰를 구독한 다음 구독 데이터를 ChartJS가 예상하는 구조로 변환해 보겠습니다. 변환 논리는 다음과 같습니다.

 let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); })

참고 : GraphQL 응답의 데이터를 ChartJS가 예상하는 형식으로 변환하기 위해 오픈 소스 라이브러리인 graphq2chartjs를 사용할 수도 있습니다.

구독 구성 요소 내에서 이것을 사용한 후 App.js 는 다음과 같습니다.

 import React, { Component } from 'react'; import { Line } from 'react-chartjs-2'; import { Subscription } from 'react-apollo'; import gql from 'graphql-tag'; import moment from 'moment'; const TWENTY_MIN_TEMP_SUBSCRIPTION= gql' subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } } ' class App extends Component { render() { return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Subscription subscription={TWENTY_MIN_TEMP_SUBSCRIPTION}> { ({data, error, loading}) => { if (error) { console.error(error); return "Error"; } if (loading) { return "Loading"; } let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); }) return ( <Line data={chartJSData} options={{ animation: {duration: 0}, scales: { yAxes: [{ticks: { min: 5, max: 20 }}]} }} /> ); } } </Subscription> </div> ); } } export default App;

https://localhost:3000 에 완벽하게 작동하는 실시간 차트가 준비되어 있습니다. 그러나 비어 있으므로 실제로 마술이 일어나는 것을 볼 수 있도록 일부 샘플 데이터를 채우겠습니다.

참고 : ChartJS의 멋진 애니메이션이 마음에 들지 않기 때문에 꺾은선형 차트에 몇 가지 옵션을 더 추가했습니다. 시계열은 단순할 때 멋지게 보이지만 원하는 경우 옵션 소품을 제거할 수 있습니다.

샘플 데이터 삽입

더미 데이터로 데이터베이스를 채우는 스크립트를 작성해 보겠습니다. 별도의 디렉터리(이 앱 외부)를 만들고 다음 내용으로 script.js 라는 파일을 만듭니다.

 const fetch = require('node-fetch'); setInterval( () => { const randomTemp = (Math.random() * 5) + 10; fetch( `https://localhost:8080/v1alpha1/graphql`, { method: 'POST', body: JSON.stringify({ query: ` mutation ($temp: numeric) { insert_temperature ( objects: [{ temperature: $temp location: "London" }] ) { returning { recorded_at temperature } } } `, variables: { temp: randomTemp } }) } ).then((resp) => resp.json().then((respObj) => console.log(JSON.stringify(respObj, null, 2)))); }, 2000 );

이제 다음 두 명령을 실행합니다.

 npm install --save node-fetch node script.js

https://localhost:3000 으로 돌아가서 차트 업데이트를 볼 수 있습니다.

마무리

위에서 논의한 아이디어를 사용하여 대부분의 실시간 차트를 작성할 수 있습니다. 알고리즘은 다음과 같습니다.

  1. Postgres로 GraphQL 엔진 배포
  2. 데이터를 저장하려는 테이블을 만듭니다.
  3. React 앱에서 해당 테이블을 구독하십시오.
  4. 차트를 렌더링합니다.

여기에서 소스 코드를 찾을 수 있습니다.