- 이전 포스팅에서 Nodejs 서버에서 Push 알림 전송에 성공했으니, 이제 이 Push 알림을 내가 원하는 형태와 동작으로 구체화하기 위한 작업을 시작한다!
- 처음 기획할 때 체크리스트 기능을 푸시 알림을 통해 접근할 수 있으면 좋겠다고 생각했고, 그 기능을 완벽히 구현했다. 별 거 없지만 그 방법을 작성해보겠다.
[01 Options ]
- Service Worker가 showNotification 메소드를 실행할 때는 여러가지 옵션들을 설정해줄 수 있다.
- 종류가 꽤 많은데 자주 쓸만한 건 다음과 같다.
- body : 알람 본문에 표시될 내용
- icon : 알람에 표시될 앱 아이콘, 보통 웹 사이트의 로고나 정체성을 나타내는 아이콘을 사용
- badge : 모바일(Android)에서만 동작하는 속성, 알람을 받을 때 상단 상태바에 표시될 조그만 아이콘, 설정을 안하면 브라우저 모양이 표시됨
- image : 알람 창 안에 이미지를 삽입
- actions : 알람에 특정 동작을 수행하는 버튼을 추가
- tag : 새로운 알람이 뜰 때 이전 알람의 태그 값을 참고하여 이전 알람은 지우고 새로운 알람을 띄우게 하는 역할, 한 마디로 딱 한 번의 알람 전송을 보장함
- vibrate : 모바일(Android)에서만 동작하는 속성, 알람이 표시될 때의 진동 패턴, 무음 모드가 아니어야 함
- sound : 모바일(Android)에서만 동작하는 속성, 알람이 표시될 때 나는 소리
- data : 알람창에 임의의 정보를 전달, 어떤 타입이든 가능
- requireInteraction : true로 설정하면 PC 같이 큰 화면에서는 사용자가 닫기를 누를 때까지 알람창이 사라지지 않음
- 이외에도 여러 종류가 있어 이 문서를 읽어보고 본인이 구현하고자 하는 기능에 필요한 걸 갖다 쓰면 될 것 같다.
- 그리고 아래 참고한 블로그를 가면 사진과 함께 설명이 있어 이해하기 쉽다.
참고 : https://joshua1988.github.io/web-development/pwa/pwa-push-noti-guide/#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8B%9C%EB%B8%8C-%EC%9B%B9%EC%95%B1%EC%9D%B4%EB%9E%80
[02 옵션 적용 ]
(1) 옵션 설정
- 일단 나는 sendNotification 을 전송할 때 messageType 값을 전송해 상황에 따른 옵션 사용을 나눠주었다.
- infoMessageOptions 는 구독 / 구독 취소시 전송되는 알람에 사용하며 icon, badge, vibrate, body 옵션만 사용한다.
- supplementMessageOptions 는 영양제 복용 시간에 전송되는 알람에 사용하며 tag, actions, data, requireInteraction 옵션을 추가로 사용했다.
- 각각의 actions에도 버튼 앞에 넣을 icon을 설정해줄 수 있지만 안예쁠 것 같아 넣지 않았다.
- data 옵션은 actions 버튼을 눌렀을 때 전달해줄 수 있는 정보를 담을 수 있다.
const infoMessageOptions = {
icon: "./icon-192x192.png",
badge: "./badge-72x72.png", // android에서만 보임
vibrate: [200, 100, 200, 100, 200, 100, 200], // android에서만 동작
body: data.body,
};
const supplementMessageOptions = {
icon: "./icon-192x192.png",
badge: "./badge-72x72.png", // android에서만 보임
vibrate: [200, 100, 200, 100, 200, 100, 200], // android에서만 동작
body: data.body,
tag: "supplement",
actions: [
{
action: "homepage-action",
title: "Pill my rhythm",
// icon: "/images/demos/action-1-128x128.png",
},
{
action: "checklist-action",
title: "Today's Checklist",
// icon: "/images/demos/action-4-128x128.png",
},
],
// data로 action 실행 시 객체 전송 가능
data: {
encryptedToken: data.encryptedToken,
},
requireInteraction: true, // chrome과 같이 충분히 큰 창에서 사용자가 직접 닫을 때까지 알림 사라지지 않음
};
- 여기서 잠깐, tag 옵션은 필요에 따라 너무너무너무 중요하다!
💥 Push Notification 알림이 여러 개씩 오는 문제
후에 배포까지 완료한 뒤에 테스트를 해보니 같은 Push 알림이 여러 개가 연달아 오는 문제가 발생했다.
그런데 더 이상한건 사람에 따라, 기기에 따라, 경우에 따라 그 개수가 모두 다르다는 것...
생각해 볼 수 있는 문제는
◽ 코드의 중첩
◽ cron job 요청 횟수
◽ cront job 함수의 중첩
◽ service worker 문제
등이 있었는데 하나씩 생각해봐도 문제가 될만한 게 없었다... service worker 빼고
근데 service worker는 이상하게 작동해도 어떻게 고쳐야하는지를 몰랐으니..!
폭풍 검색에 들어갔고 이런 글을 확인했다.
tag라는 옵션이 개별 푸시 알림의 unique함을 보장해서 하나씩만 전송되게 한다는 것이었다.
문서를 보면 tag에 대한 설명이 An ID for a given notification that allows you to find, replace, or remove the notification using a script if necessary. 라고 나와있는데 이런 설명으로 그런 용도라고 어떻게... 알아먹는지..
어쨌든 이 해결 방법이 최선인지는 모르겠지만, local에서 실행시 문제 없이 모두에게 하나씩만 전송되기 때문에 로직 문제는 아닌 것 같고 배포 후의 service worker 작동 방식의 문제였던 것 같아 이렇게밖에 해결을 못했다.
그래도 많으면 7개까지 오던;; 푸시 알림이 깔끔하게 한 번만 와서 행복했다.
(2) actions 동작 정의
- actions 옵션을 사용하면 그 버튼을 눌렀을 때 어떤 동작이 실행되게 할 지를 정의해줘야 한다.
- 사용자가 actions 버튼을 누르면 notificationclick 이라는 이벤트가 발생한다.
- 나는 두 가지 actions 값을 주었으니 두 가지 동작으로 나눠서 작성한다.
- "Pill my rhythm" 버튼 클릭시 사이트의 메인페이지로 이동
- "Today's Checklist" 버튼 클릭시 오늘의 체크리스트 작성 페이지로 이동
- default case를 작성하여 그냥 알림창 자체를 눌렀을 때 작동할 함수도 설정할 수 있다.
- 나는 Pill my rhythm 버튼을 따로 만들어 default 동작은 없앴고 따라서 알람창 자체를 누르면 아무런 동작도 하지 않는다.
- 위에서 설정해 준 data 옵션에 들어있는 값을 여기서 접근 가능하다.
- 나는 두 가지 actions 값을 주었으니 두 가지 동작으로 나눠서 작성한다.
self.addEventListener(
"notificationclick",
(event) => {
// 알림창 닫음
event.notification.close();
// User selected the Archive action.
switch (event.action) {
case "homepage-action":
// URL을 로드하는 새 창이나 탭이 열림
event.waitUntil(self.clients.openWindow("http://localhost:3000"));
break;
case "checklist-action": // 오늘 날짜의 체크리스트
const { encryptedToken } = event.notification.data;
// public 폴더 안에서는 .env 변수 접근 안 됨
event.waitUntil(self.clients.openWindow(`http://localhost:3000/m/checklist?token=${encryptedToken}`));
break;
// no default
}
},
false,
);
- 아래는 참고사항이다.
💡 알람 클릭 시 이미 열려있는 창에 집중시키기
이게 무슨 말이냐면 actions 버튼을 클릭했을 때 사용자의 환경에 이미 우리 서비스 페이지가 열려있는 창이 있다면 그 창으로 이동한다는 뜻이다. 나는 그냥 새로운 창으로 열리게 뒀는데 필요시 아래 코드로 추가할 수 있다.
// new URL() : url이 products/10 이런식이면 http://products/10 와 같이 바꿔줍니다. var urlToOpen = new URL(examplePage, self.location.origin).href; var promiseChain = clients.matchAll({ // matchAll() 은 탭만 반환하고, 웹 워커는 제외합니다. type: 'window', includeUncontrolled: true // 현재 서비스워커 이외의 다른 서비스워커가 제어하는 탭들도 포함합니다. 그냥 default로 항상 넣어주세요. }) .then((windowClients) => { // windowClients 는 현재 열린 탭들의 값입니다. var matchingClient = null; for (var i = 0; i < windowClients.length; i++) { var windowClient = windowClients[i]; if (windowClient.url === urlToOpen) { matchingClient = windowClient; break; } } if (matchingClient) { return matchingClient.focus(); } else { return clients.openWindow(urlToOpen); } }); // promiseChain은 위 matchingClient.focus()의 실행이 끝난 후 waitUntil()을 수행하기 위한 프로미스 체인입니다. event.waitUntil(promiseChain);
- 전체 service-worker.js 파일은 다음과 같다.
더보기
/* eslint-disable no-restricted-globals */
// Service Workers 설정에서 Update on reload는 이 파일이 변경됐을 때만 체크
// 서비스 워커에서 발생하는(back에서 보낸) 푸시 이벤트를 수신
// self는 서비스 워커 자체를 참조
self.addEventListener("push", (event) => {
const data = event.data.json();
const messageType = data.messageType;
const infoMessageOptions = {
icon: "./icon-192x192.png",
badge: "./badge-72x72.png", // android에서만 보임
vibrate: [200, 100, 200, 100, 200, 100, 200], // android에서만 동작
body: data.body,
};
const supplementMessageOptions = {
icon: "./icon-192x192.png",
badge: "./badge-72x72.png", // android에서만 보임
vibrate: [200, 100, 200, 100, 200, 100, 200], // android에서만 동작
// sound: "./"
body: data.body,
tag: "supplement",
actions: [
{
action: "homepage-action",
title: "Pill my rhythm",
// icon: "/images/demos/action-1-128x128.png",
},
{
action: "checklist-action",
title: "Today's Checklist",
// icon: "/images/demos/action-4-128x128.png",
},
],
// data로 action 실행 시 객체 전송 가능
data: {
encryptedToken: data.encryptedToken,
},
requireInteraction: true, // chrome과 같이 충분히 큰 창에서 사용자가 직접 닫을 때까지 알림 사라지지 않음
};
switch (messageType) {
case "info": {
// 브라우저는 전달된 Promise가 확인될 때까지 서비스 워커를 활성화 및 실행 상태로 유지
event.waitUntil(self.registration.showNotification(data.title, infoMessageOptions));
break;
}
case "supplement": {
event.waitUntil(self.registration.showNotification(data.title, supplementMessageOptions));
}
// no default
}
});
self.addEventListener(
"notificationclick",
(event) => {
// 알림창 닫음
event.notification.close();
// User selected the Archive action.
switch (event.action) {
case "homepage-action":
// URL을 로드하는 새 창이나 탭이 열림
event.waitUntil(self.clients.openWindow("http://localhost:3000"));
break;
case "checklist-action": // 오늘 날짜의 체크리스트
const { encryptedToken } = event.notification.data;
// public 폴더 안에서는 .env 변수 접근 안 됨
event.waitUntil(self.clients.openWindow(`http://localhost:3000/m/checklist?token=${encryptedToken}`));
break;
// no default
}
},
false,
);
[03 상황별 다른 알림 확인 ]
- service-worker.js 에서 messageType에 따라 옵션을 나눠줬으니 back에서 sendNotification 을 보낼 때 아래와 같이 messageType을 나눠 전송해야 한다.
// 구독
const notificationData = {
messageType: "info",
title: "Pill my rhythm",
body: "영양제 스케줄 알림 기능을 활성화합니다.",
};
await webpush.sendNotification(device_token, JSON.stringify(notificationData))
// 영양제 일정 알림
const notificationData = {
messageType: "supplement",
title: `${pushData.name}님, ${pushData.when} 영양제 드실 시간이에요!`,
body: `${pushData.supplements} 영양제를 복용해주세요.`,
encryptedToken: AES.encrypt(pushData.jwtToken, secretKey).toString(),
};
await webpush.sendNotification(
subscription.device_token,
JSON.stringify(notificationData)
);
- 그래서 최종적으로 알림이 온 모습은 다음과 같다.
구독 / 구독 취소 알림
영양제 일정 알림
'엘리스 AI트랙 4기 > 프로젝트' 카테고리의 다른 글
[PWA] Push Notification 안드로이드 QR 코드로 구독하기 (0) | 2022.07.25 |
---|---|
[PWA] Push Notification 알림 자동 발송하기 (feat. node-schedule) (1) | 2022.07.18 |
[PWA] Push Notification Nodejs 프로젝트에 적용하기 (0) | 2022.07.15 |
[PWA] Push Notification 튜토리얼 따라해보기 (0) | 2022.07.11 |
[PWA] Push Notification 개요 (0) | 2022.07.11 |