웹 푸시 라이브러리를 사용하여 메시지 전송

웹 푸시를 사용할 때의 문제점 중 하나는 푸시 메시지를 트리거하는 것이 매우 '까다롭다'는 점입니다. 푸시 메시지를 트리거하려면 애플리케이션이 웹 푸시 프로토콜에 따라 푸시 서비스에 POST 요청을 해야 합니다. 모든 브라우저에서 푸시를 사용하려면 VAPID(애플리케이션 서버 키라고도 함)를 사용해야 합니다. 기본적으로 애플리케이션이 사용자에게 메시지를 보낼 수 있음을 증명하는 값이 포함된 헤더를 설정해야 합니다. 푸시 메시지로 데이터를 전송하려면 데이터를 암호화해야 하며 브라우저에서 메시지를 올바르게 복호화할 수 있도록 특정 헤더를 추가해야 합니다.

푸시 트리거의 주요 문제는 문제가 발생하면 문제를 진단하기 어렵다는 점입니다. 시간이 지남에 따라 브라우저 지원이 확대되면서 이 문제가 개선되고 있지만 쉽지는 않습니다. 따라서 라이브러리를 사용하여 푸시 메시지의 암호화, 형식 지정, 트리거를 처리하는 것이 좋습니다.

라이브러리가 하는 일을 자세히 알아보려면 다음 섹션을 참고하세요. 지금은 구독을 관리하고 기존 웹 푸시 라이브러리를 사용하여 푸시 요청을 실행하는 방법을 살펴보겠습니다.

이 섹션에서는 web-push Node 라이브러리를 사용합니다. 다른 언어에는 차이가 있지만 그다지 다르지 않습니다. Node는 JavaScript이므로 가장 쉽게 접근할 수 있어야 하므로 Node를 살펴보고 있습니다.

다음 단계를 따르세요.

  1. 구독을 백엔드로 전송하고 저장합니다.
  2. 저장된 구독을 검색하고 푸시 메시지를 트리거합니다.

구독 저장

데이터베이스에서 PushSubscription를 저장하고 쿼리하는 방법은 서버 측 언어와 데이터베이스 선택에 따라 다르지만, 이를 수행하는 방법의 예를 보는 것이 유용할 수 있습니다.

데모 웹페이지에서는 간단한 POST 요청을 통해 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.');
      }
    });
}

데모의 Express 서버에는 /api/save-subscription/ 엔드포인트에 해당하는 요청 리스너가 있습니다.

app.post('/api/save-subscription/', function (req, res) {

이 경로에서는 요청이 정상적이고 가비지가 가득하지 않은지 확인하기 위해 구독을 검증합니다.

const isValidSaveRequest = (req, res) => {
  // Check the request body has at least an endpoint.
  if (!req.body || !req.body.endpoint) {
    // Not a valid subscription.
    res.status(400);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'no-endpoint',
          message: 'Subscription must have an endpoint.',
        },
      }),
    );
    return false;
  }
  return true;
};

구독이 유효하면 이를 저장하고 적절한 JSON 응답을 반환해야 합니다.

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.',
        },
      }),
    );
  });

이 데모에서는 nedb를 사용하여 정기 결제를 저장합니다. 간단한 파일 기반 데이터베이스이지만 원하는 데이터베이스를 사용할 수 있습니다. 설정이 필요하지 않으므로 이 방법만 사용합니다. 프로덕션에서는 더 안정적인 것을 사용하는 것이 좋습니다. 저는 오래된 MySQL을 사용하는 경향이 있습니다.

function saveSubscriptionToDatabase(subscription) {
  return new Promise(function (resolve, reject) {
    db.insert(subscription, function (err, newDoc) {
      if (err) {
        reject(err);
        return;
      }

      resolve(newDoc._id);
    });
  });
}

푸시 메시지 보내기

푸시 메시지를 전송할 때는 궁극적으로 사용자에게 메시지를 전송하는 프로세스를 트리거하는 이벤트가 필요합니다. 일반적인 접근 방식은 푸시 메시지를 구성하고 트리거할 수 있는 관리 페이지를 만드는 것입니다. 하지만 로컬에서 실행할 프로그램을 만들거나 PushSubscription 목록에 액세스하고 코드를 실행하여 푸시 메시지를 트리거할 수 있는 다른 접근 방식을 사용할 수 있습니다.

데모에는 푸시를 트리거할 수 있는 '관리자와 유사한' 페이지가 있습니다. 데모이므로 공개 페이지입니다.

데모를 실행하는 데 필요한 각 단계를 살펴보겠습니다. 노드를 처음 접하는 사용자를 포함하여 누구나 따라할 수 있는 초보 단계입니다.

사용자 구독을 논의할 때 subscribe() 옵션에 applicationServerKey를 추가하는 방법을 다뤘습니다. 백엔드에서 이 비공개 키가 필요합니다.

데모에서는 다음과 같이 이러한 값이 Node 앱에 추가됩니다 (지루한 코드이지만 마법이 아니라는 점을 알려드리고자 합니다).

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

다음으로 노드 서버용 web-push 모듈을 설치해야 합니다.

npm install web-push --save

그런 다음 Node 스크립트에서 다음과 같이 web-push 모듈이 필요합니다.

const webpush = require('web-push');

이제 web-push 모듈을 사용할 수 있습니다. 먼저 web-push 모듈에 애플리케이션 서버 키를 알려야 합니다. VAPID 키라고도 합니다(사양의 이름이기 때문).

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

webpush.setVapidDetails(
  'mailto:web-push-book@gauntface.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey,
);

'mailto:' 문자열도 포함되어 있습니다. 이 문자열은 URL 또는 mailto 이메일 주소여야 합니다. 이 정보는 푸시를 트리거하는 요청의 일부로 웹 푸시 서비스로 전송됩니다. 이는 웹 푸시 서비스에서 발신자에게 연락해야 할 경우 이를 가능하게 하는 정보가 있기 때문입니다.

이제 web-push 모듈을 사용할 준비가 되었습니다. 다음 단계는 푸시 메시지를 트리거하는 것입니다.

이 데모에서는 가상 관리 패널을 사용하여 푸시 메시지를 트리거합니다.

관리 페이지의 스크린샷

'푸시 메시지 트리거' 버튼을 클릭하면 백엔드에서 푸시 메시지를 전송하라는 신호인 /api/trigger-push-msg/에 POST 요청이 전송되므로 이 엔드포인트에 대한 라우트를 express에서 만듭니다.

app.post('/api/trigger-push-msg/', function (req, res) {

이 요청이 수신되면 데이터베이스에서 구독을 가져와 각각에 대해 푸시 메시지를 트리거합니다.

return getSubscriptionsFromDatabase().then(function (subscriptions) {
  let promiseChain = Promise.resolve();

  for (let i = 0; i < subscriptions.length; i++) {
    const subscription = subscriptions[i];
    promiseChain = promiseChain.then(() => {
      return triggerPushMsg(subscription, dataToSend);
    });
  }

  return promiseChain;
});

그러면 triggerPushMsg() 함수가 웹 푸시 라이브러리를 사용하여 제공된 구독에 메시지를 보낼 수 있습니다.

const triggerPushMsg = function (subscription, dataToSend) {
  return webpush.sendNotification(subscription, dataToSend).catch((err) => {
    if (err.statusCode === 404 || err.statusCode === 410) {
      console.log('Subscription has expired or is no longer valid: ', err);
      return deleteSubscriptionFromDatabase(subscription._id);
    } else {
      throw err;
    }
  });
};

webpush.sendNotification() 호출은 프로미스를 반환합니다. 메일이 성공적으로 전송되면 약속이 해결되고 개발자가 취해야 할 조치는 없습니다. 약속이 거부되면 PushSubscription가 여전히 유효한지 여부를 알려주므로 오류를 검사해야 합니다.

푸시 서비스의 오류 유형을 확인하려면 상태 코드를 확인하는 것이 가장 좋습니다. 오류 메시지는 푸시 서비스마다 다르며 일부는 다른 메시지보다 유용합니다.

이 예에서는 '찾을 수 없음' 및 '존재하지 않음'의 HTTP 상태 코드인 상태 코드 404410를 확인합니다. 이러한 응답 중 하나가 수신되면 구독이 만료되었거나 더 이상 유효하지 않다는 의미입니다. 이러한 시나리오에서는 데이터베이스에서 구독을 삭제해야 합니다.

다른 오류가 발생하면 throw err만 실행하면 됩니다. 그러면 triggerPushMsg()에서 반환된 프로미스가 거부됩니다.

다음 섹션에서 웹 푸시 프로토콜을 자세히 살펴볼 때 다른 상태 코드도 다룹니다.

구독을 반복한 후 JSON 응답을 반환해야 합니다.

.then(() => {
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-send-messages',
    message: `We were unable to send messages to all subscriptions : ` +
        `'${err.message}'`
    }
}));
});

주요 구현 단계를 살펴보았습니다.

  1. 웹페이지에서 백엔드로 구독을 전송하여 데이터베이스에 저장할 수 있는 API를 만듭니다.
  2. 푸시 메시지 전송을 트리거하는 API (이 경우 가짜 관리 패널에서 호출되는 API)를 만듭니다.
  3. 백엔드에서 모든 구독을 가져오고 웹 푸시 라이브러리 중 하나를 사용하여 각 구독에 메시지를 전송합니다.

백엔드 (Node, PHP, Python 등)와 관계없이 푸시를 구현하는 단계는 동일합니다.

다음으로, 이러한 웹 푸시 라이브러리는 정확히 무엇을 할까요?

다음에 수행할 작업

Codelab