使用者 購讀

첫 番째 段階는 使用者에게 푸시 메시지를 보낼 權限을 얻는 것입니다. 그러면 PushSubscription 를 使用할 수 있습니다.

이를 위한 JavaScript API는 相當히 簡單하므로 로직 흐름을 段階別로 살펴보겠습니다.

機能 感知

먼저, 現在 브라우저가 實際로 푸시 메시지를 支援하는지 確認해야 합니다. 簡單한 두 가지 檢査를 통해 푸시가 지원되는지 確認할 수 있습니다.

  1. navgator 에서 serviceWorker 가 있는지 確認합니다.
  2. window 에서 PushManager 를 確認합니다.
if (!('serviceWorker' in navigator)) {
  // Service Worker isn't supported on this browser, disable or hide UI.
  return;
}

if (!('PushManager' in window)) {
  // Push isn't supported on this browser, disable or hide UI.
  return;
}

서비스 워커와 푸시 메시징 모두에 對한 브라우저 支援이 빠르게 增加하고 있지만, 恒常 두 機能 모두를 위한 機能 感知를 使用하여 漸進的으로 改善 하는 것이 좋습니다.

서비스 워커 登錄

機能 感知를 使用하면 서비스 워커와 푸시가 모두 지원된다는 것을 알 수 있습니다. 다음 段階는 서비스 워커를 '登錄'하는 것입니다.

서비스 워커를 登錄할 때 서비스 워커 파일의 位置를 브라우저에 알립니다. 파일은 如前히 JavaScript이지만 브라우저는 푸시를 비롯하여 서비스 워커 API에 對한 '액세스 權限을 附與'합니다. 더 正確하게 말하자면 브라우저는 파일을 서비스 워커 環境에서 實行합니다.

서비스 워커를 登錄하려면 navigator.serviceWorker.register() 를 呼出하여 파일 經路를 傳達합니다. 方法은 다음과 같습니다.

function registerServiceWorker() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      console.log('Service worker successfully registered.');
      return registration;
    })
    .catch(function (err) {
      console.error('Unable to register service worker.', err);
    });
}

이 函數는 서비스 워커 파일과 이 파일의 位置를 브라우저에 알립니다. 이 境遇 서비스 워커 파일은 /service-worker.js 에 있습니다. 브라우저에서 register() 를 呼出한 後 다음 段階를 實行합니다.

  1. 서비스 워커 파일을 다운로드합니다.

  2. JavaScript를 實行합니다.

  3. 모든 것이 올바르게 實行되고 誤謬가 없으면 register() 에서 返還된 프로미스가 確認됩니다. 種類에 關係없이 誤謬가 있으면 프로미스가 拒否됩니다.

register() 에서 拒否하면 Chrome DevTools에서 JavaScript에 誤打 / 誤謬가 있는지 다시 確認하세요.

register() 가 確認되면 ServiceWorkerRegistration 이 返還됩니다. 이 登錄을 使用하여 PushManager API 에 액세스합니다.

PushManager API 브라우저 互換性

브라우저 支援

  • 42名
  • 17
  • 44
  • 16

소스

權限 要請

서비스 워커를 登錄했으며 使用者를 購讀할 準備가 되었습니다. 다음 段階는 使用者로부터 푸시 메시지를 보낼 權限을 얻는 것입니다.

權限을 얻기 위한 API는 比較的 簡單하지만 短點은 API가 最近 콜백을 使用하던 方式에서 프로미스(Promise)를 返還하는 것으로 變更 되었다는 것입니다. 이 問題의 問題는 現在 브라우저에서 어떤 버전의 API를 具現했는지 알 수 없기 때문에 두 버전을 모두 具現하고 둘 다 處理해야 한다는 것입니다.

function askPermission() {
  return new Promise(function (resolve, reject) {
    const permissionResult = Notification.requestPermission(function (result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  }).then(function (permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error("We weren't granted permission.");
    }
  });
}

位 코드에서 重要한 코드 스니펫은 Notification.requestPermission() 呼出입니다. 이 메서드는 使用者에게 메시지를 表示합니다.

데스크톱 및 모바일 Chrome에 권한 메시지 표시

使用者가 許容, 遮斷을 누르거나 畵面을 닫아 權限 프롬프트와 相互作用하면 結果가 'granted' , 'default' 또는 'denied' 와 같은 文字列로 提供됩니다.

위의 샘플 코드에서 askPermission() 가 返還하는 프로미스는 權限이 附與되면 決定되고, 權限이 附與되지 않으면 프로미스를 拒否하는 誤謬가 發生합니다.

使用者가 '遮斷' 버튼을 클릭하는 境遇를 處理해야 합니다. 이 境遇 웹 앱은 使用者에게 權限을 다시 要請할 수 없습니다. 설정 패널에 숨겨진 權限 狀態를 變更하여 앱을 手動으로 '遮斷 解除'해야 합니다. 使用者에게 權限을 要請하는 方法과 時期를 신중하게 考慮하세요. 使用者가 遮斷을 클릭하면 決定을 飜覆하기가 쉽지 않기 때문입니다.

多幸히 大部分의 使用者는 權限 要請 理由를 알고 있는 한 大部分 기꺼이 權限을 提供합니다.

一部 人氣 사이트에서 權限을 要請하는 方式은 追後에 살펴보겠습니다.

PushManager로 使用者 購讀

서비스 워커를 登錄하고 權限을 얻었다면 registration.pushManager.subscribe() 를 呼出하여 使用者를 購讀할 수 있습니다.

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

subscribe() 메서드를 呼出할 때 必須 媒介變數와 選擇的 媒介變數로 構成된 options 客體를 傳達합니다.

傳達할 수 있는 모든 옵션을 살펴보겠습니다.

userVisibleOnly 옵션

브라우저에 푸시가 처음 追加되었을 때는 開發者가 푸시 메시지를 보내고 알림을 表示하지 않아야 하는지에 對한 確信이 없었습니다. 이는 一般的으로 使用者가 백그라운드에서 무언가가 發生했다는 事實을 알지 못하기 때문에 自動 푸시라고 합니다.

問題는 開發者가 使用者 모르게 使用者의 位置를 持續的으로 追跡하는 等 惡意的인 作業을 할 수 있다는 것이었습니다.

이러한 시나리오를 避하고 仕樣 作成者에게 이 機能을 가장 잘 支援하는 方法을 考慮할 時間을 주기 위해 userVisibleOnly 옵션이 追加되었으며 true 의 값을 傳達하는 것은 푸시가 受信될 때마다 (卽, 自動 푸시 없음) 알림을 웹 앱이 表示한다는 브라우저와의 심볼릭 契約입니다.

現在 true 값을 傳達 해야 합니다. userVisibleOnly 키를 包含하지 않거나 false 를 傳達하지 않으면 다음과 같은 誤謬가 發生합니다.

Chrome은 現在 使用者에게 標示되는 메시지가 標示되는 購讀의 境遇에만 Push API를 支援합니다. 代身 pushManager.subscribe({userVisibleOnly: true}) 를 呼出하여 이를 나타낼 수 있습니다. 仔細한 內容은 https://goo.gl/yqv4Q4 를 參照하세요.

現在로서는 包括的 自動 푸시가 Chrome에 具現되지 않을 것으로 보입니다. 代身 仕樣 作成者는 웹 앱 使用에 따라 웹 앱에 特定 數의 自動 푸시 메시지를 許容하는 豫算 API의 槪念을 살펴보고 있습니다.

applicationServerKey 옵션

移轉 섹션에서 '애플리케이션 서버 키'에 對해 簡單히 言及했습니다. '애플리케이션 서버 키'는 푸시 서비스에서 使用者를 購讀하는 애플리케이션을 識別하고 同一한 애플리케이션이 該當 使用者에게 메시지를 電送하는지 確認하는 데 使用됩니다.

애플리케이션 서버 키는 애플리케이션에 固有한 公開 키와 非公開 키 雙입니다. 非公開 키는 애플리케이션에서 祕密로 維持해야 하며, 公開 키는 자유롭게 共有할 수 있습니다.

subscribe() 呼出에 傳達되는 applicationServerKey 옵션은 애플리케이션의 公開 키입니다. 使用者 購讀 時 브라우저는 이를 푸시 서비스에 傳達합니다. 卽, 푸시 서비스는 애플리케이션의 公開 키를 使用者의 PushSubscription 에 連結할 수 있습니다.

아래 다이어그램은 이러한 段階를 보여줍니다.

  1. 웹 앱이 브라우저에 로드되고 subscribe() 를 呼出하여 公開 애플리케이션 서버 키를 傳達합니다.
  2. 그런 다음 브라우저가 엔드포인트를 生成할 푸시 서비스에 네트워크 要請을 하고, 이 엔드포인트를 애플리케이션 公開 키와 連結하고, 엔드포인트를 브라우저에 返還합니다.
  3. 브라우저에서는 subscribe() 프로미스를 通해 返還되는 PushSubscription 에 이 엔드포인트를 追加합니다.

공개 애플리케이션 서버 키가 구독 메서드에 사용되는 그림

나중에 푸시 메시지를 보내려면 애플리케이션 서버의 非公開 키 로 署名된 情報가 包含된 Authorization 헤더를 만들어야 합니다. 푸시 서비스가 푸시 메시지 電送 要請을 受信하면 要請을 受信하는 엔드포인트에 連結된 公開 키를 照會하여 이 署名된 Authorization 헤더의 有效性을 檢査할 수 있습니다. 署名이 有效하면 푸시 서비스는 署名이 一致하는 非公開 키 가 있는 애플리케이션 서버에서 提供되어야 한다는 것을 알게 됩니다. 基本的으로 다른 使用者가 애플리케이션 使用者에게 메시지를 보내지 못하게 하는 保安 措置입니다.

메시지를 보낼 때 비공개 애플리케이션 서버 키가 사용되는 방법

嚴密히 말해 applicationServerKey 는 選擇事項입니다. 그러나 Chrome에서 가장 쉽게 具現하는 方法은 이 機能이 必要하며, 다른 브라우저에서도 이 機能이 必要할 수 있습니다. Firefox에서는 選擇事項입니다.

애플리케이션 서버 키가 있어야 하는 內容 을 定義하는 辭讓은 VAPID 仕樣 입니다. '애플리케이션 서버 키' 또는 'VAPID 키' 와 關聯된 內容을 읽을 때마다 恒常 같은 키임을 記憶하세요.

애플리케이션 서버 키를 만드는 方法

web-push-codelab.glitch.me 를 訪問하여 애플리케이션 서버 키의 公開 및 非公開 세트를 만들거나 웹-푸시 命令줄 에서 다음과 같은 方法으로 키를 生成할 수 있습니다.

    $ npm install -g web-push
    $ web-push generate-vapid-keys

애플리케이션에서 이러한 키는 한 番만 만들면 되며 非公開 키는 非公開로 維持해야 합니다. (네, 方今 말한 거네요.)

權限 및subscribe()

subscribe() 를 呼出하는 데는 한 가지 副作用이 있습니다. subscribe() 를 呼出할 때 알림을 標示할 權限이 웹 앱에 없다면 브라우저에서 代身 權限을 要請합니다. 이는 UI가 이 흐름에서 作動할 때 有用하지만 더 細部的으로 制御하려는 境遇 (大部分의 開發者가 그렇게 할 것으로 豫想) 以前에 使用한 Notification.requestPermission() API를 繼續 使用하세요.

PushSubscription이란 무엇인가요?

subscribe() 를 呼出하고 몇 가지 옵션을 傳達하면 PushSubscription 로 確認되는 프로미스를 가져와서 다음과 같은 코드를 生成합니다.

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

PushSubscription 客體에는 該當 使用者에게 푸시 메시지를 보내는 데 必要한 모든 必須 情報가 包含됩니다. JSON.stringify() 를 使用하여 콘텐츠를 出力하면 다음이 標示됩니다.

    {
      "endpoint": "https://some.pushservice.com/something-unique",
      "keys": {
        "p256dh":
    "BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
        "auth":"FPssNDTKnInHVndSTdbKFw=="
      }
    }

endpoint 는 푸시 서비스 URL입니다. 푸시 메시지를 트리거하려면 이 URL에 對한 POST 要請을 實行합니다.

keys 客體에는 푸시 메시지와 함께 電送되는 메시지 데이터를 暗號化하는 데 使用되는 값이 包含되어 있습니다. 이에 對해서는 이 섹션의 뒷部分에서 說明합니다.

定期 決濟 滿了를 防止하기 위해 定期 決濟 再申請

푸시 알림을 購讀하면 null PushSubscription.expirationTime 을(를) 受信하는 境遇가 많습니다. 理論的으로는 定期 決濟가 滿了되지 않는다는 意味입니다 (定期 決濟가 滿了되는 正確한 時點을 알려주는 DOMHighResTimeStamp 를 受信하는 境遇와 달리). 그러나 實際로 더 오랜 時間 푸시 알림이 受信되지 않았거나 브라우저에서 使用者가 푸시 알림 權限이 있는 앱을 使用하고 있지 않음을 感知한 境遇 브라우저에서 購讀이 繼續 滿了되는 境遇가 一般的입니다. 이를 防止하기 위한 한 가지 패턴은 다음 스니펫과 같이 알림이 受信될 때마다 使用者를 다시 購讀하는 것입니다. 이를 위해서는 브라우저에서 購讀이 自動으로 滿了되지 않도록 알림을 자주 보내야 합니다. 또한 購讀이 滿了되지 않도록 使用者에게 非自發的으로 스팸을 보내는 適法한 알림 必要性의 長點과 短點을 愼重하게 檢討해야 합니다. 結局 오랫동안 잊혀진 알림 購讀으로부터 使用者를 保護하기 위해 브라우저에 맞서 싸우려 해서는 안 됩니다.

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

서버에 購讀 보내기

푸시 購讀이 있으면 이를 서버로 電送해야 합니다. 方法은 開發者가 選擇하지만 한 가지 팁은 JSON.stringify() 를 使用하여 定期 決濟 客體에서 必要한 모든 데이터를 가져오는 것입니다. 또는 다음과 같이 同一한 結果를 手動으로 結合할 수 있습니다.

const subscriptionObject = {
  endpoint: pushSubscription.endpoint,
  keys: {
    p256dh: pushSubscription.getKeys('p256dh'),
    auth: pushSubscription.getKeys('auth'),
  },
};

// The above is the same output as:

const subscriptionObjectToo = JSON.stringify(pushSubscription);

購讀 電送은 다음과 같이 웹페이지에서 이루어집니다.

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

노드 서버는 이 要請을 受信하고 나중에 使用할 수 있도록 데이터를 데이터베이스에 貯藏합니다.

app.post('/api/save-subscription/', function (req, res) {
  if (!isValidSaveRequest(req, res)) {
    return;
  }

  return saveSubscriptionToDatabase(req.body)
    .then(function (subscriptionId) {
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify({data: {success: true}}));
    })
    .catch(function (err) {
      res.status(500);
      res.setHeader('Content-Type', 'application/json');
      res.send(
        JSON.stringify({
          error: {
            id: 'unable-to-save-subscription',
            message:
              'The subscription was received but we were unable to save it to our database.',
          },
        }),
      );
    });
});

서버의 PushSubscription 細部情報를 使用하면 願할 때마다 使用者에게 메시지를 보낼 수 있습니다.

定期 決濟 滿了를 防止하기 위해 定期 決濟 再申請

푸시 알림을 購讀하면 null PushSubscription.expirationTime 을(를) 受信하는 境遇가 많습니다. 理論的으로는 定期 決濟가 滿了되지 않는다는 意味입니다 (定期 決濟가 滿了되는 正確한 時點을 알려주는 DOMHighResTimeStamp 를 受信하는 境遇와 달리). 그러나 實際로는 오랫동안 푸시 알림이 受信되지 않았거나 브라우저에서 使用者가 푸시 알림 權限이 있는 앱을 使用하지 않음을 感知한 境遇 브라우저에서 購讀이 繼續 滿了되는 境遇가 많습니다. 이를 防止하기 위한 한 가지 패턴은 다음 스니펫과 같이 알림이 受信될 때마다 使用者를 다시 購讀하는 것입니다. 이를 위해서는 브라우저에서 購讀이 自動으로 滿了되지 않도록 알림을 자주 보내야 하며, 購讀이 滿了되지 않도록 使用者에게 스팸을 보내는 適法한 알림의 長點과 短點을 매우 愼重하게 檢討해야 합니다. 結局 오랫동안 잊혀진 알림 購讀으로부터 使用者를 保護하기 위해 브라우저에 맞서 싸우려 해서는 안 됩니다.

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

FAQ

이 時點에서 사람들이 묻는 몇 가지 一般的인 質問은 다음과 같습니다.

브라우저에서 使用하는 푸시 서비스를 變更할 수 있나요?

아니요. 푸시 서비스는 브라우저에 依해 選擇되며 subscribe() 呼出에서 確認한 바와 같이 브라우저는 푸시 서비스에 네트워크 要請을 보내 PushSubscription 을 構成하는 細部情報를 가져옵니다.

브라우저마다 다른 푸시 서비스를 使用하는데 API가 서로 다르지 않나요?

모든 푸시 서비스에 同一한 API가 必要합니다.

이 共通 API를 웹 푸시 프로토콜 이라고 하며 푸시 메시지를 트리거하기 爲해 애플리케이션에서 實行해야 하는 네트워크 要請을 說明합니다.

데스크톱에서 使用者를 購讀하면 使用者 携帶電話에서도 購讀하나요?

아니요. 使用者는 메시지를 受信하려는 各 브라우저에서 푸시에 登錄해야 합니다. 이렇게 하려면 使用者가 各 機器에 權限을 附與해야 합니다.

다음에 遂行할 作業

Codelab