๐ฑ 2025๋ PWA ์์ ์ ๋ณต! WebAssembly๋ถํฐ ES Module Service Worker๊น์ง ์ค๋ฌด ๊ฐ์ด๋
๊ด๋ฆฌ์
9์ผ ์
๐ฑ 2025๋ PWA ์์ ์ ๋ณต! WebAssembly๋ถํฐ ES Module Service Worker๊น์ง ์ค๋ฌด ๊ฐ์ด๋
๐ PWA์ ์๋ก์ด ์๋๊ฐ ์ด๋ ธ๋ค
์๋ ํ์ธ์! 2025๋ 8์ ํ์ฌ **Progressive Web Apps (PWA)**๊ฐ ์์ ํ ์๋ก์ด ์ฐจ์์ผ๋ก ์งํํ์ด์. ์ด์ PWA๋ ๋จ์ํ "์น์ฌ์ดํธ๋ฅผ ์ฑ์ฒ๋ผ ๋ณด์ด๊ฒ ํ๋ ๊ธฐ์ "์ ๋์ด์ ๋ค์ดํฐ๋ธ ์ฑ์ ๋ฅ๊ฐํ๋ ์ฑ๋ฅ๊ณผ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ํ๋ซํผ์ด ๋์๊ฑฐ๋ ์!
ํนํ WebAssembly 3.0 ํตํฉ๊ณผ ES Module Service Workers ์ง์์ผ๋ก ์ธํด, ์ด์ PWA๋ก 3D ๋ชจ๋ธ๋ง, ๋น๋์ค ํธ์ง, ์ฌ์ง์ด CAD ๋ทฐ์ด๊น์ง ๋ง๋ค ์ ์๊ฒ ๋์์ด์. ์ ๋ง ๋๋ผ์ด ๋ฐ์ ์ด์ฃ !
๊ธ๋ก๋ฒ PWA ์์ฅ ๊ท๋ชจ๊ฐ 2025๋ 28์ต ๋ฌ๋ฌ์ ๋๋ฌํ ๊ฒ์ผ๋ก ์์๋๋ ์ง๊ธ, ํ๊ตญ ๊ฐ๋ฐ์๋ค๋ ์ด ๊ฑฐ๋ํ ๋ณํ์ ๋์ฐธํด์ผ ํ ๋์ ๋๋ค.
๐ ๏ธ 2025๋ PWA์ ํ์ ์ ๋ณํ๋ค
1. ES Module Service Workers: ๋ชจ๋ํ์ ํ๋ช
๊ธฐ์กด Service Worker์ ๋ฌธ์ ์ :
// ์์ ๋ฐฉ์ - ๋ชจ๋ ์ฝ๋๊ฐ ํ๋์ ํ์ผ์
self.addEventListener('fetch', event => {
// ๋ณต์กํ ๋ก์ง์ด ๋ชจ๋ ํ๋์ ํ์ผ์ ์์
if (event.request.url.includes('/api/')) {
// API ์บ์ฑ ๋ก์ง 200์ค...
} else if (event.request.url.includes('/images/')) {
// ์ด๋ฏธ์ง ์บ์ฑ ๋ก์ง 150์ค...
}
// ๊ณ์ํด์ ๋์ด๋๋ ์คํ๊ฒํฐ ์ฝ๋...
})
2025๋ ES Module ๋ฐฉ์:
// ์๋น์ค ์์ปค ๋ฑ๋ก (๋ฉ์ธ ์ค๋ ๋)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
type: 'module' // ๐ฅ ES Module ์ง์!
});
}
// service-worker.js
import { cacheFirst } from './strategies/cache-strategies.js';
import { handleApiRequests } from './handlers/api-handler.js';
import { processImages } from './processors/image-processor.js';
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(handleApiRequests(event.request));
} else if (event.request.destination === 'image') {
event.respondWith(processImages(event.request));
} else {
event.respondWith(cacheFirst(event.request));
}
});
์ฅ์ :
- ๐ฏ ๋ชจ๋ํ๋ ์ฝ๋: ๊ธฐ๋ฅ๋ณ๋ก ํ์ผ ๋ถ๋ฆฌ ๊ฐ๋ฅ
- ๐ณ ํธ๋ฆฌ ์ ฐ์ดํน: ์ฌ์ฉํ์ง ์๋ ์ฝ๋ ์๋ ์ ๊ฑฐ
- ๐ ์ฌ์ฌ์ฉ์ฑ: ๋ค๋ฅธ ํ๋ก์ ํธ์์ ๋ชจ๋ ์ฌํ์ฉ
- ๐ ๋๋ฒ๊น ์ฉ์ด: ๋ฌธ์ ๊ฐ ์๊ธด ๋ชจ๋๋ง ์ง์ค ์์
2. WebAssembly ํตํฉ: ๋ค์ดํฐ๋ธ๊ธ ์ฑ๋ฅ
์ค์ ์ฑ๊ณต ์ฌ๋ก - AutoCAD Web:
// WebAssembly ๋ชจ๋ ๋ก๋ฉ
const loadCADEngine = async () => {
const wasmModule = await import('./cad-engine.wasm');
const engine = await wasmModule.default();
return {
renderModel: (vertices, faces) => {
// 100๋ง๊ฐ ํด๋ฆฌ๊ณค๋ 200ms ์์ ๋ ๋๋ง!
return engine.render(vertices, faces);
},
optimizeMesh: (mesh) => {
// C++๋ก ๊ตฌํ๋ ์ต์ ํ ์๊ณ ๋ฆฌ์ฆ
return engine.optimize(mesh);
}
};
};
// PWA์์ 3D ๋ชจ๋ธ๋ง ์ฑ ๊ตฌํ
const ModelViewer = () => {
const [engine, setEngine] = useState(null);
useEffect(() => {
loadCADEngine().then(setEngine);
}, []);
const handleFileUpload = async (file) => {
if (!engine) return;
const modelData = await parseCADFile(file);
const renderedModel = engine.renderModel(
modelData.vertices,
modelData.faces
);
// Canvas์ ๋ ๋๋ง
displayModel(renderedModel);
};
return (
<div>
<input type="file" onChange={handleFileUpload} />
<canvas id="model-viewer" />
</div>
);
};
3. ๊ณ ๊ธ ์คํ๋ผ์ธ ์ํคํ ์ฒ
3๋จ๊ณ ์บ์ฑ ์ ๋ต:
import { registerRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// 1. ์ ์ ์์ฐ - Cache First
registerRoute(
({ request }) => request.destination === 'script' ||
request.destination === 'style',
new CacheFirst({
cacheName: 'static-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30์ผ
}),
],
})
);
// 2. ๋์ ์ฝํ
์ธ - Stale While Revalidate
registerRoute(
//api/content//,
new StaleWhileRevalidate({
cacheName: 'content-cache',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 24 * 60 * 60, // 1์ผ
purgeOnQuotaError: true
})
]
})
);
// 3. ์ค์๊ฐ ๋ฐ์ดํฐ - Network First
registerRoute(
//api/live//,
new NetworkFirst({
cacheName: 'live-cache',
networkTimeoutSeconds: 3,
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 5 * 60 // 5๋ถ
})
]
})
);
๐ ์ค๋ฌด์์ ๋ฐ๋ก ์จ๋จน๋ PWA ๊ธฐ๋ฒ๋ค
1. ๋ฐฑ๊ทธ๋ผ์ด๋ ๋๊ธฐํ๋ก ๋ฐ์ดํฐ ์ ์ ๋ ์ ์ง
// ์๋น์ค ์์ปค์์ ์ฃผ๊ธฐ์ ๋๊ธฐํ ์ค์
self.addEventListener('periodicsync', event => {
if (event.tag === 'check-updates') {
event.waitUntil(updateContentCache());
}
});
const updateContentCache = async () => {
try {
const response = await fetch('/api/news/latest');
const data = await response.json();
const cache = await caches.open('news-cache');
await cache.put('/api/news/latest',
new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
);
// ์ฌ์ฉ์์๊ฒ ์ ์ฝํ
์ธ ์๋ฆผ
self.registration.showNotification('์๋ก์ด ๋ด์ค๊ฐ ๋์ฐฉํ์ด์!', {
body: `${data.length}๊ฐ์ ์๋ก์ด ๊ธฐ์ฌ๊ฐ ์ถ๊ฐ๋์์ต๋๋ค.`,
icon: '/icons/news-icon.png',
badge: '/icons/badge.png',
tag: 'news-update'
});
} catch (error) {
console.error('๋ฐฑ๊ทธ๋ผ์ด๋ ์
๋ฐ์ดํธ ์คํจ:', error);
}
};
// ๋ฉ์ธ ์ค๋ ๋์์ ์ฃผ๊ธฐ์ ๋๊ธฐํ ๋ฑ๋ก
navigator.serviceWorker.ready.then(registration => {
registration.periodicSync.register('check-updates', {
minInterval: 6 * 60 * 60 * 1000 // 6์๊ฐ๋ง๋ค
});
});
2. IndexedDB๋ฅผ ํ์ฉํ ๋์ฉ๋ ๋ฐ์ดํฐ ๊ด๋ฆฌ
// IndexedDB ํฌํผ ํด๋์ค
class PWADatabase {
constructor() {
this.dbName = 'PWAContentDB';
this.version = 1;
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// ๋ด์ค ๊ธฐ์ฌ ์คํ ์ด
const articlesStore = db.createObjectStore('articles', {
keyPath: 'id'
});
articlesStore.createIndex('category', 'category', { unique: false });
articlesStore.createIndex('publishedAt', 'publishedAt', { unique: false });
// ์ด๋ฏธ์ง ์บ์ ์คํ ์ด (์ต๋ 2GB๊น์ง ๊ฐ๋ฅ!)
const imagesStore = db.createObjectStore('images', {
keyPath: 'url'
});
imagesStore.createIndex('size', 'size', { unique: false });
};
});
}
async saveArticles(articles) {
const transaction = this.db.transaction(['articles'], 'readwrite');
const store = transaction.objectStore('articles');
for (const article of articles) {
await store.put(article);
}
return transaction.complete;
}
async getArticlesByCategory(category) {
const transaction = this.db.transaction(['articles'], 'readonly');
const store = transaction.objectStore('articles');
const index = store.index('category');
return new Promise((resolve, reject) => {
const request = index.getAll(category);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// ์คํ ๋ฆฌ์ง ์ฉ๋ ๊ด๋ฆฌ
async cleanupOldData() {
const transaction = this.db.transaction(['articles'], 'readwrite');
const store = transaction.objectStore('articles');
const index = store.index('publishedAt');
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
const request = index.openCursor(IDBKeyRange.upperBound(oneWeekAgo));
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
cursor.delete(); // ์ค๋๋ ๊ธฐ์ฌ ์ญ์
cursor.continue();
}
};
}
}
3. ์ค๋งํธํ ํธ์ ์๋ฆผ ์ ๋ต
// ๊ฐ์ธํ๋ ํธ์ ์๋ฆผ
class SmartNotificationManager {
constructor() {
this.userPreferences = this.getUserPreferences();
}
async sendPersonalizedNotification(data) {
// ์ฌ์ฉ์์ ํ๋ ํจํด ๋ถ์
const userActivity = await this.analyzeUserActivity();
// ์ต์ ์ ์๋ฆผ ์๊ฐ ๊ณ์ฐ
const optimalTime = this.calculateOptimalTime(userActivity);
// ์ฌ์ฉ์๊ฐ ์ ํธํ๋ ์๋ฆผ ํ์
ํ์ธ
if (!this.userPreferences.categories.includes(data.category)) {
return; // ๊ด์ฌ ์๋ ์นดํ
๊ณ ๋ฆฌ๋ ์๋ฆผ ์ ํจ
}
// ์๋ฆผ ๋น๋ ์ ํ (์คํธ ๋ฐฉ์ง)
const recentNotifications = await this.getRecentNotifications();
if (recentNotifications.length > 3) {
return; // ์ต๊ทผ 3๊ฐ ์ด์ ์๋ฆผ์ด ์์ผ๋ฉด ๊ฑด๋๋ฐ๊ธฐ
}
// ๊ฐ์ธํ๋ ์๋ฆผ ๋ด์ฉ ์์ฑ
const personalizedContent = this.generatePersonalizedContent(data);
self.registration.showNotification(personalizedContent.title, {
body: personalizedContent.body,
icon: '/icons/notification-icon.png',
badge: '/icons/badge.png',
image: data.imageUrl,
actions: [
{
action: 'read',
title: '์ง๊ธ ์ฝ๊ธฐ',
icon: '/icons/read-icon.png'
},
{
action: 'save',
title: '๋์ค์ ์ฝ๊ธฐ',
icon: '/icons/save-icon.png'
}
],
tag: data.id,
requireInteraction: data.priority === 'high',
timestamp: Date.now()
});
}
generatePersonalizedContent(data) {
const userName = this.userPreferences.name || '๊ฐ๋ฐ์๋';
return {
title: `${userName}, ์๋ก์ด ${data.category} ์์์ด์์!`,
body: `${data.title}\n\n์์ ์ฝ๊ธฐ ์๊ฐ: ${data.readingTime}๋ถ`
};
}
}
๐ 2025๋ PWA ์ฑ๋ฅ ๋ฒค์น๋งํฌ
์ค์ ์ฑ๊ณผ ๋ฐ์ดํฐ
AutoCAD Web PWA:
- 100๋ง ํด๋ฆฌ๊ณค ๋ชจ๋ธ ๋ ๋๋ง: 200ms ์ดํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋: ๋ค์ดํฐ๋ธ ์ฑ ๋๋น 30% ์ ์ฝ
- ์ค์น ์ฉ๋: ๋ค์ดํฐ๋ธ ์ฑ ๋๋น 25๋ฐฐ ์์
Economic Times PWA:
- LCP (Largest Contentful Paint): 2.5์ด (80% ๊ฐ์ )
- CLS (Cumulative Layout Shift): 0.09 ๋ฌ์ฑ
- ๊ฒฐ๊ณผ: ์ดํ๋ฅ 43% ๊ฐ์
Yahoo! JAPAN PWA:
- ์ธ์ ๋น ํ์ด์ง๋ทฐ: +15.1%
- ์ธ์ ์ง์์๊ฐ: +13.3%
- ์ดํ๋ฅ : -1.72%
HTTP/3 ๋์ ํจ๊ณผ
// HTTP/3 ํ์ฉ ์ต์ ํ
const optimizedFetch = async (url, options = {}) => {
// HTTP/3 ์ง์ ํ์ธ
if ('connection' in navigator && navigator.connection.type === 'http3') {
// HTTP/3 ์ ์ฉ ์ต์ ํ ์ต์
return fetch(url, {
...options,
keepalive: true, // ์ฐ๊ฒฐ ์ ์ง
priority: 'high' // ์ฐ์ ์์ ์ค์
});
}
// ๊ธฐ๋ณธ fetch
return fetch(url, options);
};
// ์ฑ๋ฅ ์ธก์
const measurePerformance = () => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'navigation') {
console.log('TTFB (Time to First Byte):', entry.responseStart - entry.requestStart);
console.log('DOM ๋ก๋ฉ ์๊ฐ:', entry.domContentLoadedEventEnd - entry.requestStart);
}
});
});
observer.observe({ entryTypes: ['navigation'] });
};
๐ข ๊ธฐ์ ์ ์ฉ ์ฌ๋ก์ ROI ๋ถ์
1. ๋ฌผ๋ฅ ํ์ฌ ํ์ฅ ์์ PWA
๋์ ๋ฐฐ๊ฒฝ:
- ํ์ฅ ์์ ์๋ค์ด ์คํ๋ผ์ธ ํ๊ฒฝ์์ ์์ ํด์ผ ํจ
- ๋ค์ดํฐ๋ธ ์ฑ ๊ฐ๋ฐ ๋น์ฉ ๋ถ๋ด
- ๋ค์ํ ๋๋ฐ์ด์ค ์ง์ ํ์
PWA ์๋ฃจ์ :
// ์คํ๋ผ์ธ ์ฐ์ ๋ฐ์ดํฐ ๋๊ธฐํ
class FieldOperationsPWA {
constructor() {
this.pendingOperations = [];
this.setupBackgroundSync();
}
// ์์
๋ฐ์ดํฐ ์คํ๋ผ์ธ ์ ์ฅ
async saveOperation(operationData) {
// IndexedDB์ ์ฆ์ ์ ์ฅ
await this.db.saveOperation(operationData);
// ์จ๋ผ์ธ ์ํ๋ฉด ์ฆ์ ๋๊ธฐํ
if (navigator.onLine) {
await this.syncOperation(operationData);
} else {
// ์คํ๋ผ์ธ์ด๋ฉด ๋๊ธฐ์ด์ ์ถ๊ฐ
this.pendingOperations.push(operationData);
this.showOfflineStatus();
}
}
// ๋ฐฑ๊ทธ๋ผ์ด๋ ๋๊ธฐํ ์ค์
setupBackgroundSync() {
navigator.serviceWorker.ready.then(registration => {
// ๋คํธ์ํฌ ์ฐ๊ฒฐ ๋ณต๊ตฌ์ ์๋ ๋๊ธฐํ
registration.sync.register('sync-operations');
});
// ์จ๋ผ์ธ ์ํ ๋ณํ ๊ฐ์ง
window.addEventListener('online', () => {
this.syncPendingOperations();
});
}
async syncPendingOperations() {
for (const operation of this.pendingOperations) {
try {
await this.syncOperation(operation);
this.pendingOperations = this.pendingOperations.filter(
op => op.id !== operation.id
);
} catch (error) {
console.error('๋๊ธฐํ ์คํจ:', error);
}
}
}
}
๊ฒฐ๊ณผ:
- ๊ฐ๋ฐ ๋น์ฉ: 70% ์ ์ฝ (iOS/Android ๊ฐ๊ฐ ๊ฐ๋ฐ vs PWA ๋จ์ผ ๊ฐ๋ฐ)
- ๋ฐฐํฌ ์๊ฐ: 3๊ฐ์ โ 2์ฃผ
- ์คํ๋ผ์ธ ์์ ํจ์จ: 40% ํฅ์
- ๋๋ฐ์ด์ค ํธํ์ฑ: 100% (๋ชจ๋ ์ค๋งํธํฐ์์ ๋์)
2. ๋ฏธ๋์ด ํ์ฌ ๋ด์ค ํธ์ง PWA
๋์ ๋ฐฐ๊ฒฝ:
- ๊ธฐ์๋ค์ด ์ธ๋ถ์์ ์ค์๊ฐ ๋ด์ค ํธ์ง ํ์
- ๋น ๋ฅธ ๋ฐฐํฌ์ ์ ๋ฐ์ดํธ ์๊ตฌ
- ๋ค์ํ ๋ฏธ๋์ด ํ์ผ ์ฒ๋ฆฌ ํ์
PWA ์๋ฃจ์ :
// ๋ฏธ๋์ด ํ์ผ ์ฒ๋ฆฌ ์ต์ ํ
class MediaEditorPWA {
constructor() {
this.wasmEncoder = null;
this.initializeWasm();
}
async initializeWasm() {
// WebAssembly ๋น๋์ค ์ธ์ฝ๋ ๋ก๋ฉ
const { default: createEncoder } = await import('./video-encoder.wasm');
this.wasmEncoder = await createEncoder();
}
// ์ด๋ฏธ์ง ์ต์ ํ ์ฒ๋ฆฌ
async processImage(imageFile) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
// WebP ํ์์ผ๋ก ๋ณํํ์ฌ ํฌ๊ธฐ 70% ์ ์ฝ
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
resolve(blob);
}, 'image/webp', 0.85);
};
img.src = URL.createObjectURL(imageFile);
});
}
// ์ค์๊ฐ ํ์
ํธ์ง
async enableRealtimeCollaboration() {
const websocket = new WebSocket('wss://newsroom.example.com/collaborate');
websocket.onmessage = (event) => {
const { type, data } = JSON.parse(event.data);
switch (type) {
case 'editor-join':
this.showCollaboratorJoined(data.editor);
break;
case 'content-change':
this.updateContentInRealtime(data.changes);
break;
case 'media-upload':
this.addMediaToStory(data.media);
break;
}
};
}
}
๊ฒฐ๊ณผ:
- ํธ์ง ์๋: 300% ํฅ์
- ํ์ผ ์ฉ๋: 70% ์ ์ฝ (WebP ๋ณํ)
- ์ค์๊ฐ ํ์ : ๋์ ํธ์ง์ 10๋ช ์ง์
- ๋ฐฐํฌ ์ฃผ๊ธฐ: 1์ผ 2ํ โ ์ค์๊ฐ ์ ๋ฐ์ดํธ
๐ฏ PWA ๊ฐ๋ฐ ์ ์ฃผ์์ฌํญ๊ณผ ํด๊ฒฐ์ฑ
1. iOS Safari ์ ์ฝ์ฌํญ ๋์
๋ฌธ์ ์ ๊ณผ ํด๊ฒฐ์ฑ :
// iOS PWA ๊ฐ์ง ๋ฐ ์ต์ ํ
const isPWAiOS = () => {
return window.navigator.standalone === true;
};
const optimizeForIOS = () => {
if (isPWAiOS()) {
// iOS PWA ์ ์ฉ ์ต์ ํ
document.body.classList.add('pwa-ios');
// ์ํ๋ฐ ์คํ์ผ ์กฐ์
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.content = '#000000'; // iOS์์ ๋ ์์ฐ์ค๋ฌ์ด ์์
}
// iOS ์์ ์์ญ ์ฒ๋ฆฌ
const style = document.createElement('style');
style.textContent = `
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
`;
document.head.appendChild(style);
}
};
// ํธ์ ์๋ฆผ iOS ๋์
const handleNotificationsiOS = () => {
if (isPWAiOS()) {
// iOS์์๋ Web Push๊ฐ ์ ํ์ ์ด๋ฏ๋ก ๋์ ์ ์
return {
requestPermission: () => {
// ์ฌ์ฉ์์๊ฒ ์๋ฆผ ์ค์ ๊ฐ์ด๋ ํ์
showIOSNotificationGuide();
return Promise.resolve('default');
},
showNotification: (title, options) => {
// ์ธ์ฑ ์๋ฆผ์ผ๋ก ๋์ฒด
showInAppNotification(title, options);
}
};
}
return {
requestPermission: () => Notification.requestPermission(),
showNotification: (title, options) => new Notification(title, options)
};
};
2. ์คํ ๋ฆฌ์ง ํ ๋น๋ ๊ด๋ฆฌ
// ์คํ ๋ฆฌ์ง ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
class StorageManager {
async checkStorageQuota() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
const usedMB = (estimate.usage / 1024 / 1024).toFixed(2);
const quotaMB = (estimate.quota / 1024 / 1024).toFixed(2);
console.log(`์คํ ๋ฆฌ์ง ์ฌ์ฉ๋: ${usedMB}MB / ${quotaMB}MB`);
// ์ฌ์ฉ๋์ด 80% ๋์ผ๋ฉด ์ ๋ฆฌ
if (estimate.usage / estimate.quota > 0.8) {
await this.cleanupStorage();
}
return {
used: estimate.usage,
quota: estimate.quota,
percentage: (estimate.usage / estimate.quota) * 100
};
}
}
async cleanupStorage() {
// ์ค๋๋ ์บ์ ๋ฐ์ดํฐ ์ ๋ฆฌ
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name =>
name.includes('v1') || name.includes('old')
);
await Promise.all(
oldCaches.map(cacheName => caches.delete(cacheName))
);
// IndexedDB ์ค๋๋ ๋ฐ์ดํฐ ์ ๋ฆฌ
const db = new PWADatabase();
await db.init();
await db.cleanupOldData();
console.log('์คํ ๋ฆฌ์ง ์ ๋ฆฌ ์๋ฃ');
}
// ์คํ ๋ฆฌ์ง ์๋ฐ ์ ์ฌ์ฉ์ ์๋ด
showStorageWarning(percentage) {
if (percentage > 90) {
const notification = document.createElement('div');
notification.className = 'storage-warning';
notification.innerHTML = `
<div class="warning-content">
<h3>์ ์ฅ ๊ณต๊ฐ์ด ๋ถ์กฑํฉ๋๋ค</h3>
<p>PWA๊ฐ ์ํํ ์๋ํ๋ ค๋ฉด ์ ์ฅ ๊ณต๊ฐ ์ ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.</p>
<button onclick="this.parentElement.parentElement.remove()">
ํ์ธ
</button>
</div>
`;
document.body.appendChild(notification);
}
}
}
๐ 2025๋ ํ๋ฐ๊ธฐ PWA ๋ก๋๋งต
9์ ์์ ์ ๋ฐ์ดํธ
- ํ์ผ ์์คํ ์ก์ธ์ค API ํ์ฅ: ๋ก์ปฌ ํ์ผ ์ง์ ํธ์ง ๊ฐ๋ฅ
- WebCodecs API ์์ ํ: ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ ๋น๋์ค ์ธ์ฝ๋ฉ
- Background Execution API: ๋ ๊ฐ๋ ฅํ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ
12์ ๋ชฉํ
- Web Locks API: ํญ ๊ฐ ๋ฆฌ์์ค ๋๊ธฐํ
- Persistent Storage: ๋ธ๋ผ์ฐ์ ๊ฐ ์์๋ก ์ญ์ ํ์ง ์๋ ์ ์ฅ์
- Advanced Camera API: ์ ๋ฌธ๊ฐ๊ธ ์นด๋ฉ๋ผ ์ ์ด
๐ก PWA ๊ฐ๋ฐ ์์ํ๊ธฐ - ์ค์ ์ฒดํฌ๋ฆฌ์คํธ
1. ๊ธฐ๋ณธ ์ค์ ์ฒดํฌ๋ฆฌ์คํธ
// ํ์ PWA ์ค์ ํ์ธ
const PWA_CHECKLIST = {
manifest: {
required: ['name', 'short_name', 'start_url', 'display', 'theme_color'],
icons: 'sizes: 192x192, 512x512 ํ์',
screenshots: '๋ชจ๋ฐ์ผ, ๋ฐ์คํฌํฑ ์คํฌ๋ฆฐ์ท ๊ฐ 1๊ฐ ์ด์'
},
serviceWorker: {
registration: '์๋น์ค ์์ปค ๋ฑ๋ก ์ฝ๋',
caching: '์ต์ ์คํ๋ผ์ธ ํ์ด์ง ์บ์ฑ',
fallbacks: '๋คํธ์ํฌ ์คํจ์ ๋์ฒด ํ์ด์ง'
},
https: 'HTTPS ์ธ์ฆ์ ํ์ (localhost ์ ์ธ)',
responsive: '๋ชจ๋ ๊ธฐ๊ธฐ์์ ๋ฐ์ํ ๋์์ธ'
};
// ์๋ ์ฒดํฌ ํจ์
const checkPWAReadiness = async () => {
const results = {};
// Manifest ํ์ธ
const manifestLink = document.querySelector('link[rel="manifest"]');
results.manifest = !!manifestLink;
// Service Worker ํ์ธ
results.serviceWorker = 'serviceWorker' in navigator;
// HTTPS ํ์ธ
results.https = location.protocol === 'https:' ||
location.hostname === 'localhost';
console.table(results);
return results;
};
2. ์ฑ๋ฅ ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ
// ์ฑ๋ฅ ์ธก์ ๋ฐ ์ต์ ํ
const measurePWAPerformance = () => {
// Core Web Vitals ์ธก์
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
});
// PWA ํนํ ๋ฉํธ๋ฆญ
const measurePWAMetrics = () => {
// ์ฒซ ํ๋ฉด ๋ก๋ฉ ์๊ฐ
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
console.log(`${entry.name}: ${entry.startTime}ms`);
});
// ์๋น์ค ์์ปค ํ์ฑํ ์๊ฐ
navigator.serviceWorker.ready.then(() => {
console.log('Service Worker Ready');
});
};
measurePWAMetrics();
};
๐ ๋ง๋ฌด๋ฆฌ: PWA๋ก ๋ฏธ๋๋ฅผ ์ค๋นํ์ธ์
2025๋ ์ PWA๋ ๋ ์ด์ "์น์ฌ์ดํธ๋ฅผ ์ฑ์ฒ๋ผ ๋ง๋๋ ๊ธฐ์ "์ด ์๋์์. ๋ค์ดํฐ๋ธ ์ฑ์ ๋ฅ๊ฐํ๋ ์๋ก์ด ํ๋ซํผ์ ๋๋ค.
PWA์ ํต์ฌ ๊ฐ์น:
- ๐ฐ ๋น์ฉ ํจ์จ์ฑ: ๋จ์ผ ์ฝ๋๋ฒ ์ด์ค๋ก ๋ชจ๋ ํ๋ซํผ ์ง์
- โก ์ฑ๋ฅ: WebAssembly์ HTTP/3๋ก ๋ค์ดํฐ๋ธ๊ธ ์๋
- ๐ ์ ๋ฐ์ดํธ: ์ค์๊ฐ ๋ฐฐํฌ, ์ฑ์คํ ์ด ์ฌ์ฌ ๋ถํ์
- ๐ ์ ๊ทผ์ฑ: URL๋ง์ผ๋ก ์ฆ์ ์ ๊ทผ ๊ฐ๋ฅ
ํ๊ตญ ๊ฐ๋ฐ์๋ค์๊ฒ ํนํ ์ค์ํ ์ด์ :
- ๊ธ๋ก๋ฒ ์ง์ถ: ๋จ์ผ PWA๋ก ์ธ๊ณ ์์ฅ ๊ณต๋ต
- ๊ฐ๋ฐ ๋น์ฉ ์ ์ฝ: ์คํํธ์ ์นํ์ ๊ฐ๋ฐ ๋น์ฉ
- ๋น ๋ฅธ MVP: ์์ด๋์ด๋ฅผ ๋น ๋ฅด๊ฒ ๊ฒ์ฆ ๊ฐ๋ฅ
- SEO ์นํ์ : ๊ฒ์ ์์ง ์ต์ ํ ์๋ ์ง์
์ง๊ธ์ด ๋ฐ๋ก PWA ์ ๋ฌธ๊ฐ๊ฐ ๋๊ธฐ์ ์ต์ ์ ์๊ธฐ์ ๋๋ค. ์ฌ๋ฌ๋ถ์ ๋ค์ ํ๋ก์ ํธ๋ PWA๋ก ์์ํด๋ณด์ธ์! ๐
PWA ๊ฐ๋ฐ ๊ณผ์ ์์ ๊ถ๊ธํ ์ ์ด๋ ๋์์ด ํ์ํ ๋ถ๋ถ์ด ์์ผ์๋ฉด ์ธ์ ๋ ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์. ํจ๊ป PWA ์ํ๊ณ๋ฅผ ํค์๋๊ฐ์!
๋๊ธ 0๊ฐ
์์ง ๋๊ธ์ด ์์ต๋๋ค
์ฒซ ๋ฒ์งธ ๋๊ธ์ ์์ฑํด๋ณด์ธ์!