반응형
service-worker.ts
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
precacheAndRoute(self.__WB_MANIFEST);
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
({ request, url }: { request: Request; url: URL }) => {
if (request.mode !== 'navigate') {
return false;
}
if (url.pathname.startsWith('/_')) {
return false;
}
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
registerRoute(
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
serviceWorkerRegstration.ts
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
window.location.hostname === '[::1]' ||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
checkValidServiceWorker(swUrl, config);
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA'
);
});
} else {
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
console.log('Content is cached for offline use.');
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then((response) => {
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}
npm install react-device-detect
App.tsx
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import {isMobile } from 'react-device-detect';
function App() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const handleBeforeInstallPrompt = (event: any) => {
event.preventDefault();
setDeferredPrompt(event);
};
const handleInstallApp = () => {
if (isMobile && isIOS()) {
alert("iOS 기기에서는 지원하지 않습니다. iOS 기기에서는 '사파리 브라우저 > 옵션 > 홈 화면 추가' 버튼을 통해 설치해주세요.");
}
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult: { outcome: string; }) => {
if (choiceResult.outcome === "accepted") {
console.log("사용자가 앱 설치를 동의했습니다.");
} else {
console.log("사용자가 앱 설치를 동의하지 않았습니다.");
}
setDeferredPrompt(null);
});
}
}
const isIOS = () => {
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
console.log("This is an iOS device.");
return true;
} else {
console.log("This is NOT an iOS device!");
return false;
}
}
useEffect(() => {
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
return () => {
window.removeEventListener(
"beforeinstallprompt",
handleBeforeInstallPrompt
);
};
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<button onClick={handleInstallApp}>Install App</button>
</header>
</div>
);
}
export default App;
커스텀 훅으로 만들어 사용하기
import { useEffect, useState } from "react";
import {isMobile } from 'react-device-detect';
export const usePrompt = () => {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const handleBeforeInstallPrompt = (event: any) => {
event.preventDefault();
setDeferredPrompt(event);
};
const handleInstallApp = () => {
if (isMobile && isIOS()) {
alert("iOS 기기에서는 지원하지 않습니다. iOS 기기에서는 '사파리 브라우저 > 옵션 > 홈 화면 추가' 버튼을 통해 설치해주세요.");
}
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult: { outcome: string; }) => {
if (choiceResult.outcome === "accepted") {
console.log("사용자가 앱 설치를 동의했습니다.");
} else {
console.log("사용자가 앱 설치를 동의하지 않았습니다.");
}
setDeferredPrompt(null);
});
}
}
const isIOS = () => {
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
console.log("This is an iOS device.");
return true;
} else {
console.log("This is NOT an iOS device!");
return false;
}
}
useEffect(() => {
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
return () => {
window.removeEventListener(
"beforeinstallprompt",
handleBeforeInstallPrompt
);
};
}, []);
return {deferredPrompt, handleInstallApp};
}
참고
https://kagrin97-blog.vercel.app/react/pwa-beforeInstallPrompt
https://web.dev/articles/customize-install?hl=ko
https://velog.io/@jduckling_1024/PWA
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
반응형
'Reactjs' 카테고리의 다른 글
[React] dom-to-image 이용하여 스크린캡쳐 저장 기능 구현하기(feat. html2canvas와 비교) (0) | 2024.09.09 |
---|---|
[React] html2Canvas를 이용하여 화면 스크린샷 및 저장 기능 구현하기 (2) | 2024.09.08 |
[React] 리액트 Hooks - useEffect와 useLayoutEffect (0) | 2024.06.05 |
[React] 리액트 컴포넌트의 생명주기(Life Cycle) (0) | 2024.04.02 |