들어가기 전
회사에서 운영 중인 웹 서비스는 사용자에게 알림을 제공하는 기능이 있었다. 기존에는 HTTP GET 요청을 사용해 데이터를 가져오는 방식을 사용하였으나, 실시간으로 반영해달라는 사업부에 요청이 있었고, 이를 위해 Socket을 사용하는 건 오버스펙이라고 판단하였고, 운영 자원이 많지 않아 Polling으로 5초 단위로 API를 호출하는 방식으로 처리하였다.
시간이 지나고 서비스 사용자가 많아짐에 따라 Polling 방식은 너무 사용자의 수에 따라 과도하게 API를 요청하는 문제가 있었기에 이를 개선하기 위해 SSE를 도입하였다.
SSE
SSE는 Server Side Event의 약자로, HTTP 통신을 이용하여 서버에서 클라이언트에게 이벤트를 발송하는 기법이다. 이는 양방향 통신이 아닌 단방향 통신이기에 Socket보다 가볍지만 서버에서만 발송할 수 있다는 점이 있었고 이는 알림 서비스의 Polling을 제거하기에 좋은 대안이였다.
SSE 기법은 클라이언트에서 HTTP GET Request를 이용하여 서버와의 연결을 활성화하고, 연결이 끊어지기 전까지 서버에서 자유롭게 이벤트를 발송한다.
NestJS에서 SSE 구현
NestJS 공식 문서에는 SSE에 대한 구현 방법이 명시되어있다.
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea
docs.nestjs.com
먼저 연결을 생성하기 위한 Controller를 구성한다.
import { Sse, MessageEvent, Controller } from "@nestjs/common";
import { interval, map, Observable } from "rxjs";
@Controller({ version: "1", path: "/app" })
export class AppController {
@Sse("sse")
sse(): Observable<MessageEvent> {
return interval(1000).pipe(map((_) => ({ data: { hello: "world" } })));
}
}
문서에 따르면 무조건 리턴 타입은 Observable이어야 한다고한다. 그리고 Postman을 이용하여 해당 endpoint에 GET 요청을 보내면 아래와 같이 연결이 생기며 1초에 1번씩 아래 데이터가 반환되고 있음을 확인할 수 있다.
{
"hello": "world"
}
비즈니스 로직 내에서 사용자에게 Message를 발송하고 싶다면 EventEmitter2를 사용하면 된다. 또한 SSE API에서는 from Event를 사용하여 수신하면 메세지를 발송하게 처리한다.
export class AppService {
constructor(private readonly eventEmitter: EventEmitter2) {}
public async push(event: { message: string; userId: number }): Promise<void> {
this.eventEmitter.emit("sse.push", event);
}
}
@Controller({ version: "1", path: "/app" })
export class AppController {
@Sse("sse")
sse(): Observable<MessageEvent> {
return fromEvent(this.eventEmitter, "ses.push").pipe(map((_data) => _data));
}
}
만약 특정 유저에게만 발송을 하고 싶다면, Request에 유저 식별 값을 받아서 아래와 같이 처리 가능하다.
@Controller({ version: "1", path: "/app" })
export class AppController {
@Sse("sse/:userId")
sse(@Param("userId") userId: number): Observable<MessageEvent> {
return fromEvent(this.eventEmitter, "sse").pipe(
map((_data) => {
if (_data.userId == userId) {
return _data;
}
})
);
}
}
참고 자료
https://docs.nestjs.com/techniques/server-sent-events
https://medium.com/@leejm.dev/nestjs-%EB%A1%9C-sse-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0-feat-%EC%97%AC%EB%9F%AC%EA%B0%9C%EC%9D%98-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-b25ac46e26cf
'B.E > Nest JS' 카테고리의 다른 글
node-optional를 이용한 Exception 관리 (0) | 2024.10.18 |
---|---|
NestJS Request Lifecycle (0) | 2024.05.16 |
NestJS에서 typeorm-transactional를 사용하여 트랜잭션 관리 (0) | 2024.05.13 |
NestJs Event 처리(@nestjs/event-emitter) (0) | 2024.04.11 |
NestJS V10 마이그레이션 + SWC (0) | 2023.10.11 |