본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Node.js/Nestjs (Nest.js)

Nestjs 프레임워크 서버(emitter, rxjs, schedule) -15

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2022. 9. 22.

 

#1. Emitter, rxjs

Nestjs에서 재미있는 기능을 뽑으라면 에미터(emitter : 방출하다) 기능 입니다.

에미터라는 기능은 리엑트나 앵귤러를 한 사람에게는 친숙한 기능인데..

특정  이벤트를 동작시켜 해당 동작을 바라보거나, 구독하는 대상에게 내용을 전달 해 주는 기능 입니다.

프론트 프레임워크 기준으로는, 앵귤러의 rxjs 기능이며 리엑트는 레덕스(또는 모빅스) 기능과 유사 합니다.

 

이걸 서버코드에서 이걸 어떻게 적용할지 참 고민이 많았습니다만, 아직 멋진 방법을 못했습니다...^^;

백문이 불여일견~!

필요한 라이브러리를 설치하여 줍니다.

npm install @nestjs/event-emitter

 

다음으로 모듈에서 에미터 기능을 사용하기 위해 추가를 해 줍니다.

* 파일이름 : app.module.ts

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

import { AppController } from './app.controller';
import {AppService} from './app.service';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [
    EventEmitterModule.forRoot(),    //이벤트에미터 사용
  ],
  controllers: [
    AppController
  ],
  providers: [AppService, ],
})
export class AppModule {}

 

에미터(emitter)는 이벤트를 발생하는 기능 1개, 해당 이벤트를 받는기능 1개로 구성되어 있습니다.

이벤트를 방출(emit)하면 받아서(OnEvent) 사용자가 정의한 내용에 따라 동작합니다.

이를 위해 일반 서비스 클래스에서 에미터를 보내도록 해 보았습니다.

* 파일이름 : app.service.rs

import { Injectable } from '@nestjs/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class AppService {

  constructor(private event: EventEmitter2) {}

  //event-emitter 방식의 데이터 전달
  public addDataEmitter(eventName : string, data : any) {
    this.event.emit(eventName, data);
  }

}

 

이제 받는 클래스를 만들어 주도록 합니다.

받는 클래스 내용은 OnEvent라는 데코레이터를 붙여주면 쉽게 사용 할 수 있습니다.

* 파일이름 : 이벤트를받는컨트롤러.rs

import { Request, Response } from 'express';
import { Controller, All,   Res, Req, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';
import { OnEvent } from '@nestjs/event-emitter';
@Controller()
export class 이벤트를받는컨트롤러 {

  constructor(private service : AppService){

  }

  @All('/data')
  default(@Req() req: Request, @Res() res: Response){
    res.status(HttpStatus.OK).send({ result : 1234 });
  }  

  @OnEvent('event1')
  private reciver(arg : any) : void{
    console.log('이벤트를받는컨트롤러에서 event-emitter 이벤트(event1) 수신 : ',arg);
  }
 
  @OnEvent('event2')
  private reciver2(arg : any) : void{
    console.log('이벤트를받는컨트롤러에서 event-emitter 이벤트(event2) 수신 : ',arg);
  }  
}

 

"event1" 이라는 이름 "event2" 라는 이름에 동작하게 하였습니다.

주의 해야되는 점은, 이벤트를 받는 함수는 데이터를 응답받는 기능에 쓸 수 없습니다.

* Post, Get, All 이러한 데코레이터와 같이 못씁니다

그러므로 위 코드는 컨트롤러에 기능을 붙였지만 가급적 서비스에서 쓰는건 어떨까 생각 해 봅니다.

 

이제 이벤트를 발생시키는 컨트롤러를 만들어 봅니다.

* 파일이름 : app.controller.ts

import { Request, Response } from 'express';
import { Controller, All,   Res, Req, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';


@Controller()
export class AppController {

  constructor(private service : AppService){  

  }

  @All('/')
  default(@Req() req: Request, @Res() res: Response){
    res.status(HttpStatus.OK).send({ result : 1234 });
    let data = {name:'text', number : 12345};
    this.service.addDataEmitter('event1', data);
  }  
 
}

 

일반 요청이 동작하면 서비스에 만들어놓은 addDataEmitter 함수를 동작하게 하였습니다.

해당 함수는 "event1" 이라는 이름의 이벤트 입니다.

서버를 실행하여 일반접속을 해 봅니다.

오호!

 

"app.controller"에서 "이벤트를받는컨트롤러" 한테 이벤트를 정상적으로 전달 해 준 것을 볼 수 있습니다.

물론 단순한 데이터 뿐만 아니라 모든 객체를 전달 할 수 있습니다!

위 단계는 아래와 같습니다.

1. "app.service" 에서 이벤트를 주는 함수를 만듬
2. "app.controller" 에서 app.service에서 만든 함수를 통해 이벤트 발생
3. "이벤트를받는컨트롤러" 에서 이벤트 수신

 

이와 비슷한 기능으로는 rxjs가 있습니다.

rxjs는 nestjs에 기본으로 들어가 있는 패키지 입니다.

rxjs와의 문법 비교를 위해 app.service를 조금 수정 해 보도록 합니다.

* 파일이름 : app.service.rs

import { Injectable } from '@nestjs/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class AppService {
  constructor(private event: EventEmitter2) {}

  //#1. rxjs 형식
  private FACTORY: BehaviorSubject<any> = new BehaviorSubject([]); //발행기관
  public readonly TV: Observable<any> = this.FACTORY.asObservable(); //발행 기관에서 구독기관 만들기

  //데이터 발행하기
  public addDataRxjs(info?: any): void {
    this.FACTORY.next([...this.FACTORY.value, info]);
  }

  //#2. event-emitter 방식의 데이터 전달
  public addDataEmitter(eventName : string, data : any) {
    this.event.emit(eventName, data);
  }

}

 

rxjs는 발행자(publisher)와 구독자(subscriber) 형태로 기능이 구분되어 있으며,

app.service 서비스는 발행자(publisher)의 기능으로 되어 있습니다.

이를 구독하는 클래스는 아래처럼 만들어 줍니다.

* 파일이름 : 이벤트를받는컨트롤러.rs

import { Request, Response } from 'express';
import { Controller, All,   Res, Req, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';
import { OnEvent } from '@nestjs/event-emitter';
@Controller()
export class 이벤트를받는컨트롤러 {

  constructor(private service : AppService){
    service.TV.subscribe( arg=>{
        console.log('이벤트를받는컨트롤러에서 rxjs 이벤트 수신 : ',arg);
    });
  }

  @All('/data')
  default(@Req() req: Request, @Res() res: Response){
    res.status(HttpStatus.OK).send({ result : 1234 });
  }  

  @OnEvent('event1')
  private reciver(arg : any) : void{
    console.log('이벤트를받는컨트롤러에서 event-emitter 이벤트(event1) 수신 : ',arg);
  }
 
  @OnEvent('event2')
  private reciver2(arg : any) : void{
    console.log('이벤트를받는컨트롤러에서 event-emitter 이벤트(event2) 수신 : ',arg);
  }  
}

 

rxjs의 특징은 "구독한다(subscribe)" 라는 함수를 한번 호출하면 계속해서 기능이 동작한다는 점 입니다.

rxjs와 emitter의 차이는 아래 코드를 작성한 뒤 확인 가능 합니다.

* 파일이름 : app.controller.ts

import { Request, Response } from 'express';
import { Controller, All,   Res, Req, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';


@Controller()
export class AppController {

  constructor(private service : AppService){  
    let data = {name:'text', number : 12345};
    this.service.addDataRxjs(data);
    this.service.addDataEmitter('event1', data);
  }

  @All('/')
  default(@Req() req: Request, @Res() res: Response){
    res.status(HttpStatus.OK).send({ result : 1234 });
    let data = {name:'text', number : 12345};
    this.service.addDataRxjs(data);
    this.service.addDataEmitter('event1', data);
  }  
 
}

 

app.controller에서 생성자에서 바로 기능이 동작하게 하였습니다.

이를 실행 해 본 모습 입니다.

 

당연한 이야기지만, 생성자에서 동작을 하게 하였으므로 웹페이지 접근의 요청 없이도 사용자가 정의한 행동이 동작하는 걸 볼 수 있습니다.

이러한 에미터와 rxjs를 활용 한 다면 데이터의 변경, 반응에 따른 방식으로의 개발패턴을 바꿀 수 있을 것 입니다!

좋은 구조가 뭐가 있는지 누가좀...

 

#2. Schedule

서버 어플리케이션이 해야되는 일 중의 하나가 바로 스케줄링(scheduling)입니다.

역시나 필요한 라이브러리를 설치하여 줍니다.

npm install @nestjs/schedule
npm install -dev @types/cron

 

스케줄러의 클래스 형태는 서비스의 형태와 같습니다.

스케줄러의 클래스를 만드려면 Injectable 데코레이터를 붙여 준 클래스 이여야 합니다.

그러므로 만들어준 스케줄러 클래스는 모듈의 providers에 추가해야합니다.

* 파일이름 : app.module.ts

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

import { AppController } from './app.controller';
import {AppService} from './app.service';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { 이벤트를받는컨트롤러 } from './이벤트를받는컨트롤러';
import { 스케줄러 } from './스케줄러';

@Module({
  imports: [
    EventEmitterModule.forRoot(),    //이벤트에미터 사용
    ScheduleModule.forRoot()  //스케줄 사용
  ],
  controllers: [
    AppController,
    이벤트를받는컨트롤러
  ],
  providers: [AppService, 스케줄러 ],  //스케줄러
})
export class AppModule {}

 

import 부분에 스케줄러 모듈을 사용하기 위한 설정을 해 주었습니다.

이제 아직 만들지 않는 "스케줄러" 클래스를 만들어 줍니다.

* 파일이름 : 스케줄러.ts

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, Interval, SchedulerRegistry, Timeout } from '@nestjs/schedule';
import { CronJob } from 'cron';
import * as moment from 'moment'; //npm install moment

@Injectable()
export class 스케줄러 {
  constructor(private registry: SchedulerRegistry) {}

  @Cron('*/10 * * * * *', { name: '1번' })
  스케줄10초() {
    let day: string = moment(new Date()).format('HH:mm:ss');
    console.log('1번 동작', day);
  }
  
}

 

스케줄러 클래스에서 스케줄링 데코레이터에서 Cron이라는 데코레이터를 사용 해 보았습니다.

Cron 데코레이터는 크론탭 형식의 표현식으로 내용을 채울 수 있습니다.

위 코드는 10초마다 함수가 동작하는 코드 입니다.

기능을 몇개 더 추가하여 보았습니다.

* 파일이름 : 스케줄러.ts

  //생략...

  @Cron('*/10 * * * * *', { name: '1번' })
  스케줄10초() {
    let day: string = moment(new Date()).format('HH:mm:ss');
    console.log('1번 동작', day);
  }

  @Cron(CronExpression.EVERY_10_SECONDS, { name: '2번' })
  스케줄10초버전2() {
    let day: string = moment(new Date()).format('HH:mm:ss');
    console.log('2번 동작', day);
  }

  @Interval('3번', 5000)
  인터벌형식() {
    let day: string = moment(new Date()).format('HH:mm:ss');
    console.log('3번 동작', day);
  }

 

가운데 크론형식은 크론탭 표현식을 잘 모르는(?)  분들을 위한 enum 형태의 자료입니다.

Interval 데코레이터는 사용자가 지정한 초단위(1000 = 1초)로 동작하는 스케줄러 입니다.

요렇게 많은 표현식을 볼 수 있습니다..ㅋ

 

선언적인 스케줄링을 위해서는 데코레이터를 사용하면 가능하지만, 가끔 스케줄을 동적으로 추가하거나 제거하고 싶을 때가 종종..있습니다 ...거..진짜요?

동작하는 스케줄을 종료하거나 추가하는 경우에는 생성자로 의존성을 주입받은 SchedulerRegistry 클래스를 사용 해 주도록 합니다.

* 파일이름 : 스케줄러.ts

  //생략...
  
  @Interval('4번', 20000)
  종료해보기() {
    const map: Map<string, CronJob> = this.registry.getCronJobs();
    map.forEach((job, key) => {
      console.log('제거 : ',key);
      this.registry.deleteCronJob(key);
    });

    const inter: string[] = this.registry.getIntervals();
    inter.forEach((key) => {
      if (key !== '4번') {
        console.log('제거 : ',key);
        this.registry.deleteInterval(key);
      }
    });
    console.log('-- 종료 스케줄 동작 완료 --');
  }

  @Timeout('타임아웃', 25000)
  추가해보기() {
    //새로운 cron 형식의 스케줄러를 생성 합니다.
    const name: string = '새로만든크론잡';
    const job = new CronJob(`*/5 * * * * *`, () => {
      let day: string = moment(new Date()).format('HH:mm:ss');
      console.log(`${name} 동작`, day);
    });
    this.registry.addCronJob(name, job);
    job.start();

    //새로운 interval 형식의 스케줄러를 생성 합니다.
    const name2: string = '새로만든인터벌잡';
    const interval = setInterval(() => {
      let day: string = moment(new Date()).format('HH:mm:ss');
      console.log(`${name2} 동작`, day);
    }, 5000);
    this.registry.addInterval(name2, interval);
  }

 

생성하고 삭제하는 스케줄링 기능을 붙여보았습니다.

스케줄을 정확하게 제거하기 위해서는 이름이 필요하기 때 문에 위 샘플코드 대부분에 이름을 표기하였습니다.

여기서 재미있게도 스케줄을 추가하는 메서드인 "추가해보기"에서는 Timeout 이라는 데코레이터를 사용되었습니다.

Timeout 데코레이터를 사용하면 지정된 초단위 시간(1000=1초)에 해당 기능을 1번만 동작하게 할 수 있습니다.'

잘 동작하네요!

 

스케줄 데코레이터를 적용받는 메서드는 해당 메서드의 동작이 끝나기 전 까지 다른동작을 하지 않습니다.

A라는 스케줄을 동작해야되는 메서드가 동작이 안끝났는데 다시 스케줄을 동작해야되는 시점이 오더라도 해당 시점은 건너뛰게 됩니다(sync 효과!)

샘플 소스코드에 wait이라는 함수를 만들어 놓았으므로 해당 함수를 원하는 스케줄러에 붙여 테스트 해 보면 쉽게 이해할 수 있습니다!

 

위 내용에 사용된 코드는 아래 제 깃허브에서 받아볼 수 있습니다.

https://github.com/TaeSeungRyu/NestProject/tree/main/step14

 

GitHub - TaeSeungRyu/NestProject: Nestjs 프로젝트

Nestjs 프로젝트. Contribute to TaeSeungRyu/NestProject development by creating an account on GitHub.

github.com

 

공부하면 할 수록 재미있고 즐거운 nestjs!!!
다음 포스팅에서는 로깅에 대해서 살펴보겠습니다.
궁금한점 또는 틀린부분은 언제든 연락 주세요!

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글