내용으로 건너뛰기
웹 소켓이 있는 고성능 Angular 그리드

웹 소켓이 있는 고성능 Angular 그리드

데이터를 Angular 그리드에 실시간으로 푸시해야 한다는 요구 사항을 접했을 수 있습니다. 브라우저로 데이터를 푸시하려면 WebSocket이라는 기술이 필요합니다. NodeJS 또는 ASP.NET SignalR을 사용하여 구현할 수 있습니다. 이 기사에서는 NodeJS와 함께 웹 소켓을 사용합니다.

10min read

데이터를 Angular 그리드에 실시간으로 푸시해야 한다는 요구 사항을 접했을 수 있습니다. 브라우저로 데이터를 푸시하려면 WebSocket이라는 기술이 필요합니다. NodeJS 또는 ASP.NET SignalR을 사용하여 구현할 수 있습니다. 이 기사에서는 NodeJS와 함께 웹 소켓을 사용합니다.

이 글의 전반부에서는 웹 소켓을 이용해 데이터를 클라이언트에 푸시하는 API를 만들고, 후반부에서는 이를 소비할 Angular 애플리케이션을 만들 것입니다. Angular 애플리케이션에서는 Ignite UI for Angular Grid를 사용할 것입니다.  하지만 간단한 HTML 테이블을 사용해 웹 소켓에서 실시간으로 데이터를 소비할 수도 있습니다. 이 글에서는 NodeJS Web Socket의 HTML 테이블과 Angular Data Grid를 Ignite UI 실시간으로 데이터를 소비하는 방법을 배울 것입니다. 이 두 접근법에서 성능 차이를 목격할 것입니다.

Ignite UI for Angular에 대해 더 알아보실 수 있습니다.

NodeJS API

NodeJS API를 만드는 것부터 시작해 보겠습니다. 빈 폴더를 만들고 package.json 이라는 파일을 추가하세요. package.json에서 다음 종속성을 추가한다

  • Core-js
  • Express
  • io

대략 package.json 파일은 다음과 같을 것입니다:

{
  "name": "demo1",
  "version": "1.0.0",
  "description": "nodejs web socket demo",
  "main": "server.js",
  "dependencies": {
    "core-js": "^2.4.1",
    "express": "^4.16.2",
    "socket.io": "^2.0.4"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Dhananjay Kumar",
  "license": "ISC"
}

관계형 데이터베이스, No SQL 데이터베이스 등 어떤 종류의 데이터베이스에서도 데이터를 가져올 수 있습니다. 하지만 이 글에서는 간단하게 유지하고 data.js 파일에 하드코딩된 데이터를 넣으려고 합니다. 이 파일은 JSON 배열을 내보내며, 웹 소켓과 타이머를 사용해 이를 푸시할 것입니다.

data.js라는 폴더에 파일을 추가하고 다음 코드를 넣으세요.

data.js

module.exports = {
    data: TradeBlotterCDS()
};
 
function TradeBlotterCDS() {
    return [
        {
            "TradeId": "1",
            "TradeDate": "11/02/2016",
            "BuySell": "Sell",
            "Notional": "50000000",
            "Coupon": "500",
            "Currency": "EUR",
            "ReferenceEntity": "Linde Aktiengesellschaft",
            "Ticker": "LINDE",
            "ShortName": "Linde AG",
            "Counterparty": "MUFJ",
            "MaturityDate": "20/03/2023",
            "EffectiveDate": "12/02/2016",
            "Tenor": "7",
            "RedEntityCode": "DI537C",
            "EntityCusip": "D50348",
            "EntityType": "Corp",
            "Jurisdiction": "Germany",
            "Sector": "Basic Materials",
            "Trader": "Yael Rich",
            "Status": "Pending"
        }
        // ... other rows of data 
    ]
}

1200행 데이터도 여기에서 확인할 수 있습니다.

data.js 파일에서 TradeBlotter 데이터를 반환하고 있습니다. 프로젝트 폴더에는 두 개의 파일이 있을 거예요: package.json와 data.js

이 시점에서 npm install 명령어를 실행해 파일에 언급된 모든 의존성을 설치package.json. 명령어를 실행하면 프로젝트 폴더에 node_modules 폴더가 생깁니다.  또한 프로젝트에 server.js 파일을 추가하세요.  이 모든 단계를 거치면 프로젝트 구조에는 다음 파일과 폴더가 있어야 합니다.

  • js
  • js
  • Node_modules folder

server.js에서는 먼저 필요한 모듈을 가져오는 것부터 시작할 것입니다.

const express = require('express'),
    app = express(),
    server = require('http').createServer(app);
io = require('socket.io')(server);
let timerId = null,
    sockets = new Set();
var tradedata = require('./data');

필요한 모듈이 임포트된 후에는 다음과 같이 경로 사용 익스프레스를 추가하세요:

app.use(express.static(__dirname + '/dist'));

소켓을 연결할 때 다음과 같은 작업을 수행합니다:

  1. Fetching data
  2. 시작 타이머(이 기능에 대해서는 나중에 포스트에서 다룰 예정입니다)
  3. 소켓 삭제 시 연결 해제 이벤트
io.on('connection', socket => {
 
    console.log(`Socket ${socket.id} added`);
    localdata = tradedata.data;
    sockets.add(socket);
    if (!timerId) {
        startTimer();
    }
    socket.on('clientdata', data => {
        console.log(data);
    });
    socket.on('disconnect', () => {
        console.log(`Deleting socket: ${socket.id}`);
        sockets.delete(socket);
        console.log(`Remaining sockets: ${sockets.size}`);
    });
 
});

다음으로 startTimer() 함수를 구현해야 합니다. 이 함수에서는 JavaScript setInterval() 함수를 사용하여 각 10밀리초 시간 프레임마다 데이터를 방출합니다.

function startTimer() {
    timerId = setInterval(() => {
        if (!sockets.size) {
            clearInterval(timerId);
            timerId = null;
            console.log(`Timer stopped`);
        }
        updateData();
        for (const s of sockets) {
            s.emit('data', { data: localdata });
        }
 
    }, 10);
}

우리는 데이터를 업데이트하는 함수 updateData()를 호출하고 있습니다. 이 함수에서는 로컬 데이터를 반복하며 Coupon 속성과 Notional, 두 속성을 범위 간의 난수로 업데이트합니다.

function updateData() {
    localdata.forEach(
        (a) => {
            a.Coupon = getRandomInt(10, 500);
            a.Notional = getRandomInt(1000000, 7000000);
        });
}

We have implemented getRandomInit function as shown below:

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
}

모든 것을 합치면 다음과 같은 코드가 sever.js 있어야 합니다

Server.js

const express = require('express'),
    app = express(),
    server = require('http').createServer(app);
io = require('socket.io')(server);
let timerId = null,
    sockets = new Set();
var tradedata = require('./data');
 
var localdata;
 
app.use(express.static(__dirname + '/dist'));
 
io.on('connection', socket => {
 
    console.log(`Socket ${socket.id} added`);
    localdata = tradedata.data;
    sockets.add(socket);
    if (!timerId) {
        startTimer();
    }
    socket.on('clientdata', data => {
        console.log(data);
    });
    socket.on('disconnect', () => {
        console.log(`Deleting socket: ${socket.id}`);
        sockets.delete(socket);
        console.log(`Remaining sockets: ${sockets.size}`);
    });
 
});
 
function startTimer() {
    timerId = setInterval(() => {
        if (!sockets.size) {
            clearInterval(timerId);
            timerId = null;
            console.log(`Timer stopped`);
        }
        updateData();
        for (const s of sockets) {
            s.emit('data', { data: localdata });
        }
 
    }, 10);
}
 
function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
}
 
function updateData() {
    localdata.forEach(
        (a) => {
            a.Coupon = getRandomInt(10, 500);
            a.Notional = getRandomInt(1000000, 7000000);
        });
}
 
server.listen(8080);
console.log('Visit http://localhost:8080 in your browser');

NodeJS에서 웹 소켓을 만들었는데, 이 시스템은 10밀리초마다 데이터 청크를 반환합니다.

Creating Angular Application

이 단계에서는 Angular 애플리케이션을 만들어 봅시다. Angular CLI를 사용해 애플리케이션을 만들고 Grid Ignite UI for Angular 추가할 예정입니다. 아래 기사를 따라 Angular 애플리케이션을 만들고 Ignite UI for Angular 그리드를 추가하세요.

위 글을 따르고 계시다면, API를 소비할 서비스를 만드는 3단계에서 변경 Angular 필요합니다.

먼저 Angular 프로젝트에 socket.io-client 설치부터 시작하겠습니다. 이를 위해 npm 설치를 실행하세요,

npm i socket.io-client

NodeJS Web Socket과의 연결을 생성하기 위해 Angular 서비스를 작성할 예정입니다. app.service.ts 시작해 수입부터 시작해 보겠습니다.

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { map, catchError } from 'rxjs/operators';
import * as socketIo from 'socket.io-client';
import { Socket } from './interfaces';

필요한 모듈을 수입했습니다. 나중에 파일 내에서 소켓 타입이 어떻게 정의되는지 보interface.ts 것입니다. 다음으로, 웹 소켓에 연결을 만들고 응답에서 다음 데이터를 가져오겠습니다. 웹 소켓에서 다음 데이터 청크를 반환하기 전에 이를 Observable으로 변환합니다.

getQuotes(): Observable < any > {
    this.socket = socketIo('http://localhost:8080');
    this.socket.on('data', (res) => {
        this.observer.next(res.data);
    });
    return this.createObservable();
}
 
createObservable(): Observable < any > {
    return new Observable<any>(observer => {
        this.observer = observer;
    });
}

위의 두 함수는 웹 소켓에 연결하고, 데이터 청크를 가져오며, 이를 관측 가능한 것으로 변환합니다. 모든 것을 합치면 아래와 app.service.ts 같습니다:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { map, catchError } from 'rxjs/operators';
import * as socketIo from 'socket.io-client';
import { Socket } from './interfaces';
 
@Injectable()
export class AppService {
 
    socket: Socket;
    observer: Observer<any>;
 
    getQuotes(): Observable<any> {
        this.socket = socketIo('http://localhost:8080');
        this.socket.on('data', (res) => {
            this.observer.next(res.data);
        });
        return this.createObservable();
    }
 
    createObservable(): Observable<any> {
        return new Observable<any>(observer => {
            this.observer = observer;
        });
    }
 
    private handleError(error) {
        console.error('server error:', error);
        if (error.error instanceof Error) {
            let errMessage = error.error.message;
            return Observable.throw(errMessage);
        }
        return Observable.throw(error || 'Socket.io server error');
    }
 
}

서비스에서는 Socket이라는 타입을 사용하고 있습니다. 우리는 아래와 같이 파일 interfaces.ts에서 이 유형을 만들었습니다:

export interface Socket {
    on(event: string, callback: (data: any) => void);
    emit(event: string, data: any);
}

이제 NodeJS 웹 소켓에 연결하고 API에서 데이터를 가져올 수 있는 서비스가 준비되어 Angular 서비스가 준비되었습니다.

이는 일반적인 Angular 서비스이며, 일반적인 방식으로 구성 요소로 소비할 수 있습니다. 모듈에서 importin으로 시작하고, 아래에 보이는 컴포넌트 구성자에 이를 주입합니다:

constructor(private dataService: AppService) { }

OnInit 생애주기에서 데이터 가져오기 위해 서비스 메서드를 호출할 수 있습니다,

ngOnInit() {
    this.sub = this.dataService.getQuotes()
        .subscribe(quote => {
            this.stockQuote = quote;
            console.log(this.stockQuote);
        });
}

모든 것을 합치면 구성 요소 클래스는 아래와 같습니다.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { AppService } from './app.service';
import { Subscription } from 'rxjs/Subscription';
 
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
 
    stockQuote: number;
    sub: Subscription;
    columns: number;
    rows: number;
    selectedTicker: string;
 
    constructor(private dataService: AppService) { }
 
    ngOnInit() {
        this.sub = this.dataService.getQuotes()
            .subscribe(quote => {
                this.stockQuote = quote;
                console.log(this.stockQuote);
            });
    }
    ngOnDestroy() {
        this.sub.unsubscribe();
    }
}

중요한 점 중 하나는 OnDestroy 라이프사이클 훅에서 해당 컴포넌트의 관찰 가능한 반환 데이터를 구독 해제하고 있다는 것입니다.  템플릿에서는 아래 표로 데이터를 렌더링하기만 하면 됩니다:

<table>
    <tr *ngFor="let f of stockQuote">
        <td>{{f.TradeId}}</td>
        <td>{{f.TradeDate}}</td>
        <td>{{f.BuySell}}</td>
        <td>{{f.Notional}}</td>
        <td>{{f.Coupon}}</td>
        <td>{{f.Currency}}</td>
        <td>{{f.ReferenceEntity}}</td>
        <td>{{f.Ticker}}</td>
        <td>{{f.ShortName}}</td>
    </tr>
</table>

일반 HTML 테이블에서 데이터를 실시간으로 렌더링하기 때문에 깜빡임이나 성능 문제가 발생할 수 있습니다. HTML 테이블을 그리드로 대체해 봅시 Ignite UI for Angular.

Learn more about Ignite UI for Angular Grid here: https://ko.infragistics.com/products/ignite-ui-angular/angular/components/grid.html.

아래 Angular Grid Ignite UI 적용할 수 있습니다. igxGrid의 데이터 소스를 데이터 속성 바인딩으로 설정한 후 수동으로 열을 그리드에 추가했습니다.

<igx-grid [width]="'1172px'" #grid1 id="grid1" [rowHeight]="30" [data]="stockQuote"
          [height]="'600px'" [autoGenerate]="false">
    <igx-column [pinned]="true" [sortable]="true" width="50px" field="TradeId" header="Trade Id" [dataType]="'number'"> </igx-column>
    <igx-column [sortable]="true" width="120px" field="TradeDate" header="Trade Date" dataType="string"></igx-column>
    <igx-column width="70px" field="BuySell" header="Buy Sell" dataType="string"></igx-column>
    <igx-column [sortable]="true" [dataType]="'number'" width="110px" field="Notional" header="Notional">
    </igx-column>
    <igx-column width="120px" [sortable]="true" field="Coupon" header="Coupon" dataType="number"></igx-column>
    <igx-column [sortable]="true" width="100px" field="Price" header="Price" dataType="number">
    </igx-column>
    <igx-column width="100px" field="Currency" header="Currency" dataType="string"></igx-column>
    <igx-column width="350px" field="ReferenceEntity" header="Reference Entity" dataType="string"></igx-column>
    <igx-column [sortable]="true" [pinned]="true" width="130px" field="Ticker" header="Ticker" dataType="string"></igx-column>
    <igx-column width="350px" field="ShortName" header="Short Name" dataType="string"></igx-column>
</igx-grid>

우리가 만든 그리드에서 집중해야 할 몇 가지 사항들:

  1. 기본적으로 Ignite UI for Angular 그리드에서 가상화가 활성화되어 있습니다.
  2. 정렬 가능한 속성을 설정하면 해당 열에 대한 정렬을 활성화할 수 있습니다.
  3. 고정 속성을 설정하면 그리드 왼쪽에 열을 고정할 수 있습니다.
  4. 데이터 속성을 설정하면 그리드의 데이터 소스를 설정할 수 있습니다.
  5. <igx-column/>을 사용하면 수동으로 열을 추가할 수 있습니다.
  6. <igx-column/>의 필드와 헤더는 해당 열의 필드 속성과 헤더를 설정하는 데 사용됩니다.

이제 애플리케이션을 실행하면 그리드가 실시간으로 데이터를 업데이트하고 깜빡임도 없습니다. 그리드가 10밀리초마다 업데이트되는 것을 알게 될 것입니다. 아래와 같이 실시간으로 데이터가 업데이트되는 그리드가 있어야 합니다:

그리드를 실행하며 데이터가 실시간으로 업데이트되는 방식이 보입니다.

이렇게 하면 NodeJS Web Socket API를 통해 애플리케이션에서 실시간으로 데이터를 푸시할 수 Angular. 이 글이 도움이 되길 바랍니다. 이 글이 마음에 드셨다면 꼭 공유해 주세요. 또한, 아직 Infragistics Ignite UI for Angular Components를 확인하지 않으셨다면 꼭 확인해 보세요! 웹 앱을 더 빠르게 코딩할 수 있도록 50+ Material 기반 Angular 컴포넌트를 갖추고 있습니다.

데모 요청