[01 문제 상황]
- 기능을 구현해놓고 구체화하며 테스트를 진행하는데, 계속해서 토큰이 만료되어 만료된 토큰에 푸시 알림을 보내려 하면 서버가 죽는 현상이 일어났다.
- 서버가 죽는건 에러 처리를 하지 않았기 때문이므로 try catch문으로 해결해줬는데, 토큰이 만료되는 문제는 내 문제가 아니기 때문에 해결 방법을 몰랐다.
- 구글링 결과, 만료시 pushsubscriptionchange 이벤트리스너로 service worker에게 새로운 구독 정보로 갱신해달라고 하면 되는데 중요한 점은 chorome은 해당 이벤트를 지원하지 않는다는 것이었따..?!
- firefox만 지원하는건 너무하잖아 ㅠㅠ
- 따라서 이런 글들을 읽으며... 나는 어떻게 처리할지 고민했다.
- 실서비스에서 PWA를 사용하면 무조건 이런 문제가 있을텐데 최근글을 많이 못찾았다...
- 스택오버플로우의 어떤 댓글들에서는 chrome도 가능하다고 해서 헷갈리는데 공식적으로는 지원하지 않는게 맞았다.
[02 해결 방안 ]
- 먼저 만료된 구독 정보는 그냥 DB에서 바로 soft delete 처리했다.
- 그러는 동시에 사용자에게 서비스를 다시 구독할 수 있는 QR과 링크를 첨부한 메일을 전송한다.
[03 코드 ]
(1) subscribeService.ts
- 이전에 만들어 둔 모바일 구독 페이지와 QR코드를 첨부해서 email을 전송한다.
import webpush from "web-push";
import { AES } from "crypto-js";
import { Subscribe } from "../db/Subscribe";
import { Schedule } from "../db/Schedule";
import { pushData } from "../interfaces/subscribeInput";
import { HttpException } from "../utils/error-util";
import { makeChecklistToken, makeResubscribeToken } from "../utils/jwt-util";
import { emailUtil } from "../utils/emailUtil";
const SubscribeService = {
pushSupplementSchedules: async (time: Date) => {
const supplementSchedulesDataArray = await Schedule.findByOnlyTime(time);
supplementSchedulesDataArray.forEach(async (scheduleData: any) => {
const supplementArray: string[] = [];
for (const supplement of scheduleData.User.DailySupplements) {
supplementArray.push(supplement.Supplement.name);
}
// Today's Checklist 페이지에 쓸 refresh token(만료 기간 하루) 발급
const checklistToken = makeChecklistToken({
userId: scheduleData.User["pk_user_id"],
});
const pushData: pushData = {
name: scheduleData.User["user_name"],
when: scheduleData.to_do,
supplements: supplementArray.join(", "),
jwtToken: checklistToken,
};
const secretKey: any = process.env.SECRET_KEY;
const notificationData = {
messageType: "supplement",
title: `${pushData.name}님, ${pushData.when} 영양제 드실 시간이에요!`,
body: `${pushData.supplements} 영양제를 복용해주세요.`,
encryptedToken: AES.encrypt(pushData.jwtToken, secretKey).toString(),
};
const subscriptionArray = scheduleData.User.Subscribes;
for (const subscription of subscriptionArray) {
try {
await webpush.sendNotification(
subscription.device_token,
JSON.stringify(notificationData)
);
} catch (error) {
// 사용자 토큰 만료되면 에러 발생
if (error instanceof Error) {
// DB에서 만료된 기기 정보 삭제 후 메일 전송하기
await Subscribe.delete(subscription.device_token);
const resubscribeToken = makeResubscribeToken({
userId: scheduleData.User["pk_user_id"],
});
const emailData = {
user_name: scheduleData.User["user_name"],
email: scheduleData.User["email"],
encryptedToken: AES.encrypt(
resubscribeToken,
secretKey
).toString(),
};
await emailUtil.expirationEmail(emailData);
console.log(
`사용자 ${scheduleData.User["pk_user_id"]}의 토큰 정보가 만료되어 삭제 후 재갱신 메일을 전송했습니다.`
);
}
}
}
});
return supplementSchedulesDataArray;
},
};
export { SubscribeService };
(2) emailUtil.ts
- 메일 내용은 html을 사용할 수 있는데 이번엔 위에서 봤다시피 css를 좀 신경써서 작성해봤다.
- 참고로 inline-style 관련 설정( flex-direction: colume )이 제대로 적용되지 않아 확인해보니 HTML 버전이 낮다나 그래서 결국 padding-left 로 배치를 조정했다.
import nodemailer from "nodemailer";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const transporter = nodemailer.createTransport({
service: "gmail", //사용하고자 하는 서비스
port: 587,
host: "smtp.gmail.com",
secure: true, // true for 587, false for other ports
requireTLS: true,
auth: {
user: process.env.GMAIL_ID, // gmail 주소
pass: process.env.GMAIL_PASSWORD, // gmail 패스워드 혹은 앱 비밀번호
},
});
interface expirationData {
user_name: string;
email: string;
encryptedToken: string;
}
interface pushErrorData {
developers: string[];
time: Date;
errorContent: {
syscall: string;
code: string;
errno: string;
};
}
const emailUtil = {
expirationEmail: async (data: expirationData) => {
const pageLink = `${process.env.APP_MODE}:${process.env.FRONT_PORT}/m/subscribe?token=${data.encryptedToken}`;
const encodedPageLink = encodeURIComponent(
`${process.env.APP_MODE}:${process.env.FRONT_PORT}/m/subscribe?token=${data.encryptedToken}`
);
const QRcode = `https://quickchart.io/qr?text=${encodedPageLink}&ecLevel=L&size=200¢erImageUrl=https://ifh.cc/g/Y4Z5z3.png`;
await transporter
.sendMail({
from: process.env.GMAIL_ID, // 보내는 주소 입력
to: data.email, // 위에서 선언해준 받는사람 이메일
subject: `안녕하세요, Pill my rhythm 서비스입니다.`, // 메일 제목
// 내용
html:
`<div style="display: grid; justify-content: center; align-items: center; width: 100%; height: 100%">` +
`<br />` +
`<p style="color: black"><b>${data.user_name}</b>님 안녕하세요, 회원님의 구독 정보가 <b>만료</b>되었습니다.</p>` +
`<p style="color: black">저희의 서비스에 만족하셨다면 아래의 재구독 버튼이나 QR 코드로 접속하여 다시 <b>구독 버튼</b>을 눌러주세요.</p>` +
`<br />` +
`<div style="width: 620px; height: 250px; border: 4px solid #00dfd7; border-radius: 4px">` +
`<br /><div style="padding-left: 250px">
<p><a href='${pageLink}' target="_blank"><button style="color: #268c88; border: 0px; border-radius: 4px; width: 88pt; height: 20pt">
<b>재구독하러 가기</b>
</button></a></p>` +
`<img src=${QRcode} alt="QRcode" width="120" height="120" /></div>` +
`<div style="padding-left: 180px"><p style="color: #8f8f8f; font-size: 9px">* QR 코드 접속은 Android에서만 가능합니다. (ios는 불가능)</p></div>` +
`<br />
</div>
<br />
</div>`,
})
.then((res) => {
console.log(`${data.email}님께 재구독 요청 메일을 보냈습니다.`);
if (res.rejected.length != 0) {
console.log(`${res.rejected[0]}`);
}
});
},
};
export { emailUtil };
[04 한계점 ]
- 이렇게 해도 사용자가 직접 재구독을 해야 하고, 구독 갱신 주기가 어떻게 되는지도 확실하지 않아 대처를 할수가 없다.
- 그래서 사실 chrome에서 pushsubscriptionchange 이벤트가 지원되는게 가장 좋은데 지원되지 않는게 맞는 것 같지만 됐다는 사람들도 있어 시간 날 때 추가해봐야겠다.
- 추가를 해도 sevice worker가 관리하는 구독 정보를 강제로 만료시킬 방법은 없어서 테스트하기도 힘들다...휴
- 좋은 방법을 알고 있는 분이 나타나서 댓글을 달아주면 좋겠다.
[05 마무리 ]
- 어쨌든 여기까지 해서 내가 웹 푸시 알림 기능을 구현하며 겪었던 시행착오, 구현 순서, 디벨롭한 부분을 꽤나 상세하게 적어봤다.
- 중간 중간 빠진 내용이 있는 것 같긴 한데 처음부터 다시 읽어보며 생각날 때마다 틈틈이 채워넣어야겠다.
'엘리스 AI트랙 4기 > 프로젝트' 카테고리의 다른 글
[PWA] Push Notification 외전1. query string으로 사용자 인증값 전달하기 (feat. AES 암호화) (0) | 2022.07.27 |
---|---|
[PWA] Push Notification 안드로이드 QR 코드로 구독하기 (0) | 2022.07.25 |
[PWA] Push Notification 알림 자동 발송하기 (feat. node-schedule) (1) | 2022.07.18 |
[PWA] Push Notification 상황별 다른 알림 옵션 설정하기 (0) | 2022.07.16 |
[PWA] Push Notification Nodejs 프로젝트에 적용하기 (0) | 2022.07.15 |