νλ‘κ·Έλ μλΈ μΉ μ±(PWA)μ μν μλΉμ€ μ컀 ꡬν μ’ ν© κ°μ΄λ. μ μ μΊμ±, μ€νλΌμΈ κΈ°λ₯ νμ±ν, κΈλ‘λ² μ¬μ©μ κ²½ν ν₯μ λ°©λ²μ λ°°μ보μΈμ.
νλ‘ νΈμλ νλ‘κ·Έλ μλΈ μΉ μ±: μλΉμ€ μ컀 ꡬν λ§μ€ν°νκΈ°
νλ‘κ·Έλ μλΈ μΉ μ±(PWA)μ κΈ°μ‘΄ μΉμ¬μ΄νΈμ λ€μ΄ν°λΈ λͺ¨λ°μΌ μ ν리μΌμ΄μ κ°μ 격차λ₯Ό ν΄μνλ©° μΉ κ°λ°μ μ€μν λ°μ μ λνλ λλ€. PWAλ₯Ό λ·λ°μΉ¨νλ ν΅μ¬ κΈ°μ μ€ νλλ μλΉμ€ μ컀μ λλ€. μ΄ κ°μ΄λλ μλΉμ€ μ컀 ꡬνμ λν ν¬κ΄μ μΈ κ°μλ₯Ό μ 곡νλ©°, κΈλ‘λ² μ¬μ©μλ₯Ό μν κ°λ ₯νκ³ λ§€λ ₯μ μΈ PWA ꡬμΆμ μν ν΅μ¬ κ°λ , μ€μ μμ λ° λͺ¨λ² μ¬λ‘λ₯Ό λ€λ£Ήλλ€.
μλΉμ€ μ컀λ 무μμΈκ°?
μλΉμ€ μ컀λ μΉ νμ΄μ§μ λ³κ°λ‘ λ°±κ·ΈλΌμ΄λμμ μ€νλλ μλ°μ€ν¬λ¦½νΈ νμΌμ λλ€. νλ‘κ·Έλλ° κ°λ₯ν λ€νΈμν¬ νλ‘μ μν μ νμ¬ λ€νΈμν¬ μμ²μ κ°λ‘μ±κ³ PWAκ° μ΄λ₯Ό μ²λ¦¬νλ λ°©μμ μ μ΄ν μ μκ² ν΄μ€λλ€. μ΄λ₯Ό ν΅ν΄ λ€μκ³Ό κ°μ κΈ°λ₯μ΄ κ°λ₯ν©λλ€:
- μ€νλΌμΈ κΈ°λ₯: μ¬μ©μκ° μ€νλΌμΈ μνμΌ λλ μ½ν μΈ μ μ κ·Όνκ³ μ±μ μ¬μ©ν μ μλλ‘ ν©λλ€.
- μΊμ±: μ μ (HTML, CSS, μλ°μ€ν¬λ¦½νΈ, μ΄λ―Έμ§)μ μ μ₯νμ¬ λ‘λ© μκ°μ κ°μ ν©λλ€.
- νΈμ μλ¦Ό: μ¬μ©μκ° μ±μ μ κ·Ήμ μΌλ‘ μ¬μ©νμ§ μμ λλ μκΈ°μ μ ν μ λ°μ΄νΈλ₯Ό μ λ¬νκ³ μνΈμμ©ν©λλ€.
- λ°±κ·ΈλΌμ΄λ λκΈ°ν: μ¬μ©μκ° μμ μ μΈ μΈν°λ· μ°κ²°μ κ°μ§ λκΉμ§ μμ μ μ§μ°μν΅λλ€.
μλΉμ€ μ컀λ μΉμμ μ§μ ν μ±κ³Ό κ°μ κ²½νμ λ§λλ λ° μ€μν μμμ΄λ©°, PWAλ₯Ό λμ± μ λ’°ν μ μκ³ λ§€λ ₯μ μ΄λ©° μ±λ₯μ΄ λ°μ΄λκ² λ§λλλ€.
μλΉμ€ μ컀 μλͺ μ£ΌκΈ°
μλΉμ€ μ컀 μλͺ μ£ΌκΈ°λ₯Ό μ΄ν΄νλ κ²μ μ¬λ°λ₯Έ ꡬνμ νμμ μ λλ€. μλͺ μ£ΌκΈ°λ μ¬λ¬ λ¨κ³λ‘ ꡬμ±λ©λλ€:
- λ±λ‘(Registration): λΈλΌμ°μ κ° νΉμ λ²μ(μ μ΄νλ URL)μ λν΄ μλΉμ€ μ컀λ₯Ό λ±λ‘ν©λλ€.
- μ€μΉ(Installation): μλΉμ€ μμ»€κ° μ€μΉλ©λλ€. μ΄ λ¨κ³μμ μΌλ°μ μΌλ‘ νμ μ μ μ μΊμν©λλ€.
- νμ±ν(Activation): μλΉμ€ μμ»€κ° νμ±νλμ΄ λ€νΈμν¬ μμ²μ μ μ΄νκΈ° μμν©λλ€.
- μ ν΄(Idle): μλΉμ€ μμ»€κ° λ°±κ·ΈλΌμ΄λμμ μ€νλλ©° μ΄λ²€νΈλ₯Ό κΈ°λ€λ¦½λλ€.
- μ λ°μ΄νΈ(Update): μ λ²μ μ μλΉμ€ μμ»€κ° κ°μ§λλ©΄ μ λ°μ΄νΈ νλ‘μΈμ€κ° μμλ©λλ€.
- μ’ λ£(Termination): λΈλΌμ°μ κ° λ¦¬μμ€λ₯Ό μ μ½νκΈ° μν΄ μλΉμ€ μ컀λ₯Ό μ’ λ£ν©λλ€.
μλΉμ€ μ컀 ꡬν: λ¨κ³λ³ κ°μ΄λ
1. μλΉμ€ μ컀 λ±λ‘νκΈ°
첫 λ²μ§Έ λ¨κ³λ λ©μΈ μλ°μ€ν¬λ¦½νΈ νμΌ(μ: `app.js`)μ μλΉμ€ μ컀λ₯Ό λ±λ‘νλ κ²μ λλ€.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}
μ΄ μ½λλ λΈλΌμ°μ κ° `serviceWorker` APIλ₯Ό μ§μνλμ§ νμΈν©λλ€. μ§μνλ κ²½μ° `service-worker.js` νμΌμ λ±λ‘ν©λλ€. μλΉμ€ μ컀λ₯Ό μ§μνμ§ μλ λΈλΌμ°μ λ₯Ό μν΄ μ μμ μΈ λ체 κΈ°λ₯μ μ 곡νκΈ° μν΄ λ±λ‘ μ€ λ°μν μ μλ μ€λ₯λ₯Ό μ²λ¦¬νλ κ²μ΄ μ€μν©λλ€.
2. μλΉμ€ μ컀 νμΌ μμ±νκΈ° (service-worker.js)
μ΄κ³³μ μλΉμ€ μ컀μ ν΅μ¬ λ‘μ§μ΄ μμΉν©λλ€. μ€μΉ λ¨κ³λΆν° μμνκ² μ΅λλ€.
μ€μΉ
μ€μΉ λ¨κ³μμλ μΌλ°μ μΌλ‘ PWAκ° μ€νλΌμΈμΌλ‘ μλνλ λ° νμν νμ μ μ μ μΊμν©λλ€. μ¬κΈ°μλ HTML, CSS, μλ°μ€ν¬λ¦½νΈ λ° μ μ¬μ μΌλ‘ μ΄λ―Έμ§μ ν°νΈκ° ν¬ν¨λ©λλ€.
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/images/logo.png',
'/manifest.json'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
μ΄ μ½λλ μΊμ μ΄λ¦(`CACHE_NAME`)κ³Ό μΊμν URL λ°°μ΄(`urlsToCache`)μ μ μν©λλ€. `install` μ΄λ²€νΈ 리μ€λλ μλΉμ€ μμ»€κ° μ€μΉλ λ νΈλ¦¬κ±°λ©λλ€. `event.waitUntil()` λ©μλλ μλΉμ€ μμ»€κ° νμ±νλκΈ° μ μ μ€μΉ νλ‘μΈμ€κ° μλ£λλλ‘ λ³΄μ₯ν©λλ€. λ΄λΆμμλ μ§μ λ μ΄λ¦μΌλ‘ μΊμλ₯Ό μ΄κ³ λͺ¨λ URLμ μΊμμ μΆκ°ν©λλ€. μ±μ μ λ°μ΄νΈν λ μΊμλ₯Ό μ½κ² 무ν¨ννκΈ° μν΄ μΊμ μ΄λ¦μ λ²μ κ΄λ¦¬(`my-pwa-cache-v1`)λ₯Ό μΆκ°νλ κ²μ κ³ λ €νμΈμ.
νμ±ν
νμ±ν λ¨κ³λ μλΉμ€ μμ»€κ° νμ±νλμ΄ λ€νΈμν¬ μμ²μ μ μ΄νκΈ° μμνλ λμ λλ€. μ΄ λ¨κ³μμ μ€λλ μΊμλ₯Ό μ 리νλ κ²μ΄ μ’μ κ΄νμ λλ€.
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
μ΄ μ½λλ λͺ¨λ μΊμ μ΄λ¦ λͺ©λ‘μ κ°μ Έμ `cacheWhitelist`μ μλ μΊμλ₯Ό μμ ν©λλ€. μ΄λ κ² νλ©΄ PWAκ° νμ μ΅μ λ²μ μ μ μ μ μ¬μ©νλλ‘ λ³΄μ₯λ©λλ€.
리μμ€ κ°μ Έμ€κΈ°
`fetch` μ΄λ²€νΈ 리μ€λλ λΈλΌμ°μ κ° λ€νΈμν¬ μμ²μ ν λλ§λ€ νΈλ¦¬κ±°λ©λλ€. μ¬κΈ°μ μμ²μ κ°λ‘μ±κ³ μΊμλ μ½ν μΈ λ₯Ό μ 곡νκ±°λ, μΊμλμ§ μμ κ²½μ° λ€νΈμν¬μμ 리μμ€λ₯Ό κ°μ Έμ¬ μ μμ΅λλ€.
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
// Not in cache - fetch and add to cache
return fetch(event.request).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two independent copies.
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
μ΄ μ½λλ λ¨Όμ μμ²λ 리μμ€κ° μΊμμ μλμ§ νμΈν©λλ€. μμΌλ©΄ μΊμλ μλ΅μ λ°νν©λλ€. μμΌλ©΄ λ€νΈμν¬μμ 리μμ€λ₯Ό κ°μ Έμ΅λλ€. λ€νΈμν¬ μμ²μ΄ μ±κ³΅νλ©΄ μλ΅μ 볡μ νμ¬ μΊμμ μΆκ°ν ν λΈλΌμ°μ μ λ°νν©λλ€. μ΄ μ λ΅μ μΊμ μ°μ , κ·Έ λ€μ λ€νΈμν¬(Cache-First, then Network)λ‘ μλ €μ Έ μμ΅λλ€.
μΊμ± μ λ΅
λ€μν μΊμ± μ λ΅μ λ€μν μ νμ 리μμ€μ μ ν©ν©λλ€. λ€μμ λͺ κ°μ§ μΌλ°μ μΈ μ λ΅μ λλ€:
- μΊμ μ°μ , κ·Έ λ€μ λ€νΈμν¬(Cache-First, then Network): μλΉμ€ μ컀λ λ¨Όμ 리μμ€κ° μΊμμ μλμ§ νμΈν©λλ€. μμΌλ©΄ μΊμλ μλ΅μ λ°νν©λλ€. μμΌλ©΄ λ€νΈμν¬μμ 리μμ€λ₯Ό κ°μ Έμ μΊμμ μΆκ°ν©λλ€. μ΄κ²μ HTML, CSS, μλ°μ€ν¬λ¦½νΈμ κ°μ μ μ μ μ μ μ’μ μ λ΅μ λλ€.
- λ€νΈμν¬ μ°μ , κ·Έ λ€μ μΊμ(Network-First, then Cache): μλΉμ€ μ컀λ λ¨Όμ λ€νΈμν¬μμ 리μμ€λ₯Ό κ°μ Έμ€λ €κ³ μλν©λλ€. λ€νΈμν¬ μμ²μ΄ μ±κ³΅νλ©΄ λ€νΈμν¬ μλ΅μ λ°ννκ³ μΊμμ μΆκ°ν©λλ€. λ€νΈμν¬ μμ²μ΄ μ€ν¨νλ©΄(μ: μ€νλΌμΈ λͺ¨λ) μΊμλ μλ΅μ λ°νν©λλ€. μ΄κ²μ μ΅μ μνλ₯Ό μ μ§ν΄μΌ νλ λμ μ½ν μΈ μ μ’μ μ λ΅μ λλ€.
- μΊμ μ μ©(Cache Only): μλΉμ€ μ컀λ μΊμμμλ§ λ¦¬μμ€λ₯Ό λ°νν©λλ€. μ΄κ²μ λ³κ²½λ κ°λ₯μ±μ΄ κ±°μ μλ μ μ μ μ’μ μ λ΅μ λλ€.
- λ€νΈμν¬ μ μ©(Network Only): μλΉμ€ μ컀λ νμ λ€νΈμν¬μμ 리μμ€λ₯Ό κ°μ Έμ΅λλ€. μ΄κ²μ νμ μ΅μ μνμ¬μΌ νλ 리μμ€μ μ’μ μ λ΅μ λλ€.
- Stale-While-Revalidate: μλΉμ€ μ컀λ μΊμλ μλ΅μ μ¦μ λ°νν λ€μ λ°±κ·ΈλΌμ΄λμμ λ€νΈμν¬λ‘λΆν° 리μμ€λ₯Ό κ°μ Έμ΅λλ€. λ€νΈμν¬ μμ²μ΄ μλ£λλ©΄ μ μλ΅μΌλ‘ μΊμλ₯Ό μ λ°μ΄νΈν©λλ€. μ΄κ²μ λΉ λ₯Έ μ΄κΈ° λ‘λλ₯Ό μ 곡νκ³ μ¬μ©μκ° κ²°κ΅ μ΅μ μ½ν μΈ λ₯Ό λ³Ό μ μλλ‘ λ³΄μ₯ν©λλ€.
μ¬λ°λ₯Έ μΊμ± μ λ΅μ μ ννλ κ²μ PWAμ νΉμ μꡬ μ¬νκ³Ό μμ²λλ 리μμ€ μ νμ λ°λΌ λ€λ¦ λλ€. μ λ°μ΄νΈ λΉλ, μ΅μ λ°μ΄ν°μ μ€μμ± λ° μνλ μ±λ₯ νΉμ±μ κ³ λ €νμΈμ.
μ λ°μ΄νΈ μ²λ¦¬νκΈ°
μλΉμ€ μ컀λ₯Ό μ λ°μ΄νΈνλ©΄ λΈλΌμ°μ λ λ³κ²½ μ¬νμ κ°μ§νκ³ μ λ°μ΄νΈ νλ‘μΈμ€λ₯Ό νΈλ¦¬κ±°ν©λλ€. μ μλΉμ€ μ컀λ λ°±κ·ΈλΌμ΄λμ μ€μΉλλ©°, μ΄μ μλΉμ€ μ컀λ₯Ό μ¬μ©νλ λͺ¨λ μ΄λ¦° νμ΄ λ«ν λ νμ±νλ©λλ€. `install` μ΄λ²€νΈ λ΄μμ `skipWaiting()`μ νΈμΆνκ³ `activate` μ΄λ²€νΈ λ΄μμ `clients.claim()`μ νΈμΆνμ¬ μ λ°μ΄νΈλ₯Ό κ°μ ν μ μμ΅λλ€.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
}).then(() => self.skipWaiting())
);
});
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
`skipWaiting()`μ λκΈ° μ€μΈ μλΉμ€ μ컀λ₯Ό νμ± μλΉμ€ μμ»€λ‘ κ°μ μ νν©λλ€. `clients.claim()`μ μλΉμ€ μμ»€κ° μμ μ λ²μ λ΄ λͺ¨λ ν΄λΌμ΄μΈνΈ, μ¬μ§μ΄ μμ μμ΄ μμλ ν΄λΌμ΄μΈνΈκΉμ§ μ μ΄ν μ μκ² ν©λλ€.
νΈμ μλ¦Ό
μλΉμ€ μ컀λ νΈμ μλ¦Όμ νμ±ννμ¬ μ¬μ©μκ° PWAλ₯Ό μ κ·Ήμ μΌλ‘ μ¬μ©νμ§ μμ λλ λ€μ μ°Έμ¬λ₯Ό μ λν μ μμ΅λλ€. μ΄λ₯Ό μν΄μλ νΈμ APIμ Firebase ν΄λΌμ°λ λ©μμ§(FCM)κ³Ό κ°μ νΈμ μλΉμ€λ₯Ό μ¬μ©ν΄μΌ ν©λλ€.
μ°Έκ³ : νΈμ μλ¦Ό μ€μ μ λ 볡μ‘νλ©° μλ² μΈ‘ κ΅¬μ± μμκ° νμν©λλ€. μ΄ μΉμ μμλ κ°λ΅μ μΈ κ°μλ₯Ό μ 곡ν©λλ€.
- μ¬μ©μ ꡬλ : μ¬μ©μμκ² νΈμ μλ¦Όμ λ³΄λΌ κΆνμ μμ²ν©λλ€. κΆνμ΄ λΆμ¬λλ©΄ λΈλΌμ°μ μμ νΈμ ꡬλ μ 보λ₯Ό μ»μ΅λλ€.
- ꡬλ μ 보λ₯Ό μλ²λ‘ μ μ‘: νΈμ ꡬλ μ 보λ₯Ό μλ²λ‘ μ μ‘ν©λλ€. μ΄ κ΅¬λ μ 보μλ μ¬μ©μ λΈλΌμ°μ λ‘ νΈμ λ©μμ§λ₯Ό 보λ΄λ λ° νμν μ λ³΄κ° ν¬ν¨λμ΄ μμ΅λλ€.
- νΈμ λ©μμ§ μ μ‘: FCMκ³Ό κ°μ νΈμ μλΉμ€λ₯Ό μ¬μ©νμ¬ νΈμ ꡬλ μ 보λ₯Ό μ΄μ©ν΄ μ¬μ©μ λΈλΌμ°μ λ‘ νΈμ λ©μμ§λ₯Ό 보λ λλ€.
- μλΉμ€ μ컀μμ νΈμ λ©μμ§ μ²λ¦¬: μλΉμ€ μ컀μμ `push` μ΄λ²€νΈλ₯Ό μμ νκ³ μ¬μ©μμκ² μλ¦Όμ νμν©λλ€.
λ€μμ μλΉμ€ μ컀μμ `push` μ΄λ²€νΈλ₯Ό μ²λ¦¬νλ κ°λ¨ν μμ μ λλ€:
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/icon.png'
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
λ°±κ·ΈλΌμ΄λ λκΈ°ν
λ°±κ·ΈλΌμ΄λ λκΈ°νλ μ¬μ©μκ° μμ μ μΈ μΈν°λ· μ°κ²°μ κ°μ§ λκΉμ§ μμ μ μ§μ°μν¬ μ μκ² ν΄μ€λλ€. μ΄λ μ¬μ©μκ° μ€νλΌμΈ μνμΌ λ μμμ μ μΆνκ±°λ νμΌμ μ λ‘λνλ λ±μ μλ리μ€μ μ μ©ν©λλ€.
- λ°±κ·ΈλΌμ΄λ λκΈ°ν λ±λ‘: λ©μΈ μλ°μ€ν¬λ¦½νΈ νμΌμμ `navigator.serviceWorker.ready.then(registration => registration.sync.register('my-sync'));`λ₯Ό μ¬μ©νμ¬ λ°±κ·ΈλΌμ΄λ λκΈ°νλ₯Ό λ±λ‘ν©λλ€.
- μλΉμ€ μ컀μμ λκΈ°ν μ΄λ²€νΈ μ²λ¦¬: μλΉμ€ μ컀μμ `sync` μ΄λ²€νΈλ₯Ό μμ νκ³ μ§μ°λ μμ μ μνν©λλ€.
λ€μμ μλΉμ€ μ컀μμ `sync` μ΄λ²€νΈλ₯Ό μ²λ¦¬νλ κ°λ¨ν μμ μ λλ€:
self.addEventListener('sync', event => {
if (event.tag === 'my-sync') {
event.waitUntil(
// Perform the deferred task here
doSomething()
);
}
});
μλΉμ€ μ컀 ꡬνμ μν λͺ¨λ² μ¬λ‘
- μλΉμ€ μ컀λ₯Ό μκ³ ν¨μ¨μ μΌλ‘ μ μ§νμΈμ: ν° μλΉμ€ μ컀λ PWAμ μλλ₯Ό μ νμν¬ μ μμ΅λλ€.
- μμ²λλ 리μμ€ μ νμ μ ν©ν μΊμ± μ λ΅μ μ¬μ©νμΈμ: 리μμ€λ§λ€ λ€λ₯Έ μΊμ± μ λ΅μ΄ νμν©λλ€.
- μ€λ₯λ₯Ό μ μμ μΌλ‘ μ²λ¦¬νμΈμ: μλΉμ€ μ컀λ₯Ό μ§μνμ§ μκ±°λ μλΉμ€ μμ»€κ° μ€ν¨νλ λΈλΌμ°μ λ₯Ό μν λ체 κ²½νμ μ 곡νμΈμ.
- μλΉμ€ μ컀λ₯Ό μ² μ ν ν μ€νΈνμΈμ: λΈλΌμ°μ κ°λ°μ λꡬλ₯Ό μ¬μ©νμ¬ μλΉμ€ μ컀λ₯Ό κ²μ¬νκ³ μ¬λ°λ₯΄κ² μλνλμ§ νμΈνμΈμ.
- κΈλ‘λ² μ κ·Όμ±μ κ³ λ €νμΈμ: μμΉλ μ₯μΉμ κ΄κ³μμ΄ μ₯μ κ° μλ μ¬μ©μλ PWAμ μ κ·Όν μ μλλ‘ μ€κ³νμΈμ.
- HTTPSλ₯Ό μ¬μ©νμΈμ: μλΉμ€ μ컀λ 보μμ μν΄ HTTPSκ° νμν©λλ€.
- μ±λ₯μ λͺ¨λν°λ§νμΈμ: λΌμ΄νΈνμ°μ€(Lighthouse)μ κ°μ λꡬλ₯Ό μ¬μ©νμ¬ PWAμ μ±λ₯μ λͺ¨λν°λ§νκ³ κ°μ ν λΆλΆμ μλ³νμΈμ.
μλΉμ€ μ컀 λλ²κΉ
μλΉμ€ μ컀 λλ²κΉ μ μ΄λ €μΈ μ μμ§λ§, λΈλΌμ°μ κ°λ°μ λꡬλ λ¬Έμ ν΄κ²°μ λμμ΄ λλ μ¬λ¬ κΈ°λ₯μ μ 곡ν©λλ€:
- μ ν리μΌμ΄μ ν: ν¬λ‘¬ κ°λ°μ λꡬμ μ ν리μΌμ΄μ νμ μλΉμ€ μ컀μ μν, λ²μ, μ΄λ²€νΈ λ± κ΄λ ¨ μ 보λ₯Ό μ 곡ν©λλ€.
- μ½μ: μ½μμ μ¬μ©νμ¬ μλΉμ€ μ컀μμ λ©μμ§λ₯Ό κΈ°λ‘ν©λλ€.
- λ€νΈμν¬ ν: λ€νΈμν¬ νμ PWAκ° λ§λ λͺ¨λ λ€νΈμν¬ μμ²μ 보μ¬μ£Όλ©°, μΊμμμ μ 곡λμλμ§ λ€νΈμν¬μμ μ 곡λμλμ§λ₯Ό λνλ λλ€.
κ΅μ ν λ° νμ§ν κ³ λ € μ¬ν
κΈλ‘λ² μ¬μ©μλ₯Ό μν PWAλ₯Ό ꡬμΆν λ λ€μ κ΅μ ν λ° νμ§ν μΈ‘λ©΄μ κ³ λ €νμΈμ:
- μΈμ΄ μ§μ: HTMLμμ `lang` μμ±μ μ¬μ©νμ¬ PWAμ μΈμ΄λ₯Ό μ§μ νμΈμ. λͺ¨λ ν μ€νΈ μ½ν μΈ μ λν λ²μμ μ 곡νμΈμ.
- λ μ§ λ° μκ° νμ: `Intl` κ°μ²΄λ₯Ό μ¬μ©νμ¬ μ¬μ©μμ λ‘μΌμΌμ λ°λΌ λ μ§μ μκ°μ νμμ μ§μ νμΈμ.
- μ«μ νμ: `Intl` κ°μ²΄λ₯Ό μ¬μ©νμ¬ μ¬μ©μμ λ‘μΌμΌμ λ°λΌ μ«μμ νμμ μ§μ νμΈμ.
- ν΅ν νμ: `Intl` κ°μ²΄λ₯Ό μ¬μ©νμ¬ μ¬μ©μμ λ‘μΌμΌμ λ°λΌ ν΅νμ νμμ μ§μ νμΈμ.
- μ€λ₯Έμͺ½μμ μΌμͺ½μΌλ‘(RTL) μ§μ: PWAκ° μλμ΄ λ° νλΈλ¦¬μ΄μ κ°μ RTL μΈμ΄λ₯Ό μ§μνλμ§ νμΈνμΈμ.
- μ½ν μΈ μ μ‘ λ€νΈμν¬(CDN): CDNμ μ¬μ©νμ¬ μ μΈκ³μ μμΉν μλ²μμ PWAμ μ μ μ μ 곡νμ¬ λ€λ₯Έ μ§μμ μ¬μ©μ μ±λ₯μ ν₯μμν€μΈμ.
μλ₯Ό λ€μ΄, μ μ μκ±°λ μλΉμ€λ₯Ό μ 곡νλ PWAλ₯Ό μκ°ν΄λ³΄μμμ€. λ μ§ νμμ μ¬μ©μμ μμΉμ λ§κ² μ‘°μ λμ΄μΌ ν©λλ€. λ―Έκ΅μμλ MM/DD/YYYYλ₯Ό μ¬μ©νλ κ²μ΄ μΌλ°μ μ΄μ§λ§, μ λ½μμλ DD/MM/YYYYκ° μ νΈλ©λλ€. λ§μ°¬κ°μ§λ‘ ν΅ν κΈ°νΈμ μ«μ νμλ κ·Έμ λ§κ² μ‘°μ λμ΄μΌ ν©λλ€. μΌλ³Έ μ¬μ©μλ κ°κ²©μ΄ μ μ ν νμμ JPYλ‘ νμλκΈ°λ₯Ό κΈ°λν κ²μ λλ€.
μ κ·Όμ± κ³ λ € μ¬ν
μ κ·Όμ±μ μ₯μ κ° μλ μ¬μ©μλ₯Ό ν¬ν¨νμ¬ λͺ¨λ μ¬λμ΄ PWAλ₯Ό μ¬μ©ν μ μλλ‘ λ§λλ λ° λ§€μ° μ€μν©λλ€. λ€μ μ κ·Όμ± μΈ‘λ©΄μ κ³ λ €νμΈμ:
- μλ§¨ν± HTML: μλ§¨ν± HTML μμλ₯Ό μ¬μ©νμ¬ μ½ν μΈ μ ꡬ쑰μ μλ―Έλ₯Ό λΆμ¬νμΈμ.
- ARIA μμ±: ARIA μμ±μ μ¬μ©νμ¬ PWAμ μ κ·Όμ±μ ν₯μμν€μΈμ.
- ν€λ³΄λ λ΄λΉκ²μ΄μ : PWAκ° ν€λ³΄λλ₯Ό μ¬μ©νμ¬ μμ ν νμ κ°λ₯νμ§ νμΈνμΈμ.
- μ€ν¬λ¦° 리λ νΈνμ±: μ€ν¬λ¦° 리λλ‘ PWAλ₯Ό ν μ€νΈνμ¬ μκ° μ₯μ κ° μλ μ¬μ©μκ° μ κ·Όν μ μλμ§ νμΈνμΈμ.
- μμ λλΉ: μ μλ ₯ μ¬μ©μκ° PWAλ₯Ό μ½μ μ μλλ‘ ν μ€νΈμ λ°°κ²½μ κ°μ μΆ©λΆν μμ λλΉλ₯Ό μ¬μ©νμΈμ.
μλ₯Ό λ€μ΄, λͺ¨λ μνΈμμ© μμμ μ μ ν ARIA λ μ΄λΈμ΄ μμ΄ μ€ν¬λ¦° 리λ μ¬μ©μκ° κ·Έ λͺ©μ μ μ΄ν΄ν μ μλλ‘ νμΈμ. ν€λ³΄λ λ΄λΉκ²μ΄μ μ λͺ νν ν¬μ»€μ€ μμλ‘ μ§κ΄μ μ΄μ΄μΌ ν©λλ€. ν μ€νΈλ μκ° μ₯μ κ° μλ μ¬μ©μλ₯Ό μμ©νκΈ° μν΄ λ°°κ²½μ λν΄ μΆ©λΆν λλΉλ₯Ό κ°μ ΈμΌ ν©λλ€.
κ²°λ‘
μλΉμ€ μ컀λ κ°λ ₯νκ³ λ§€λ ₯μ μΈ PWAλ₯Ό ꡬμΆνκΈ° μν κ°λ ₯ν λꡬμ λλ€. μλΉμ€ μ컀 μλͺ μ£ΌκΈ°λ₯Ό μ΄ν΄νκ³ , μΊμ± μ λ΅μ ꡬννλ©°, μ λ°μ΄νΈλ₯Ό μ²λ¦¬ν¨μΌλ‘μ¨ μ€νλΌμΈ μνμμλ μνν μ¬μ©μ κ²½νμ μ 곡νλ PWAλ₯Ό λ§λ€ μ μμ΅λλ€. κΈλ‘λ² μ¬μ©μλ₯Ό μν΄ κ΅¬μΆν λλ κ΅μ ν, νμ§ν λ° μ κ·Όμ±μ κ³ λ €νμ¬ μμΉ, μΈμ΄ λλ λ₯λ ₯μ κ΄κ³μμ΄ λͺ¨λ μ¬λμ΄ PWAλ₯Ό μ¬μ©ν μ μλλ‘ ν΄μΌ ν©λλ€. μ΄ κ°μ΄λμ μμ½λ λͺ¨λ² μ¬λ‘λ₯Ό λ°λ₯΄λ©΄ μλΉμ€ μ컀 ꡬνμ λ§μ€ν°νκ³ λ€μν κΈλ‘λ² μ¬μ©μ κΈ°λ°μ μꡬλ₯Ό μΆ©μ‘±νλ λ°μ΄λ PWAλ₯Ό λ§λ€ μ μμ΅λλ€.