이번 글에서는 Angular 앱의 PWA 기능을 한 단계 더 발전시키기 위한 동적 캐싱 전략(Dynamic Caching Strategies)과 푸시 알림(Push Notifications) 구현 방법에 대해 알아보겠습니다.
정적 캐싱과 달리 동적 캐싱은 데이터의 중요도와 업데이트 빈도에 따라 전략을 다르게 가져가야 합니다. async/await 패턴을 사용하여 Service Worker 스크립트 내에서 구현할 수 있는 대표적인 3가지 패턴입니다.
이미지나 폰트처럼 자주 변경되지 않는 리소스에 적합합니다. 캐시를 먼저 확인하고, 없을 경우에만 네트워크 요청을 보냅니다.
// sw.js (Service Worker)
const CACHE_NAME = 'asset-cache-v1';
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
(async () => {
// 1. 캐시에서 요청 확인
const cachedResponse = await caches.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
// 2. 캐시에 없다면 네트워크 요청
return fetch(event.request);
})()
);
});
실시간 데이터나 계정 정보처럼 항상 최신 상태가 중요한 경우 사용합니다. 네트워크 요청을 우선 시도하고, 오프라인 상태이거나 실패했을 때만 캐시를 사용합니다.
// sw.js
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
(async () => {
try {
// 1. 네트워크 요청 우선 시도
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// 2. 네트워크 실패 시(Offline) 캐시로 폴백(Fallback)
console.warn('Network request failed, serving from cache.', error);
return caches.match(event.request);
}
})()
);
});
성능과 최신 데이터 사이의 균형을 맞추는 전략입니다. 캐시된 데이터를 즉시 보여주어 반응 속도를 높이면서, 백그라운드에서 네트워크 요청을 통해 캐시를 최신화합니다. 뉴스 피드 등에 유용합니다.
// sw.js
const DYNAMIC_CACHE = 'dynamic-data-v1';
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
(async () => {
const cachedResponse = await caches.match(event.request);
// 1. 백그라운드에서 네트워크 요청 및 캐시 업데이트 진행
const networkFetch = fetch(event.request).then(async (res) => {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(event.request, res.clone()); // 캐시 갱신
return res;
});
// 2. 캐시 데이터가 있으면 즉시 반환(Fast), 없으면 네트워크 응답 대기
return cachedResponse || networkFetch;
})()
);
});
Service Worker를 사용하면 브라우저 탭이 닫혀 있어도 사용자에게 알림을 보낼 수 있습니다. Angular에서는 복잡한 window API 대신 @angular/service-worker 패키지의 SwPush 서비스를 사용하여 간결하게 구현할 수 있습니다.
SwPush의 requestSubscription 메소드를 사용하면 권한 요청과 구독 생성을 한 번에 처리할 수 있습니다.
// notification.component.ts
import { Component, inject } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-notification',
standalone: true,
template: `
<button (click)="subscribeToNotifications()" [disabled]="!isEnabled">
알림 켜기
</button>
`
})
export class NotificationComponent {
private swPush = inject(SwPush);
private http = inject(HttpClient);
// VAPID 키 생성 후 백엔드 및 여기에 설정 필요
readonly VAPID_PUBLIC_KEY = 'YOUR_VAPID_PUBLIC_KEY';
get isEnabled() {
return this.swPush.isEnabled;
}
subscribeToNotifications() {
this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
})
.then(sub => {
// 생성된 구독 객체(subscription)를 백엔드 서버로 전송하여 저장
this.http.post('/api/notifications/subscribe', sub).subscribe({
next: () => console.log('알림 구독 성공!'),
error: err => console.error('구독 실패', err)
});
})
.catch(err => console.error('알림 구독 중 오류 발생', err));
}
}
마지막으로 Service Worker에서 push 이벤트를 감지하여 실제 알림 UI를 띄워줍니다.
// sw.js
self.addEventListener('push', (event: PushEvent) => {
if (!event.data) return;
const payload = event.data.json();
const title = payload.title || '새로운 알림';
const options = {
body: payload.body,
icon: '/assets/icons/icon-192x192.png',
badge: '/assets/icons/badge-72x72.png',
data: payload.url // 클릭 시 이동할 URL 등을 저장
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
Stale-While-Revalidate와 같은 동적 캐싱 전략을 적절히 활용하면 Angular 앱의 체감 성능을 극대화할 수 있습니다. 또한 Angular의 SwPush 서비스를 활용하면 복잡한 푸시 구독 프로세스를 매우 직관적인 코드로 처리할 수 있습니다.
이러한 Service Worker의 심화 패턴들을 적용하여 단순한 웹사이트를 넘어선 강력한 PWA를 구축해 보시기 바랍니다.