웹 애플리케이션을 푸시 알림 기능을 갖춘 PWA로 변환하는 방법
본 글에서는 Firebase 클라우드 메시징을 활용하여 웹 애플리케이션이나 웹사이트를 PWA(프로그레시브 웹 앱)로 전환하는 과정을 자세히 살펴보겠습니다.
오늘날, 많은 웹 애플리케이션들은 오프라인 지원, 푸시 알림, 백그라운드 동기화와 같은 고급 기능을 제공하는 PWA로 진화하고 있습니다. PWA의 이러한 기능들은 웹 애플리케이션을 네이티브 앱과 유사하게 만들어 사용자에게 더욱 풍부한 경험을 선사합니다.
실제로 트위터나 아마존과 같은 선도적인 기업들은 사용자 참여를 높이기 위해 웹 앱을 PWA로 성공적으로 전환했습니다.
PWA란 무엇인가?
PWA는 ‘웹 앱 + 일부 네이티브 앱 기능’의 조합으로 볼 수 있습니다.
기본적으로 PWA는 기존 웹 앱(HTML, CSS, JavaScript)과 동일합니다. 모든 브라우저에서 기존 웹 앱처럼 작동하지만, 최신 브라우저에서 로드될 때는 네이티브 앱과 유사한 기능을 제공할 수 있습니다. 이는 웹 앱을 더욱 강력하게 만들 뿐만 아니라, 프런트엔드에서 자산을 미리 가져와 캐시함으로써 백엔드 서버에 대한 요청을 줄여 확장성을 높이는 효과도 가져옵니다.
PWA와 웹 앱의 차이점
- 설치 가능: PWA는 네이티브 앱처럼 기기에 설치할 수 있습니다.
- 점진적 향상: 웹 앱과 동일하게 동작하지만, 일부 네이티브 앱 기능을 추가로 제공합니다.
- 네이티브 앱 경험: 사용자는 PWA를 설치 후 네이티브 앱처럼 사용하고 탐색할 수 있습니다.
- 쉬운 접근성: 웹 앱과 달리, 사용자가 매번 웹 주소를 입력할 필요 없이 설치 후 탭 한 번으로 실행할 수 있습니다.
- 애플리케이션 캐싱: 기존 웹 앱은 브라우저의 HTTP 캐시에만 의존했지만, PWA는 클라이언트 측 코드를 사용하여 자체 캐싱이 가능합니다.
- 앱 스토어 게시: PWA는 Google Play 스토어나 iOS 앱 스토어에 게시할 수 있습니다.
애플리케이션을 PWA로 변환하는 것은 성능과 사용자 경험을 향상시키는 효과적인 방법입니다.
기업이 PWA를 고려해야 하는 이유
많은 고객들이 웹 앱 솔루션 개발 후 Android 및 iOS 앱 개발을 요청합니다. 하지만 이는 동일한 기능을 서로 다른 플랫폼에 별도로 개발해야 하므로 시간과 비용이 많이 소요됩니다.
일부 고객은 예산이 제한적이거나 출시 시간이 더 중요할 수 있습니다.
대부분의 고객 요구 사항은 PWA 기능 자체로 충족될 수 있습니다. PWA를 제안하고, Play 스토어 배포를 원할 경우 TWA(Trusted Web Activity)를 사용하여 PWA를 Android 앱으로 변환하는 아이디어를 제공할 수 있습니다.
만약 PWA로 충족할 수 없는 특정 네이티브 앱 기능이 정말 필요한 경우, 고객은 두 가지 애플리케이션을 모두 개발할 수 있습니다. 하지만 이 경우에도 Android 앱 개발이 완료될 때까지 PWA를 먼저 Play 스토어에 배포할 수 있습니다.
예를 들어, ‘타이탄 아이플러스’는 처음에는 PWA 앱을 개발하고 TWA를 이용하여 Play 스토어에 배포했습니다. 이후 Android 애플리케이션 개발을 완료한 후 Play 스토어에 실제 Android 애플리케이션을 배포했습니다. 이처럼 PWA를 활용하여 출시 시간과 개발 비용을 모두 효율적으로 관리할 수 있습니다.
PWA의 주요 기능
PWA는 웹 애플리케이션에 네이티브 앱과 유사한 다양한 기능을 제공합니다.
주요 기능은 다음과 같습니다:
- 설치 가능: 웹 애플리케이션을 네이티브 앱처럼 설치할 수 있습니다.
- 캐싱: 애플리케이션 캐싱을 통해 오프라인 환경에서도 사용할 수 있도록 지원합니다.
- 푸시 알림: 서버에서 푸시 알림을 전송하여 사용자의 웹사이트 참여를 유도합니다.
- 지오펜싱: 기기의 위치가 변경될 때마다 애플리케이션에 알림을 보낼 수 있습니다.
- 결제 요청: 네이티브 앱과 유사한 사용자 경험으로 애플리케이션 내에서 결제를 활성화합니다.
앞으로 더 많은 기능들이 추가될 예정입니다.
이 외에도 다음과 같은 기능들이 있습니다:
- 바로 가기: 매니페스트 파일에 추가된 빠른 접근 URL입니다.
- Web Share API: 애플리케이션이 다른 앱에서 공유된 데이터를 수신하도록 합니다.
- 배지 API: 설치된 PWA에 알림 수를 표시합니다.
- 주기적 백그라운드 동기화 API: 네트워크에 연결될 때까지 사용자 데이터를 저장합니다.
- 연락처 선택기: 사용자의 모바일 연락처를 선택하는 데 사용됩니다.
- 파일 선택기: 로컬 시스템/모바일에서 파일에 접근하는 데 사용됩니다.
네이티브 앱 대비 PWA의 장점
네이티브 앱은 PWA보다 성능이 우수하고 더 많은 기능을 제공합니다. 하지만 PWA는 여전히 네이티브 앱에 비해 다음과 같은 몇 가지 장점을 가지고 있습니다.
- PWA는 Android, iOS, 데스크톱 등 다양한 플랫폼에서 실행됩니다.
- 개발 비용을 절감할 수 있습니다.
- 네이티브 앱에 비해 기능 배포가 쉽습니다.
- PWA(웹사이트)는 SEO 친화적이므로 쉽게 검색될 수 있습니다.
- HTTPS에서만 작동하므로 보안성이 높습니다.
네이티브 앱 대비 PWA의 단점
- 네이티브 앱에 비해 사용 가능한 기능이 제한적일 수 있습니다.
- PWA 기능이 모든 장치를 지원하지 않을 수 있습니다.
- PWA는 앱 스토어에서 직접 검색되지 않으므로 브랜드 인지도가 낮을 수 있습니다.
Android를 사용하면 TWA(Trusted Web Activity)를 통해 Play 스토어에서 PWA를 Android 앱으로 배포할 수 있습니다. 이는 브랜드 인지도 향상에 도움이 될 것입니다.
웹 앱을 PWA로 변환하는 데 필요한 것들
웹 앱 또는 웹사이트를 PWA로 변환하기 위해서는 다음 요소들이 필요합니다.
- Service-Worker: 캐싱 및 푸시 알림을 처리하는 PWA의 핵심 구성 요소이며, 요청에 대한 프록시 역할을 합니다.
- 매니페스트 파일: 웹 애플리케이션에 대한 세부 정보를 포함하며, 홈 화면에 앱처럼 표시되도록 하는 데 사용됩니다.
- 앱 로고: PWA에 필요한 고품질 앱 아이콘(512 x 512픽셀)입니다. 홈 화면이나 스플래시 화면에 사용됩니다.
- 반응형 디자인: 웹 앱은 다양한 화면 크기에 반응하도록 디자인되어야 합니다.
서비스 워커란?
서비스 워커(클라이언트 측 스크립트)는 웹 앱과 외부 사이의 프록시 역할을 하며, 푸시 알림 전달과 캐싱을 지원합니다.
서비스 워커는 기본 JavaScript와 독립적으로 실행되므로 DOM API에 직접 접근할 수 없습니다. 하지만 IndexedDB API, Fetch API, Cache Storage API에 접근할 수 있으며, 메시지를 통해 메인 스레드와 통신할 수 있습니다.
서비스 워커가 제공하는 서비스:
- 원 도메인의 HTTP 요청 가로채기.
- 서버에서 푸시 알림 수신.
- 애플리케이션의 오프라인 사용 지원.
서비스 워커는 애플리케이션을 제어하고 요청을 조작할 수 있지만, 독립적으로 실행됩니다. 따라서 메시지 가로채기(man-in-the-middle) 공격을 방지하기 위해 원 도메인은 HTTPS로 활성화되어야 합니다.
매니페스트 파일이란?
매니페스트 파일(manifest.json)에는 브라우저에 알리는 PWA 앱의 상세 정보가 담겨 있습니다.
- name: 애플리케이션 이름.
- short_name: 애플리케이션의 짧은 이름(선택 사항). 이름과 short_name이 모두 제공되면 브라우저는 short_name을 사용합니다.
- description: 애플리케이션을 설명하는 설명.
- start_url: PWA가 시작될 때 애플리케이션의 홈페이지를 지정합니다.
- icons: 홈 화면 등에 사용되는 PWA용 이미지 세트.
- background_color: PWA 시작 시 스플래시 화면의 배경색을 설정합니다.
- display: PWA 앱에 표시할 브라우저 UI를 사용자 정의합니다.
- theme_color: PWA 앱의 테마 색상.
- scope: PWA에서 고려해야 할 애플리케이션 URL 범위입니다. 기본값은 매니페스트 파일의 위치입니다.
- shortcuts: PWA 애플리케이션에 대한 빠른 링크입니다.
웹 앱을 PWA로 변환하기
데모를 위해 ‘koreantech.org’ 웹사이트를 정적 파일로 구성했습니다.
- index.html – 홈 페이지
- articles/
- index.html – 기사 페이지
- authors/
- index.html – 작성자 페이지
- tools/
- index.html – 도구 페이지
- deals/
- index.html – 거래 페이지
기존 웹사이트 또는 웹 앱이 있는 경우 아래 단계를 따라 PWA로 변환해 보세요.
PWA에 필요한 이미지 생성
앱 로고를 1:1 비율로 5가지 크기로 자릅니다. 예를 들어, PWA 아이콘 생성기와 같은 도구를 사용하여 다양한 크기의 이미지를 빠르게 생성할 수 있습니다.
매니페스트 파일 생성
다음으로, 앱 세부 정보를 사용하여 웹 애플리케이션의 manifest.json 파일을 생성합니다. 다음은 ‘koreantech.org’ 웹사이트의 매니페스트 파일 예시입니다.
{ "name": "koreantech.org", "short_name": "koreantech.org", "description": "koreantech.org provides high-quality technology & finance articles, develops tools and APIs to help businesses and individuals grow.", "start_url": "/", "icons": [{ "src": "assets/icon/icon-128x128.png", "sizes": "128x128", "type": "image/png" }, { "src": "assets/icon/icon-152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "assets/icon/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "assets/icon/icon-384x384.png", "sizes": "384x384", "type": "image/png" }, { "src": "assets/icon/icon-512x512.png", "sizes": "512x512", "type": "image/png" }], "background_color": "#EDF2F4", "display": "standalone", "theme_color": "#B20422", "scope": "/", "shortcuts": [{ "name": "Articles", "short_name": "Articles", "description": "1595 articles on Security, Sysadmin, Digital Marketing, Cloud Computing, Development, and many other topics.", "url": "/articles", "icons": [{ "src": "/assets/icon/icon-152x152.png", "sizes": "152x152" }] }, { "name": "Authors", "short_name": "Authors", "description": "koreantech.org - Authors", "url": "/authors", "icons": [{ "src": "/assets/icon/icon-152x152.png", "sizes": "152x152" }] }, { "name": "Tools", "short_name": "Tools", "description": "koreantech.org - Tools", "url": "/tools", "icons": [{ "src": "/assets/icon/icon-152x152.png", "sizes": "152x152" }] }, { "name": "Deals", "short_name": "Deals", "description": "koreantech.org - Deals", "url": "/deals", "icons": [{ "src": "/assets/icon/icon-152x152.png", "sizes": "152x152" }] } ] }
서비스 워커 등록
루트 폴더에 register-service-worker.js와 service-worker.js라는 두 개의 스크립트 파일을 생성합니다.
register-service-worker.js는 DOM API에 접근 가능한 메인 스레드에서 실행되는 JavaScript 파일이며, service-worker.js는 메인 스레드와 독립적으로 실행되는 서비스 워커 스크립트입니다. 서비스 워커는 이벤트가 호출될 때마다 실행되며, 프로세스가 완료될 때까지 활성화됩니다.
메인 스레드 JavaScript 파일에서 서비스 워커가 등록되었는지 확인하고, 등록되지 않은 경우 서비스 워커 스크립트(service-worker.js)를 등록합니다.
register-service-worker.js 파일에 다음 코드를 추가합니다:
if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('/service-worker.js'); }); }
service-worker.js 파일에 다음 코드를 추가합니다:
self.addEventListener('install', (event) => { // 서비스 워커 설치 시 이벤트 console.log( 'install', event); self.skipWaiting(); }); self.addEventListener('activate', (event) => { // 서비스 워커 활성화 시 이벤트 console.log('activate', event); return self.clients.claim(); }); self.addEventListener('fetch', function(event) { // HTTP 요청 가로채기 event.respondWith(fetch(event.request)); // 캐시 로직 없이 모든 HTTP 요청 전송 /*event.respondWith( caches.match(event.request).then(function(response) { return response || fetch(event. request); }) );*/ // 캐시된 요청이 있으면 캐시에서 서비스하고, 없으면 새 요청을 보냄. });
여기서는 오프라인 지원을 위한 캐시 활성화 방법보다는 웹 앱을 PWA로 변환하는 데 초점을 맞춥니다.
HTML 페이지의 head 태그 안에 매니페스트 파일과 스크립트를 추가합니다:
<link rel="manifest" href="/manifest.json"> <script src="/register-service-worker.js"></script>
페이지를 새로 고침하면 모바일 크롬에서 애플리케이션 설치를 할 수 있습니다.
앱이 홈 화면에 추가됩니다.
워드프레스를 사용하는 경우, 기존 PWA 변환 플러그인을 활용할 수 있습니다. VueJS 또는 ReactJS를 사용하는 경우에는 위 방법을 따르거나, 기존 PWA npm 모듈을 사용하여 개발 속도를 높일 수 있습니다. PWA npm 모듈은 이미 오프라인 캐싱 등의 기능을 지원하기 때문입니다.
푸시 알림 활성화
웹 푸시 알림은 사용자가 애플리케이션에 더 자주 참여하도록 유도하기 위해 브라우저로 전송됩니다. 푸시 알림은 다음 API를 사용하여 활성화할 수 있습니다.
푸시 알림 활성화의 첫 번째 단계는 알림 API를 확인하고 사용자에게 알림 표시 권한을 요청하는 것입니다. 다음 코드를 register-service-worker.js 파일에 추가합니다.
if ('Notification' in window && Notification.permission != 'granted') { console.log('사용자 권한 요청') Notification.requestPermission(status => { console.log('상태:'+status) displayNotification('알림 활성화됨'); }); } const displayNotification = notificationTitle => { console.log('알림 표시') if (Notification.permission == 'granted') { navigator.serviceWorker.getRegistration().then(reg => { console.log(reg) const options = { body: '푸시 알림을 허용해주셔서 감사합니다!', icon: '/assets/icons/icon-512x512.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 0 } }; reg.showNotification(notificationTitle, options); }); } };
모든 설정이 올바르게 되었으면, 애플리케이션에서 알림을 수신할 수 있습니다.
창의 ‘알림’ 기능은 브라우저에서 알림 API를 지원한다는 것을 나타냅니다. Notification.permission 속성은 사용자가 알림 표시를 허용했는지 나타냅니다. 사용자가 허용한 경우 값은 ‘granted’가 되며, 거부한 경우 ‘denied’가 됩니다.
Firebase 클라우드 메시징 활성화 및 구독 생성
이제 실제 푸시 알림 기능을 구현할 차례입니다. 서버에서 사용자에게 푸시 알림을 보내려면 각 사용자에 대한 고유한 엔드포인트(구독)가 필요합니다. 이를 위해 Firebase 클라우드 메시징을 사용합니다.
먼저 Firebase에 방문하여 Firebase 계정을 생성하고 새 프로젝트를 만듭니다. 여기서는 ‘koreantech.org’라는 이름으로 프로젝트를 생성합니다.
- 새 프로젝트를 만들고 ‘계속’을 누릅니다.
- 다음 단계에서는 Google Analytics가 기본적으로 활성화됩니다. 필요하지 않으면 토글을 비활성화하고 ‘계속’을 누를 수 있습니다. 필요 시 Firebase 콘솔에서 나중에 활성화할 수 있습니다.
프로젝트가 생성되면 다음과 같은 화면을 볼 수 있습니다.
프로젝트 설정으로 이동하여 ‘클라우드 메시징’을 클릭하고 키를 생성합니다.
위 단계를 통해 다음 세 가지 키를 얻을 수 있습니다.
- 프로젝트 서버 키
- 웹 푸시 인증서 개인 키
- 웹 푸시 인증서 공개 키
이제 register-service-worker.js 파일에 다음 코드를 추가합니다.
const updateSubscriptionOnYourServer = subscription => { console.log('사용자 구독을 DB에 저장하는 ajax 코드 작성', subscription); // fetch, jquery, axios 등을 사용하여 구독 정보를 서버에 저장하는 ajax 요청 메소드 작성 }; const subscribeUser = async () => { const swRegistration = await navigator.serviceWorker.getRegistration(); const applicationServerPublicKey = 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY'; // 웹 푸시 인증서 공개 키를 붙여넣기 const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey); swRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) .then((subscription) => { console.log('새로운 사용자 구독:', subscription); updateSubscriptionOnServer(subscription); }) .catch((err) => { if (Notification.permission === 'denied') { console.warn('알림 권한이 거부되었습니다.'); } else { console.error('사용자 구독 실패: ', err); } }); }; const urlB64ToUint8Array = (base64String) => { const padding = '='.repeat((4 - base64String.length % 4) % 4) const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/') const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; }; const checkSubscription = async () => { const swRegistration = await navigator.serviceWorker.getRegistration(); swRegistration.pushManager.getSubscription() .then(subscription => { if (!!subscription) { console.log('사용자가 이미 구독 중.'); updateSubscriptionOnYourServer(subscription); } else { console.log('사용자가 구독하지 않았습니다. 새 사용자 구독.'); subscribeUser(); } }); }; checkSubscription();
service-worker.js 파일에 다음 코드를 추가합니다.
self.addEventListener('push', (event) => { const json = JSON.parse(event.data.text()) console.log('푸시 데이터', event.data.text()) self.registration.showNotification(json.header, json.options) });
이제 프런트엔드 설정이 완료되었습니다. 구독이 완료되면 사용자가 푸시 서비스를 거부하지 않는 한, 언제든지 푸시 알림을 보낼 수 있습니다.
Node.js 백엔드에서 푸시 알림 전송
web-push npm 모듈을 사용하면 푸시 알림 기능을 쉽게 구현할 수 있습니다.
다음은 NodeJS 서버에서 푸시 알림을 보내는 예제 코드입니다.
const webPush = require('web-push'); // pushSubscription은 프런트엔드에서 DB에 저장한 구독 정보입니다. const pushSubscription = {"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABh2…E0mTFsHtUqaye8UCoLBq8sHCgo2IC7UaafhjGmVCG_SCdhZ9Z88uGj-uwMcg","keys":{"auth":"qX6AMD5JWbu41cFWE3Lk8w","p256dh":"BLxHw0IMtBMzOHnXgPxxMgSYXxwzJPxpgR8KmAbMMe1-eOudcIcUTVw0QvrC5gWOhZs-yzDa4yKooqSnM3rnx7Y"}}; // 웹 인증서 공개 키 const vapidPublicKey = 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY'; // 웹 인증서 개인 키 const vapidPrivateKey = '웹 인증서 개인 키'; var payload = JSON.stringify({ "options": { "body": "백엔드에서 전송하는 PWA 푸시 알림 테스트", "badge": "/assets/icon/icon-152x152.png", "icon": "/assets/icon/icon-152x152.png", "vibrate": [100, 50, 100], "data": { "id": "458", }, "actions": [{ "action": "view", "title": "보기" }, { "action": "close", "title": "닫기" }] }, "header": "koreantech.org - PWA 데모에서 보낸 알림" }); var options = { vapidDetails: { subject: 'mailto:[email protected]', publicKey: vapidPublicKey, privateKey: vapidPrivateKey }, TTL: 60 }; webPush.sendNotification( pushSubscription, payload, options ).then(data => { return res.json({status : true, message : '알림 전송 완료'}); }).catch(err => { return res.json({status : false, message : err }); });
위 코드는 구독 정보에 푸시 알림을 전송하고, 서비스 워커의 푸시 이벤트를 트리거합니다.
PHP 백엔드에서 푸시 알림 전송
PHP 백엔드의 경우 web-push-php 컴포저 패키지를 사용할 수 있습니다. 다음은 푸시 알림을 보내는 예제 코드입니다.
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); require __DIR__.'/../vendor/autoload.php'; use MinishlinkWebPushWebPush; use MinishlinkWebPushSubscription; // DB에 저장된 구독 정보 $subsrciptionJson = '{"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABh2…E0mTFsHtUqaye8UCoLBq8sHCgo2IC7UaafhjGmVCG_SCdhZ9Z88uGj-uwMcg","keys":{"auth":"qX6AMD5JWbu41cFWE3Lk8w","p256dh":"BLxHw0IMtBMzOHnXgPxxMgSYXxwzJPxpgR8KmAbMMe1-eOudcIcUTVw0QvrC5gWOhZs-yzDa4yKooqSnM3rnx7Y"}}'; $payloadData = array ( 'options' => array ( 'body' => '백엔드에서 전송하는 PWA 푸시 알림 테스트', 'badge' => '/assets/icon/icon-152x152.png', 'icon' => '/assets/icon/icon-152x152.png', 'vibrate' => array ( 0 => 100, 1 => 50, 2 => 100, ), 'data' => array ( 'id' => '458', ), 'actions' => array ( 0 => array ( 'action' => 'view', 'title' => '보기', ), 1 => array ( 'action' => 'close', 'title' => '닫기', ), ), ), 'header' => 'koreantech.org - PWA 데모에서 보낸 알림', ); // 인증 $auth = [ 'GCM' => '프로젝트 개인 키', // deprecated, 호환성을 위해서만 추가 'VAPID' => [ 'subject' => 'mailto:[email protected]', // mailto 또는 웹사이트 주소 'publicKey' => 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY', // Base64-URL로 인코딩된 공개 키 'privateKey' => '웹 인증서 개인 키', // Base64-URL로 인코딩된 개인 키 ], ]; $webPush = new WebPush($auth); $subsrciptionData = json_decode($subsrciptionJson,true); // webpush 6.0 $webPush->sendOneNotification( Subscription::create($subsrciptionData), json_encode($payloadData) // 선택 사항 );
결론
본 글이 웹 애플리케이션을 PWA로 변환하는 데 도움이 되었기를 바랍니다. 본문의 소스 코드는 GitHub에서 확인할 수 있으며, 데모는 여기에서 확인할 수 있습니다. 예제 코드를 활용하여 백엔드에서 푸시 알림을 보내는 테스트를 진행했습니다.