Angular + PWA: 설치 가능한 앱 만들기
Le 프로그레시브 웹 앱(PWA) 웹 애플리케이션 간의 다리를 나타냅니다. 그리고 네이티브 앱. 나 같은 기술 덕분에 서비스 워커, il 웹 앱 매니페스트 그리고 푸시 알림, PWA가 제공하는 스토어에서 설치한 앱과 비슷하지만 비용과 비용이 들지 않습니다. 네이티브 배포의 복잡성. Angular는 PWA 지원을 기본적으로 통합합니다. 웹 애플리케이션을 설치 가능한 앱으로 변환하는 프로세스 만들기 간단하고 잘 문서화되어 있습니다.
이 시리즈의 여덟 번째 기사에서는 현대적인 각도 우리는 모든 측면을 탐구할 것입니다 Angular를 이용한 PWA 생성 과정: 초기 구성부터 고급 관리까지 캐시, 오프라인 지원부터 푸시 알림 및 업데이트 메커니즘까지 자동 및 사용자 정의 설치 프롬프트. 각각을 테스트하고 디버깅하는 방법을 살펴보겠습니다. Chrome DevTools 도구의 기능.
이 기사에서 배울 내용
- PWA란 무엇이며 기본 앱에 비해 어떤 이점을 제공합니까?
- Angular PWA를 설정하는 방법
ng add @angular/pwa - 아이콘, 색상 및 디스플레이 모드에 대한 웹 앱 매니페스트 사용자 정의
- 서비스 워커 구성
ngsw-config.json - 캐싱 전략: 프리페치, 지연, 네트워크 우선, 캐시 우선
- 대체 페이지 및 IndexedDB를 통한 오프라인 지원
- 서비스를 통한 푸시 알림
SwPush - 업데이트 관리
SwUpdate - 다음을 사용하여 사용자 정의 프롬프트를 설치하십시오.
beforeinstallprompt - Chrome DevTools 및 Lighthouse를 사용한 테스트 및 디버깅
최신 Angular 시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 각도 신호 | 세분화된 응답성 |
| 2 | 영역 없는 변경 감지 | Zone.js 삭제 |
| 3 | 새로운 템플릿 @if, @for, @defer | 현대적인 제어 흐름 |
| 4 | 독립형 구성 요소 | NgModule이 없는 아키텍처 |
| 5 | 신호 형태 | 신호가 포함된 반응형 양식 |
| 6 | SSR 및 증분 수화 | 서버 측 렌더링 |
| 7 | Angular의 핵심 웹 바이탈 | 성능 및 지표 |
| 8 | 현재 위치 - Angular PWA | 프로그레시브 웹 앱 |
| 9 | 고급 종속성 주입 | DI 트리 셰이크 가능 |
| 10 | Angular 17에서 21로 마이그레이션 | 마이그레이션 가이드 |
1. 프로그레시브 웹 앱이란 무엇입니까?
에이 프로그레시브 웹 앱 기술을 사용하는 웹 애플리케이션입니다. 네이티브 앱과 유사한 사용자 경험을 제공합니다. 용어 '프로그레시브'는 앱이 브라우저에 관계없이 모든 사용자에게 작동함을 의미합니다. 이를 지원하는 브라우저의 경험을 점진적으로 개선합니다. 고급 기능.
PWA의 주요 특징
PWA는 다음과 같은 일련의 기능을 통해 기존 웹 애플리케이션과 구별됩니다. 스토어를 통해 배포되는 기본 앱에 더 가까워집니다.
PWA vs 네이티브 앱 vs 기존 웹 앱
| 특성 | 웹 앱 | PWA | 네이티브 앱 |
|---|---|---|---|
| 설치 가능 | No | Si | Si |
| 오프라인으로 작동 | No | Si | Si |
| 푸시 알림 | No | Si | Si |
| 자동 업데이트 | Si | Si | 매장을 통해 |
| 하드웨어 액세스 | 제한된 | 좋은 | 완벽한 |
| 분포 | URL | URL + 설치 | 앱스토어 |
| 개발 비용 | 베이스 | 베이스 | 높은 |
| SEO | Si | Si | 아니요(스토어 색인 생성) |
PWA를 선택해야 하는 경우
PWA는 가장 많은 수의 사용자에게 다가가고자 할 때 이상적인 선택입니다. 단일 코드베이스. 전자상거래, 블로그, 회사 대시보드, 도구에 적합합니다. 생산성과 오프라인 액세스 및 알림의 이점을 활용하는 모든 애플리케이션 밀어. 앱이 하드웨어(고급 Bluetooth, NFC, 센서)에 대한 심층 액세스를 요구하는 경우 세부 사항) 기본 앱이 여전히 필요할 수 있습니다.
2. 각도 PWA 설정
Angular는 PWA 구성을 자동화하는 공식 회로도를 제공합니다. 와 단일 명령을 실행하면 CLI는 매니페스트, 구성 등 필요한 모든 파일을 생성합니다. 서비스 워커 및 기본 아이콘.
# Aggiungere PWA al progetto
ng add @angular/pwa
# Cosa viene generato:
# - src/manifest.webmanifest (Web App Manifest)
# - ngsw-config.json (configurazione Service Worker)
# - src/assets/icons/ (icone PWA in varie dimensioni)
# - Modifiche a angular.json (serviceWorker: true)
# - Modifiche a index.html (link al manifest, meta theme-color)
# - Modifiche a app.config.ts (ServiceWorkerModule / provideServiceWorker)
app.config.ts의 구성
회로도를 실행한 후, app.config.ts 업데이트되었습니다
자동으로 서비스 워커를 등록합니다. 등록은 다음에서만 이루어집니다.
생산 및 적용 안정화 후.
// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
// Registra il SW dopo la stabilizzazione dell'app
registrationStrategy: 'registerWhenStable:30000'
})
]
};
서비스 워커와 개발 환경
서비스 워커는 개발 모드(isDevMode()) 왜
공격적인 캐싱은 핫 리로딩 및 라이브 리로딩을 방해합니다. 테스트하려면
서비스 워커를 로컬에서 사용하려면 다음을 사용하여 프로덕션 빌드를 실행해야 합니다.
ng build 다음과 같은 HTTP 서버를 사용하여 정적 파일을 제공합니다.
http-server o npx serve.
3. 웹 앱 매니페스트
Il 웹 앱 매니페스트 애플리케이션을 설명하는 JSON 파일입니다. 브라우저 및 운영 체제. 앱 이름, 아이콘 등의 정보가 포함되어 있습니다. 테마 색상 및 표시 모드. 유효한 매니페스트가 없으면 브라우저는 앱 설치 가능성을 제공하지 않습니다.
{
"name": "La Mia App Angular PWA",
"short_name": "MiaApp",
"description": "Un'applicazione Angular PWA completa con supporto offline",
"theme_color": "#58a6ff",
"background_color": "#0d1117",
"display": "standalone",
"orientation": "portrait-primary",
"scope": "/",
"start_url": "/",
"id": "/",
"categories": ["productivity", "utilities"],
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"screenshots": [
{
"src": "assets/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "Dashboard principale"
},
{
"src": "assets/screenshots/mobile.png",
"sizes": "750x1334",
"type": "image/png",
"form_factor": "narrow",
"label": "Vista mobile"
}
],
"shortcuts": [
{
"name": "Dashboard",
"short_name": "Dash",
"url": "/dashboard",
"icons": [{ "src": "assets/icons/shortcut-dashboard.png", "sizes": "96x96" }]
},
{
"name": "Impostazioni",
"short_name": "Settings",
"url": "/settings",
"icons": [{ "src": "assets/icons/shortcut-settings.png", "sizes": "96x96" }]
}
]
}
매니페스트 디스플레이 모드
| 방법 | 설명 | 사용 사례 |
|---|---|---|
standalone |
앱이 주소 표시줄 없이 기본 앱으로 나타납니다. | PWA에 대한 가장 일반적인 선택 |
fullscreen |
앱이 브라우저 UI 없이 전체 화면을 차지합니다. | 게임, 몰입형 프레젠테이션 |
minimal-ui |
독립형과 유사하지만 탐색 컨트롤이 최소화됩니다. | 뒤로/앞으로 탐색이 필요한 앱 |
browser |
앱이 표준 브라우저 탭에서 열립니다. | 기존 웹사이트(PWA에는 권장되지 않음) |
4. 서비스 워커 구성
파일 ngsw-config.json Angular의 PWA 구성의 핵심입니다.
캐시되는 리소스, 업데이트 방법 및 어떤 리소스를 사용하는지 정의합니다.
전략. Angular가 자동으로 파일을 생성합니다. ngsw-worker.js 동안
이 구성을 기반으로 하는 프로덕션 빌드입니다.
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
],
"dataGroups": [
{
"name": "api-freshness",
"urls": ["/api/user/**", "/api/notifications/**"],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 50,
"maxAge": "1h",
"timeout": "5s"
}
},
{
"name": "api-performance",
"urls": ["/api/articles/**", "/api/products/**"],
"cacheConfig": {
"strategy": "performance",
"maxSize": 100,
"maxAge": "1d"
}
}
],
"navigationUrls": [
"/**",
"!/__**"
]
}
자산 그룹과 데이터 그룹
| 재산 | 자산 그룹 | 데이터그룹 |
|---|---|---|
| 범위 | 정적 앱 리소스(JS, CSS, 이미지) | API의 동적 데이터 |
| 버전 관리 | 파일 해시를 기반으로 | 시간 기반(maxAge) |
| 설치 모드 | prefetch o lazy |
해당 없음 |
| 전략 | 해당 없음 | freshness o performance |
| 업데이트 | 앱 버전이 변경되는 경우 | maxAge 및 시간 초과 기준 |
5. 캐싱 전략
캐싱 전략의 선택은 성능과 성능의 균형을 유지하는 데 필수적입니다.
데이터 신선도. Angular NGSW는 i에 대한 두 가지 주요 전략을 지원합니다.
dataGroups: 선도 (네트워크 우선) e
성능 (캐시 우선) 및 자산에 대한 정적 캐싱.
캐싱 전략의 세부 사항
| 전략 | 우선 사항 | 행동 | 사용 사례 |
|---|---|---|---|
| 미리 가져오기 (자산 그룹) | 은닉처 | SW 설치 즉시 모든 리소스 다운로드 | 셸 앱, 핵심 JS 및 CSS |
| 게으른 (자산 그룹) | 주문형 캐싱 | 요청된 경우에만 리소스를 캐시합니다. | 이미지, 글꼴, 보조 자산 |
| 선도 (데이터그룹) | 회로망 | 네트워크를 사용해 보세요. 시간 초과가 만료되면 캐시를 사용하십시오. | 사용자 프로필, 알림, 실시간 데이터 |
| 성능 (데이터그룹) | 은닉처 | 사용 가능하고 만료되지 않은 경우 캐시를 사용하세요. 그렇지 않으면 네트워크 | 블로그 기사, 제품 카탈로그, 정적 데이터 |
전략 선도 패턴과 동일합니다 네트워크 우선: 서비스 워커는 먼저 네트워크를 시도하고 제한 시간 내에 요청이 실패하면 지정하면 캐시된 버전이 필요합니다. 전략 성능 그것은이다 대신 캐시 우선: 캐시된 버전이 즉시 필요합니다(만료되지 않은 경우). 백그라운드에서 캐시를 새로 고칩니다.
Angular를 사용한 오래된 재검증 중
Angular NGSW는 기본적으로 전략을 지원하지 않습니다. 재검증하는 동안 오래된,
하지만 전략을 결합하여 시뮬레이션할 수 있습니다. 성능 con un
maxAge 짧은. 예를 들어, 설정 "maxAge": "5m" 캐시
즉시 데이터를 제공하지만 5분 후에 네트워크에 새로운 요청을 보냅니다.
다음 로그인 시. 좀 더 세밀한 제어가 필요한 경우 사용을 고려해 보세요.
직접 캐시 API 맞춤 서비스 직원과 함께.
6. 오프라인 지원
PWA의 가장 중요한 기능 중 하나는 PWA 없이도 작업할 수 있다는 것입니다. 인터넷 연결. Angular는 리소스 캐싱을 자동으로 처리합니다. 서비스 워커를 통해 정적이지만 완전한 오프라인 경험을 위해서는 이것이 필요합니다. 연결 상태, 대체 페이지 및 서버 지속성도 관리합니다. 로컬 데이터.
연결 상태 감지
// network-status.service.ts
import { Injectable, signal, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class NetworkStatusService {
private platformId = inject(PLATFORM_ID);
isOnline = signal(true);
connectionType = signal<string>('unknown');
constructor() {
if (isPlatformBrowser(this.platformId)) {
this.isOnline.set(navigator.onLine);
window.addEventListener('online', () => {
this.isOnline.set(true);
this.syncPendingData();
});
window.addEventListener('offline', () => {
this.isOnline.set(false);
});
// Network Information API (se disponibile)
const connection = (navigator as any).connection;
if (connection) {
this.connectionType.set(connection.effectiveType);
connection.addEventListener('change', () => {
this.connectionType.set(connection.effectiveType);
});
}
}
}
private async syncPendingData(): Promise<void> {
// Sincronizza i dati salvati localmente quando torna online
const pendingActions = await this.getPendingActions();
for (const action of pendingActions) {
try {
await fetch(action.url, action.options);
await this.removePendingAction(action.id);
} catch (err) {
console.error('Sync fallita per:', action.id);
}
}
}
private async getPendingActions(): Promise<any[]> {
// Recupera da IndexedDB le azioni in attesa
return [];
}
private async removePendingAction(id: string): Promise<void> {
// Rimuovi azione completata da IndexedDB
}
}
IndexedDB를 사용한 데이터 지속성
구조화된 데이터를 오프라인으로 관리하려면 IndexedDB 가장 강력한 솔루션입니다.
같지 않은 localStorage (동기, 5~10MB 제한), IndexedDB는 지원
대용량 데이터, 인덱싱된 쿼리 및 비동기 작업.
// offline-storage.service.ts
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class OfflineStorageService {
private db: IDBDatabase | null = null;
private platformId = inject(PLATFORM_ID);
private readonly DB_NAME = 'pwa-offline-store';
private readonly DB_VERSION = 1;
async init(): Promise<void> {
if (!isPlatformBrowser(this.platformId)) return;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Crea object stores per i dati offline
if (!db.objectStoreNames.contains('articles')) {
db.createObjectStore('articles', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('pendingActions')) {
const store = db.createObjectStore('pendingActions', {
keyPath: 'id', autoIncrement: true
});
store.createIndex('timestamp', 'timestamp');
}
};
request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};
request.onerror = () => reject(request.error);
});
}
async save<T>(storeName: string, data: T): Promise<void> {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(storeName, 'readwrite');
tx.objectStore(storeName).put(data);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async getAll<T>(storeName: string): Promise<T[]> {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(storeName, 'readonly');
const request = tx.objectStore(storeName).getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
오프라인 저장소의 한계
- 브라우저 공유: 브라우저는 사용 가능한 저장 공간을 제한합니다(일반적으로 디스크 공간의 50-80%). 미국
navigator.storage.estimate()사용 가능한 공간을 확인하려면 - 퇴거 정책: 공간이 부족할 경우 브라우저에서 데이터가 삭제될 수 있습니다. 다음을 사용하여 영구 저장소를 요청하세요.
navigator.storage.persist() - SSR 안전: IndexedDB는 서버 측에 존재하지 않습니다. 항상 사용
isPlatformBrowser접근하기 전에 - 동기화 충돌: 사용자가 다시 온라인 상태가 되면 로컬 데이터가 서버 데이터와 충돌할 수 있습니다. 갈등 해결 전략 구현
7. 푸시 알림
푸시 알림을 통해 PWA는 다음과 같은 경우에도 사용자에게 메시지를 보낼 수 있습니다.
앱이 활성화되어 있지 않습니다. Angular가 서비스를 제공합니다 SwPush 단순화하는
구독을 관리하고 알림을 받습니다.
웹 푸시 프로토콜.
푸시 알림 구독
// push-notification.service.ts
import { Injectable, inject } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class PushNotificationService {
private swPush = inject(SwPush);
private http = inject(HttpClient);
// Chiave pubblica VAPID (generata sul server)
private readonly VAPID_PUBLIC_KEY = 'BPxGEaVr...tua-chiave-pubblica...';
async subscribeToNotifications(): Promise<void> {
try {
// Richiedi il permesso e ottieni la sottoscrizione
const subscription = await this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
});
console.log('Sottoscrizione push:', JSON.stringify(subscription));
// Invia la sottoscrizione al backend
await this.http.post('/api/push/subscribe', subscription).toPromise();
} catch (err) {
console.error('Errore sottoscrizione push:', err);
}
}
listenForNotifications(): void {
// Notifiche ricevute mentre l'app e in primo piano
this.swPush.messages.subscribe((message: any) => {
console.log('Notifica ricevuta:', message);
// Aggiorna la UI con i nuovi dati
});
// Clic sulla notifica (apre/focalizza l'app)
this.swPush.notificationClicks.subscribe((event) => {
console.log('Clic su notifica:', event.action, event.notification);
// Naviga alla pagina appropriata
const url = event.notification.data?.url;
if (url) {
window.open(url, '_blank');
}
});
}
async unsubscribe(): Promise<void> {
try {
const subscription = await this.swPush.subscription.toPromise();
if (subscription) {
await subscription.unsubscribe();
await this.http.post('/api/push/unsubscribe', {
endpoint: subscription.endpoint
}).toPromise();
}
} catch (err) {
console.error('Errore cancellazione sottoscrizione:', err);
}
}
}
서버에서 알림 보내기
// server/push-server.ts (Node.js con web-push)
import webPush from 'web-push';
// Configurazione VAPID
webPush.setVapidDetails(
'mailto:info@example.com',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
async function sendNotification(
subscription: webPush.PushSubscription,
payload: object
): Promise<void> {
const notification = {
notification: {
title: 'Nuovo articolo disponibile!',
body: 'Scopri le novità su Angular PWA',
icon: '/assets/icons/icon-192x192.png',
badge: '/assets/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
url: '/blog/angular-pwa',
dateOfArrival: Date.now()
},
actions: [
{ action: 'leggi', title: 'Leggi Ora' },
{ action: 'chiudi', title: 'Chiudi' }
]
}
};
try {
await webPush.sendNotification(
subscription,
JSON.stringify(notification)
);
} catch (error: any) {
if (error.statusCode === 410) {
// Sottoscrizione scaduta: rimuovila dal database
await removeSubscription(subscription.endpoint);
}
}
}
VAPID 키
VAPID 키(자발적 응용 프로그램 서버 식별)는 다음 용도로 사용됩니다.
브라우저 푸시 서비스에 서버를 인증하세요. 다음을 사용하여 생성할 수 있습니다.
명령 npx web-push generate-vapid-keys. 공개키를 입력해야 합니다
프런트엔드에서는 비공개 항목이 서버에만 남아 있습니다. 절대 커밋하지 마세요.
저장소의 개인 키.
8. 업데이트 관리
PWA의 새 버전을 출시하면 서비스 워커는
리소스를 업데이트하고 사용자에게 알립니다. Angular가 서비스를 제공합니다
SwUpdate 업데이트 수명주기를 관리하는 방식
확인했습니다.
// app-update.service.ts
import {
Injectable, inject, ApplicationRef
} from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter, first, interval, concat } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
private swUpdate = inject(SwUpdate);
private appRef = inject(ApplicationRef);
initUpdateCheck(): void {
if (!this.swUpdate.isEnabled) {
console.log('Service Worker non abilitato');
return;
}
// Controlla aggiornamenti ogni 6 ore
const appIsStable$ = this.appRef.isStable.pipe(
first(stable => stable)
);
const every6Hours$ = interval(6 * 60 * 60 * 1000);
const every6HoursOnceAppStable$ = concat(appIsStable$, every6Hours$);
every6HoursOnceAppStable$.subscribe(async () => {
try {
const updateFound = await this.swUpdate.checkForUpdate();
console.log(updateFound
? 'Nuovo aggiornamento trovato!'
: 'App già aggiornata');
} catch (err) {
console.error('Errore controllo aggiornamento:', err);
}
});
// Ascolta quando una nuova versione e pronta
this.swUpdate.versionUpdates.pipe(
filter((event): event is VersionReadyEvent =>
event.type === 'VERSION_READY'
)
).subscribe(event => {
console.log('Versione corrente:', event.currentVersion.hash);
console.log('Nuova versione:', event.latestVersion.hash);
this.promptUpdate();
});
// Gestisci errori irrecuperabili del SW
this.swUpdate.unrecoverable.subscribe(event => {
console.error('SW in stato irrecuperabile:', event.reason);
// Forza il reload completo
document.location.reload();
});
}
private promptUpdate(): void {
const shouldUpdate = confirm(
'Una nuova versione dell\'app è disponibile. ' +
'Vuoi aggiornare ora?'
);
if (shouldUpdate) {
// Attiva la nuova versione e ricarica
this.swUpdate.activateUpdate().then(() => {
document.location.reload();
});
}
}
}
PWA 업데이트의 수명주기
| 단계 | SwUpdate 이벤트 | 설명 |
|---|---|---|
| 1. 탐지 | VERSION_DETECTED |
SW가 서버에서 새 버전을 찾았습니다. |
| 2. 다운로드 | VERSION_INSTALLATION_FAILED |
새 버전을 다운로드하는 중 오류 발생(실패한 경우) |
| 3. 준비 | VERSION_READY |
새 버전이 다운로드되었으며 활성화할 준비가 되었습니다. |
| 4. 활성화 | activateUpdate() |
사용자가 확인하면 SW가 새 버전을 활성화합니다. |
| 5. 재충전 | location.reload() |
업데이트된 리소스로 페이지가 다시 로드됩니다. |
복구할 수 없는 서비스 워커 오류
이벤트 unrecoverable 서비스 워커가 더 이상 작업을 수행할 수 없음을 나타냅니다.
현재 버전의 앱을 올바르게 제공합니다(예: 서버가 제거됨).
여전히 자원이 필요합니다). 이 경우 유일한 해결책은 강제로 다시 로드하는 것입니다.
페이지로 완료 document.location.reload(). 이 이벤트를 모니터링하세요
프로덕션에서 배포 문제를 식별합니다.
9. 사용자 정의 설치 프롬프트
PWA가 충족되면 브라우저는 자동으로 설치 배너를 표시합니다.
설치 가능성 기준. 그러나 기본 배너는 눈에 잘 띄지 않고 제어하기 어렵습니다.
이벤트 가로채기 beforeinstallprompt 당신은 경험을 만들 수 있습니다
전용 버튼과 앱과 일치하는 디자인으로 맞춤형 설치가 가능합니다.
// pwa-install.component.ts
import {
Component, signal, inject, PLATFORM_ID, afterNextRender
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-pwa-install',
standalone: true,
template: `
<div class="install-banner">
<p>Installa la nostra app per un'esperienza migliore!</p>
<button (click)="installApp()">Installa</button>
<button (click)="dismissBanner()">Non ora</button>
</div>
`
})
export class PwaInstallComponent {
private platformId = inject(PLATFORM_ID);
showInstallBanner = signal(false);
isAppInstalled = signal(false);
private deferredPrompt: any = null;
constructor() {
afterNextRender(() => {
this.setupInstallPrompt();
this.checkIfInstalled();
});
}
private setupInstallPrompt(): void {
if (!isPlatformBrowser(this.platformId)) return;
// Intercetta l'evento beforeinstallprompt
window.addEventListener('beforeinstallprompt', (event: Event) => {
event.preventDefault();
this.deferredPrompt = event;
this.showInstallBanner.set(true);
});
// Rileva quando l'app viene installata
window.addEventListener('appinstalled', () => {
this.isAppInstalled.set(true);
this.showInstallBanner.set(false);
this.deferredPrompt = null;
console.log('PWA installata con successo!');
});
}
async installApp(): Promise<void> {
if (!this.deferredPrompt) return;
// Mostra il prompt di installazione nativo
this.deferredPrompt.prompt();
const result = await this.deferredPrompt.userChoice;
console.log('Scelta utente:', result.outcome);
if (result.outcome === 'accepted') {
console.log('Installazione accettata');
} else {
console.log('Installazione rifiutata');
}
this.deferredPrompt = null;
this.showInstallBanner.set(false);
}
dismissBanner(): void {
this.showInstallBanner.set(false);
}
private checkIfInstalled(): void {
// Verifica se l'app e già in modalità standalone
const isStandalone = window.matchMedia(
'(display-mode: standalone)'
).matches;
this.isAppInstalled.set(isStandalone);
}
}
PWA 설치 가능성 기준
| 요구 사항 | 설명 |
|---|---|
| HTTPS | 앱은 HTTPS(또는 개발용 localhost)를 통해 제공되어야 합니다. |
| 웹 앱 매니페스트 | 반드시 포함해야 함 name, icons (192px + 512px), start_url, display |
| 서비스 워커 | 이벤트 핸들러가 있는 활성 서비스 워커 fetch |
| 아직 설치되지 않았습니다. | 앱이 기기에 이미 설치되어 있을 필요는 없습니다. |
| 사용자 상호작용 | 사용자가 도메인과 상호작용(클릭, 스크롤 등)한 적이 있어야 합니다. |
10. 테스트 및 디버깅
PWA를 테스트하려면 서비스 워커가 활성화되어 있기 때문에 특정 도구가 필요합니다.
프로덕션에서는 PWA 기능을 개발 중에는 사용할 수 없습니다.
ng serve. Chrome DevTools 및 Lighthouse는 필요한 모든 것을 제공합니다.
PWA의 모든 측면을 테스트하고 디버깅합니다.
테스트를 위한 빌드 및 로컬 서비스
# 1. Build di produzione
ng build
# 2. Installa un server HTTP statico (una tantum)
npm install -g http-server
# 3. Servi la build di produzione
cd dist/tua-app/browser
http-server -p 8080 -c-1
# Oppure con npx (senza installazione globale)
npx serve dist/tua-app/browser -l 8080
# 4. Apri il browser su http://localhost:8080
# 5. Apri DevTools > Application tab
Chrome DevTools - 애플리케이션 탭
La tab 애플리케이션 Chrome DevTools의 메인 패널은 PWA의 모든 측면을 검사하고 디버그합니다.
PWA용 DevTools 패널
| 패널 | 기능 | 확인하다 |
|---|---|---|
| 명백한 | 구문 분석된 매니페스트 표시 | 이름, 아이콘, 표시 모드, 시작 URL이 정확합니다. |
| 서비스 워커 | 등록된 SW 현황 | SW 활성, 업데이트 보류 중, 오류 |
| 캐시 저장 | 캐시 내용 | 올바른 리소스가 캐시에 있음, 전체 크기 |
| IndexedDB | 로컬 데이터베이스 | 오프라인 데이터가 성공적으로 유지되었습니다. |
| 저장 | 스토리지 할당량 및 사용량 | 사용된 공간과 사용 가능한 공간 |
등대 PWA 감사
Lighthouse에는 PWA 검증 전용 섹션이 포함되어 있습니다. 감사에서는 기준을 확인합니다. 설치 가능성, 오프라인 최적화 및 모범 사례. 잘 구성된 PWA Lighthouse PWA 점수는 다음과 같아야 합니다. 100/100.
등대 PWA 체크리스트
- 서비스 워커가 핸들러를 등록합니다.
fetch - 페이지는 오프라인에서도 코드 200으로 응답합니다.
start_url매니페스트에 있고 오프라인으로 연결할 수 있음- 매니페스트에는
nameoshort_name - 매니페스트에는 최소 192px 및 512px의 아이콘이 있습니다.
- 매니페스트는 다음을 지정합니다.
display(독립형/전체 화면/최소 UI) - 콘텐츠의 크기는 뷰포트에 맞게 적절하게 조정됩니다.
- 메타 태그
theme-colorHTML에 존재 - 페이지에는
viewport유효한 메타태그 - 사이트는 HTTPS를 사용합니다
- HTTP가 HTTPS로 리디렉션됩니다.
서비스 워커 디버깅
// Nel browser, via DevTools Console:
// 1. Verificare lo stato del service worker
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => {
console.log('SW Scope:', reg.scope);
console.log('SW State:', reg.active?.state);
console.log('SW Waiting:', reg.waiting);
console.log('SW Installing:', reg.installing);
});
});
// 2. Forzare l'aggiornamento del service worker
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => reg.update());
});
// 3. Deregistrare tutti i service worker (per reset completo)
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => reg.unregister());
});
// 4. Ispezionare la cache del service worker
caches.keys().then(names => {
names.forEach(name => {
caches.open(name).then(cache => {
cache.keys().then(requests => {
console.log(`Cache "${name}":`, requests.length, 'entries');
requests.forEach(req => console.log(' -', req.url));
});
});
});
});
// 5. Simulare stato offline via DevTools
// Network tab > seleziona "Offline" nel dropdown throttling
// Oppure: Application > Service Workers > spunta "Offline"
일반적인 문제 및 해결 방법
- SW가 업데이트되지 않습니다: 개발 중에 DevTools > Application > Service Workers에서 "새로고침 시 업데이트"를 사용하세요.
- 지속적인 이전 캐시: DevTools > 애플리케이션 > 저장소 > "사이트 데이터 지우기"에서 전체 저장소 지우기
- 설치 프롬프트가 나타나지 않습니다: 매니페스트 패널에서 모든 설치 가능성 기준이 충족되는지 확인하세요.
- 푸시 알림이 도착하지 않습니다: 다음을 통해 권한이 "부여"되었는지 확인하세요.
Notification.permission그리고 구독이 유효하다는 것 - CORS 캐시 오류: 외부 리소스는 SW에서 캐시할 적절한 CORS 헤더와 함께 제공되어야 합니다.
- "대기" 상태의 SW: 새로운 SW는 모든 앱 탭이 닫힐 때까지 기다립니다. 미국
skipWaiting()즉시 활성화를 강제하기 위해
요약 및 다음 단계
프로그레시브 웹 앱은 웹 애플리케이션의 자연스러운 진화를 나타냅니다. 몇 년 전까지만 해도 기본 앱에만 있던 기능을 제공합니다. Angular는 내장된 지원 덕분에 PWA 구축을 훨씬 쉽게 만듭니다. 서비스 워커, 지능형 캐싱, 푸시 알림 및 관리형 업데이트.
이 기사의 주요 개념
- PWA: 오프라인 지원, 푸시 알림 및 자동 업데이트를 갖춘 설치 가능한 웹 애플리케이션
- 설정:
ng add @angular/pwa매니페스트, SW 구성 및 아이콘을 생성합니다. - 명백한: 앱(이름, 아이콘, 표시 모드, 시작 URL)을 설명하는 JSON 파일
- ngsw-config.json: AssetGroups(정적 리소스) 및 dataGroups(동적 API) 구성
- 캐싱: 자산에 대한 프리페치/지연 전략, API 데이터에 대한 최신성/성능
- 오프라인: 네트워크 상태 감지, 지속성을 위한 IndexedDB, 다시 온라인 상태일 때 동기화
- 푸시:
SwPush구독 및 알림 수신용, 서버 인증용 VAPID - 업데이트:
SwUpdate새 릴리스를 확인하고 알리고 활성화하려면 - 설치 프롬프트:
beforeinstallprompt맞춤 설정 UI용 - 테스트: Chrome DevTools 애플리케이션 탭, Lighthouse PWA 감사, SW 디버깅
시리즈의 다음 기사에서는 고급 종속성 주입
Angular에서는 사용자 정의 주입 토큰, 트리 셰이크 가능 공급자를 분석하고,
다중 공급자, 공장 공급자 및 범위 지정 전략 providedIn.







