푸시 알림 서버 빌드

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

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

샘플 앱 리믹스 및 새 탭에서 보기

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

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

게시된 앱이 새 Chrome 탭에서 열립니다. 삽입된 Glitch에서 View Source를 클릭하여 코드를 다시 표시합니다.

이 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 앱에서 Tools ->을 클릭합니다. 로그.

    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:로 표시된 4개의 TODO 항목이 있습니다. 다음 작업을 수행해야 합니다.

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

VAPID 세부정보 생성 및 로드

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

배경

알림을 구독하는 사용자는 앱과 앱의 ID를 신뢰해야 합니다. 또한 사용자는 알림을 받을 때 정기 결제를 설정한 동일한 앱에서 보낸 것임을 확신할 수 있어야 합니다. 또한 그 누구도 알림 내용을 읽을 수 없다는 것을 신뢰해야 합니다.

푸시 알림을 비공개로 안전하게 만드는 프로토콜을 VAPID (Voluntary Application Server Identification for Web Push)라고 합니다. 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 키를 보려면 도구 -> 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()가 호출될 때 알림을 자동으로 암호화하므로 신경 쓸 필요가 없습니다.

web-push는 여러 알림 옵션을 허용합니다. 예를 들어 메일에 헤더를 첨부하거나 콘텐츠 인코딩을 지정할 수 있습니다.

이 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. 클라이언트가 POST 요청을 /add-subscription URL로 전송하며, 여기에는 본문에 문자열화된 JSON으로 표시된 구독이 포함됩니다.

  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);
  });
드림