일반적인 알림 패턴

웹 푸시의 몇 가지 일반적인 구현 패턴을 살펴보겠습니다.

이를 위해서는 서비스 워커에서 사용할 수 있는 몇 가지 API를 사용해야 합니다.

알림 닫기 이벤트

이전 섹션에서는 notificationclick 이벤트를 수신 대기하는 방법을 살펴봤습니다.

사용자가 알림 중 하나를 닫으면 (즉, 사용자가 알림을 클릭하는 대신 교차를 클릭하거나 알림을 스와이프하여 없애는 경우) 호출되는 notificationclose 이벤트도 있습니다.

이 이벤트는 일반적으로 알림을 통한 사용자 참여를 추적하기 위한 분석에서 사용됩니다.

self.addEventListener('notificationclose', function (event) {
  const dismissedNotification = event.notification;

  const promiseChain = notificationCloseAnalytics();
  event.waitUntil(promiseChain);
});

알림에 데이터 추가

푸시 메시지가 수신되면 사용자가 알림을 클릭한 경우에만 유용한 데이터가 있는 것이 일반적입니다. 예를 들어 알림을 클릭할 때 열리는 URL입니다.

푸시 이벤트에서 데이터를 가져와서 알림에 연결하는 가장 쉬운 방법은 다음과 같이 showNotification()에 전달된 옵션 객체에 data 매개변수를 추가하는 것입니다.

const options = {
  body:
    'This notification has data attached to it that is printed ' +
    "to the console when it's clicked.",
  tag: 'data-notification',
  data: {
    time: new Date(Date.now()).toString(),
    message: 'Hello, World!',
  },
};
registration.showNotification('Notification with Data', options);

클릭 핸들러 내에서 event.notification.data를 사용하여 데이터에 액세스할 수 있습니다.

const notificationData = event.notification.data;
console.log('');
console.log('The notification data has the following parameters:');
Object.keys(notificationData).forEach((key) => {
  console.log(`  ${key}: ${notificationData[key]}`);
});
console.log('');

창 열기

알림에 관한 가장 일반적인 응답 중 하나는 특정 URL로 창 / 탭을 여는 것입니다. clients.openWindow() API를 사용하면 됩니다.

notificationclick 이벤트에서는 다음과 같은 코드를 실행합니다.

const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);

다음 섹션에서는 사용자를 연결할 페이지가 이미 열려 있는지 여부를 확인하는 방법을 알아보겠습니다. 이렇게 하면 새 탭이 열리지 않고 열려 있는 탭에 포커스를 둘 수 있습니다.

기존 창에 포커스

가능하면 사용자가 알림을 클릭할 때마다 새 창을 여는 대신 창에 포커스를 두어야 합니다.

이 작업을 실행하는 방법을 살펴보기 전에 이 방법은 원본 페이지에만 가능하다는 점을 강조하는 것이 좋습니다. 이는 사이트에 속한 페이지 중 어떤 페이지가 열려 있는지만 볼 수 있기 때문입니다. 이렇게 하면 개발자가 사용자가 보고 있는 모든 사이트를 볼 수 없습니다.

앞의 예에서 /demos/notification-examples/example-page.html가 이미 열려 있는지 확인하기 위해 코드를 변경합니다.

const urlToOpen = new URL(examplePage, self.location.origin).href;

const promiseChain = clients
  .matchAll({
    type: 'window',
    includeUncontrolled: true,
  })
  .then((windowClients) => {
    let matchingClient = null;

    for (let i = 0; i < windowClients.length; i++) {
      const windowClient = windowClients[i];
      if (windowClient.url === urlToOpen) {
        matchingClient = windowClient;
        break;
      }
    }

    if (matchingClient) {
      return matchingClient.focus();
    } else {
      return clients.openWindow(urlToOpen);
    }
  });

event.waitUntil(promiseChain);

코드를 단계별로 살펴보겠습니다.

먼저 URL API를 사용하여 예시 페이지를 파싱합니다. 이건 Jeff Posnick에게서 받은 멋진 묘사야. location 객체로 new URL()를 호출하면 전달된 문자열이 상대적인 경우 (즉, /https://example.com/가 됨) 절대 URL이 반환됩니다.

나중에 창 URL과 일치시킬 수 있도록 절대 URL을 만듭니다.

const urlToOpen = new URL(examplePage, self.location.origin).href;

그런 다음 현재 열려 있는 탭 및 창의 목록인 WindowClient 객체 목록을 가져옵니다. (원본을 위한 탭이라는 점에 유의하세요.)

const promiseChain = clients.matchAll({
  type: 'window',
  includeUncontrolled: true,
});

matchAll에 전달된 옵션은 브라우저에 '창' 유형 클라이언트만 검색한다고 알립니다 (즉, 탭과 창만 찾고 웹 작업자 제외). includeUncontrolled를 사용하면 현재 서비스 워커에 의해 제어되지 않는 모든 탭(즉, 이 코드를 실행하는 서비스 워커)을 원본에서 검색할 수 있습니다. 일반적으로 matchAll()를 호출할 때는 includeUncontrolled이 true가 되도록 하는 것이 좋습니다.

반환된 프로미스를 promiseChain로 캡처하여 나중에 event.waitUntil()에 전달할 수 있도록 하고 서비스 워커를 유지합니다.

matchAll() 프로미스가 해결되면 반환된 창 클라이언트를 반복하고 URL을 열려는 URL과 비교합니다. 일치하는 결과를 찾으면 해당 클라이언트에 초점을 맞추고 이 창에서 사용자의 주의를 끌 수 있습니다 포커스는 matchingClient.focus() 호출을 통해 실행됩니다.

일치하는 클라이언트를 찾을 수 없는 경우 이전 섹션과 마찬가지로 새 창을 엽니다.

.then((windowClients) => {
  let matchingClient = null;

  for (let i = 0; i < windowClients.length; i++) {
    const windowClient = windowClients[i];
    if (windowClient.url === urlToOpen) {
      matchingClient = windowClient;
      break;
    }
  }

  if (matchingClient) {
    return matchingClient.focus();
  } else {
    return clients.openWindow(urlToOpen);
  }
});

알림 병합

알림에 태그를 추가하면 동일한 태그를 가진 기존 알림이 교체되는 동작이 선택되는 것을 확인했습니다.

그러나 Notifications API를 사용하여 알림을 축소하여 더 정교하게 만들 수 있습니다. 채팅 앱이 있다고 가정해 보겠습니다. 개발자는 최신 메시지만 표시하는 대신 '매트가 보낸 두 개의 메시지가 있습니다'와 비슷한 메시지를 새 알림으로 표시할 수 있습니다.

웹 앱에 현재 표시되는 모든 알림에 액세스할 수 있는 registration.getNotifications() API를 사용하여 이를 수행하거나 다른 방식으로 현재 알림을 조작할 수 있습니다.

이 API를 사용하여 채팅 예시를 구현하는 방법을 살펴보겠습니다.

채팅 앱에서 각 알림에 사용자 이름을 포함한 데이터가 있다고 가정해 보겠습니다.

가장 먼저 할 일은 특정 사용자 이름을 가진 사용자에게 열려 있는 알림을 모두 찾는 것입니다. registration.getNotifications()를 가져와서 루프 처리하고 notification.data에서 특정 사용자 이름을 확인합니다.

const promiseChain = registration.getNotifications().then((notifications) => {
  let currentNotification;

  for (let i = 0; i < notifications.length; i++) {
    if (notifications[i].data && notifications[i].data.userName === userName) {
      currentNotification = notifications[i];
    }
  }

  return currentNotification;
});

다음 단계는 이 알림을 새 알림으로 바꾸는 것입니다.

이 가짜 메시지 앱에서는 새 알림 데이터에 개수를 추가하여 새 메시지 수를 추적하고 새 알림마다 개수를 늘립니다.

.then((currentNotification) => {
  let notificationTitle;
  const options = {
    icon: userIcon,
  }

  if (currentNotification) {
    // We have an open notification, let's do something with it.
    const messageCount = currentNotification.data.newMessageCount + 1;

    options.body = `You have ${messageCount} new messages from ${userName}.`;
    options.data = {
      userName: userName,
      newMessageCount: messageCount
    };
    notificationTitle = `New Messages from ${userName}`;

    // Remember to close the old notification.
    currentNotification.close();
  } else {
    options.body = `"${userMessage}"`;
    options.data = {
      userName: userName,
      newMessageCount: 1
    };
    notificationTitle = `New Message from ${userName}`;
  }

  return registration.showNotification(
    notificationTitle,
    options
  );
});

현재 표시된 알림이 있는 경우 메시지 수를 늘리고 이에 따라 알림 제목과 본문 메시지를 설정합니다. 알림이 없으면 newMessageCount가 1인 새 알림을 만듭니다.

결과적으로 첫 번째 메시지는 다음과 같이 표시됩니다.

병합하지 않은 첫 번째 알림입니다.

두 번째 알림은 알림을 다음과 같이 축소합니다.

병합에 관한 두 번째 알림

이 접근 방식의 장점은 사용자가 서로 간에 표시되는 알림을 목격하면 알림을 최신 메시지로 바꾸는 것보다 더 일관되고 잘 어울린다는 것입니다.

이 규칙의 예외

푸시를 받으면 알림을 표시해야 한다고 말씀드렸는데, 대부분의 경우 알림이 표시됩니다. 알림을 표시할 필요가 없는 한 가지 시나리오는 사용자가 사이트를 열고 포커스를 맞추는 경우입니다.

푸시 이벤트 내에서 창 클라이언트를 검사하고 포커스가 맞춰진 창을 찾아 알림을 표시해야 하는지 여부를 확인할 수 있습니다.

모든 창을 가져오고 포커스가 맞춰진 창을 찾는 코드는 다음과 같습니다.

function isClientFocused() {
  return clients
    .matchAll({
      type: 'window',
      includeUncontrolled: true,
    })
    .then((windowClients) => {
      let clientIsFocused = false;

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i];
        if (windowClient.focused) {
          clientIsFocused = true;
          break;
        }
      }

      return clientIsFocused;
    });
}

clients.matchAll()를 사용하여 모든 창 클라이언트를 가져온 다음 focused 매개변수를 확인하여 클라이언트를 순환합니다.

푸시 이벤트 내에서 이 함수를 사용하여 알림을 표시해야 하는지 결정합니다.

const promiseChain = isClientFocused().then((clientIsFocused) => {
  if (clientIsFocused) {
    console.log("Don't need to show a notification.");
    return;
  }

  // Client isn't focused, we need to show a notification.
  return self.registration.showNotification('Had to show a notification.');
});

event.waitUntil(promiseChain);

푸시 이벤트에서 페이지에 메시지 보내기

사용자가 현재 사이트에 있다면 알림 표시를 건너뛸 수 있는 것으로 확인되었습니다. 이벤트가 발생했지만 알림이 지나치게 복잡하다는 것을 사용자에게 알리고 싶다면 어떻게 해야 할까요?

한 가지 방법은 서비스 워커에서 페이지로 메시지를 전송하는 것입니다. 이렇게 하면 웹페이지에서 사용자에게 알림이나 업데이트를 표시하여 이벤트를 알릴 수 있습니다. 이는 페이지의 미묘한 알림이 사용자에게 더 좋고 친근한 상황에 유용합니다.

푸시를 수신하고 웹 앱에 포커스가 있는지 확인한 후 다음과 같이 각 열린 페이지에 '메시지를 게시'할 수 있다고 가정해 보겠습니다.

const promiseChain = isClientFocused().then((clientIsFocused) => {
  if (clientIsFocused) {
    windowClients.forEach((windowClient) => {
      windowClient.postMessage({
        message: 'Received a push message.',
        time: new Date().toString(),
      });
    });
  } else {
    return self.registration.showNotification('No focused windows', {
      body: 'Had to show a notification instead of messaging each page.',
    });
  }
});

event.waitUntil(promiseChain);

각 페이지에서 메시지 이벤트 리스너를 추가하여 메시지를 수신 대기합니다.

navigator.serviceWorker.addEventListener('message', function (event) {
  console.log('Received a message from service worker: ', event.data);
});

이 메시지 리스너에서는 원하는 작업을 하거나 페이지에 맞춤 UI를 표시하거나 메시지를 완전히 무시할 수 있습니다.

또한 웹페이지에서 메시지 리스너를 정의하지 않으면 서비스 워커의 메시지가 아무 작업도 수행하지 않습니다.

페이지 캐시 및 창 열기

이 가이드의 범위를 벗어나지만 살펴볼 만한 한 가지 시나리오는 사용자가 알림을 클릭한 후 방문할 것으로 예상되는 웹페이지를 캐시하여 웹 앱의 전반적인 UX를 개선할 수 있는 것입니다.

이렇게 하려면 fetch 이벤트를 처리하도록 서비스 워커를 설정해야 하지만 fetch 이벤트 리스너를 구현하는 경우 알림을 표시하기 전에 필요한 페이지와 애셋을 캐시하여 push 이벤트에서 이를 활용해야 합니다.

브라우저 호환성

notificationclose 이벤트

브라우저 지원

  • 50
  • 17
  • 44
  • 16

소스

Clients.openWindow()

브라우저 지원

  • 40
  • 17
  • 44
  • 11.1

소스

ServiceWorkerRegistration.getNotifications()

브라우저 지원

  • 40
  • 17
  • 44
  • 16

소스

clients.matchAll()

브라우저 지원

  • 42
  • 17
  • 54
  • 11.1

소스

자세한 내용은 서비스 워커 소개 게시물을 참조하세요.

다음에 수행할 작업

Codelab