푸시 알림에 사용자 등록

푸시 메시지를 보내려면 먼저 사용자로부터 권한을 얻은 다음 기기를 푸시 서비스에 구독해야 합니다. 여기에는 JavaScript API를 사용하여 PushSubscription 객체를 가져온 후 서버로 전송하는 작업이 포함됩니다.

JavaScript API는 이 프로세스를 간단하게 관리합니다. 이 가이드에서는 기능 감지, 권한 요청, 정기 결제 프로세스 관리 등 전체 흐름을 설명합니다.

기능 감지

먼저 브라우저가 푸시 메시지를 지원하는지 확인합니다. 다음 두 가지 검사를 통해 푸시 지원을 확인할 수 있습니다.

  • navigator 객체에서 serviceWorker를 확인합니다.
  • 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 브라우저 호환성

Browser Support

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

Source

권한 요청

서비스 워커를 등록하고 권한을 획득한 후 사용자로부터 푸시 메시지를 보낼 권한을 얻습니다.

권한을 가져오는 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 currently only supports the Push API for subscriptions that will result
in user-visible messages. You can indicate this by calling
`pushManager.subscribe({userVisibleOnly: true})` instead. See
[https://goo.gl/yqv4Q4](https://goo.gl/yqv4Q4) for more details.

Chrome은 사용자에게 표시되는 메시지를 생성하는 구독에 대해서만 푸시 API를 지원합니다. pushManager.subscribe({userVisibleOnly: true})을 호출하여 이를 나타냅니다. 자세한 내용은 https://goo.gl/yqv4Q4를 참고하세요.

전체 무음 푸시는 Chrome에 구현되지 않을 것으로 보입니다. 대신 사양 작성자는 웹 앱이 웹 앱 사용량에 따라 특정 수의 자동 푸시 메시지를 보낼 수 있는 예산 API를 살펴보고 있습니다.

applicationServerKey 옵션

이전에는 이 문서에서 애플리케이션 서버 키를 언급했습니다. 푸시 서비스는 애플리케이션 서버 키를 사용하여 사용자를 구독하는 애플리케이션을 식별하고 동일한 애플리케이션이 사용자에게 메시지를 보내도록 합니다.

애플리케이션 서버 키는 애플리케이션에 고유한 공개 키와 비공개 키 쌍입니다. 애플리케이션의 비공개 키는 보안 비밀로 유지하고 공개 키는 자유롭게 공유하세요.

subscribe() 호출에 전달된 applicationServerKey 옵션은 애플리케이션의 공개 키입니다. 브라우저는 사용자를 구독할 때 이 키를 푸시 서비스에 전달하므로 푸시 서비스가 애플리케이션의 공개 키를 사용자의 PushSubscription에 연결할 수 있습니다.

다음 다이어그램에서는 이러한 단계를 보여줍니다.

  1. 브라우저에서 웹 앱을 로드하고 공개 애플리케이션 서버 키를 전달하여 subscribe()를 호출합니다.
  2. 그러면 브라우저가 푸시 서비스에 네트워크 요청을 전송합니다. 푸시 서비스는 엔드포인트를 생성하고 이 엔드포인트를 애플리케이션의 공개 키와 연결한 후 엔드포인트를 브라우저에 반환합니다.
  3. 브라우저가 이 엔드포인트를 PushSubscription에 추가하고, subscribe() 프로미스가 이를 반환합니다.

`subscribe()` 메서드에서 공개 애플리케이션 서버 키가 사용되는 방식을 보여주는 다이어그램

푸시 메시지를 보낼 때는 애플리케이션 서버의 비공개 키로 서명된 정보가 포함된 Authorization 헤더를 만듭니다. 푸시 서비스가 푸시 메시지를 전송하라는 요청을 받으면 요청을 수신하는 엔드포인트에 연결된 공개 키를 조회하여 서명된 Authorization 헤더를 검증합니다. 서명이 유효하면 푸시 서비스는 요청이 일치하는 비공개 키가 있는 애플리케이션 서버에서 전송되었음을 알 수 있습니다. 이는 다른 사람이 애플리케이션의 사용자에게 메시지를 보내지 못하도록 하는 보안 조치입니다.

메시지를 전송할 때 비공개 애플리케이션 서버 키가 사용되는 방식을 보여주는 다이어그램

엄밀히 말해 applicationServerKey는 선택사항입니다. 하지만 Chrome에서 가장 간단한 구현에는 이 기능이 필요하며 향후 다른 브라우저에서도 필요할 수 있습니다. Firefox에서는 선택사항입니다.

VAPID 사양은 애플리케이션 서버 키를 정의합니다. 애플리케이션 서버 키 또는 VAPID 키에 대한 언급이 표시되면 동일한 키임을 기억하세요.

애플리케이션 서버 키 만들기

web-push-codelab.glitch.me를 방문하거나 web-push 명령줄을 사용하여 다음과 같이 키를 생성하여 공개 및 비공개 애플리케이션 서버 키를 만들 수 있습니다.

    $ 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 객체에는 푸시 메시지와 함께 전송되는 메시지 데이터를 암호화하는 데 사용되는 값이 포함됩니다. (메일 암호화에 대한 내용은 이 문서의 뒷부분에서 설명합니다.)

서버에 정기 결제 전송

푸시 구독이 있으면 서버로 전송합니다. 전송 방법은 개발자가 결정하지만, 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.');
      }
    });
}

Node.js 서버는 이 요청을 수신하고 나중에 사용할 수 있도록 데이터를 데이터베이스에 저장합니다.

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 세부정보를 사용하면 언제든지 사용자에게 메시지를 보낼 수 있습니다.

만료되지 않도록 정기적으로 다시 구독하세요

푸시 알림을 구독하면 nullPushSubscription.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