-뚱땅뚱땅 평화로운(?) 푸시알림 기능 개발 시간-
예약 발송 기능이 필요해 방법을 알아보고 있었다.
어찌저찌 구글링을 통해 NestJS Task Scheduling - Dynamic timeouts에 대해 알게 되었다 !
스케줄러는 이미 사용해본 경험이 있었는데(매일 @@시 ##분 마다 동작 등), 동적으로 필요에 따라 생성했다 없앴다 할 수도 있었다니 ! (문서를 끝까지 읽어본 적이 없는 사람)
사용 방법은 어렵지 않아서 내가 원하는 타이밍에 푸시알림을 발송할 수 있게 되었다.
너무너무 단순하다! 진짜로
즉시 발송과 예약 발송 모두 잘 작동하는 것을 확인하고 필요한 곳 마다(6~7군데 정도) 알림 발송 코드를 때려 박았는데, 막상 이렇게 해놓고 나니 나중에 수정이 필요하거나 또 푸시알림이 필요한 곳이 생기면 같은 작업을 반복해야한다고 생각하니 좀 간지가 안나는 것 같아서 정리를 좀 하기로 했다.
일단 내가 원하는 것이 무엇인가 정리
필요한 api 마다 푸시알림을 쏘는 코드를 심는 것 보다는 한 지점(?)에서 처리하고 싶었는데 그러기 위해선 어떤 요청이 들어왔는지 구분하고 푸시알림이 필요한 요청 및 상황인지 판단, 알맞은 대상 및 내용 구성을 할 수 있어야 했다.
어떤 요청인지("특정 상황")는 요청 end point로 구분할 수 있다.
발송 대상은 요청에 jwt 토큰도 있고 앱에서 오는 요청이 아닌 경우(서비스 특성 상 서버가 임베디드와도 통신 중) 식별 가능한 데이터가 함께 담겨 있어 어렵지 않았다.
내용 역시 왔다갔다하는 데이터들로 충분히 구성할 수 있다.
즉시발송/예약발송은 0초 뒤 발송, n초 뒤 발송 으로 설정이 가능하다.
이런 점들을 바탕으로 DB를 꾸려보았다. 푸시알림을 보내길 원하는 상황과 내용을 미리 등록해둘 수 있도록.
{
알림발송상황: "유저가 ~~한 상황";
endPoint: "/request/endpoint";
n밀리초뒤발송: 300000;
푸시알림제목: "이것은";
푸시알림내용: "name씨에게 보내는 푸시알림";
내용에들어가는변수이름: ["name"];
클릭시이동url: "어디어디";
}
문제는 한 곳에서 처리하려면 과연 그 한 곳은 어디로 해야 좋을까 였다. 미들웨어, 인터셉터 등 뭔가 이것저것 알긴 아는데 정확한 작동 방식이나 그런 것을 아직 완전히 깨우치지 못한 상태라 GPT씨에게 상담 요청을 했다.
위 상황들을 죽 설명하고 어쩌면 좋을지 물어보니 그(?)(그녀?) 역시 미들웨어나 인터셉터에 심을 것을 추천해주었는데, 간단한 테스트와 모든 데이터를 처리한 뒤 응답을 보내기 직전에 푸시알림을 쏘고 싶다는 추가 질문으로 인터셉터를 활용하는 것으로 확정을 지었다.
이렇게 내가 원하던 대로 '한 곳' 까지 정해졌다.
-뚱땅뚱땅 코딩 타임-
그렇게 뚱땅뚱땅 코드를 작성해놓고 테스트를 하다보니 문제점들이 발견되었다.
여러 고민과 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) {
// 인터셉터에서 호출해 사용
// 푸시알림 보낼지 말지 판단 후 내용 구성해 발송 예약
}
}
이런식으로 구현을 해놓았지만 솔직히 지금도 마음에 썩 들지는 않아서 계속 고민 중이다. 조금 더 깔끔하게 할 수 있을 것 같은데 막 아이디어가 떠오르지는 않아서 다른거 할 거 하면서 계속 지켜보는 중이다..