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

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

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

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

이 섹션에서는 웹-푸시 노드 라이브러리를 사용합니다. 다른 언어도 차이가 있을 수 있지만 너무 똑같지는 않습니다. 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 목록에 액세스하고 코드를 실행하여 푸시 메시지를 트리거할 수 있는 다른 접근 방식을 만들 수 있습니다.

데모에는 푸시를 트리거할 수 있는 'admin like' 페이지가 있습니다. 이 페이지는 데모에 불과하므로 공개 페이지입니다

데모가 작동하도록 하는 각 단계를 살펴보겠습니다. 이는 Node를 처음 사용하는 사람을 포함하여 누구나 따라할 수 있는 기초 단계입니다.

사용자 구독에 관해 논의할 때 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 요청이 전송되므로 이 엔드포인트에 대한 경로를 명시적으로 만듭니다.

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() 호출은 프로미스를 반환합니다. 메시지가 성공적으로 전송되었다면 프로미스가 확인되고 Google에서 취해야 할 조치는 없습니다. 프로미스가 거부되면 PushSubscription가 여전히 유효한지 알 수 있으므로 오류를 검토해야 합니다.

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

이 예시에서는 'Not Found' 및 'Gone'의 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