์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ฒ PWA(Progressive Web App)๋ฅผ ํ™œ์šฉํ•œ์„ค์น˜ํ˜• ์•ฑ ๋Œ€์ฒด ๊ธฐ์ˆ ์˜ ํ˜„์žฌ์™€ ๋ฏธ๋ž˜
๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ/์•ฑ ยท ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ

๐Ÿ“ฒ PWA(Progressive Web App)๋ฅผ ํ™œ์šฉํ•œ
์„ค์น˜ํ˜• ์•ฑ ๋Œ€์ฒด ๊ธฐ์ˆ ์˜ ํ˜„์žฌ์™€ ๋ฏธ๋ž˜

์•ฑ์Šคํ† ์–ด ์—†์ด๋„ ์•ฑ์ฒ˜๋Ÿผ ์“ด๋‹ค๊ณ ? ์ง„์งœ์ž„ ใ„นใ…‡ ๐Ÿ”ฅ

์•ผ ๊ทผ๋ฐ ์†”์งํžˆ ๋งํ•ด๋ด. ์•ฑ ํ•˜๋‚˜ ์“ฐ๋ ค๊ณ  ์•ฑ์Šคํ† ์–ด ๋“ค์–ด๊ฐ€์„œ ๋‹ค์šด๋ฐ›๊ณ , ์„ค์น˜ ๊ธฐ๋‹ค๋ฆฌ๊ณ , ๊ถŒํ•œ ํ—ˆ์šฉํ•˜๊ณ ... ์ด ๊ณผ์ •์ด ์–ผ๋งˆ๋‚˜ ๊ท€์ฐฎ์Œ? ๐Ÿ˜ค
๊ทผ๋ฐ ์š”์ฆ˜ ์›น์‚ฌ์ดํŠธ ๋“ค์–ด๊ฐ”๋”๋‹ˆ "ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€ํ•˜์‹œ๊ฒ ์–ด์š”?" ๋œจ๊ณ , ์ถ”๊ฐ€ํ•˜๋‹ˆ๊นŒ ์ง„์งœ ์•ฑ์ฒ˜๋Ÿผ ์‹คํ–‰๋˜๋Š” ๊ฑฐ ๊ฒฝํ—˜ํ•ด๋ณธ ์  ์žˆ์ง€ ์•Š์Œ?
๊ทธ๊ฒŒ ๋ฐ”๋กœ PWA(Progressive Web App)์ž„. ์›น ๊ธฐ์ˆ ๋กœ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ ์•ฑ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ๊ทธ ์‹ ๊ธฐํ•œ ๊ฒƒ ใ…‹ใ…‹ใ…‹

๐ŸŽฏ ์ด ๊ธ€์—์„œ ๋‹ค๋ฃจ๋Š” ๊ฒƒ๋“ค PWA๊ฐ€ ๋ญ”์ง€๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ, ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€, ๊ธฐ์กด ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ด๋ž‘ ๋ญ๊ฐ€ ๋‹ค๋ฅธ์ง€, ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ ๊ธฐ์—…๋“ค์ด ์–ด๋–ป๊ฒŒ ์จ๋จน๊ณ  ์žˆ๋Š”์ง€๊นŒ์ง€ โ€” ํŒฉํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์‹น ๋‹ค ์ •๋ฆฌํ•ด์ค„๊ฒŒ ๐Ÿ˜Ž
PWA = ์›น + ์•ฑ์˜ ๊ต์ง‘ํ•ฉ ์›น ๊ธฐ์ˆ  HTML/CSS/JS URL ์ ‘๊ทผ ๊ฒ€์ƒ‰ ๋…ธ์ถœ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ์˜คํ”„๋ผ์ธ ๋™์ž‘ ํ‘ธ์‹œ ์•Œ๋ฆผ ํ™ˆ ํ™”๋ฉด ์•„์ด์ฝ˜ PWA ์›น์ฒ˜๋Ÿผ ๋ฐฐํฌ ์•ฑ์ฒ˜๋Ÿผ ๋™์ž‘ ์„ค์น˜ ๋ถˆํ•„์š” ์›น์˜ ์ ‘๊ทผ์„ฑ + ์•ฑ์˜ ๊ธฐ๋Šฅ์„ฑ = PWA์˜ ํ•ต์‹ฌ ๊ฐ€์น˜

๐Ÿค” PWA๊ฐ€ ๋ญ”๋ฐ? ์ง„์งœ๋กœ

PWA๋Š” Progressive Web App์˜ ์•ฝ์ž์•ผ. ๊ตฌ๊ธ€์ด 2015๋…„์— ์ฒ˜์Œ ๊ฐœ๋…์„ ์ œ์‹œํ–ˆ๊ณ , ํฌ๋กฌ ๊ฐœ๋ฐœ์ž Alex Russell์ด ๊ณต์‹์ ์œผ๋กœ ๋ช…๋ช…ํ•œ ๊ธฐ์ˆ ์ด์•ผ.
ํ•œ๋งˆ๋””๋กœ ์ •์˜ํ•˜๋ฉด โ€” ์›น ๊ธฐ์ˆ (HTML, CSS, JavaScript)๋กœ ๋งŒ๋“ค์—ˆ์ง€๋งŒ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด์•ผ.

๐Ÿ’ก Progressive(์ ์ง„์ )์ด๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ํ•ต์‹ฌ์ž„ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ๊ทธ๋ƒฅ ์ผ๋ฐ˜ ์›น์‚ฌ์ดํŠธ๋กœ ๋™์ž‘ํ•˜๊ณ , ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์•ฑ ์ˆ˜์ค€์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ํฌ์šฉํ•˜๋ฉด์„œ ์ ์ง„์ ์œผ๋กœ ๊ธฐ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚จ๋‹ค๋Š” ์ฒ ํ•™์ด ๋‹ด๊ฒจ์žˆ์Œ.

๐Ÿ“‹ PWA์˜ 3๊ฐ€์ง€ ํ•ต์‹ฌ ์กฐ๊ฑด

๊ตฌ๊ธ€์ด ์ •์˜ํ•œ PWA์˜ ๊ธฐ๋ณธ ์š”๊ฑด์€ ํฌ๊ฒŒ 3๊ฐ€์ง€์•ผ:

๐Ÿ”’ ์‹ ๋ขฐ์„ฑ (Reliable)

๋„คํŠธ์›Œํฌ ์ƒํƒœ๊ฐ€ ๋ถˆ์•ˆ์ •ํ•˜๊ฑฐ๋‚˜ ์˜คํ”„๋ผ์ธ์ด์–ด๋„ ๋™์ž‘ํ•ด์•ผ ํ•จ.
Service Worker๊ฐ€ ์ด๊ฑธ ๋‹ด๋‹นํ•จ.
โšก ๋น ๋ฆ„ (Fast)

์Šคํฌ๋กค, ์• ๋‹ˆ๋ฉ”์ด์…˜, ํ™”๋ฉด ์ „ํ™˜์ด ๋ถ€๋“œ๋Ÿฌ์›Œ์•ผ ํ•จ.
60fps ๊ธฐ์ค€์œผ๋กœ ๋ฒ„๋ฒ…์ž„ ์—†์–ด์•ผ ํ•จ.
๐Ÿค ์ฐธ์—ฌ ์œ ๋„ (Engaging)

ํ™ˆ ํ™”๋ฉด ์„ค์น˜, ํ‘ธ์‹œ ์•Œ๋ฆผ, ์ „์ฒด ํ™”๋ฉด ์‹คํ–‰ ๋“ฑ
์•ฑ๋‹ค์šด ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ด์•ผ ํ•จ.

๐Ÿ—๏ธ PWA๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ํ•ต์‹ฌ ๊ธฐ์ˆ ๋“ค

PWA๋Š” ๋‹จ์ผ ๊ธฐ์ˆ ์ด ์•„๋‹ˆ๋ผ ์—ฌ๋Ÿฌ ์›น ํ‘œ์ค€ ๊ธฐ์ˆ ์˜ ์กฐํ•ฉ์ด์•ผ. ๊ฐ๊ฐ ๋ญ”์ง€ ์•Œ์•„๋ณด์ž:

1
Service Worker
๋ธŒ๋ผ์šฐ์ € ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰๋˜๋Š” JavaScript ํŒŒ์ผ. ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ์˜คํ”„๋ผ์ธ ๋™์ž‘์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์คŒ. PWA์˜ ์‹ฌ์žฅ์ด๋ผ๊ณ  ๋ณด๋ฉด ๋จ.
2
Web App Manifest
JSON ํŒŒ์ผ๋กœ, ์•ฑ ์ด๋ฆ„, ์•„์ด์ฝ˜, ์‹œ์ž‘ URL, ํ™”๋ฉด ๋ฐฉํ–ฅ, ํ…Œ๋งˆ ์ƒ‰์ƒ ๋“ฑ์„ ์ •์˜ํ•จ. ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€ํ•  ๋•Œ ์ด ํŒŒ์ผ์„ ์ฐธ์กฐํ•ด์„œ ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ๋งŒ๋“ค์–ด์คŒ.
3
HTTPS
PWA๋Š” ๋ฐ˜๋“œ์‹œ HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘ํ•จ. Service Worker๊ฐ€ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ๊ฐ–๊ณ  ์žˆ์–ด์„œ, ๋ณด์•ˆ์„ ์œ„ํ•ด ํ•„์ˆ˜ ์กฐ๊ฑด์ž„.
4
Cache API
Service Worker์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜๋Š” ์บ์‹œ ์ €์žฅ์†Œ. ์ •์  ํŒŒ์ผ(HTML, CSS, JS, ์ด๋ฏธ์ง€)์„ ๋กœ์ปฌ์— ์ €์žฅํ•ด์„œ ์˜คํ”„๋ผ์ธ์—์„œ๋„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ.
5
Push API & Notifications API
์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ. ์•ฑ์„ ์—ด์ง€ ์•Š์•„๋„ ์•Œ๋ฆผ์ด ์˜ค๋Š” ๊ทธ ๊ธฐ๋Šฅ์ž„.

โš™๏ธ Service Worker: PWA์˜ ์ง„์งœ ํ•ต์‹ฌ

Service Worker ์—†์œผ๋ฉด PWA ์•„๋‹˜. ์ง„์งœ๋กœ. ์ด๊ฒŒ PWA๋ฅผ ์ผ๋ฐ˜ ์›น์‚ฌ์ดํŠธ์™€ ๊ตฌ๋ถ„์ง“๋Š” ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ธฐ์ˆ ์ด์•ผ.
๊ทผ๋ฐ ์ด๊ฒŒ ์ข€ ๊ฐœ๋…์ด ํŠน์ดํ•ด์„œ ์ฒ˜์Œ์—” ํ—ท๊ฐˆ๋ฆด ์ˆ˜ ์žˆ์Œ. ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ•ด์ค„๊ฒŒ ๐Ÿ˜Š

๐Ÿ”‘ Service Worker์˜ ์ •์ฒด Service Worker๋Š” ์›น ํŽ˜์ด์ง€์™€ ๋ณ„๊ฐœ๋กœ ๋ธŒ๋ผ์šฐ์ € ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰๋˜๋Š” ์Šคํฌ๋ฆฝํŠธ์•ผ.
DOM์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๊ณ , ์›น ํŽ˜์ด์ง€์™€ ์ง์ ‘ ํ†ต์‹ ํ•˜๋ ค๋ฉด postMessage API๋ฅผ ์จ์•ผ ํ•ด.
๋Œ€์‹  ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ”„๋ก์‹œ์ฒ˜๋Ÿผ ๊ฐ€๋กœ์ฑ„๊ณ , ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด.

๐Ÿ“Š Service Worker ์ƒ๋ช…์ฃผ๊ธฐ

Service Worker๋Š” ์„ค์น˜ โ†’ ํ™œ์„ฑํ™” โ†’ ์‹คํ–‰์˜ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์นจ:

1๋‹จ๊ณ„: ๋“ฑ๋ก (Registration) ์›น ํŽ˜์ด์ง€์—์„œ navigator.serviceWorker.register()๋ฅผ ํ˜ธ์ถœํ•ด์„œ SW๋ฅผ ๋“ฑ๋กํ•จ.

2๋‹จ๊ณ„: ์„ค์น˜ (Install)
install ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , ์ด๋•Œ ํ•„์š”ํ•œ ํŒŒ์ผ๋“ค์„ ์บ์‹œ์— ์ €์žฅํ•จ.

3๋‹จ๊ณ„: ํ™œ์„ฑํ™” (Activate)
์ด์ „ ๋ฒ„์ „์˜ SW๊ฐ€ ์žˆ์œผ๋ฉด ๊ต์ฒดํ•˜๊ณ , ์˜ค๋ž˜๋œ ์บ์‹œ๋ฅผ ์ •๋ฆฌํ•จ.

4๋‹จ๊ณ„: ์‹คํ–‰ (Fetch/Push/Sync)
๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„๊ฑฐ๋‚˜, ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™”๋ฅผ ์ˆ˜ํ–‰ํ•จ.

๐Ÿ’ป ์‹ค์ œ Service Worker ์ฝ”๋“œ ์˜ˆ์‹œ

// sw.js (Service Worker ํŒŒ์ผ)

const CACHE_NAME = 'my-pwa-v1';
const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/icons/icon-192.png'
];

// ์„ค์น˜ ์ด๋ฒคํŠธ: ํ•ต์‹ฌ ํŒŒ์ผ๋“ค์„ ์บ์‹œ์— ์ €์žฅ
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('์บ์‹œ ์ €์žฅ ์‹œ์ž‘!');
      return cache.addAll(ASSETS_TO_CACHE);
    })
  );
});

// ํ™œ์„ฑํ™” ์ด๋ฒคํŠธ: ์˜ค๋ž˜๋œ ์บ์‹œ ์‚ญ์ œ
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      );
    })
  );
});

// fetch ์ด๋ฒคํŠธ: ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฐ€๋กœ์ฑ„๊ธฐ
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      // ์บ์‹œ์— ์žˆ์œผ๋ฉด ์บ์‹œ์—์„œ ๋ฐ˜ํ™˜, ์—†์œผ๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ
      return cachedResponse || fetch(event.request);
    })
  );
});
๐Ÿ’ก ์บ์‹ฑ ์ „๋žต์ด ์—ฌ๋Ÿฌ ๊ฐœ์•ผ! ์œ„ ์ฝ”๋“œ๋Š” "Cache First" ์ „๋žต์ž„. ์ด ์™ธ์—๋„:
โ€ข Network First: ๋„คํŠธ์›Œํฌ ๋จผ์ €, ์‹คํŒจํ•˜๋ฉด ์บ์‹œ
โ€ข Stale While Revalidate: ์บ์‹œ ๋ฐ˜ํ™˜ํ•˜๋ฉด์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์—…๋ฐ์ดํŠธ
โ€ข Cache Only: ์บ์‹œ๋งŒ ์‚ฌ์šฉ
โ€ข Network Only: ๋„คํŠธ์›Œํฌ๋งŒ ์‚ฌ์šฉ
์ƒํ™ฉ์— ๋งž๊ฒŒ ์ „๋žต์„ ์„ ํƒํ•ด์•ผ ํ•จ!

๐Ÿ“ฑ Service Worker ๋“ฑ๋ก ์ฝ”๋“œ

// main.js (๋ฉ”์ธ ์•ฑ ํŒŒ์ผ)

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        console.log('SW ๋“ฑ๋ก ์„ฑ๊ณต! ์Šค์ฝ”ํ”„:', registration.scope);
      })
      .catch((error) => {
        console.log('SW ๋“ฑ๋ก ์‹คํŒจ:', error);
      });
  });
}
๐Ÿ”ฎ Workbox ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฟ€ํŒ ๊ตฌ๊ธ€์ด ๋งŒ๋“  Workbox ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋ฉด Service Worker ์ฝ”๋“œ๋ฅผ ํ›จ์”ฌ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด. ์บ์‹ฑ ์ „๋žต์„ ๋ช‡ ์ค„๋กœ ์„ค์ • ๊ฐ€๋Šฅํ•˜๊ณ , ์ž๋™์œผ๋กœ ์บ์‹œ ๋ฒ„์ „ ๊ด€๋ฆฌ๋„ ํ•ด์คŒ. ์‹ค๋ฌด์—์„œ๋Š” ๊ฑฐ์˜ Workbox ์”€ ใ…‹ใ…‹

๐Ÿ“„ Web App Manifest: ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ํ•˜๋Š” ๋งˆ๋ฒ•

Service Worker๊ฐ€ PWA์˜ ๊ธฐ๋Šฅ์  ์‹ฌ์žฅ์ด๋ผ๋ฉด, Web App Manifest๋Š” PWA์˜ ์™ธ๋ชจ๋ฅผ ๋‹ด๋‹นํ•ด.
์ด JSON ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์•ฑ ์ด๋ฆ„, ์•„์ด์ฝ˜, ์ƒ‰์ƒ, ํ™”๋ฉด ๋ชจ๋“œ ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ.

๐Ÿ’ป manifest.json ์˜ˆ์‹œ

{
  "name": "๋‚˜์˜ ๋ฉ‹์ง„ PWA ์•ฑ",
  "short_name": "๋ฉ‹์ง„์•ฑ",
  "description": "PWA๋กœ ๋งŒ๋“  ์ตœ๊ณ ์˜ ์•ฑ์ž…๋‹ˆ๋‹ค",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#ffffff",
  "theme_color": "#0f3460",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/screen1.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ],
  "shortcuts": [
    {
      "name": "์ƒˆ ๊ธ€ ์ž‘์„ฑ",
      "url": "/new-post",
      "icons": [{"src": "/icons/new-post.png", "sizes": "96x96"}]
    }
  ]
}

๐Ÿ–ฅ๏ธ display ์†์„ฑ ์˜ต์…˜๋“ค

display ์†์„ฑ์ด PWA ๊ฒฝํ—˜์—์„œ ์—„์ฒญ ์ค‘์š”ํ•ด. 4๊ฐ€์ง€ ์˜ต์…˜์ด ์žˆ์Œ:

์˜ต์…˜ ์„ค๋ช… ์ฃผ์†Œ์ฐฝ
fullscreen ์™„์ „ ์ „์ฒดํ™”๋ฉด, ์ƒํƒœ๋ฐ”๋„ ์—†์Œ ์—†์Œ
standalone ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ž„, ์ƒํƒœ๋ฐ”๋Š” ์žˆ์Œ ์—†์Œ
minimal-ui ์ตœ์†Œํ•œ์˜ ๋ธŒ๋ผ์šฐ์ € UI๋งŒ ํ‘œ์‹œ ์ตœ์†Œํ™”
browser ์ผ๋ฐ˜ ๋ธŒ๋ผ์šฐ์ € ํƒญ์œผ๋กœ ์—ด๋ฆผ ์žˆ์Œ
๐Ÿ’ก ์‹ค๋ฌด์—์„œ๋Š” standalone์ด ๊ตญ๋ฃฐ ๋Œ€๋ถ€๋ถ„์˜ PWA๋Š” standalone ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•จ. ์ฃผ์†Œ์ฐฝ์ด ์—†์–ด์„œ ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ด๋ฉด์„œ๋„, ์™„์ „ ์ „์ฒดํ™”๋ฉด๋ณด๋‹ค๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋œ ๋‹ต๋‹ตํ•ดํ•จ. ๊ฒŒ์ž„ ์•ฑ์ด๋ฉด fullscreen ์“ฐ๊ธฐ๋„ ํ•จ.

๐Ÿ”— HTML์—์„œ Manifest ์—ฐ๊ฒฐํ•˜๊ธฐ

<!-- index.html์˜ <head> ์•ˆ์— ์ถ”๊ฐ€ -->
<link rel="manifest" href="/manifest.json">

<!-- iOS Safari๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ๋ฉ”ํƒ€ํƒœ๊ทธ (iOS๋Š” manifest ์ผ๋ถ€ ๋ฏธ์ง€์›) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="๋ฉ‹์ง„์•ฑ">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">

<!-- ํ…Œ๋งˆ ์ƒ‰์ƒ (๋ธŒ๋ผ์šฐ์ € ์ฃผ์†Œ์ฐฝ ์ƒ‰์ƒ) -->
<meta name="theme-color" content="#0f3460">
โš ๏ธ iOS Safari ์ฃผ์˜์‚ฌํ•ญ! iOS Safari๋Š” Web App Manifest๋ฅผ ์™„์ „ํžˆ ์ง€์›ํ•˜์ง€ ์•Š์•„. ํŠนํžˆ ํ‘ธ์‹œ ์•Œ๋ฆผ์€ iOS 16.4 ์ดํ›„๋ถ€ํ„ฐ ์ง€์›ํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ๊ณ , ์•„์ง๋„ ์ผ๋ถ€ ๊ธฐ๋Šฅ์— ์ œํ•œ์ด ์žˆ์Œ.
๊ทธ๋ž˜์„œ iOS ์ง€์›์„ ์œ„ํ•ด apple-mobile-web-app-* ๋ฉ”ํƒ€ํƒœ๊ทธ๋ฅผ ๋ณ„๋„๋กœ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด.
PWA vs ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ๋น„๊ต ๋น„๊ต ํ•ญ๋ชฉ PWA ๐ŸŒ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ๐Ÿ“ฑ ๋ฐฐํฌ ๋ฐฉ๋ฒ• URL ๊ณต์œ  (์ฆ‰์‹œ) ์•ฑ์Šคํ† ์–ด ์‹ฌ์‚ฌ (1~7์ผ) ์„ค์น˜ ์šฉ๋Ÿ‰ ์ˆ˜ KB ~ ์ˆ˜ MB ์ˆ˜์‹ญ MB ~ ์ˆ˜ GB ์—…๋ฐ์ดํŠธ ์ž๋™ (์„œ๋ฒ„ ๋ฐฐํฌ ์ฆ‰์‹œ) ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์—…๋ฐ์ดํŠธ ๊ฐœ๋ฐœ ๋น„์šฉ ๋‚ฎ์Œ (์›น ๊ธฐ์ˆ  ํ•˜๋‚˜๋กœ) ๋†’์Œ (iOS/Android ๋ณ„๋„) ํ•˜๋“œ์›จ์–ด ์ ‘๊ทผ ์ œํ•œ์  (์ ์  ํ™•๋Œ€ ์ค‘) ์™„์ „ํ•œ ์ ‘๊ทผ ๊ฐ€๋Šฅ ๊ฒ€์ƒ‰ ์—”์ง„ ๋…ธ์ถœ ๊ฐ€๋Šฅ (SEO ์ ์šฉ) ๋ถˆ๊ฐ€๋Šฅ ์˜คํ”„๋ผ์ธ ์ง€์› Service Worker๋กœ ๊ฐ€๋Šฅ ์™„์ „ ์ง€์›

๐Ÿ“ฒ "ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€" โ€” ์„ค์น˜ ๊ฒฝํ—˜ ๊ตฌํ˜„ํ•˜๊ธฐ

PWA์˜ ๊ฝƒ์€ ์—ญ์‹œ ํ™ˆ ํ™”๋ฉด ์„ค์น˜ ๊ฒฝํ—˜์ด์ง€. ๊ทผ๋ฐ ์ด๊ฒŒ ๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ๋™์ž‘์ด ๋‹ฌ๋ผ์„œ ์ข€ ๋ณต์žกํ•จ ใ…‹ใ…‹
์ œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์Šคํ† ์–ด ์—†์ด๋„ ์•ฑ์ฒ˜๋Ÿผ ์“ธ ์ˆ˜ ์žˆ์–ด.

๐Ÿ”” ์„ค์น˜ ํ”„๋กฌํ”„ํŠธ ์ œ์–ดํ•˜๊ธฐ

Chrome/Edge์—์„œ๋Š” beforeinstallprompt ์ด๋ฒคํŠธ๋กœ ์„ค์น˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด:

let deferredPrompt; // ์„ค์น˜ ํ”„๋กฌํ”„ํŠธ ์ €์žฅ

// ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์„ค์น˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋„์šฐ๋ ค ํ•  ๋•Œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
window.addEventListener('beforeinstallprompt', (e) => {
  // ์ž๋™์œผ๋กœ ๋œจ๋Š” ๊ฑธ ๋ง‰๊ณ 
  e.preventDefault();
  // ๋‚˜์ค‘์— ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ์ €์žฅ
  deferredPrompt = e;
  // ์ปค์Šคํ…€ ์„ค์น˜ ๋ฒ„ํŠผ ๋ณด์—ฌ์ฃผ๊ธฐ
  showInstallButton();
});

// ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์น˜ ๋ฒ„ํŠผ ํด๋ฆญํ–ˆ์„ ๋•Œ
async function handleInstallClick() {
  if (!deferredPrompt) return;
  
  // ์„ค์น˜ ํ”„๋กฌํ”„ํŠธ ํ‘œ์‹œ
  deferredPrompt.prompt();
  
  // ์‚ฌ์šฉ์ž ์„ ํƒ ๊ฒฐ๊ณผ ํ™•์ธ
  const { outcome } = await deferredPrompt.userChoice;
  
  if (outcome === 'accepted') {
    console.log('์‚ฌ์šฉ์ž๊ฐ€ ์„ค์น˜๋ฅผ ์ˆ˜๋ฝํ–ˆ์–ด์š”! ๐ŸŽ‰');
  } else {
    console.log('์‚ฌ์šฉ์ž๊ฐ€ ์„ค์น˜๋ฅผ ๊ฑฐ์ ˆํ–ˆ์–ด์š” ๐Ÿ˜ข');
  }
  
  deferredPrompt = null;
  hideInstallButton();
}

// ์•ฑ์ด ์„ค์น˜๋œ ํ›„ ์ด๋ฒคํŠธ
window.addEventListener('appinstalled', () => {
  console.log('PWA ์„ค์น˜ ์™„๋ฃŒ! ๐Ÿš€');
  deferredPrompt = null;
});
๐ŸŽ iOS Safari๋Š” ๋‹ค๋ฆ„! iOS Safari๋Š” beforeinstallprompt ์ด๋ฒคํŠธ๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์•„.
iOS์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ Safari ๊ณต์œ  ๋ฒ„ํŠผ โ†’ "ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€"๋ฅผ ๋ˆŒ๋Ÿฌ์•ผ ํ•ด.
๊ทธ๋ž˜์„œ iOS ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ๋ณ„๋„๋กœ "์ด๋ ‡๊ฒŒ ์„ค์น˜ํ•˜์„ธ์š”" ์•ˆ๋‚ด UI๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒŒ ์ข‹์Œ.

๐Ÿ“Š ์„ค์น˜ ์—ฌ๋ถ€ ๊ฐ์ง€ํ•˜๊ธฐ

// ํ˜„์žฌ PWA๋กœ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ
function isPWA() {
  return window.matchMedia('(display-mode: standalone)').matches
    || window.navigator.standalone === true; // iOS Safari
}

if (isPWA()) {
  console.log('PWA ๋ชจ๋“œ๋กœ ์‹คํ–‰ ์ค‘!');
} else {
  console.log('๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ ์ค‘');
}

๐Ÿ”” ํ‘ธ์‹œ ์•Œ๋ฆผ ๊ตฌํ˜„ โ€” ์•ฑ์ด๋ž‘ ๋˜‘๊ฐ™์ด

ํ‘ธ์‹œ ์•Œ๋ฆผ์€ ์‚ฌ์šฉ์ž ์žฌ์ฐธ์—ฌ(Re-engagement)์—์„œ ์—„์ฒญ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์ด์•ผ.
PWA์—์„œ๋„ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ฒ˜๋Ÿผ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ด๊ฒŒ ์ƒ๊ฐ๋ณด๋‹ค ๊ตฌํ˜„์ด ์ข€ ๋ณต์žกํ•จ.

๐Ÿ”‘ Web Push ๋™์ž‘ ์›๋ฆฌ

์ „์ฒด ํ๋ฆ„: 1๏ธโƒฃ ์‚ฌ์šฉ์ž๊ฐ€ ์•Œ๋ฆผ ๊ถŒํ•œ ํ—ˆ์šฉ
2๏ธโƒฃ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Push Service(FCM ๋“ฑ)์— ๊ตฌ๋… ์ •๋ณด ์š”์ฒญ
3๏ธโƒฃ ๊ตฌ๋… ์ •๋ณด(endpoint + ์•”ํ˜ธํ™” ํ‚ค)๋ฅผ ์„œ๋ฒ„์— ์ €์žฅ
4๏ธโƒฃ ์„œ๋ฒ„์—์„œ Push Service๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก
5๏ธโƒฃ Push Service๊ฐ€ ์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ €๋กœ ์ „๋‹ฌ
6๏ธโƒฃ Service Worker๊ฐ€ push ์ด๋ฒคํŠธ ์ˆ˜์‹  ํ›„ ์•Œ๋ฆผ ํ‘œ์‹œ
// ํด๋ผ์ด์–ธํŠธ: ํ‘ธ์‹œ ๊ตฌ๋… ์š”์ฒญ
async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;
  
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true, // ๋ฐ˜๋“œ์‹œ true (์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์ด๋Š” ์•Œ๋ฆผ๋งŒ)
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
  });
  
  // ์„œ๋ฒ„์— ๊ตฌ๋… ์ •๋ณด ์ „์†ก
  await fetch('/api/push-subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  });
}

// Service Worker: ํ‘ธ์‹œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
self.addEventListener('push', (event) => {
  const data = event.data.json();
  
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/badge-72.png',
      data: { url: data.url },
      actions: [
        { action: 'open', title: '์—ด๊ธฐ' },
        { action: 'close', title: '๋‹ซ๊ธฐ' }
      ]
    })
  );
});

// ์•Œ๋ฆผ ํด๋ฆญ ์ฒ˜๋ฆฌ
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  
  if (event.action === 'open') {
    event.waitUntil(
      clients.openWindow(event.notification.data.url)
    );
  }
});
๐Ÿ’ก VAPID ํ‚ค๊ฐ€ ๋ญ๋ƒ๊ณ ? VAPID(Voluntary Application Server Identification)๋Š” Web Push ํ”„๋กœํ† ์ฝœ์—์„œ ์„œ๋ฒ„ ์‹ ์›์„ ํ™•์ธํ•˜๋Š” ๊ณต๊ฐœํ‚ค/๋น„๊ณต๊ฐœํ‚ค ์Œ์ด์•ผ.
web-push ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์‰ฝ๊ฒŒ ์ƒ์„ฑ ๊ฐ€๋Šฅ: npx web-push generate-vapid-keys

๐Ÿ“ก ์˜คํ”„๋ผ์ธ ์ „๋žต โ€” ์ธํ„ฐ๋„ท ์—†์–ด๋„ ๋™์ž‘ํ•˜๊ฒŒ

PWA์˜ ํ•ต์‹ฌ ๊ฐ€์น˜ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์˜คํ”„๋ผ์ธ ๋™์ž‘์ด์•ผ. ์ง€ํ•˜์ฒ ์—์„œ ์ธํ„ฐ๋„ท ๋Š๊ฒจ๋„ ์•ฑ์ด ๋™์ž‘ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์—„์ฒญ ์ข‹์•„์ง€๊ฑฐ๋“ .
๊ทผ๋ฐ ์˜คํ”„๋ผ์ธ ์ „๋žต์„ ์–ด๋–ป๊ฒŒ ์งœ๋А๋ƒ์— ๋”ฐ๋ผ UX๊ฐ€ ์™„์ „ ๋‹ฌ๋ผ์ง.

๐Ÿ—‚๏ธ ์บ์‹ฑ ์ „๋žต ์ƒ์„ธ ๋น„๊ต

Cache First

์–ธ์ œ ์“ฐ๋‚˜: ์ž์ฃผ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ์ •์  ํŒŒ์ผ (ํฐํŠธ, ์ด๋ฏธ์ง€, CSS)

์บ์‹œ โ†’ ์—†์œผ๋ฉด ๋„คํŠธ์›Œํฌ
์†๋„ ์ตœ์šฐ์„ , ์ตœ์‹ ์„ฑ ๋‚ฎ์Œ
Network First

์–ธ์ œ ์“ฐ๋‚˜: ์ž์ฃผ ์—…๋ฐ์ดํŠธ๋˜๋Š” API ๋ฐ์ดํ„ฐ

๋„คํŠธ์›Œํฌ โ†’ ์‹คํŒจํ•˜๋ฉด ์บ์‹œ
์ตœ์‹ ์„ฑ ์šฐ์„ , ์˜คํ”„๋ผ์ธ ํด๋ฐฑ
Stale While Revalidate

์–ธ์ œ ์“ฐ๋‚˜: ๋‰ด์Šค ํ”ผ๋“œ, ์†Œ์…œ ๋ฏธ๋””์–ด

์บ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์—…๋ฐ์ดํŠธ
์†๋„์™€ ์ตœ์‹ ์„ฑ ๊ท ํ˜•

๐Ÿ’พ IndexedDB๋กœ ์˜คํ”„๋ผ์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ

๋‹จ์ˆœ ํŒŒ์ผ ์บ์‹œ ์™ธ์—๋„, ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ์ปฌ์— ์ €์žฅํ•˜๋ ค๋ฉด IndexedDB๋ฅผ ์จ์•ผ ํ•ด:

// IndexedDB ์‚ฌ์šฉ ์˜ˆ์‹œ (idb ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ)
import { openDB } from 'idb';

const db = await openDB('my-pwa-db', 1, {
  upgrade(db) {
    db.createObjectStore('posts', { keyPath: 'id' });
  }
});

// ๋ฐ์ดํ„ฐ ์ €์žฅ
async function savePost(post) {
  await db.put('posts', post);
}

// ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ
async function getPosts() {
  return await db.getAll('posts');
}

// ์˜จ๋ผ์ธ ๋ณต๊ท€ ์‹œ ๋™๊ธฐํ™”
window.addEventListener('online', async () => {
  const offlinePosts = await db.getAll('offline-queue');
  for (const post of offlinePosts) {
    await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(post)
    });
  }
});

๐Ÿ”„ Background Sync API

์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ ์ž‘์—…์„ ํ์— ์ €์žฅํ•˜๊ณ , ์˜จ๋ผ์ธ ๋ณต๊ท€ ์‹œ ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”ํ•˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ:

// ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ ํผ ์ œ์ถœ ์‹œ
async function submitForm(data) {
  try {
    await fetch('/api/submit', { method: 'POST', body: data });
  } catch (error) {
    // ์˜คํ”„๋ผ์ธ์ด๋ฉด ํ์— ์ €์žฅํ•˜๊ณ  ๋‚˜์ค‘์— ๋™๊ธฐํ™” ์š”์ฒญ
    await saveToQueue(data);
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('sync-form-data');
  }
}

// Service Worker์—์„œ sync ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-form-data') {
    event.waitUntil(syncFormData());
  }
});
Service Worker ์บ์‹ฑ ์ „๋žต ์‹œ๊ฐํ™” ์•ฑ (์š”์ฒญ) Service Worker ์บ์‹œ Cache Storage ๋„คํŠธ์›Œํฌ ์„œ๋ฒ„ ์š”์ฒญ ์š”์ฒญ โ‘  ์บ์‹œ ํ™•์ธ โ‘ก ์—†์œผ๋ฉด ๋„คํŠธ์›Œํฌ Cache First ์ „๋žต ์บ์‹œ ํ™•์ธ โ†’ ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜ ์—†์œผ๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ โœ… ์ •์  ํŒŒ์ผ์— ์ตœ์  โœ… ๋น ๋ฅธ ์‘๋‹ต ์†๋„ Network First ์ „๋žต ๋„คํŠธ์›Œํฌ ์š”์ฒญ โ†’ ์‹คํŒจ์‹œ ์บ์‹œ ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ ์šฐ์„  โœ… API ๋ฐ์ดํ„ฐ์— ์ตœ์  โœ… ์˜คํ”„๋ผ์ธ ํด๋ฐฑ ์ง€์› Stale While Revalidate ์บ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์—…๋ฐ์ดํŠธ โœ… ๋‰ด์Šค/ํ”ผ๋“œ์— ์ตœ์  โœ… ์†๋„+์ตœ์‹ ์„ฑ ๊ท ํ˜• ์ƒํ™ฉ์— ๋งž๋Š” ์ „๋žต ์„ ํƒ์ด PWA ์„ฑ๋Šฅ์˜ ํ•ต์‹ฌ!

๐Ÿข ์‹ค์ œ ๊ธฐ์—…๋“ค์˜ PWA ๋„์ž… ์‚ฌ๋ก€ โ€” ์ˆซ์ž๋กœ ์ฆ๋ช…

์ด๋ก ์€ ์ด๋ก ์ด๊ณ , ์‹ค์ œ๋กœ PWA๊ฐ€ ํšจ๊ณผ๊ฐ€ ์žˆ๋ƒ๊ณ ? ์žˆ์Œ. ๋ฐ์ดํ„ฐ๋กœ ์ฆ๋ช…๋จ ใ…‹ใ…‹
๊ธ€๋กœ๋ฒŒ ๊ธฐ์—…๋“ค์˜ PWA ๋„์ž… ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์ง„์งœ ๋†€๋ผ์›€.

๐Ÿ“Š ์ฃผ์š” ๊ธฐ์—… PWA ๋„์ž… ์„ฑ๊ณผ

๐Ÿฆ Twitter Lite (PWA) โ€ข ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ๋Ÿ‰ 70% ๊ฐ์†Œ
โ€ข ์„ธ์…˜๋‹น ํŽ˜์ด์ง€ ์ˆ˜ 65% ์ฆ๊ฐ€
โ€ข ํŠธ์œ— ์ˆ˜ 75% ์ฆ๊ฐ€
โ€ข ์•ฑ ํฌ๊ธฐ: ๊ธฐ์กด ์•ฑ ๋Œ€๋น„ 97% ๊ฐ์†Œ (600KB vs 23.5MB)
๐Ÿ›’ Flipkart (์ธ๋„ ์ตœ๋Œ€ ์ด์ปค๋จธ์Šค) โ€ข ์žฌ๋ฐฉ๋ฌธ์œจ 3๋ฐฐ ์ฆ๊ฐ€
โ€ข ์‚ฌ์šฉ์ž๋‹น ์‹œ๊ฐ„ ์†Œ๋น„ 3๋ฐฐ ์ฆ๊ฐ€
โ€ข ์ „ํ™˜์œจ 70% ์ฆ๊ฐ€
โ€ข ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ๋Ÿ‰ 3๋ฐฐ ๊ฐ์†Œ
๐Ÿช AliExpress โ€ข ์‹ ๊ทœ ์‚ฌ์šฉ์ž ์ „ํ™˜์œจ 104% ์ฆ๊ฐ€
โ€ข iOS ์‚ฌ์šฉ์ž ์„ธ์…˜๋‹น ํŽ˜์ด์ง€ ์ˆ˜ 2๋ฐฐ ์ฆ๊ฐ€
โ€ข ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฒด๋ฅ˜ ์‹œ๊ฐ„ 74% ์ฆ๊ฐ€
๐Ÿ  Trivago (์—ฌํ–‰ ํ”Œ๋žซํผ) โ€ข ์‚ฌ์šฉ์ž ์ฐธ์—ฌ๋„ 150% ์ฆ๊ฐ€
โ€ข ์•Œ๋ฆผ ์ˆ˜์‹  ๋™์˜ ์‚ฌ์šฉ์ž์˜ ์ง์ ‘ ๋ฐฉ๋ฌธ 97% ์ฆ๊ฐ€
โ€ข ์˜คํ”„๋ผ์ธ ์‚ฌ์šฉ์ž ์ „ํ™˜์œจ 67% ์ฆ๊ฐ€
๐Ÿ“ฐ Forbes โ€ข ์„ธ์…˜ ๊ธธ์ด 43% ์ฆ๊ฐ€
โ€ข ์Šคํฌ๋กค ๊นŠ์ด 10% ์ฆ๊ฐ€
โ€ข ๊ด‘๊ณ  ๊ฐ€์‹œ์„ฑ 20% ์ฆ๊ฐ€
โ€ข ์ดํƒˆ๋ฅ  20% ๊ฐ์†Œ
๐Ÿ‡ฐ๐Ÿ‡ท ๊ตญ๋‚ด ์‚ฌ๋ก€๋„ ์žˆ์–ด! ๊ตญ๋‚ด์—์„œ๋„ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋“ค์ด PWA๋ฅผ ๋„์ž…ํ•˜๊ณ  ์žˆ์–ด. ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ ์žฌ๋Šฅ๋„ท(jaenung.net)์ฒ˜๋Ÿผ ์›น ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค๋“ค์ด PWA ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•˜๋ฉด ์•ฑ์Šคํ† ์–ด ์—†์ด๋„ ์•ฑ ์ˆ˜์ค€์˜ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ ์  ๊ด€์‹ฌ์ด ๋†’์•„์ง€๊ณ  ์žˆ์Œ.

๐Ÿ› ๏ธ PWA ๊ฐœ๋ฐœ ๋„๊ตฌ & ํ”„๋ ˆ์ž„์›Œํฌ ์ด์ •๋ฆฌ

PWA๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค ๋งŒ๋“ค ํ•„์š” ์—†์–ด. ์ด๋ฏธ ์ข‹์€ ๋„๊ตฌ๋“ค์ด ๋งŽ์ด ์žˆ๊ฑฐ๋“ .
์–ด๋–ค ๊ฑธ ์จ์•ผ ํ• ์ง€ ์ •๋ฆฌํ•ด์ค„๊ฒŒ ๐Ÿ˜Š

โšก Vite PWA Plugin

์š”์ฆ˜ ๊ฐ€์žฅ ํ•ซํ•œ PWA ์„ค์ • ๋ฐฉ๋ฒ•. Vite ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ์—์„œ ๋ช‡ ์ค„๋กœ PWA ์„ค์ • ์™„๋ฃŒ:

// vite.config.js
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 10,
                maxAgeSeconds: 60 * 60 * 24 // 24์‹œ๊ฐ„
              }
            }
          }
        ]
      },
      manifest: {
        name: '๋‚˜์˜ PWA ์•ฑ',
        short_name: 'PWA์•ฑ',
        theme_color: '#0f3460',
        icons: [
          { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'icon-512.png', sizes: '512x512', type: 'image/png' }
        ]
      }
    })
  ]
});

โš›๏ธ React์—์„œ PWA ์„ค์ •

# Create React App์œผ๋กœ PWA ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ
npx create-react-app my-pwa --template cra-template-pwa

# ๋˜๋Š” ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€
# src/index.js์—์„œ serviceWorkerRegistration import ํ›„ register() ํ˜ธ์ถœ
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
serviceWorkerRegistration.register();

๐Ÿ’š Next.js์—์„œ PWA ์„ค์ •

# next-pwa ํŒจํ‚ค์ง€ ์„ค์น˜
npm install next-pwa

// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === 'development'
});

module.exports = withPWA({
  // ๊ธฐ์กด Next.js ์„ค์ •
});

๐Ÿ…ฐ๏ธ Angular์—์„œ PWA ์„ค์ •

# Angular PWA ์Šคํ‚ค๋งคํ‹ฑ ์ถ”๊ฐ€ (ํ•œ ์ค„๋กœ ๋!)
ng add @angular/pwa

# ์ด ๋ช…๋ น์–ด ํ•˜๋‚˜๋กœ:
# - ngsw-config.json ์ƒ์„ฑ
# - manifest.webmanifest ์ƒ์„ฑ
# - ์•„์ด์ฝ˜ ํŒŒ์ผ ์ƒ์„ฑ
# - index.html ์—…๋ฐ์ดํŠธ
# - app.module.ts ์—…๋ฐ์ดํŠธ
# ๋‹ค ์ž๋™์œผ๋กœ ํ•ด์คŒ ใ„ทใ„ท

๐Ÿ”ง Lighthouse๋กœ PWA ํ’ˆ์งˆ ์ธก์ •

๊ตฌ๊ธ€์˜ Lighthouse ๋„๊ตฌ๋กœ PWA ํ’ˆ์งˆ์„ ์ ์ˆ˜๋กœ ์ธก์ •ํ•  ์ˆ˜ ์žˆ์–ด:

Lighthouse PWA ์ฒดํฌ๋ฆฌ์ŠคํŠธ:
  • โœ… HTTPS ์‚ฌ์šฉ
  • โœ… Service Worker ๋“ฑ๋ก
  • โœ… Web App Manifest ์œ ํšจ
  • โœ… ์˜คํ”„๋ผ์ธ ํŽ˜์ด์ง€ ์ œ๊ณต
  • โœ… ์•„์ด์ฝ˜ ํฌ๊ธฐ ์ ์ ˆ (192x192, 512x512)
  • โœ… ์‹œ์ž‘ URL์ด ์˜คํ”„๋ผ์ธ์—์„œ๋„ ๋™์ž‘
  • โœ… viewport ๋ฉ”ํƒ€ํƒœ๊ทธ ์„ค์ •
  • โœ… ํŽ˜์ด์ง€ ๋กœ๋“œ ์†๋„ (3G์—์„œ 10์ดˆ ์ด๋‚ด)
  • โœ… ์ฝ˜ํ…์ธ  ํฌ๊ธฐ๊ฐ€ ๋ทฐํฌํŠธ์— ๋งž์Œ
  • โœ… ํƒญ ์ˆœ์„œ๊ฐ€ ๋…ผ๋ฆฌ์ 
๐Ÿ”ฎ Chrome DevTools์—์„œ ๋ฐ”๋กœ ํ…Œ์ŠคํŠธ! Chrome DevTools โ†’ Application ํƒญ์—์„œ Service Worker ์ƒํƒœ, ์บ์‹œ ๋‚ด์šฉ, Manifest ์ •๋ณด๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด.
"Offline" ์ฒดํฌ๋ฐ•์Šค๋กœ ์˜คํ”„๋ผ์ธ ๋™์ž‘๋„ ๋ฐ”๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•จ!

โš ๏ธ PWA์˜ ํ•œ๊ณ„ โ€” ์†”์งํ•˜๊ฒŒ ๋งํ•ด์ค„๊ฒŒ

PWA๊ฐ€ ๋งŒ๋Šฅ์€ ์•„๋‹˜. ์†”์งํžˆ ๋งํ•˜๋ฉด ์•„์ง ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์„ ์™„์ „ํžˆ ๋Œ€์ฒดํ•˜๊ธฐ์—” ํ•œ๊ณ„๊ฐ€ ์žˆ์–ด.
๊ทผ๋ฐ ์ด ํ•œ๊ณ„๋“ค์ด ์ ์  ์ค„์–ด๋“ค๊ณ  ์žˆ๋‹ค๋Š” ๊ฒŒ ํฌ์ธํŠธ์ž„ ใ…‹ใ…‹

โš ๏ธ ํ˜„์žฌ ํ•œ๊ณ„์ 

๐Ÿ“ต iOS์—์„œ ๊ธฐ๋Šฅ ์ œํ•œ
(ํ‘ธ์‹œ ์•Œ๋ฆผ iOS 16.4+๋ถ€ํ„ฐ)

๐Ÿ”‹ ๋ฐฐํ„ฐ๋ฆฌ/์„ฑ๋Šฅ ์ตœ์ ํ™” ํ•œ๊ณ„
(๋„ค์ดํ‹ฐ๋ธŒ ๋Œ€๋น„ ์•ฝ๊ฐ„ ๋–จ์–ด์ง)

๐Ÿ“ท ์ผ๋ถ€ ํ•˜๋“œ์›จ์–ด API ๋ฏธ์ง€์›
(NFC, ๋ธ”๋ฃจํˆฌ์Šค ๋“ฑ)

๐Ÿช ์•ฑ์Šคํ† ์–ด ๋…ธ์ถœ ๋ถˆ๊ฐ€
(๊ฒ€์ƒ‰ ์œ ์ž… ์ฑ„๋„ ์ œํ•œ)

๐Ÿ” ๋ณด์•ˆ ๋ฏผ๊ฐ ์•ฑ ํ•œ๊ณ„
(๋ฑ…ํ‚น, ๊ฒฐ์ œ ๋“ฑ)
โœ… ๊ทน๋ณต ๋ฐฉ๋ฒ• & ํŠธ๋ Œ๋“œ

๐Ÿ“ฑ iOS ์ง€์› ๊ณ„์† ํ™•๋Œ€ ์ค‘
(Apple์ด ์ ์ง„์ ์œผ๋กœ ์ง€์› ์ถ”๊ฐ€)

๐Ÿš€ WebAssembly๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
(๋„ค์ดํ‹ฐ๋ธŒ ์ˆ˜์ค€ ์„ฑ๋Šฅ ๊ฐ€๋Šฅ)

๐Ÿ”Œ Web API ๊ณ„์† ํ™•์žฅ ์ค‘
(WebBluetooth, WebNFC ๋“ฑ)

๐Ÿช Google Play์— TWA๋กœ ๋“ฑ๋ก ๊ฐ€๋Šฅ
(Trusted Web Activity)

๐Ÿ”’ Web Crypto API๋กœ ๋ณด์•ˆ ๊ฐ•ํ™”

๐Ÿ” iOS Safari PWA ์ง€์› ํ˜„ํ™ฉ (2024 ๊ธฐ์ค€)

๊ธฐ๋Šฅ ์ง€์› ์—ฌ๋ถ€ ๋ฒ„์ „
ํ™ˆ ํ™”๋ฉด ์ถ”๊ฐ€ โœ… ์ง€์› iOS 1.1.3+
Service Worker โœ… ์ง€์› iOS 11.3+
Web App Manifest โš ๏ธ ๋ถ€๋ถ„ ์ง€์› iOS 11.3+
ํ‘ธ์‹œ ์•Œ๋ฆผ โœ… ์ง€์› (ํ™ˆ ํ™”๋ฉด ์„ค์น˜ ํ›„) iOS 16.4+
Background Sync โŒ ๋ฏธ์ง€์› -
Web Share API โœ… ์ง€์› iOS 12.2+
Payment Request API โœ… ์ง€์› iOS 11.3+
Badging API โœ… ์ง€์› iOS 16.4+

๐Ÿช TWA(Trusted Web Activity)๋กœ ์•ฑ์Šคํ† ์–ด ๋“ฑ๋กํ•˜๊ธฐ

PWA๋ฅผ Google Play Store์— ๋“ฑ๋กํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด TWA๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

๐Ÿ’ก TWA๋ž€? Trusted Web Activity๋Š” PWA๋ฅผ Android ์•ฑ์œผ๋กœ ํŒจํ‚ค์ง•ํ•ด์„œ Google Play Store์— ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ์ˆ ์ด์•ผ.
Chrome Custom Tabs๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๊ณ , ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ ์‹ค์ œ๋กœ๋Š” ์›น ์ฝ˜ํ…์ธ ๋ฅผ ํ‘œ์‹œํ•จ.
Bubblewrap CLI ๋„๊ตฌ๋กœ ์‰ฝ๊ฒŒ TWA ์•ฑ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด.
# Bubblewrap์œผ๋กœ TWA ์ƒ์„ฑ
npm install -g @bubblewrap/cli

# ์ดˆ๊ธฐํ™” (manifest.json URL ์ž…๋ ฅ)
bubblewrap init --manifest https://your-pwa.com/manifest.json

# APK ๋นŒ๋“œ
bubblewrap build

๐Ÿš€ ์ตœ์‹  Web API๋“ค โ€” PWA๊ฐ€ ์ ์  ๊ฐ•ํ•ด์ง€๊ณ  ์žˆ์Œ

Project Fugu๋ผ๊ณ  ๋“ค์–ด๋ดค์–ด? ๊ตฌ๊ธ€์ด ์ฃผ๋„ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์ธ๋ฐ, ์›น์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ์ˆ˜์ค€์˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ƒˆ๋กœ์šด Web API๋ฅผ ๊ณ„์† ์ถ”๊ฐ€ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์•ผ.
๋•๋ถ„์— PWA๊ฐ€ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค์ด ์—„์ฒญ ๋Š˜์–ด๋‚˜๊ณ  ์žˆ์Œ ใ„ทใ„ท

๐Ÿ†• ์ฃผ๋ชฉํ•  ๋งŒํ•œ ์ตœ์‹  Web API๋“ค

File System Access API

๋กœ์ปฌ ํŒŒ์ผ ์‹œ์Šคํ…œ์— ์ง์ ‘ ์ ‘๊ทผ ๊ฐ€๋Šฅ.
ํŒŒ์ผ ์—ด๊ธฐ/์ €์žฅ/์ˆ˜์ •์„ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ฒ˜๋Ÿผ ํ•  ์ˆ˜ ์žˆ์Œ.
VS Code ์›น ๋ฒ„์ „์ด ์ด๊ฑธ ์”€!
Web Share API

OS ๊ธฐ๋ณธ ๊ณต์œ  ์‹œํŠธ๋ฅผ ์—ด ์ˆ˜ ์žˆ์Œ.
์นด์นด์˜คํ†ก, ์ธ์Šคํƒ€๊ทธ๋žจ ๋“ฑ์œผ๋กœ ๊ณต์œ ํ•˜๋Š” ๊ทธ UI.
๋ชจ๋ฐ”์ผ์—์„œ ํŠนํžˆ ์œ ์šฉํ•จ.
Contact Picker API

๊ธฐ๊ธฐ์˜ ์—ฐ๋ฝ์ฒ˜์— ์ ‘๊ทผ ๊ฐ€๋Šฅ.
์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ์—ฐ๋ฝ์ฒ˜๋งŒ ์•ฑ์— ์ „๋‹ฌ.
ํ”„๋ผ์ด๋ฒ„์‹œ ๋ณดํ˜ธ ์„ค๊ณ„.
Badging API

์•ฑ ์•„์ด์ฝ˜์— ๋ฐฐ์ง€(์ˆซ์ž) ํ‘œ์‹œ ๊ฐ€๋Šฅ.
์นด์นด์˜คํ†ก ์•Œ๋ฆผ ์ˆซ์ž ๊ฐ™์€ ๊ทธ๊ฑฐ.
iOS 16.4+, Android Chrome ์ง€์›.
Screen Wake Lock API

ํ™”๋ฉด์ด ๊บผ์ง€์ง€ ์•Š๋„๋ก ์œ ์ง€.
๋ ˆ์‹œํ”ผ ์•ฑ, ๋‚ด๋น„๊ฒŒ์ด์…˜ ์•ฑ์— ์œ ์šฉ.
Chrome, Edge, Safari ์ง€์›.
Web Bluetooth API

๋ธ”๋ฃจํˆฌ์Šค ๊ธฐ๊ธฐ์™€ ํ†ต์‹  ๊ฐ€๋Šฅ.
IoT ๊ธฐ๊ธฐ ์ œ์–ด, ํ—ฌ์Šค์ผ€์–ด ์•ฑ ๋“ฑ.
Chrome, Edge ์ง€์› (iOS ๋ฏธ์ง€์›).

๐Ÿ’ป Badging API ์‚ฌ์šฉ ์˜ˆ์‹œ

// ์•ฑ ์•„์ด์ฝ˜์— ๋ฐฐ์ง€ ํ‘œ์‹œ
async function updateBadge(count) {
  if ('setAppBadge' in navigator) {
    if (count > 0) {
      await navigator.setAppBadge(count);
    } else {
      await navigator.clearAppBadge();
    }
  }
}

// ์ƒˆ ๋ฉ”์‹œ์ง€ 5๊ฐœ ์žˆ์„ ๋•Œ
updateBadge(5); // ์•„์ด์ฝ˜์— "5" ํ‘œ์‹œ

// ๋ชจ๋‘ ์ฝ์—ˆ์„ ๋•Œ
updateBadge(0); // ๋ฐฐ์ง€ ์ œ๊ฑฐ

๐Ÿ“ File System Access API ์˜ˆ์‹œ

// ํŒŒ์ผ ์—ด๊ธฐ (๋„ค์ดํ‹ฐ๋ธŒ ํŒŒ์ผ ๋‹ค์ด์–ผ๋กœ๊ทธ)
async function openFile() {
  const [fileHandle] = await window.showOpenFilePicker({
    types: [
      {
        description: 'ํ…์ŠคํŠธ ํŒŒ์ผ',
        accept: { 'text/plain': ['.txt', '.md'] }
      }
    ]
  });
  
  const file = await fileHandle.getFile();
  const content = await file.text();
  return content;
}

// ํŒŒ์ผ ์ €์žฅ
async function saveFile(content) {
  const fileHandle = await window.showSaveFilePicker({
    suggestedName: 'document.txt',
    types: [{ accept: { 'text/plain': ['.txt'] } }]
  });
  
  const writable = await fileHandle.createWritable();
  await writable.write(content);
  await writable.close();
}
PWA ๊ธฐ์ˆ  ๋ฐœ์ „ ํƒ€์ž„๋ผ์ธ 2015 PWA ํƒ„์ƒ PWA ๊ฐœ๋… ๋“ฑ์žฅ Alex Russell ์ œ์•ˆ Service Worker ํ‘œ์ค€ํ™” 2017 ํ™•์‚ฐ ์‹œ์ž‘ ๋Œ€๊ธฐ์—… ๋„์ž… Twitter Lite ์ถœ์‹œ Flipkart PWA ์„ฑ๊ณต 2019 ์„ฑ์ˆ™๊ธฐ TWA ๋“ฑ์žฅ Play Store ๋“ฑ๋ก ๊ฐ€๋Šฅ Project Fugu ์‹œ์ž‘ 2021 ๊ธฐ๋Šฅ ํ™•์žฅ Web API ํญ๋ฐœ File System API Badging API ๋“ฑ 2024+ ๋ฏธ๋ž˜ iOS ์™„์ „ ์ง€์› WebAssembly ํ†ตํ•ฉ AI ๊ธฐ๋Šฅ ๋‚ด์žฅ PWA๋Š” 2015๋…„๋ถ€ํ„ฐ ์ง€๊ธˆ๊นŒ์ง€ ๊พธ์ค€ํžˆ ์„ฑ์žฅ ์ค‘ ๐Ÿš€

โœ… ์‹ค์ „ PWA ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

PWA ๋งŒ๋“ค ๋•Œ ๋†“์น˜๊ธฐ ์‰ฌ์šด ๊ฒƒ๋“ค ์ •๋ฆฌํ•ด์ค„๊ฒŒ. ์ด๊ฑฐ ๋‹ค ์ฒดํฌํ•˜๋ฉด ์ง„์งœ ์ œ๋Œ€๋กœ ๋œ PWA์ž„ ใ…‹ใ…‹

๐Ÿ”ง ๊ธฐ๋ณธ ์„ค์ • ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๐Ÿ“‹ ํ•„์ˆ˜ ํ•ญ๋ชฉ
  • โ˜ HTTPS ์ ์šฉ (Let's Encrypt๋กœ ๋ฌด๋ฃŒ ๊ฐ€๋Šฅ)
  • โ˜ manifest.json ์ž‘์„ฑ ๋ฐ HTML์— ์—ฐ๊ฒฐ
  • โ˜ ์•„์ด์ฝ˜ ์ค€๋น„ (์ตœ์†Œ 192x192, 512x512 PNG)
  • โ˜ Service Worker ๋“ฑ๋ก
  • โ˜ ์˜คํ”„๋ผ์ธ ํด๋ฐฑ ํŽ˜์ด์ง€ ๊ตฌํ˜„
  • โ˜ viewport ๋ฉ”ํƒ€ํƒœ๊ทธ ์„ค์ •
  • โ˜ theme-color ๋ฉ”ํƒ€ํƒœ๊ทธ ์„ค์ •
  • โ˜ iOS์šฉ apple-touch-icon ์„ค์ •
๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํ•ญ๋ชฉ
  • โ˜ ํ•ต์‹ฌ ํŒŒ์ผ Pre-caching ์„ค์ •
  • โ˜ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (WebP ํฌ๋งท ์‚ฌ์šฉ)
  • โ˜ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ
  • โ˜ Critical CSS ์ธ๋ผ์ธ ์ฒ˜๋ฆฌ
  • โ˜ ํฐํŠธ preload ์„ค์ •
  • โ˜ Lazy loading ์ ์šฉ
  • โ˜ Lighthouse ์ ์ˆ˜ 90์  ์ด์ƒ ๋ชฉํ‘œ
๐Ÿ“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ•ญ๋ชฉ
  • โ˜ ์„ค์น˜ ํ”„๋กฌํ”„ํŠธ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
  • โ˜ iOS ์„ค์น˜ ์•ˆ๋‚ด UI ๊ตฌํ˜„
  • โ˜ ์˜คํ”„๋ผ์ธ ์ƒํƒœ ํ‘œ์‹œ UI
  • โ˜ ์•ฑ ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ ๊ตฌํ˜„
  • โ˜ ์Šคํ”Œ๋ž˜์‹œ ์Šคํฌ๋ฆฐ ์„ค์ •
  • โ˜ ์ „์ฒด ํ™”๋ฉด ๋ชจ๋“œ ํ…Œ์ŠคํŠธ
  • โ˜ ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธ

๐Ÿ”„ ์•ฑ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌํ•˜๊ธฐ

PWA์—์„œ ์ƒˆ ๋ฒ„์ „ ๋ฐฐํฌ ์‹œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์—…๋ฐ์ดํŠธ๋ฅผ ์•Œ๋ฆฌ๋Š” ํŒจํ„ด:

// Service Worker ์—…๋ฐ์ดํŠธ ๊ฐ์ง€ ๋ฐ ์•Œ๋ฆผ
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then((registration) => {
    
    // ์—…๋ฐ์ดํŠธ ๊ฐ์ง€
    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing;
      
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          // ์ƒˆ ๋ฒ„์ „ ์‚ฌ์šฉ ๊ฐ€๋Šฅ! ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ
          showUpdateNotification();
        }
      });
    });
  });
}

function showUpdateNotification() {
  // ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€๋‚˜ ๋ฐฐ๋„ˆ๋กœ ์•Œ๋ฆผ
  const banner = document.createElement('div');
  banner.innerHTML = `
    ์ƒˆ ๋ฒ„์ „์ด ์žˆ์–ด์š”! 
    <button onclick="location.reload()">์—…๋ฐ์ดํŠธ</button>
  `;
  document.body.appendChild(banner);
}

// Service Worker์—์„œ skipWaiting์œผ๋กœ ์ฆ‰์‹œ ํ™œ์„ฑํ™”
self.addEventListener('message', (event) => {
  if (event.data === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

๐Ÿค” PWA vs ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ โ€” ๋ญ˜ ์„ ํƒํ•ด์•ผ ํ•ด?

์•ฑ ๊ฐœ๋ฐœํ•  ๋•Œ ์„ ํƒ์ง€๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ž–์•„. PWA, ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ(React Native, Flutter), ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ...
์–ด๋–ค ์ƒํ™ฉ์—์„œ ๋ญ˜ ์„ ํƒํ•ด์•ผ ํ•˜๋Š”์ง€ ์ •๋ฆฌํ•ด์ค„๊ฒŒ.

๐Ÿ“Š ๊ธฐ์ˆ  ์„ ํƒ ๊ฐ€์ด๋“œ

์ƒํ™ฉ ์ถ”์ฒœ ๊ธฐ์ˆ  ์ด์œ 
๋น ๋ฅธ MVP ์ถœ์‹œ PWA ์•ฑ์Šคํ† ์–ด ์‹ฌ์‚ฌ ์—†์ด ์ฆ‰์‹œ ๋ฐฐํฌ
์ฝ˜ํ…์ธ  ์ค‘์‹ฌ ์„œ๋น„์Šค PWA SEO + ์•ฑ ๊ฒฝํ—˜ ๋™์‹œ์—
iOS/Android ๋™์‹œ ์ง€์› React Native / Flutter ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ ์™„์ „ ์ง€์›
๊ณ ์„ฑ๋Šฅ ๊ฒŒ์ž„/๊ทธ๋ž˜ํ”ฝ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ GPU ์ง์ ‘ ์ ‘๊ทผ ํ•„์š”
๊ธฐ์—… ๋‚ด๋ถ€ ๋„๊ตฌ PWA ๋ฐฐํฌ/๊ด€๋ฆฌ ๊ฐ„ํŽธ, ์•ฑ์Šคํ† ์–ด ๋ถˆํ•„์š”
์ด์ปค๋จธ์Šค / ๋ฏธ๋””์–ด PWA ์ „ํ™˜์œจ ํ–ฅ์ƒ, ๋น ๋ฅธ ๋กœ๋”ฉ
์นด๋ฉ”๋ผ/AR ์•ฑ ๋„ค์ดํ‹ฐ๋ธŒ / ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ํ•˜๋“œ์›จ์–ด ์™„์ „ ์ œ์–ด ํ•„์š”
๋ฑ…ํ‚น/ํ•€ํ…Œํฌ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ, ์ƒ์ฒด์ธ์ฆ ๋“ฑ
"PWA๋Š” ๋ชจ๋“  ์•ฑ์„ ๋Œ€์ฒดํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์›น ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค๊ฐ€ ์•ฑ ์ˆ˜์ค€์˜ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ์ˆ ์ด์•ผ. ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์ฐพ๋Š” ๊ฒŒ ํ•ต์‹ฌ์ž„."

๐Ÿ’ฐ ๊ฐœ๋ฐœ ๋น„์šฉ ๋น„๊ต

PWA ๊ฐœ๋ฐœ ๋น„์šฉ ๊ตฌ์กฐ: โ€ข ๊ธฐ์กด ์›น ๊ฐœ๋ฐœ์ž๋กœ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ โ†’ ๋ณ„๋„ ๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž ๋ถˆํ•„์š”
โ€ข iOS + Android ๋™์‹œ ์ง€์› โ†’ ๋‹จ์ผ ์ฝ”๋“œ๋ฒ ์ด์Šค
โ€ข ์•ฑ์Šคํ† ์–ด ์ˆ˜์ˆ˜๋ฃŒ ์—†์Œ โ†’ Apple 30%, Google 15~30% ์ ˆ์•ฝ
โ€ข ์—…๋ฐ์ดํŠธ ๋น„์šฉ ์ตœ์†Œํ™” โ†’ ์„œ๋ฒ„ ๋ฐฐํฌ๋งŒ์œผ๋กœ ์™„๋ฃŒ

์˜ˆ์ƒ ์ ˆ๊ฐ ํšจ๊ณผ: ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ๋Œ€๋น„ ๊ฐœ๋ฐœ ๋น„์šฉ 40~60% ์ ˆ๊ฐ ๊ฐ€๋Šฅ

๐Ÿ—๏ธ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“œ๋Š” ๋ฏธ๋‹ˆ PWA ์˜ˆ์ œ

์ด๋ก ๋งŒ ํ•˜๋ฉด ์žฌ๋ฏธ์—†์ž–์•„. ๊ฐ„๋‹จํ•œ PWA๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณด์ž.
ํ•  ์ผ ๋ชฉ๋ก(Todo) ์•ฑ์„ PWA๋กœ ๋งŒ๋“œ๋Š” ์˜ˆ์ œ์•ผ. ์˜คํ”„๋ผ์ธ์—์„œ๋„ ๋™์ž‘ํ•˜๊ณ , ํ™ˆ ํ™”๋ฉด์— ์„ค์น˜๋„ ๊ฐ€๋Šฅํ•œ ๋ฒ„์ „์œผ๋กœ!

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

my-todo-pwa/
โ”œโ”€โ”€ index.html          # ๋ฉ”์ธ HTML
โ”œโ”€โ”€ app.js              # ์•ฑ ๋กœ์ง
โ”œโ”€โ”€ styles.css          # ์Šคํƒ€์ผ
โ”œโ”€โ”€ sw.js               # Service Worker
โ”œโ”€โ”€ manifest.json       # Web App Manifest
โ””โ”€โ”€ icons/
    โ”œโ”€โ”€ icon-192.png
    โ””โ”€โ”€ icon-512.png

๐Ÿ“„ index.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>๋‚˜์˜ ํ•  ์ผ PWA</title>
  <link rel="manifest" href="/manifest.json">
  <meta name="theme-color" content="#0f3460">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-title" content="ํ• ์ผ์•ฑ">
  <link rel="apple-touch-icon" href="/icons/icon-192.png">
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div id="app">
    <header>
      <h1>๐Ÿ“ ๋‚˜์˜ ํ•  ์ผ</h1>
      <div id="offline-indicator" hidden>์˜คํ”„๋ผ์ธ ๋ชจ๋“œ</div>
    </header>
    <main>
      <form id="todo-form">
        <input type="text" id="todo-input" placeholder="ํ•  ์ผ ์ž…๋ ฅ..." required>
        <button type="submit">์ถ”๊ฐ€</button>
      </form>
      <ul id="todo-list"></ul>
    </main>
  </div>
  <script src="app.js"></script>
</body>
</html>

โš™๏ธ app.js (ํ•ต์‹ฌ ๋กœ์ง)

// Service Worker ๋“ฑ๋ก
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW ๋“ฑ๋ก ์™„๋ฃŒ!', reg.scope))
      .catch(err => console.log('SW ๋“ฑ๋ก ์‹คํŒจ:', err));
  });
}

// ์˜คํ”„๋ผ์ธ ๊ฐ์ง€
window.addEventListener('online', () => {
  document.getElementById('offline-indicator').hidden = true;
});
window.addEventListener('offline', () => {
  document.getElementById('offline-indicator').hidden = false;
});

// Todo ์•ฑ ๋กœ์ง
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');

// localStorage์—์„œ ํ•  ์ผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (์˜คํ”„๋ผ์ธ ์ง€์›)
let todos = JSON.parse(localStorage.getItem('todos') || '[]');

function renderTodos() {
  todoList.innerHTML = '';
  todos.forEach((todo, index) => {
    const li = document.createElement('li');
    li.innerHTML = `
      <span class="${todo.done ? 'done' : ''}">${todo.text}</span>
      <button onclick="toggleTodo(${index})">
        ${todo.done ? 'โ†ฉ๏ธ' : 'โœ…'}
      </button>
      <button onclick="deleteTodo(${index})">๐Ÿ—‘๏ธ</button>
    `;
    todoList.appendChild(li);
  });
}

function saveTodos() {
  localStorage.setItem('todos', JSON.stringify(todos));
}

todoForm.addEventListener('submit', (e) => {
  e.preventDefault();
  todos.push({ text: todoInput.value, done: false });
  saveTodos();
  renderTodos();
  todoInput.value = '';
});

function toggleTodo(index) {
  todos[index].done = !todos[index].done;
  saveTodos();
  renderTodos();
}

function deleteTodo(index) {
  todos.splice(index, 1);
  saveTodos();
  renderTodos();
}

renderTodos();

๐Ÿ”ง sw.js (Service Worker)

const CACHE_NAME = 'todo-pwa-v1';
const ASSETS = ['/', '/index.html', '/app.js', '/styles.css', '/icons/icon-192.png'];

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
  );
  self.skipWaiting(); // ์ฆ‰์‹œ ํ™œ์„ฑํ™”
});

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
    )
  );
  self.clients.claim(); // ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์ฆ‰์‹œ ์ œ์–ด
});

self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then(cached => cached || fetch(e.request))
  );
});
๐Ÿ’ก ์ด ์˜ˆ์ œ์˜ ํŠน์ง• โ€ข ์˜คํ”„๋ผ์ธ์—์„œ๋„ ์™„์ „ํžˆ ๋™์ž‘ํ•จ (localStorage + Service Worker ์บ์‹œ)
โ€ข ํ™ˆ ํ™”๋ฉด์— ์„ค์น˜ ๊ฐ€๋Šฅ
โ€ข ๋„คํŠธ์›Œํฌ ์—†์–ด๋„ ํ•  ์ผ ์ถ”๊ฐ€/์‚ญ์ œ/์™„๋ฃŒ ๊ฐ€๋Šฅ
โ€ข ์ฝ”๋“œ 100์ค„ ๋ฏธ๋งŒ์œผ๋กœ ์™„์„ฑ๋˜๋Š” ์‹ค์šฉ์ ์ธ PWA!

๐Ÿ”ฎ PWA์˜ ๋ฏธ๋ž˜ โ€” ์•ž์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

PWA ๊ธฐ์ˆ ์€ ์ง€๊ธˆ๋„ ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. ์•ž์œผ๋กœ ์–ด๋–ค ๋ฐฉํ–ฅ์œผ๋กœ ๊ฐˆ์ง€ ์‚ดํŽด๋ณด์ž.

๐ŸŒŸ ์ฃผ๋ชฉํ•  ํŠธ๋ Œ๋“œ๋“ค

๐Ÿค– AI + PWA

WebNN(Web Neural Network) API๋กœ ๋ธŒ๋ผ์šฐ์ €์—์„œ AI ๋ชจ๋ธ ์‹คํ–‰ ๊ฐ€๋Šฅ.
์˜คํ”„๋ผ์ธ AI ๊ธฐ๋Šฅ์„ PWA์— ๋‚ด์žฅํ•˜๋Š” ์‹œ๋Œ€๊ฐ€ ์˜ด.
๐Ÿ”— WebAssembly ํ†ตํ•ฉ

C/C++/Rust ์ฝ”๋“œ๋ฅผ ์›น์—์„œ ์‹คํ–‰.
๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ ์ˆ˜์ค€์˜ ์„ฑ๋Šฅ์„ PWA์—์„œ ๊ตฌํ˜„ ๊ฐ€๋Šฅ.
๐Ÿฅฝ WebXR + PWA

AR/VR ๊ฒฝํ—˜์„ PWA๋กœ ์ œ๊ณต.
๋ฉ”ํƒ€๋ฒ„์Šค ์„œ๋น„์Šค์˜ ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ.
๐ŸŽ Apple์˜ ๋ณ€ํ™”

EU ๊ทœ์ œ๋กœ iOS์—์„œ ํƒ€์‚ฌ ๋ธŒ๋ผ์šฐ์ € ์—”์ง„ ํ—ˆ์šฉ.
iOS PWA ๊ธฐ๋Šฅ ์ œํ•œ์ด ์ ์ฐจ ํ•ด์†Œ๋  ์ „๋ง.
๐ŸŽฏ ๊ฒฐ๋ก : PWA๋Š” ์„ ํƒ์ด ์•„๋‹Œ ํ•„์ˆ˜๊ฐ€ ๋˜์–ด๊ฐ€๊ณ  ์žˆ์Œ 2024๋…„ ๊ธฐ์ค€์œผ๋กœ ์ „ ์„ธ๊ณ„ ์›น ํŠธ๋ž˜ํ”ฝ์˜ 60% ์ด์ƒ์ด ๋ชจ๋ฐ”์ผ์—์„œ ๋ฐœ์ƒํ•ด.
์•ฑ์Šคํ† ์–ด ์—†์ด๋„ ์•ฑ ์ˆ˜์ค€์˜ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋Š” PWA๋Š” ํŠนํžˆ ๊ฐœ๋ฐœ ๋ฆฌ์†Œ์Šค๊ฐ€ ์ œํ•œ๋œ ์Šคํƒ€ํŠธ์—…์ด๋‚˜ ์ค‘์†Œ๊ธฐ์—…์—๊ฒŒ ์—„์ฒญ๋‚œ ๊ธฐํšŒ์•ผ.
์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ์›น ๊ธฐ๋ฐ˜ ํ”Œ๋žซํผ๋“ค์ด PWA๋ฅผ ๋„์ž…ํ•˜๋ฉด ์‚ฌ์šฉ์ž ์žฌ๋ฐฉ๋ฌธ์œจ๊ณผ ์ฐธ์—ฌ๋„๋ฅผ ํฌ๊ฒŒ ๋†’์ผ ์ˆ˜ ์žˆ์–ด.

๐Ÿ“ˆ PWA ์‹œ์žฅ ํ˜„ํ™ฉ (2024)

โ€ข ์ „ ์„ธ๊ณ„ PWA ์‹œ์žฅ ๊ทœ๋ชจ: ์•ฝ 14์–ต ๋‹ฌ๋Ÿฌ (2023๋…„ ๊ธฐ์ค€)
โ€ข ์—ฐํ‰๊ท  ์„ฑ์žฅ๋ฅ (CAGR): 31.9% (2024~2030 ์˜ˆ์ƒ)
โ€ข Fortune 500 ๊ธฐ์—… ์ค‘ PWA ๋„์ž… ๋น„์œจ: ์•ฝ 60%
โ€ข PWA ์‚ฌ์šฉ์ž์˜ ํ‰๊ท  ์„ธ์…˜ ์‹œ๊ฐ„: ์ผ๋ฐ˜ ์›น ๋Œ€๋น„ 2~3๋ฐฐ
โ€ข PWA ์ „ํ™˜์œจ: ์ผ๋ฐ˜ ๋ชจ๋ฐ”์ผ ์›น ๋Œ€๋น„ ํ‰๊ท  36% ๋†’์Œ
PWA ์™„์„ฑ๋„ ๋ ˆ์ด๋” ์ฐจํŠธ ์˜คํ”„๋ผ์ธ ์ง€์› ์„ค์น˜ ๊ฒฝํ—˜ ํ‘ธ์‹œ ์•Œ๋ฆผ ์„ฑ๋Šฅ ๋ณด์•ˆ ์ ‘๊ทผ์„ฑ 90% 80% 70% 80% 78% 69% PWA ํ‰๊ท  ๊ตฌํ˜„ ์™„์„ฑ๋„ (2024 ๊ธฐ์ค€)

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ โ€” PWA, ์ด์ œ ์‹œ์ž‘ํ•ด๋ณผ ๋•Œ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ PWA์— ๋Œ€ํ•ด ์ง„์งœ ๊นŠ๊ฒŒ ํŒŒ๋ดค์–ด. ์ •๋ฆฌํ•˜๋ฉด:

๐Ÿ“Œ ํ•ต์‹ฌ ์š”์•ฝ 1๏ธโƒฃ PWA = ์›น ๊ธฐ์ˆ  + ์•ฑ ๊ฒฝํ—˜ โ€” HTML/CSS/JS๋กœ ๋งŒ๋“ค์ง€๋งŒ ์•ฑ์ฒ˜๋Ÿผ ๋™์ž‘
2๏ธโƒฃ Service Worker๊ฐ€ ํ•ต์‹ฌ โ€” ์˜คํ”„๋ผ์ธ, ์บ์‹ฑ, ํ‘ธ์‹œ ์•Œ๋ฆผ ๋‹ด๋‹น
3๏ธโƒฃ Web App Manifest๋กœ ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ โ€” ์•„์ด์ฝ˜, ์ด๋ฆ„, ํ™”๋ฉด ๋ชจ๋“œ ์„ค์ •
4๏ธโƒฃ ์‹ค์ œ ํšจ๊ณผ ์žˆ์Œ โ€” Twitter, Flipkart, AliExpress ๋“ฑ ์ˆ˜์‹ญ~์ˆ˜๋ฐฑ% ์ง€ํ‘œ ํ–ฅ์ƒ
5๏ธโƒฃ iOS ํ•œ๊ณ„ ์žˆ์ง€๋งŒ ๊ฐœ์„  ์ค‘ โ€” iOS 16.4+๋ถ€ํ„ฐ ํ‘ธ์‹œ ์•Œ๋ฆผ ์ง€์›
6๏ธโƒฃ TWA๋กœ ์•ฑ์Šคํ† ์–ด ๋“ฑ๋ก๋„ ๊ฐ€๋Šฅ โ€” Google Play Store์— PWA ๋“ฑ๋ก ๊ฐ€๋Šฅ
7๏ธโƒฃ ๊ฐœ๋ฐœ ๋น„์šฉ ์ ˆ๊ฐ โ€” ๋‹จ์ผ ์ฝ”๋“œ๋ฒ ์ด์Šค๋กœ iOS/Android ๋™์‹œ ์ง€์›
๐Ÿš€ ์ง€๊ธˆ ๋‹น์žฅ ์‹œ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ• ๊ธฐ์กด ์›น์‚ฌ์ดํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด:
1. HTTPS ์ ์šฉ (์ด๋ฏธ ๋˜์–ด์žˆ์œผ๋ฉด ํŒจ์Šค)
2. manifest.json ์ž‘์„ฑ
3. ๊ธฐ๋ณธ Service Worker ์ถ”๊ฐ€
4. Lighthouse๋กœ ์ ์ˆ˜ ํ™•์ธ

์ด 4๋‹จ๊ณ„๋งŒ ํ•ด๋„ ๊ธฐ๋ณธ์ ์ธ PWA๊ฐ€ ์™„์„ฑ๋ผ!
Vite๋‚˜ Next.js ์“ฐ๊ณ  ์žˆ๋‹ค๋ฉด ํ”Œ๋Ÿฌ๊ทธ์ธ ํ•˜๋‚˜๋กœ ๋” ์‰ฝ๊ฒŒ ๊ฐ€๋Šฅํ•จ ใ…‹ใ…‹
"์›น์€ ์ด๋ฏธ ์•ฑ์ด๋‹ค. PWA๋Š” ๊ทธ ์‚ฌ์‹ค์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ œ๋Œ€๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ๋ฒ•์ผ ๋ฟ์ด๋‹ค."

PWA๋Š” ํŠนํžˆ ์ฝ˜ํ…์ธ  ์ค‘์‹ฌ ์„œ๋น„์Šค, ์ด์ปค๋จธ์Šค, ๋‰ด์Šค/๋ฏธ๋””์–ด, ๊ธฐ์—… ๋‚ด๋ถ€ ๋„๊ตฌ์—์„œ ์—„์ฒญ๋‚œ ํšจ๊ณผ๋ฅผ ๋ฐœํœ˜ํ•ด.
์•ฑ์Šคํ† ์–ด ์‹ฌ์‚ฌ ์—†์ด ์ฆ‰์‹œ ๋ฐฐํฌํ•˜๊ณ , SEO๋„ ๋˜๊ณ , ์˜คํ”„๋ผ์ธ๋„ ๋˜๊ณ , ์„ค์น˜๋„ ๋˜๋Š” โ€” ์ด๊ฒŒ PWA์˜ ์ง„์งœ ๋งค๋ ฅ์ด์•ผ.
์ด์ œ PWA ๋ชจ๋ฅด๋ฉด ๋ชจ๋ฐ”์ผ ์›น ๊ฐœ๋ฐœ์ž๋ผ๊ณ  ํ•˜๊ธฐ ์ข€ ๋ฏผ๋งํ•œ ์‹œ๋Œ€๊ฐ€ ๋์Œ ใ…‹ใ…‹ใ…‹ ์ง€๊ธˆ ๋ฐ”๋กœ ์‹œ์ž‘ํ•ด๋ด! ๐Ÿ’ช

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.