웹 소켓이 있는 고성능 Angular 그리드
데이터를 Angular 그리드에 실시간으로 푸시해야 한다는 요구 사항을 접했을 수 있습니다. 브라우저로 데이터를 푸시하려면 WebSocket이라는 기술이 필요합니다. NodeJS 또는 ASP.NET SignalR을 사용하여 구현할 수 있습니다. 이 기사에서는 NodeJS와 함께 웹 소켓을 사용합니다.
데이터를 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'));
소켓을 연결할 때 다음과 같은 작업을 수행합니다:
- Fetching data
- 시작 타이머(이 기능에 대해서는 나중에 포스트에서 다룰 예정입니다)
- 소켓 삭제 시 연결 해제 이벤트
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>
우리가 만든 그리드에서 집중해야 할 몇 가지 사항들:
- 기본적으로 Ignite UI for Angular 그리드에서 가상화가 활성화되어 있습니다.
- 정렬 가능한 속성을 설정하면 해당 열에 대한 정렬을 활성화할 수 있습니다.
- 고정 속성을 설정하면 그리드 왼쪽에 열을 고정할 수 있습니다.
- 데이터 속성을 설정하면 그리드의 데이터 소스를 설정할 수 있습니다.
- <igx-column/>을 사용하면 수동으로 열을 추가할 수 있습니다.
- <igx-column/>의 필드와 헤더는 해당 열의 필드 속성과 헤더를 설정하는 데 사용됩니다.
이제 애플리케이션을 실행하면 그리드가 실시간으로 데이터를 업데이트하고 깜빡임도 없습니다. 그리드가 10밀리초마다 업데이트되는 것을 알게 될 것입니다. 아래와 같이 실시간으로 데이터가 업데이트되는 그리드가 있어야 합니다:

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