이 Codelab에서는 푸시 알림 서버를 빌드합니다. 서버는 푸시 구독 목록을 관리하고 알림을 전송합니다.
클라이언트 코드는 이미 완성되어 있습니다. 이 Codelab에서는 서버 측 기능을 다룹니다.
샘플 앱을 리믹스하고 새 탭에서 보기
삽입된 Glitch 앱에서는 알림이 자동으로 차단되므로 이 페이지에서 앱을 미리 볼 수 없습니다. 대신 다음 단계를 따르세요.
- 리믹스하여 수정을 클릭하여 프로젝트를 수정할 수 있도록 합니다.
- 사이트를 미리 보려면 앱 보기를 누른 다음 전체 화면을 누릅니다.
실시간 앱이 새 Chrome 탭에서 열립니다. 삽입된 Glitch에서 소스 보기를 클릭하여 코드를 다시 표시합니다.
이 Codelab을 진행하면서 이 페이지에 삽입된 Glitch의 코드를 변경합니다. 실시간 앱이 있는 새 탭을 새로고침하여 변경사항을 확인합니다.
시작 앱과 코드 익히기
먼저 앱의 클라이언트 UI를 살펴보세요.
새 Chrome 탭에서:
`Control+Shift+J` (또는 Mac의 경우 `Command+Option+J`)를 눌러 DevTools를 엽니다. 콘솔 탭을 클릭합니다.
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 키 쌍을 생성하고 환경 변수에 추가합니다. 서버에 환경 변수를 로드하고 클라이언트 코드에 공개 키를 상수로 추가합니다.
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);Glitch가 앱을 다시 시작하면 생성된 키가 Chrome 콘솔이 아닌 Glitch 인터페이스 내의 Node.js 로그에 출력됩니다. VAPID 키를 보려면 Glitch 인터페이스에서 Tools -> Logs를 선택합니다.
동일한 키 쌍에서 공개 키와 비공개 키를 복사해야 합니다.
Glitch는 코드를 수정할 때마다 앱을 다시 시작하므로 더 많은 출력이 이어지면서 생성한 첫 번째 키 쌍이 보이지 않을 수 있습니다.
.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"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);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
}공개 키도 복사하여 클라이언트 코드에 붙여넣습니다.
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} `);
});
});
}
신규 정기 결제 처리
배경
사용자가 푸시 알림을 구독하면 다음과 같은 일이 발생합니다.
사용자가 푸시 구독을 클릭합니다.
클라이언트는
VAPID_PUBLIC_KEY
상수 (서버의 공개 VAPID 키)를 사용하여 서버별 고유한subscription
객체를 생성합니다.subscription
객체는 다음과 같습니다.{
"endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
"expirationTime": null,
"keys":
{
"p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
"auth": "0IyyvUGNJ9RxJc83poo3bA"
}
}클라이언트는 본문에 문자열로 변환된 JSON으로 구독을 포함하여
/add-subscription
URL에POST
요청을 전송합니다.서버는 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);
});