nextjs 14버전에서 pwa 를 셋업하는 방법에 대해 알아보았습니다.
1. next-pwa 설치
yarn add next-pwa
yarn add -D webpack
2. next.config.mjs 수정
아래와 같이 수정합니다.
import withPWAInit from "next-pwa";
const withPWA = withPWAInit({
dest: "public",
});
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withPWA(nextConfig);
3. 퍼블릭에 manifest.json
/public 폴더에 아래와 같이 manifest.json 파일을 작성합니다.
{
"name": "My Next.js PWA",
"short_name": "NextPWA",
"description": "My awesome Next.js PWA!",
"icons": [
{
"src": "/test_icon.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/test_icon.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/",
"background_color": "#ffffff",
"theme_color": "#000000",
"display": "standalone"
}
4. layout.tsx
루트 layout.tsx 에 아래와 같이 viewport 와 metadata 를 설정해줍니다.
export const viewport: Viewport = {
themeColor: "black",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
viewportFit: "cover",
};
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
manifest: "/manifest.json",
icons: {
icon: "/test_icon.png",
shortcut: "/test_icon.png",
apple: "/test_icon.png",
other: {
rel: "apple-touch-icon-precomposed",
url: "/test_icon.png",
},
},
};
5. 설치 유도
public/sw.js
설치 유도를 하려면, service worker와 BeforeInstallPromptEvent를 사용해야 합니다.
https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent
먼저, public/sw.js 파일을 작성합니다.
// public/sw.js
import { clientsClaim } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
clientsClaim();
// self.__WB_MANIFEST is injected by workbox-build during the build process
precacheAndRoute(self.__WB_MANIFEST || []);
// Cache CSS, JS, and web worker requests with a network-first strategy.
registerRoute(
({ request }) => request.destination === 'style' || request.destination === 'script' || request.destination === 'worker',
new NetworkFirst({
cacheName: 'static-resources',
})
);
// Cache image files with a cache-first strategy.
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
}),
],
})
);
// Cache API calls with a network-first strategy.
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api',
networkTimeoutSeconds: 10,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
);
// Cache the start URL with a network-first strategy.
registerRoute(
'/',
new NetworkFirst({
cacheName: 'start-url',
plugins: [
{
cacheWillUpdate: async ({ request, response }) => {
if (response && response.type === 'opaqueredirect') {
return new Response(response.body, {
status: 200,
statusText: 'OK',
headers: response.headers,
});
}
return response;
},
},
],
})
);
// Cache everything else with a network-only strategy.
registerRoute(
({ request }) => true,
new CacheFirst({
cacheName: 'catch-all',
})
);
utils/isPWA.ts
export const isPWA = (): boolean => {
return (
window.matchMedia("(display-mode: standalone)").matches ||
(window.navigator as any).standalone === true
);
};
위 코드는 주소창 존재 여부를 판별해 줍니다.
그리하여 현재 앱이 pwa 모드로 작동되고있는지를 판별할 수 있습니다.
응용.useCheckPwa
유틸함수를 응용하여 아래처럼 훅을 만들 수 있을 것 같습니다.
import { useEffect, useState } from 'react';
const useCheckPwa = (): boolean => {
const [isPwa, setIsPwa] = useState(false);
useEffect(() => {
const checkPwa = (): boolean => {
return window.matchMedia('(display-mode: standalone)').matches
|| (window.navigator as any).standalone === true;
};
setIsPwa(checkPwa());
}, []);
return isPwa;
};
export default useCheckPwa;
버튼 컴포넌트
실험결과, 자동으로 판단해서 설치프롬프트를 띄워줄 수는 없습니다.
특히 모바일에서 사용자 상호작용이 없이는 안되더라고요.
그래서 아래처럼 버튼 컴포넌트로 만들 수 있습니다.
"use client";
import useCheckPwa from '@/hooks/useCheckPwa';
import { useEffect, useState } from 'react';
const InstallPromptHandler = () => {
const [deferredPrompt, setDeferredPrompt] = useState<Event | null>(null);
const isPwa = useCheckPwa();
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
};
window.addEventListener('beforeinstallprompt', handler as any);
return () => {
window.removeEventListener('beforeinstallprompt', handler as any);
};
}, []);
const handleInstallClick = () => {
if (deferredPrompt) {
(deferredPrompt as any).prompt();
(deferredPrompt as any).userChoice.then((choiceResult: any) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
setDeferredPrompt(null);
});
}
};
if (isPwa) {
return null;
}
if (!isPwa) {
return (
<button
onClick={handleInstallClick}
className="bg-blue-500 text-white px-4 py-2 rounded-md"
>
홈 화면에 추가하기
</button>
</>
)
}
};
export default InstallPromptHandler;
그런데 아직 모바일에서 프롬프트가 잘 안표시되는 증상이 있어서 개선이 필요합니다!
'nextjs' 카테고리의 다른 글
[240725 TIL] 다이나믹 헤더(middleware) (2) | 2024.07.25 |
---|---|
[240717 TIL] Next.js Sharp (0) | 2024.07.17 |
[240715 TIL] Supabase 인증인가 with NextJs 3편 (0) | 2024.07.15 |
[240714 TIL] Supabase 인증인가 with NextJs 2편 (0) | 2024.07.13 |
[240711 TIL] Supabase 인증인가 with NextJs #1 (1) | 2024.07.11 |