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

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

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

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

이 섹션에서는 web-push Node 라이브러리를 사용합니다. 다른 언어에는 차이가 있지만 그다지 다르지 않습니다. Node.js는 JavaScript이며 독자가 가장 간편하게 액세스할 수 있어야 하기 때문입니다.

단계는 다음과 같습니다.

  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 목록에 액세스하고 코드를 실행하여 푸시 메시지를 트리거할 수 있는 다른 접근 방식을 사용할 수 있습니다.

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

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

사용자 구독에 관해 설명할 때 applicationServerKeysubscribe() 옵션에 추가하는 방법을 다루었습니다. 백엔드에서 이 비공개 키가 필요합니다.

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

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

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

npm install web-push --save

그러면 노드 스크립트에서 다음과 같이 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