푸시 알림 서버 빌드

이 Codelab에서는 푸시 알림 서버를 빌드합니다. 서버는 푸시 구독 목록을 관리하고 알림을 전송합니다.

클라이언트 코드는 이미 완성되어 있습니다. 이 Codelab에서는 서버 측 기능을 다룹니다.

삽입된 Glitch 앱에서는 알림이 자동으로 차단되므로 이 페이지에서 앱을 미리 볼 수 없습니다. 대신 다음 단계를 따르세요.

  1. 리믹스하여 수정을 클릭하여 프로젝트를 수정할 수 있도록 합니다.
  2. 사이트를 미리 보려면 앱 보기를 누른 다음 전체 화면전체 화면을 누릅니다.

실시간 앱이 새 Chrome 탭에서 열립니다. 삽입된 Glitch에서 소스 보기를 클릭하여 코드를 다시 표시합니다.

이 Codelab을 진행하면서 이 페이지에 삽입된 Glitch의 코드를 변경합니다. 실시간 앱이 있는 새 탭을 새로고침하여 변경사항을 확인합니다.

먼저 앱의 클라이언트 UI를 살펴보세요.

새 Chrome 탭에서:

  1. `Control+Shift+J` (또는 Mac의 경우 `Command+Option+J`)를 눌러 DevTools를 엽니다. 콘솔 탭을 클릭합니다.

  2. UI에서 버튼을 클릭해 봅니다 (Chrome 개발자 콘솔에서 출력을 확인).

    • 서비스 워커 등록은 Glitch 프로젝트 URL의 범위에 서비스 워커를 등록합니다. 서비스 워커 등록 취소를 사용하면 서비스 워커가 삭제됩니다. 푸시 구독이 연결되어 있으면 푸시 구독도 비활성화됩니다.

    • 푸시 구독을 선택하면 푸시 구독이 생성됩니다. 서비스 워커가 등록되고 클라이언트 코드에 VAPID_PUBLIC_KEY 상수가 있는 경우에만 사용할 수 있으므로 (나중에 자세히 설명함) 아직 클릭할 수 없습니다.

    • 활성 상태의 푸시 정기 결제가 있는 경우 현재 정기 결제 알림은 서버가 엔드포인트로 알림을 전송하도록 요청합니다.

    • 모든 구독에 알림은 데이터베이스에 있는 모든 구독 엔드포인트에 알림을 보내도록 서버에 지시합니다.

      이러한 엔드포인트 중 일부는 비활성 상태일 수 있습니다. 서버에서 정기 결제에 알림을 보낼 때까지 정기 결제가 사라질 수 있습니다.

서버 측에서 어떤 일이 일어나고 있는지 살펴보겠습니다. 서버 코드의 메시지를 보려면 Glitch 인터페이스 내의 Node.js 로그를 확인하세요.

  • Glitch 앱에서 도구 -> 로그를 클릭합니다.

    Listening on port 3000와 같은 메시지가 표시될 수 있습니다.

    실시간 앱 UI에서 현재 구독 알림 또는 모든 구독 알림을 클릭한 경우 다음 메시지도 표시됩니다.

    TODO: Implement sendNotifications()
    Endpoints to send to:  []

이제 코드를 살펴보겠습니다.

  • public/index.js에는 완성된 클라이언트 코드가 포함되어 있습니다. 이 스크립트는 기능 감지를 실행하고, 서비스 워커를 등록 및 등록 취소하며, 푸시 알림에 대한 사용자의 구독을 제어합니다. 또한 새 정기 결제 및 삭제된 정기 결제에 관한 정보를 서버로 전송합니다.

    서버 기능만 작업할 것이므로 VAPID_PUBLIC_KEY 상수를 채우는 것 외에는 이 파일을 수정하지 않습니다.

  • public/service-worker.js는 푸시 이벤트를 캡처하고 알림을 표시하는 간단한 서비스 워커입니다.

  • /views/index.html에는 앱 UI가 포함되어 있습니다.

  • .env에는 Glitch가 시작될 때 앱 서버에 로드하는 환경 변수가 포함됩니다. 알림을 전송하기 위한 인증 세부정보로 .env를 채웁니다.

  • server.js는 이 Codelab에서 대부분의 작업을 수행할 파일입니다.

    시작 코드는 간단한 Express 웹 서버를 만듭니다. 코드 주석에 TODO:로 표시된 TODO 항목이 4개 있습니다. 다음 작업을 수행해야 합니다.

    이 Codelab에서는 이러한 TODO 항목을 하나씩 살펴봅니다.

VAPID 세부정보 생성 및 로드

첫 번째 TODO 항목은 VAPID 세부정보를 생성하고 Node.js 환경 변수에 추가한 후 클라이언트 및 서버 코드를 새 값으로 업데이트하는 것입니다.

배경

사용자가 알림을 구독하려면 앱과 서버의 ID를 신뢰해야 합니다. 또한 사용자는 알림을 받을 때 정기 결제를 설정한 것과 동일한 앱에서 전송한 알림임을 확신할 수 있어야 합니다. 또한 다른 사람이 알림 내용을 읽을 수 없다고 신뢰해야 합니다.

푸시 알림을 안전하고 비공개로 만드는 프로토콜을 웹 푸시용 자발적 애플리케이션 서버 ID (VAPID)라고 합니다. VAPID는 공개 키 암호화를 사용하여 앱, 서버, 구독 엔드포인트의 ID를 확인하고 알림 콘텐츠를 암호화합니다.

이 앱에서는 web-push npm 패키지를 사용하여 VAPID 키를 생성하고 알림을 암호화하여 전송합니다.

구현

이 단계에서는 앱의 VAPID 키 쌍을 생성하고 환경 변수에 추가합니다. 서버에 환경 변수를 로드하고 클라이언트 코드에 공개 키를 상수로 추가합니다.

  1. web-push 라이브러리의 generateVAPIDKeys 함수를 사용하여 VAPID 키 쌍을 만듭니다.

    server.js에서 다음 코드 줄 주변의 주석을 삭제합니다.

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */

    const vapidKeys = webpush.generateVAPIDKeys();
    console
    .log(vapidKeys);
  2. Glitch가 앱을 다시 시작하면 생성된 키가 Chrome 콘솔이 아닌 Glitch 인터페이스 내의 Node.js 로그에 출력됩니다. VAPID 키를 보려면 Glitch 인터페이스에서 Tools -> Logs를 선택합니다.

    동일한 키 쌍에서 공개 키와 비공개 키를 복사해야 합니다.

    Glitch는 코드를 수정할 때마다 앱을 다시 시작하므로 더 많은 출력이 이어지면서 생성한 첫 번째 키 쌍이 보이지 않을 수 있습니다.

  3. .env에서 VAPID 키를 복사하여 붙여넣습니다. 키를 큰따옴표 ("...")로 묶습니다.

    VAPID_SUBJECT의 경우 "mailto:test@test.test"를 입력할 수 있습니다.

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY
    =
    VAPID_PRIVATE_KEY
    =
    VAPID_SUBJECT
    =
    VAPID_PUBLIC_KEY
    ="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY
    ="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT
    ="mailto:test@test.test"
  4. VAPID 키는 한 번만 생성하면 되므로 server.js에서 이 두 줄의 코드를 다시 주석 처리합니다.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */

    const vapidKeys = webpush.generateVAPIDKeys();
    console
    .log(vapidKeys);
  5. server.js에서 환경 변수에서 VAPID 세부정보를 로드합니다.

    server.js

    const vapidDetails = {
     
    // TODO: Load VAPID details from environment variables.
      publicKey
    : process.env.VAPID_PUBLIC_KEY,
      privateKey
    : process.env.VAPID_PRIVATE_KEY,
      subject
    : process.env.VAPID_SUBJECT
    }
  6. 공개 키도 복사하여 클라이언트 코드에 붙여넣습니다.

    public/index.js에서 .env 파일에 복사한 것과 동일한 VAPID_PUBLIC_KEY 값을 입력합니다.

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````

알림을 보내는 기능 구현

배경

이 앱에서는 web-push npm 패키지를 사용하여 알림을 보냅니다.

이 패키지는 webpush.sendNotification()가 호출될 때 알림을 자동으로 암호화하므로 개발자가 걱정할 필요가 없습니다.

웹 푸시는 알림에 여러 옵션을 허용합니다. 예를 들어 메시지에 헤더를 첨부하고 콘텐츠 인코딩을 지정할 수 있습니다.

이 Codelab에서는 다음 코드 줄로 정의된 두 가지 옵션만 사용합니다.

let options = {
  TTL
: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails
: vapidDetails; // VAPID keys from .env
};

TTL (수명) 옵션은 알림에 만료 시간 제한을 설정합니다. 이는 더 이상 관련이 없는 후에 서버가 사용자에게 알림을 보내지 않도록 하는 방법입니다.

vapidDetails 옵션에는 환경 변수에서 로드한 VAPID 키가 포함됩니다.

구현

server.js에서 sendNotifications 함수를 다음과 같이 수정합니다.

server.js

function sendNotifications(database, endpoints) {
 
// TODO: Implement functionality to send notifications.
  console
.log('TODO: Implement sendNotifications()');
  console
.log('Endpoints to send to: ', endpoints);
  let notification
= JSON.stringify(createNotification());
  let options
= {
    TTL
: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails
: vapidDetails // VAPID keys from .env
 
};
  endpoints
.map(endpoint => {
    let subscription
= database[endpoint];
    webpush
.sendNotification(subscription, notification, options);
 
});
}

webpush.sendNotification()가 프로미스를 반환하므로 오류 처리를 쉽게 추가할 수 있습니다.

server.js에서 sendNotifications 함수를 다시 수정합니다.

server.js

function sendNotifications(database, endpoints) {
  let notification
= JSON.stringify(createNotification());
  let options
= {
    TTL
: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails
: vapidDetails; // VAPID keys from .env
 
};
  endpoints
.map(endpoint => {
    let subscription
= database[endpoint];
    webpush
.sendNotification(subscription, notification, options);
    let id
= endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush
.sendNotification(subscription, notification, options)
   
.then(result => {
      console
.log(`Endpoint ID: ${id}`);
      console
.log(`Result: ${result.statusCode} `);
   
})
   
.catch(error => {
      console
.log(`Endpoint ID: ${id}`);
      console
.log(`Error: ${error.body} `);
   
});
 
});
}

신규 정기 결제 처리

배경

사용자가 푸시 알림을 구독하면 다음과 같은 일이 발생합니다.

  1. 사용자가 푸시 구독을 클릭합니다.

  2. 클라이언트는 VAPID_PUBLIC_KEY 상수 (서버의 공개 VAPID 키)를 사용하여 서버별 고유한 subscription 객체를 생성합니다. subscription 객체는 다음과 같습니다.

       {
         
    "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         
    "expirationTime": null,
         
    "keys":
         
    {
           
    "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           
    "auth": "0IyyvUGNJ9RxJc83poo3bA"
         
    }
       
    }
  3. 클라이언트는 본문에 문자열로 변환된 JSON으로 구독을 포함하여 /add-subscription URL에 POST 요청을 전송합니다.

  4. 서버는 POST 요청의 본문에서 문자열화된 subscription를 가져와 JSON으로 다시 파싱한 후 구독 데이터베이스에 추가합니다.

    데이터베이스는 자체 엔드포인트를 키로 사용하여 구독을 저장합니다.

    {
     
"https://fcm...1234": {
        endpoint
: "https://fcm...1234",
        expirationTime
: ...,
        keys
: { ... }
     
},
     
"https://fcm...abcd": {
        endpoint
: "https://fcm...abcd",
        expirationTime
: ...,
        keys
: { ... }
     
},
     
"https://fcm...zxcv": {
        endpoint
: "https://fcm...zxcv",
        expirationTime
: ...,
        keys
: { ... }
     
},
   
}

이제 서버에서 알림을 보내기 위해 새 구독을 사용할 수 있습니다.

구현

새 구독 요청은 POST URL인 /add-subscription 경로로 전달됩니다. server.js에 스텁 경로 핸들러가 표시됩니다.

server.js

app.post('/add-subscription', (request, response) => {
 
// TODO: implement handler for /add-subscription
  console
.log('TODO: Implement handler for /add-subscription');
  console
.log('Request body: ', request.body);
  response
.sendStatus(200);
});

구현에서 이 핸들은 다음을 실행해야 합니다.

  • 요청 본문에서 새 구독을 가져옵니다.
  • 활성 상태인 구독의 데이터베이스에 액세스합니다.
  • 활성 정기 결제 목록에 새 정기 결제를 추가합니다.

새 정기 결제를 처리하려면 다음 단계를 따르세요.

  • server.js에서 /add-subscription의 경로 핸들러를 다음과 같이 수정합니다.

    server.js

    app.post('/add-subscription', (request, response) => {
     
// TODO: implement handler for /add-subscription
      console
.log('TODO: Implement handler for /add-subscription');
      console
.log('Request body: ', request.body);
      let subscriptions
= Object.assign({}, request.session.subscriptions);
      subscriptions
[request.body.endpoint] = request.body;
      request
.session.subscriptions = subscriptions;
      response
.sendStatus(200);
   
});

정기 결제 취소 처리

배경

구독이 비활성 상태가 되는 시점을 서버에서 항상 알 수는 없습니다. 예를 들어 브라우저가 서비스 워커를 종료하면 구독이 완전히 삭제될 수 있습니다.

하지만 서버는 앱 UI를 통해 취소된 정기 결제에 관해 알 수 있습니다. 이 단계에서는 데이터베이스에서 구독을 삭제하는 기능을 구현합니다.

이렇게 하면 서버가 존재하지 않는 엔드포인트로 많은 알림을 보내지 않습니다. 간단한 테스트 앱에서는 이러한 문제가 중요하지 않지만 대규모에서는 중요해집니다.

구현

구독 취소 요청은 /remove-subscription POST URL로 전송됩니다.

server.js의 스텁 경로 핸들러는 다음과 같습니다.

server.js

app.post('/remove-subscription', (request, response) => {
 
// TODO: implement handler for /remove-subscription
  console
.log('TODO: Implement handler for /remove-subscription');
  console
.log('Request body: ', request.body);
  response
.sendStatus(200);
});

구현에서 이 핸들은 다음을 실행해야 합니다.

  • 요청 본문에서 취소된 정기 결제의 엔드포인트를 가져옵니다.
  • 활성 상태인 구독의 데이터베이스에 액세스합니다.
  • 활성 구독 목록에서 취소된 구독을 삭제합니다.

클라이언트의 POST 요청 본문에는 제거해야 하는 엔드포인트가 포함되어 있습니다.

{
 
"endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

정기 결제 취소를 처리하려면 다음 단계를 따르세요.

  • server.js에서 /remove-subscription의 경로 핸들러를 다음과 같이 수정합니다.

    server.js

  app.post('/remove-subscription', (request, response) => {
   
// TODO: implement handler for /remove-subscription
    console
.log('TODO: Implement handler for /remove-subscription');
    console
.log('Request body: ', request.body);
    let subscriptions
= Object.assign({}, request.session.subscriptions);
   
delete subscriptions[request.body.endpoint];
    request
.session.subscriptions = subscriptions;
    response
.sendStatus(200);
 
});