일반적인 알림 패턴

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

이를 위해서는 서비스 워커에서 사용할 수 있는 몇 가지 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);

다음 섹션에서는 사용자를 안내하려는 페이지가 이미 열려 있는지 확인하는 방법을 살펴봅니다. 이렇게 하면 새 탭을 여는 대신 열려 있는 탭에 포커스를 맞출 수 있습니다.

기존 창에 포커스 맞추기

가능한 경우 사용자가 알림을 클릭할 때마다 새 창을 여는 대신 창에 포커스를 맞춰야 합니다.

이를 실행하는 방법을 살펴보기 전에 출처의 페이지에서만 가능하다는 점에 유의하세요. Google에서는 사이트에 속한 열려 있는 페이지만 볼 수 있기 때문입니다. 이렇게 하면 개발자가 사용자가 보고 있는 모든 사이트를 볼 수 없습니다.

이전 예를 사용하여 코드를 변경하여 /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를 사용하여 예시 페이지를 파싱합니다. 제프 폴스닉에게서 배운 멋진 방법입니다. location 객체로 new URL()를 호출하면 전달된 문자열이 상대적 (즉, /https://example.com/이 됨)인 경우 절대 URL이 반환됩니다.

나중에 창 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여야 합니다.

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

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를 사용하여 알림 접기를 더 정교하게 처리할 수 있습니다. 채팅 앱을 예로 들 수 있습니다. 개발자는 새 알림에서 최신 메시지만 표시하는 대신 'Matt님으로부터 받은 메시지 2개'와 같은 메시지를 표시하고 싶을 수 있습니다.

웹 앱에 현재 표시되는 모든 알림에 액세스할 수 있는 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 이벤트

브라우저 지원

  • Chrome: 50.
  • Edge: 17.
  • Firefox: 44
  • Safari: 16

소스

Clients.openWindow()

브라우저 지원

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44
  • Safari: 11.1.

소스

ServiceWorkerRegistration.getNotifications()

브라우저 지원

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44
  • Safari: 16

소스

clients.matchAll()

브라우저 지원

  • Chrome: 42.
  • Edge: 17.
  • Firefox: 54.
  • Safari: 11.1.

소스

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

다음에 수행할 작업

Codelab