관련지식
javascript, notification, service worker, promise

이번에는 Notification 팝업에 버튼을 추가하면서 아래 세가지 기능을 구현할 것입니다.

  1. 알림 팝업 버튼 클릭시 브라우저 포커스
  2. 알림 팝업 닫기 버튼
  3. 화면 스크립트에서 알림팝업 닫기

하지만 위 기능을 구현하기 위해선 생각보다 많은 작업이 필요하므로, 구역을 나누어서 정리하겠습니다.

ServiceWorkerRegistration.showNotification()

Notification 팝업에 버튼을 추가하기 위해선 아래와 같은 action 옵션을 지정하면 됩니다.

  1. notiAlarm = new Notification(chn_text, {
  2. body: pinyin + '\n' + means,
  3. image : url,
  4. actions: [{
  5. title: '화면보기',
  6. action: 'goTab'
  7. },
  8. {
  9. title: '닫기',
  10. action: 'close'
  11. }]
  12. });

하지만 위 코드는 실행되지 않고 아래와 같은 오류가 발생합니다.

  1. Uncaught TypeError: Failed to construct 'Notification': Actions are only supported for persistent notifications shown using ServiceWorkerRegistration.showNotification().

action 기능을 사용하기 위해선 Service Worker의 showNotification() 함수를 통해서 호출해야 한다는 메세지입니다. 따라서 서비스워커가 무엇인지 이해해 두는것이 좋습니다. 아래 링크에서 어떠한 것인지 한번 읽어보시기 바랍니다.

서비스워커
https://terms.naver.com/entry.nhn?docId=5714711&cid=42346&categoryId=42346

아래 소스는 기존의 제가 사용한 알림 팝업 코드입니다.

  1. notiAlarm = new Notification(chn_text, {
  2. body: pinyin + '\n' + means,
  3. image : url
  4. });

이 소스를 다음과 같이 수정하겠습니다.

  1. navigator.serviceWorker.ready.then(function(registration) {
  2. notiAlarm = registration.showNotification(chn_text, {
  3. body: pinyin + '\n' + means,
  4. image : url,
  5. actions: [{
  6. title: '화면보기',
  7. action: 'goTab'
  8. },
  9. {
  10. title: '닫기',
  11. action: 'close'
  12. }]
  13. });
  14. });

복잡해 보이지만 서비스워커가 준비되었음을 확인하는 navigator.serviceWorker.ready 와 Notification 객체를 registration.showNotification() 함수로 교체한것 외엔 동일합니다.

그리고 화면이 열릴때 아래의 내용을 실행하도록 추가합니다. 이 소스는 서비스워커의 내용이 정의된 스크립트를 등록하는 내용입니다.

  1. navigator.serviceWorker.register('sw.js').then(function(registration) {
  2. console.log('Service worker successfully registered.');
  3. return registration;
  4. })
  5. .catch(function(err) {
  6. console.error('Unable to register service worker.', err);
  7. });

sw.js 파일은 빈 상태여도 상관없지만 특별한 이유가 없다면 아래 소스를 추가하는것이 테스트하기에 좋습니다.

sw.js)

  1. self.addEventListener('install', function(event) {
  2. self.skipWaiting();
  3. });
  4. self.addEventListener('activate', function(event) {
  5. event.waitUntil(self.clients.claim());
  6. });

이제 교체한 소스에서도 알림 팝업이 잘 보이는지 확인하면 됩니다.

버튼 클릭시 브라우저에 포커스하기

브라우저가 최소화가 되었거나, 다른탭을 보는중이더라도 알림 팝업은 실행 가능합니다. 아래와 같이 알림 팝업이 보일때 ‘화면보기’ 버튼을 클릭하면 해당 화면이 열렸으면 합니다.

위 소스에서 버튼은 추가했으니, 버튼에 대한 액션을 구현하면 됩니다. 물론 서비스워커인 ‘sw.js’ 에 추가해야하죠. 알림 팝업에서 버튼을 클릭하면 notificationclick 이벤트가 발생합니다. 아래 소스는 클릭 이벤트 발생시 window 객체를 찾아서 focus() 함수를 실행합니다.

sw.js에 추가)

  1. self.addEventListener('notificationclick', function(event) { //알림 팝업의 버튼 액션
  2. switch (event.action) {
  3. case 'goTab':
  4. event.waitUntil(
  5. clients.matchAll({
  6. type: "window",
  7. includeUncontrolled: true
  8. })
  9. .then(function(clientList) {
  10. if (clientList.length) {
  11. clientList[0].focus();
  12. }
  13. })
  14. );
  15. break;
  16. default:
  17. console.log(`Unknown action clicked: '${event.action}'`);
  18. break;
  19. }
  20. });

알림 팝업 수동으로 닫기

이번엔 수동으로 팝업을 닫는 버튼을 추가할 것입니다. 팝업의 우측 상단에 X 버튼이 있지만 너무 작아서 클릭하기 불편하잖아요.

방법은 위 방식과 동일합니다.

sw.js 에 추가)

  1. case 'goTab':
  2. event.waitUntil(
  3. clients.matchAll({
  4. type: "window",
  5. includeUncontrolled: true
  6. })
  7. .then(function(clientList) {
  8. if (clientList.length) {
  9. clientList[0].focus();
  10. }
  11. })
  12. );
  13. case 'close': //추가
  14. event.notification.close();
  15. break;

버튼은 이미 추가했으므로 notificationclick 이벤트에 닫기에 대한 액션만 정의하면 됩니다. 간단하죠?

화면 스크립트에서 알림팝업 제어

위 두가지 케이스는 알림 팝업에서 이벤트가 발생하면, 서비스워커인 sw.js에서 처리를 하는 형태였습니다. 하지만 알림 팝업을 호출한 화면에서 알림 팝업을 닫으려면 어떻게 해야할까요? 물론 notificationclick 이벤트는 발생하지 않을겁니다.

기존에 Notification 객체를 사용했을때 방식은 아래와 같습니다.

  1. notiAlarm = new Notification(chn_text, { ... }); //팝업 실행
  2. notiAlarm.close(); //팝업닫기

하지만 showNotification() 함수로 교체한 지금은 notiAlarm 에 Promise 객체가 있게 되고 resolve()에 전달되는 파라미터도 없습니다. 팝업 실행의 유무를 판단할수는 있지만 팝업을 닫을수는 없는 상태인 것입니다. 이제는 서비스워커 쪽으로 이벤트 메시지를 전달해서 닫기를 실행해야 합니다.

  1. //notiAlarm.close(); //이전 소스
  2. navigator.serviceWorker.controller.postMessage({'action': 'close'}); //추가

예전에 ‘[javascript] 다른 도메인의 iframe 리사이즈 하기’ 글에서 다뤘던 postMessage() 함수를 이용하면 됩니다.

sw.js에 추가)

  1. function notificationClose() { //팝업 닫기
  2. self.registration.getNotifications().then(function(notifications) {
  3. for(var i = 0; i < notifications.length; i++) {
  4. notifications[i].close();
  5. }
  6. });
  7. }
  8. self.addEventListener('message', function (evt) {
  9. if(evt.data.action == 'close') {
  10. notificationClose();
  11. }
  12. });

Service Worker 안에서도 registration은 있기 때문에 그 객체를 통해 전체 알림 팝업을 조회하고 닫을수 있습니다. 방금 적은것처럼 위 소스는 현재 열려있는 알림 팝업 전체를 닫는 소스입니다. 제 상황에는 저 코드면 되는데, 만약 특정 팝업만 닫고 싶다면 수정되어야 하겠네요.

참고자료

알림 팝업에 버튼 기능 몇개를 추가할 뿐인데, 꽤 복잡해진것 같은 느낌입니다. 평소 일반적인 웹 개발만 했다면 Service Worker 는 개념이 조금 어려울수도 있습니다. 이해하는데 도움이 될만한 링크를 몇개 추가하니 읽어보시면 도움이 되실겁니다.

최종 소스

sw.js)

  1. self.addEventListener('install', function(event) {
  2. self.skipWaiting();
  3. });
  4. self.addEventListener('activate', function(event) {
  5. event.waitUntil(self.clients.claim());
  6. });
  7. self.addEventListener('notificationclick', function(event) { //알림 팝업의 버튼 액션
  8. switch (event.action) {
  9. case 'goTab':
  10. event.waitUntil(
  11. clients.matchAll({
  12. type: "window",
  13. includeUncontrolled: true
  14. })
  15. .then(function(clientList) {
  16. if (clientList.length) {
  17. clientList[0].focus();
  18. }
  19. })
  20. );
  21. case 'close':
  22. event.notification.close();
  23. break;
  24. default:
  25. console.log(`Unknown action clicked: '${event.action}'`);
  26. break;
  27. }
  28. });
  29. function notificationClose() { //팝업 닫기
  30. self.registration.getNotifications().then(function(notifications) {
  31. for(var i = 0; i < notifications.length; i++) {
  32. notifications[i].close();
  33. }
  34. });
  35. }
  36. self.addEventListener('message', function (evt) {
  37. if(evt.data.action == 'close') {
  38. notificationClose();
  39. }
  40. });