회사에서 만들고 있는 서비스는 paid 마케팅을 하고 있지 않다. 따라서 앱 서비스에서 사용하는 푸시는 주요한 마케팅 수단이다. 유저에게 직접적으로 메시지를 전달할 수 있고, 무엇보다도 푸시 알림 sass 이용료 등을 제외하면 무료이기 때문이다.
그렇지만 현재 만들고 있는 서비스의 MAU에서 앱 사용자들이 차지하는 비율은 크지 않다. 상당수의 유저는 웹을 통해 우리 서비스를 사용하고 있다. 앞서 말했듯 앱 푸시를 주요 마케팅 수단으로 활용하고 있지만 훨씬 많은 유저를 대상으로 마케팅을 할 수 있다면 좋겠다는 생각이 들었다가 이전에 어떤 영상에서 잠깐 소개했던 웹 푸시가 떠올랐다.
앱 푸시 알림에 사용하는 원시그널이라는 서비스에서도 웹 푸시 기능을 지원하고 있는 만큼 실제 마케팅 수단으로 활용될 수 있는 기술이라는 생각이 있었고, WWDC 2022에서 발표된 바와 같이 mac os / ios도 지원하는 기술이라 잘만 파악해두면 우리 서비스에도 적용할 수도 있겠다는 생각이 들었다.
따라서 (1) 웹 푸시가 어떻게 동작하는지를 큰 틀에서 학습하고 (2) 여러 웹 푸시 sass 들을 살펴보며 실무에 웹 푸시를 사용할 수 있을지 생각해보고자 한다. 검색을 좀 해보니 PWA나 서비스 워커 등 다른 개념들에 대한 이해가 조금 더 필요하긴 한데, 이번 글에서는 학습한 내용 중 '어떻게 웹 푸시를 보낼 수 있는가?'에 해당하는 부분만 먼저 단순하게 정리해보고자 한다.
먼저 웹 푸시가 동작하는 큰 틀을 간단하게 이해할 필요가 있다.
+----------------+ +--------------+ +-------------+
| UA (Browser) | | Push Service | | Application |
+----------------+ +--------------+ | Server |
| | +-------------+
| Subscribe | |
|---------------------->| |
| subscription resource | |
|<=====================>| |
| | |
| Distribute Push Resource |
|--------------------------------------------->|
| | |
: : :
| | Push Message |
| Push Message |<---------------------|
|<----------------------| |
| | |
위와 같이 세 주체로 이루어져있는데 (1) 서버에서 대상(사용자)과 내용이 담긴 메시지 데이터를 푸시 서비스에 전달하면, (2) 푸시 서비스에서 사용자 정보를 식별 후 목적지(즉 브라우저)로 전달하는 것이 큰 흐름이다.
구체적으로 이 과정은 웹 푸시 프로토콜이라는 규약을 통해 진행된다.
(1) 유저가 푸시 서비스를 구독하면, 푸시 서비스는 그 유저에게 private한 구독 정보(subscription resource)를 전달한다.
(2) 유저는 서버에 이 구독 정보를 전달한다. 그럼 푸시를 보내는 주체(서버)는 해당 유저에게 푸시를 보내기 위해 필요한 유저 식별 값을 얻게 된다.
(3) 서버에서 푸시를 보낼 때는 메시지를 푸시 서비스에 보낸다. 이 때 (2)를 통해 얻은 유저 식별값을 함께 보낸다.
(4) 푸시 서비스는 서버로 부터 받은 유저 식별값을 가지고 어떤 유저에게 푸시를 전달해야 할 지 결정한다.
이 때 푸시 메시지 또한 당연히 보안의 대상이기 때문에 암호화가 필요하다.
이번 글에서 Application Server에 해당 하는 서버는 구현하지 않는다. 주 학습 목표가 웹이기도 하고 실제 웹 푸시를 발송하고 확인하는 과정을 보다 간편하게 하기 위함이다.
Folder path
는 1을 통해 받은 디렉토리의 app
폴더를 설정하면 된다.이러면 환경 세팅이 완료된다.
http://localhost:8080/
로 접속하면 아래와 같은 화면이 보인다.
개발자 도구를 켠 뒤 Application
-> Service Workers
를 클릭한 뒤 이미지와 같이 Update on reload
를 활성화 시켜주면 준비가 완료된다. Service Worker
에 대해서는 바로 다음 절에서 설명한다.
앱 서비스를 생각해보면 앱 서비스를 사용하고 있지 않아도 푸시 메시지를 수신한다. 웹 푸시는 어떨까? 브라우저가 실행되고 있지 않아도 푸시 메시지를 수신할 수 있을까?
service worker(이하 서비스워커)는 이를 가능하게 해준다. 서비스워커는 워커의 일종으로 브라우저 단에서 클라이언트와 서버 사이를 중개하는 프록시 미들웨어로서 동작한다.
이러한 특성에 의거해 네트워크에 연결되지 않아 서버에 API 요청을 보낼 수 없더라도 서비스워커 단에서 특정 리소스들을 캐싱하고 있다가 마치 API 요청이 성공적으로 수행되고 응답을 받아온 것처럼 브라우저에 리소스를 전달할 수 있다. 이러한 특성은 PWA의 핵심 요소이다.
PWA에 대해서는 추후 별도의 포스트에서 다뤄보기로 하고, 웹 푸시에서 서비스워커는 푸시 서비스로서 동작한다. 따라서 우리는 푸시 서비스에 구독하듯이 서비스워커에 브라우저를 구독시켜야 한다. 이를 위해서는 먼저 서비스워커를 '등록'해야 한다.
(이번 글에서는 서비스 워커에 대해서도 웹 푸시를 보내는데 필요한 정도만 언급하고 자세한 내용은 추후 별도의 포스트에서 다룬다. 참고할 수 있는 좋은 자료는 다음과 같다: MDN, web dev)
구글의 코드랩 레포지토리를 잘 클론받았다면 app/sw.js
파일을 확인할 수 있을 것이다. 우리는 이 파일에 서비스워커에 관련된 코드들을 작성할 것이다.
우선은 html 로드 시 실행되는 scripts/main.js
파일에서 먼저 서비스워커를 등록해보자.
// app/scripts/main.js
async function registerServiceWorker() {
if (!("serviceWorker" in navigator)) return;
console.log("Service Worker and Push are supported");
swRegistration = await navigator.serviceWorker.getRegistration();
if (!swRegistration) {
swRegistration = await navigator.serviceWorker.register("sw.js");
console.log("Service Worker is registered", swRegistration);
} else {
console.log("Service Worker is already registered", swRegistration);
}
};
registerServiceWorker();
먼저 브라우저가 서비스워커를 지원하는지 확인한다.
지원한다면 navigator.serviceWorker.getRegistration
함수를 통해 서비스워커의 등록정보를 확인한다. 만약 아직 서비스워커가 등록되지 않았다면 navigator.serviceWorker.register("sw.js");
코드를 통해 서비스워커를 등록하고 swRegistration
변수에 해당 등록 객체를 저장한다. 위에서도 언급했듯이 sw.js
파일을 통해 서비스워커의 동작을 정의한다.
우리의 웹 사이트에서 ENABLE PUSH MESSAGING
버튼은 비활성화 되어있다. 이를 활성화 시키는 코드를 작성하자.
마찬가지로 main.js
파일에 다음 두 함수를 추가한다.
async function initializeUI() {
const subscription = await swRegistration.pushManager.getSubscription();
isSubscribed = subscription;
console.log(isSubscribed ? 'User iS subscribed.' : 'User is NOT subscribed.');
updateBtn();
}
function updateBtn() {
if (isSubscribed) {
pushButton.textContent = 'Disable Push Messaging';
} else {
pushButton.textContent = 'Enable Push Messaging';
}
pushButton.disabled = false;
}
이후 initializeUI
함수를 registerServiceWorker
함수 몸체 최하단에 추가한 후 새로고침을 하면 웹 사이트가 다음과 같이 변한다.
또한 처음과 달리 개발자 도구에서 등록한 서비스워커를 확인할 수도 있다. 만약 디버깅 등의 이유로 등록해둔 서비스워커를 직접 해제하고 싶다면 이미지 우측의 Unregister
버튼을 클릭한 뒤 새로고침하면 된다.
브라우저는 푸시 서비스를 구독해야 한다. 하지만 우리의 ENABLE PUSH MESSAGING
버튼은 활성화 되었지만 어떠한 동작도 하지 않는 상태이다. 해당 버튼을 클릭 시 구독을 수행해보자.
먼저 initializeUI
함수에서 버튼에 클릭 이벤트 리스너를 달아준다.
async function initializeUI() {
pushButton.addEventListener('click', function() {
pushButton.disabled = true;
if (isSubscribed) {
unsubscribeUser();
} else {
subscribeUser();
}
});
const subscription = await swRegistration.pushManager.getSubscription();
isSubscribed = subscription;
updateSubscriptionOnServer(subscription);
console.log(isSubscribed ? 'User IS subscribed.' : 'User is NOT subscribed.');
updateBtn();
}
먼저 pushButton
element에 클릭 이벤트를 달아준다. 구독되지 않았다면 subscribeUser
함수를 호출하여 브라우저를 구독한다.
async function subscribeUser() {
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
const subscription = await swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
})
if (subscription) {
console.log('User is subscribed.');
updateSubscriptionOnServer(subscription);
isSubscribed = true;
} else {
console.error('Failed to subscribe the user: ', error);
}
updateBtn();
}
서비스워커 등록 객체(swRegistration
에는 pushManager 객체가 존재한다. 이 인터페이스의 subscribe
함수를 통해 구독할 수 있다.
applicationServerKey
는 위에서 언급한 VAPID의 공개 키를 주입하면 된다. 이 사이트에서 임시로 공개 키와 비공개 키를 생성할 수 있다.
구독이 되면 푸시 서비스를 통해 받은 구독 정보를 서버에 전송한다. 하지만 이번 글에서는 서버를 구현하지 않으므로 실제 서버 API를 호출하지는 않고 구독 정보를 웹 사이트에 보여주도록 한다.
function updateSubscriptionOnServer(subscription) {
// TODO: Send subscription to application server
const subscriptionJson = document.querySelector('.js-subscription-json');
const subscriptionDetails =
document.querySelector('.js-subscription-details');
if (subscription) {
subscriptionJson.textContent = JSON.stringify(subscription);
subscriptionDetails.classList.remove('is-invisible');
} else {
subscriptionDetails.classList.add('is-invisible');
}
}
구독 해제도 실제 서버와 연결되지는 않기 때문에 subscribeUser
와 거의 동일하다.
function unsubscribeUser() {
swRegistration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return subscription.unsubscribe();
}
})
.catch(function(error) {
console.log('Error unsubscribing', error);
})
.then(function() {
updateSubscriptionOnServer(null);
console.log('User is unsubscribed.');
isSubscribed = false;
updateBtn();
});
}
푸시 기능을 사용하기 위해서는 브라우저의 알림 권한이 필요하다. 버튼 클릭 시 알림 권한이 없다면 구독되어선 안될 것이므로 다음과 같이 updateBtn
함수를 수정하자.
function updateBtn() {
if (Notification.permission === 'denied') {
pushButton.textContent = 'Push Messaging Blocked';
pushButton.disabled = true;
updateSubscriptionOnServer(null);
return;
}
if (isSubscribed) {
pushButton.textContent = 'Disable Push Messaging';
} else {
pushButton.textContent = 'Enable Push Messaging';
}
pushButton.disabled = false;
}
이제 서비스워커의 동작을 정의해야 한다. 서버에서 푸시 메시지를 전송하면 서비스워커는 브라우저의 Notification
API를 사용하여 해당 푸시 메시지를 알림의 형태로 바꿔준다.
// sw.js
self.addEventListener('push', function(event) {
console.log('[Service Worker] Push Received.');
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);
const title = 'Push Codelab';
const options = {
body: 'Yay it works.',
icon: 'images/icon.png',
badge: 'images/badge.png'
};
event.waitUntil(self.registration.showNotification(title, options));
});
서비스워커의 동작을 정의한 파일에서 self
객체는 서비스워커를 의미한다.
여기에서는 여러가지 이벤트에 대한 리스너를 정의할 수 있는데, 그 중 push
이벤트의 이벤트 리스너를 작성하여 서버에서 푸시 요청이 왔을 때 알림을 띄우려고 한다.
이는 registration.showNotification
함수를 통해서 실행한다.
event.waitUntil
는 서비스워커의 생명주기와 관련된 함수이다. 이 글에서는 서비스워커에 대한 글이 아니지만 간단하게 설명하자면, waitUntil
함수는 Promise
객체를 인자로 받아서 Promise
가 처리될 때까지 서비스워커가 동작하도록 한다.
여기까지 코드를 작성했다면 새로고침 후 개발자도구의 서비스워커 탭에서 push
이벤트를 직접 트리거하여 테스트해볼 수 있다.
위 이미지와 같이 Push
부분에 푸시 메시지를 작성하고 옆의 버튼을 클릭하면 다음과 같이 웹 푸시가 발송되고 콘솔 창에서 입력한 푸시 메시지를 확인할 수 있다!
https://app.slack.com/t/modoodoc/login/z-app-168678765859-6018836049047-40f0de192e0520c4095cfdbd470f5e4aeb709732d1824274256bd14d0d1e76b4?s=slack&x=x-p168678765859-2255575342918-6033372434498