avatar
LEGU
푸시알림 날리기
OO씨에게 보내는 푸시알림
NestJSFCMChatGPTTaskScheduling
May 23
·
9 min read

- 뚱땅뚱땅 평화로운(?) 푸시알림 기능 개발 시간 -

예약 발송 기능이 필요해 방법을 알아보고 있었다.

어찌저찌 구글링을 통해 NestJS Task Scheduling - Dynamic timeouts에 대해 알게 되었다 !

스케줄러는 이미 사용해본 경험이 있었는데(매일 @@시 ##분 마다 동작 등), 동적으로 필요에 따라 생성했다 없앴다 할 수도 있었다니 ! (문서를 끝까지 읽어본 적이 없는 사람)

사용 방법은 어렵지 않아서 내가 원하는 타이밍에 푸시알림을 발송할 수 있게 되었다.
너무너무 단순하다! 진짜로

즉시 발송과 예약 발송 모두 잘 작동하는 것을 확인하고 필요한 곳 마다(6~7군데 정도) 알림 발송 코드를 때려 박았는데, 막상 이렇게 해놓고 나니 나중에 수정이 필요하거나 또 푸시알림이 필요한 곳이 생기면 같은 작업을 반복해야한다고 생각하니 좀 간지가 안나는 것 같아서 정리를 좀 하기로 했다.

일단 내가 원하는 것이 무엇인가 정리

  • "특정 상황"에 유저에게 푸시알림을 보내야함 (FCM 사용)

  • 즉시발송/예약발송

  • 현재는 6~7가지 상황이 있지만 점점 늘어날 수 있음

  • 특별한 경우를 제외하고 어드민 페이지에서 푸시알림을 관리하고 싶음

    • 발송상황, 발송타이밍, 메시지 내용 관리 / 발송 내역 관리 등

  • 위 내용은 단일 대상에게 자동으로 보내는 알림. 다수에게 보내는 수동 알림은 따로 처리

필요한 api 마다 푸시알림을 쏘는 코드를 심는 것 보다는 한 지점(?)에서 처리하고 싶었는데 그러기 위해선 어떤 요청이 들어왔는지 구분하고 푸시알림이 필요한 요청 및 상황인지 판단, 알맞은 대상 및 내용 구성을 할 수 있어야 했다.

어떤 요청인지("특정 상황")는 요청 end point로 구분할 수 있다.

발송 대상은 요청에 jwt 토큰도 있고 앱에서 오는 요청이 아닌 경우(서비스 특성 상 서버가 임베디드와도 통신 중) 식별 가능한 데이터가 함께 담겨 있어 어렵지 않았다.

내용 역시 왔다갔다하는 데이터들로 충분히 구성할 수 있다.

즉시발송/예약발송은 0초 뒤 발송, n초 뒤 발송 으로 설정이 가능하다.

이런 점들을 바탕으로 DB를 꾸려보았다. 푸시알림을 보내길 원하는 상황과 내용을 미리 등록해둘 수 있도록.

{
   알림발송상황: "유저가 ~~한 상황";
   endPoint: "/request/endpoint";
   n밀리초뒤발송: 300000;
   푸시알림제목: "이것은";
   푸시알림내용: "name씨에게 보내는 푸시알림";
   내용에들어가는변수이름: ["name"];
   클릭시이동url: "어디어디";
}

문제는 한 곳에서 처리하려면 과연 그 한 곳은 어디로 해야 좋을까 였다. 미들웨어, 인터셉터 등 뭔가 이것저것 알긴 아는데 정확한 작동 방식이나 그런 것을 아직 완전히 깨우치지 못한 상태라 GPT씨에게 상담 요청을 했다.

위 상황들을 죽 설명하고 어쩌면 좋을지 물어보니 그(?)(그녀?) 역시 미들웨어나 인터셉터에 심을 것을 추천해주었는데, 간단한 테스트와 모든 데이터를 처리한 뒤 응답을 보내기 직전에 푸시알림을 쏘고 싶다는 추가 질문으로 인터셉터를 활용하는 것으로 확정을 지었다.

이렇게 내가 원하던 대로 '한 곳' 까지 정해졌다.

- 뚱땅뚱땅 코딩 타임 -

그렇게 뚱땅뚱땅 코드를 작성해놓고 테스트를 하다보니 문제점들이 발견되었다.

  • n시간 뒤 어떤 사람에게 발송하도록 예약해둔 메시지를 만약 그 사람이 특정 조건을 만족한 경우 예약을 취소해야 함

  • 예약을 해둔 메시지들이 만약 서버가 재시작되면 스케줄러가 초기화되면서 사라짐

여러 고민과 GPT씨와의 상담 끝에 두 가지 문제점 모두 스케줄러와 DB를 함께 활용해 해결할 수 있다는 결론을 내렸다.

첫번째 문제는 고유한 예약 번호와 함께 푸시알림 내역을 저장해뒀다가 원하는 상황에 예약을 취소할 수 있도록 했다. 예약을 취소하게 되는 상황은 당장에는 한가지 상황 뿐이라 중앙처리 말고 해당 api에서 하도록 했다.

나중에 조금 더 발전시킬 필요가 있을 것 같다.

두번째 문제는 발송 상태[발송완료/발송예약/발송취소]를 함께 저장해뒀다가 OnApplicationBootstrap을 사용해 서버가 재시작될 때 발송예약 상태인 메시지들을 다시 스케줄러에 집어넣어주도록 했다.

어차피 발송 내역을 저장하기로 해서 DB도 이미 구성해뒀기 때문에 몇 개의 필드만 더 추가하면서 사건을 해결할 수 있었다.

대략적인 형태

import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { firebaseAdmin } from 'src/config/firebase.config';
import { SchedulerRegistry } from '@nestjs/schedule';
@Injectable()
export class PushMessageService implements OnApplicationBootstrap {
	constructor(private readonly schedulerRegistry: SchedulerRegistry) {}
async onApplicationBootstrap() {
	// 발송예약 상태인 메시지들을 다시 스케줄러에 등록
}

async fcmSendMessage(message, sendAfterMilliSeconds, timeoutName, history) {
	const timeout = setTimeout(async () => {
		await firebaseAdmin
			.messaging()
			.sendEachForMulticast(message)
			.then((res) => {
				console.log('success: ', res);
		})
		.catch((err) => {
			console.log('error: ', err);
		});        
    	// history(발송내역)를 사용해 에약발송인 경우 발송예약 > 발송완료로 업데이트
    	// ~ code ~
	
    	// 발송한 뒤에 스케줄러에서 삭제
		this.schedulerRegistry.deleteTimeout(timeoutName);
		}, sendAfterMilliSeconds);
	this.schedulerRegistry.addTimeout(timeoutName, timeout);
}

async sendAutoPushMessage(endPoint) {
	// 인터셉터에서 호출해 사용
	// 푸시알림 보낼지 말지 판단 후 내용 구성해 발송 예약
}}

이런식으로 구현을 해놓았지만 솔직히 지금도 마음에 썩 들지는 않아서 계속 고민 중이다. 조금 더 깔끔하게 할 수 있을 것 같은데 막 아이디어가 떠오르지는 않아서 다른거 할 거 하면서 계속 지켜보는 중이다..







뚱땅뚱땅 gpt씨와 공부하기