์บ ํ์กฑ์ ์ํ ๋ค๋ฅธ ์ฌ์ดํธ๋ค๊ณผ ์บ ํํ ์ ์ฐจ๋ณ์ ์ ๋ฌด์์ผ๊น? ๋ฅผ ์๊ฐํด๋ณด์๋ค.
์ฌ์ฉ์์ ์์น๋ฅผ ๊ธฐ๋ฐ ์ผ๋ก ํ๋ฏ๋ก ์ฌ์ฉ์๊ฐ ์ฝ๊ฒ ์ ๊ทผํ ์ ์์ด์ผ ํ๊ณ ,
์ฃผ๋ณ ์ฌ์ฉ์์ ์ํต์ ์ํํ๊ฒ ์ ์งํ๊ธฐ ์ํด ์คํ๋ผ์ธ์์๋ ๊ธฐ๋ฅ ์ ์ง ๊ฐ ๊ฐ๋ฅํด์ผ ํ๋ค.
์ด๋ฐ ์๊ตฌ ์ฌํญ๋ค์ ์ถฉ์กฑ์ํค๊ธฐ ์ํด PWA( Progressive Web App ) ๊ธฐ์ ์ ๋์ ํ๊ธฐ๋ก ํ๋ค.
PWA ( Progressive Web App )
PWA๋ ๋ค์ดํฐ๋ธ ์ฑ๊ณผ ์ ์ฌํ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ ์น ์ฑ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ํน์ง์ด ์๋ค.
๐ PWA( Progressive Web App ) ํน์ง
- ์คํ๋ผ์ธ ์ง์( ๋คํธ์ํฌ๊ฐ ์๋ ํ๊ฒฝ์์ ๊ธฐ์กด ๋ฐ์ดํฐ ์ ์ง ๊ฐ๋ฅ )
- ๋ชจ๋ฐ์ผ ํ ํ๋ฉด์ ์ถ๊ฐ ๊ฐ๋ฅ( ๋ณ๋์ ์ฑ ์ค์น ์์ด ์ ๊ทผ ๊ฐ๋ฅ )
- ๊ธฐ์กด ์น ๋ณด๋ค ๋น ๋ฅธ ๋ก๋ฉ ์๋
์์ ๊ฐ์ PWA์ ํน์ง์ ์บ ํํ์ด ๋ชจ๋ฐ์ผ์์ ์คํํ ๋ ์ฆ์ ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ณ ,
์คํ๋ผ์ธ์์ ์ฑํ
๋ฑ์ ๋ํ ํธ์ ์๋ฆผ์ ์ ๊ณตํ ์ ์์ ๊ฒ์ด๋ผ๋ ๋ฐ์ ์ ํฉํ๋ค๊ณ ์๊ฐ์ด ๋ค์๋ค.
PWA
๊ตฌํํ๊ธฐmanifest.json
์ค์ ์ฑ์ฒ๋ผ ๋์ํ๊ธฐ ์ํด ์ฑ ์ด๋ฆ, ์์ด์ฝ, ์์ URL ๋ฑ์ ์ค์ ํด ์ค๋ค.
// src/app/manifest.json
{
"name": "์บ ํํ",
"short_name": "์บ ํํ",
// PWA ์ฑ ์์ด์ฝ ์ค์ ( ๋ค์ํ ํด์๋ ์ง์ )
"icons": [
{
"sizes": "128x128",
"src": "/images/maskable_icon_x128.png",
"type": "image/png"
},
{
"sizes": "192x192",
"src": "/images/maskable_icon_x192.png",
"type": "image/png"
},
{
"sizes": "512x512",
"src": "/images/maskable_icon_x512.png",
"type": "image/png"
}
],
// ํน์ ํ์ด์ง๋ก ๋ฐ๋ก ์ด๋ํ๋ ๋จ์ถ ์ค์
"shortcuts": [
{
"name": "list",
"url": "/list"
},
// ...codes
],
// ์ฑ์ด ์คํ๋ ๋ ์์๋๋ url
"start_url": "/",
// standalone์ ์ค์ ํ์ฌ ๋ค์ดํฐ๋ธ ์ฑ์ฒ๋ผ ์คํ๋๋๋ก ๊ตฌ์ฑ
"display": "standalone",
}
next-pwa
๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ์คํ๋ผ์ธ ํ๊ฒฝ์์๋ ์ฑ์ด ๋์ํ ์ ์๋๋ก ์ค์ ํ๊ธฐ ์ํด์๋ ์๋น์ค ์์ปค๋ฅผ ์ค์ ํด์ผ ํ๋ค.
์ด๋ฅผ ์ํด์ next-pwa
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ๋ค.
npm install @ducanh2912/next-pwa
// next.config.mjs
const nextConfigFunction = async (phase) => {
if (phase === PHASE_DEVELOPMENT_SERVER || phase === PHASE_PRODUCTION_BUILD) {
const withPWAPlugin = (await import('@ducanh2912/next-pwa')).default({
// PWA ๊ด๋ จ ํ์ผ public ํด๋์ ์ ์ฅ
dest: 'public',
// ์บ์ํ ํ์ผ ํฌ๊ธฐ ์ ํ
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024
});
return withPWAPlugin(nextConfig);
}
return nextConfig;
};
PWA
์ฌ๋ถ ํ์ธํ๊ธฐ์ฌ์ฉ์๊ฐ PWA๋ก ๋ฐฉ๋ฌธํ๋ ์ง, ์ผ๋ฐ ์น์ผ๋ก ๋ฐฉ๋ฌธํ๋ ์ง๋ฅผ ์ฒดํฌํด์ผ ํ๋ค.
์ด๋ฅผ ์ํด display-mode
๋ฅผ ์ฒดํฌํ๋ ํจ์๋ฅผ ๋ง๋ค์๋ค.
export const isPwa = () => {
if (typeof window !== 'undefined') {
return window.matchMedia('(display-mode: standalone)').matches;
}
return false;
};
window.matchMedia('(display-mode: standalone)').matches
๋ฅผ ํตํด
PWA๋ก ์คํ ์ค์ธ ๊ฒฝ์ฐ true
๋ฐํ, ๋ธ๋ผ์ฐ์ ์์ ์คํ ์ค์ธ ๊ฒฝ์ฐ false
๋ฅผ ๋ฐํํ๋ค.
PWA์์ ์คํ ์ค์ธ ๊ฒฝ์ฐ ์ค์น ๋ฒํผ์ ์จ๊ธด๋ค.
PWA
์ค์น ์๋ดํ๊ธฐPWA๋ ์ฌ์ฉ์๊ฐ ์ง์ ํ ํ๋ฉด์ ์ถ๊ฐํด์ผ ์ค์น ๊ฐ ๋๊ธฐ ๋๋ฌธ์ ์ด์ ๋ํ ์๋ด๊ฐ ํ์ํ๋ค.
์ด ์๋ด๋ฅผ ์ํด beforeinstallprompt
์ด๋ฒคํธ๋ฅผ ํ์ฉํ๋ค.
// custom Hook: usePwaPrompt.ts
import { usePwaStore } from '@/stores/pwaState';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
export const usePwaPrompt = () => {
const {
deferredPrompt,
setDeferredPrompt,
isPwaOpen,
setIsPwaOpen,
setClicked,
} = usePwaStore();
useEffect(() => {
const handleBeforeInstallPrompt = (e: BeforeInstallPromptEvent) => {
// ์๋ ์คํ ๋ฐฉ์ง
e.preventDefault();
setDeferredPrompt(e);
setIsPwaOpen(false);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
return () => {
window.removeEventListener(
'beforeinstallprompt',
handleBeforeInstallPrompt
);
};
}, []);
// ์ค์น ๋ฒํผ์ ๋๋ฅด๋ฉด ์คํ๋ ํจ์
const installPwa = async () => {
// ์ค์น ๋ฒํผ์ ๋๋ ์ ๊ฒฝ์ฐ ์ค์น ์๋ด ๋ชจ๋ฌ์ ๋์ด๋ค.
setClicked('install');
// iOS๋ beforeinstallprompt ์ด๋ฒคํธ๋ฅผ ์ง์ํ์ง ์๋๋ค.
// iOS๋ 'pwa-unsupported' ์ํ์ ๋ชจ๋ฌ์ ๋์ 'ํ ํ๋ฉด์ ์ถ๊ฐ' ๊ธฐ๋ฅ์ ์๋ดํ๋ค.
if (!deferredPrompt) {
setIsPwaOpen(true, 'pwa-unsupported'); // iOS: ํ ํ๋ฉด ์ถ๊ฐ ์๋ด ๋ชจ๋ฌ ํ์
} else {
setIsPwaOpen(true, 'pwa-supported'); // Android: PWA ์ค์น ํ์ธ ๋ฐ ์ค์น ํ๋ก์ธ์ค ์งํ
}
};
// ์ค์น ์๋ด ๋ชจ๋ฌ์์ ์ฑ ์ค์น ์๋ด๋ฅผ ํ๊ณ ์ค์น/์ทจ์๋ฅผ ์ ํํ ์ ์๋ค.
// ์ค์น๋ฅผ ์ ํํ ๊ฒฝ์ฐ PWA ์ค์น๋ฅผ ์งํํ๋ค. toast ์๋ฆผ์ ํตํด ์ฌ์ฉ์์๊ฒ ๊ฒฐ๊ณผ๋ฅผ ์๋ดํ๋ค.
const handleInstall = async () => {
if (!deferredPrompt) return;
setIsPwaOpen(false);
deferredPrompt.prompt();
deferredPrompt.userChoice.then((result) => {
if (result.outcome === 'accepted') {
toast.success('PWA๊ฐ ์ค์น๋์์ต๋๋ค! ๐');
} else {
toast.error('์ค์น๊ฐ ์ทจ์๋์์ต๋๋ค.');
}
setDeferredPrompt(null);
});
};
const handleClose = async () => {
setIsPwaOpen(false);
};
return {
deferredPrompt,
setDeferredPrompt,
isPwaOpen,
installPwa,
handleInstall,
handleClose,
};
};
iOS
Android