본문 바로가기
프로젝트

Nest.js, SSE(server sent event)를 이용해 알림띄우기(해결안됨)

by 해룸 2024. 4. 22.

1. 개요

이번에 구현할 서비스는 사용자가 작성한 댓글과 컬렉션에 각각 좋아요나 북마크 갯수가 일정 수 이상 추가됐을시 알림을 띄워주는 기능이다. 마이페이지의 알림 버튼을 누르면 사용자와 관련된 알림이 뜨게 하고싶은게 이번 구현 목표

추가로는 사용자를 다른 사용자가 팔로우 했을때도 알림을 띄우고싶다. 이건 아마 댓글, 북마크와 비슷할거같아서 먼저 끝내놓고 구현해 볼 계획이다.

 

이 기능을 구현하기 위해 가장 먼저 떠오른건 소켓이다. 웹소켓은 양방향 통신을 이용해 대표적으로는 채팅서비스를 만들때 많이 사용하는 기술이다. 하지만 서치를 좀 해본 결과, 내가 구현할 기능은 양방향 통신이 필요없을것으로 예상되었고 보통 알림서비스를 만들기 위해서는 SSE를 많이 사용한다는것을 알게 되었다. 그럼 둘의 차이는 무엇일까?

 

2. Socket vs SSE

Socket의 장단점 및 특징

  • 양방향 통신
  • 낮은 지연시간(연결 후 추가적인 헤더 교환이 없으므로 효율적)
  • 데이터를 자주 주고받는 상황이 아닌 경우, 오히려 리소스 소모가 큼

SSE의 장단점 및 특징

  • 단방향 통신
  • 간단한 구현(소켓에 비해 간단)
  • 데이터 전송 제한(텍스트 데이터만을 전송할 수 있음)
  • 클라이언트의 연결이 끊겨도 알 수 없음

현재 내가 구현하고 싶은 기능은 양방향이 필요없으며, 최종 프로젝트 발표까지 시간도 많이 남지 않았다. 그러므로 리소스 소모가 크지않은 방법을 택하는것이 좋을것이다. 그래서 SSE로 구현해보는것으로 결정했다.

 

3. SSE 알림 구현

구상해본 알림 로직은 다음과 같다.

알림 이벤트가 발생하는 시점은 좋아요 또는 북마크를 추가 할 때이다. 

100개 이하일때는 20개의 배수씩, 100개 이상이라면 50개의 배수씩으로 조건을 설정해 SSE 이벤트를 호출한다.

사용자는 알림 버튼을 누르면 SSE 이벤트를 구독할 수 있고, 좋아요 수가 업데이트 되면 이벤트를 푸쉬한다.

SSE 이벤트에서 유저 id를 필터링하고 해당 유저에게만 알림을 보낸다.

 

//review.service.ts - 좋아요 api
...생략
     if (findReview.likeCount <= 100) {
        if (findReview.likeCount % 20 == 0) {
          this.sseEvent(
            findReview.webContentId,
            findReview.userId,
            findReview.likeCount,
          );
        }
      } else {
        if (findReview.likeCount % 50 == 0) {
          this.sseEvent(
            findReview.webContentId,
            findReview.userId,
            findReview.likeCount,
          );
        }
      }

조건에 맞을경우 sseEvent를 호출한다.

//review.service.ts
  async sseEvent(webContentId: number, userId: number, likeCount: number) {
    const webContent = await this.webContentRepository.findOne({
      where: { id: webContentId },
    });
    this.sseService.emitReviewLikeCountEvent(
      webContent.title,
      userId,
      likeCount,
    );
  }

여기서는 webContent.title, userId, likeCount를 sseService로 넘겨준다.

 

//sse.service.ts
@Injectable()
export class SseService {
  private commentLikes$: Subject<any> = new Subject();
  private collectionLikes$: Subject<any> = new Subject();

  private commentObserver = this.commentLikes$.asObservable();
  private collectionObserver = this.collectionLikes$.asObservable();

  // 이벤트 발생 함수
  emitReviewLikeCountEvent(
    webContent: string,
    userId: number,
    likeCount: number,
  ) {
    console.log(userId);
    this.commentLikes$.next({ webContent, id: userId, likeCount });
  }

  // 컬렉션 좋아요 이벤트 발생 함수
  emitCollectionLikeCountEvent(
    webContent: string,
    userId: number,
    likeCount: number,
  ) {
    // 컬렉션 좋아요 이벤트를 발생시킴

    this.collectionLikes$.next({ webContent, id: userId, likeCount });
  }

  sendAlarm(userId: number): Observable<any> {
    console.log(userId);
    return this.commentObserver.pipe(
      filter((user) => user.id === userId),
      map((user) => {
        console.log(user);
        return {
          data: {
            message: `${user.webContent}에 작성한 댓글 좋아요 수 ${user.likeCount}개를 달성했습니다.`,
          },
        };
      }),
    );
    ...생략

이벤트 발생함수가 next를 통해 Observer로 인수를 넘겨주고.. 

user front에서는 sendAlarm을 구독한 상태에서 이벤트가 발생하면 data를 return하는게 내 기대값이었다.

//sse.controller.ts
@Controller('sse')
export class SseController {
  constructor(private readonly sseService: SseService) {}

  @Sse(':userId')
  sse(@Param('userId') userId: string): Observable<MessageEvent> {
    return this.sseService.sendAlarm(+userId);
  }
}
//sse.js - 알림버튼과 연결된 js
$(document).ready(function () {
  $('#profile-alarm-col').click(function () {
    var userId = $('#userId').text();

    let eventSource = new EventSource(`http://localhost:3000/sse/${userId}`);
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      console.log('Received data:', data);
    };

    // SSE 연결이 열렸을 때
    eventSource.onopen = () => {
      console.log('SSE connection opened');
    };

    // SSE 연결이 닫혔을 때
    eventSource.onclose = () => {
      console.log('SSE connection closed');
    };

    // SSE 연결 에러가 발생했을 때
    eventSource.onerror = (error) => {
      console.error('SSE connection error:', error);
    };

 

이 코드를 통해 단순히 바로 메시지를 전달하는건 확인했다.(네스트 공홈에 있는 예시.. 인터발로 hello world 1초에 한번 찍어내는거)

그런데 내가 원하는대로 이벤트가 발생하면 next 로 가서 Observer 메시지를 전달하는거.. 그게 구현이 안된다.

아마도 둘이 연결이 잘 안된거같은데, 아직 해결하지 못해서 일단 여기까지 기록으로 남겨보려한다. 

'프로젝트' 카테고리의 다른 글

api를 이용한 스크래핑 성능 개선  (0) 2024.04.15
퍼펫티어를 이용한 크롤링(2)  (0) 2024.04.11
퍼펫티어를 이용한 크롤링(1)  (0) 2024.04.11
최종프로젝트  (0) 2024.04.09
db 선택  (0) 2024.03.27