프로그레시브 웹 앱(PWA)이란, 인앱 설치를 묻는 화면 구현하기 (feat.beforeinstallprompt)
이전 게시글이 선행되면 좋습니다
프로그레시브 웹 앱(PWA)이란
프로그레시브 웹 앱은 웹 기술을 사용하여 개발된 애플리케이션으로, 사용자가 웹 브라우저에서 접속할 수 있으며 모바일 앱과 비슷한 사용자 경험을 제공합니다. 이러한 웹 앱은 오프라인에서도 작동할 수 있으며, 푸시 알림을 받을 수 있고, 홈 화면에 아이콘을 추가하여 쉽게 접근할 수 있습니다. 프로그레시브 웹 앱은 기기 또는 운영체제에 구애받지 않고 모든 플랫폼에서 동작할 수 있습니다.
PWA의 이점
- 스마트폰 사용자의 50%는 앱 다운로드를 원하지 않기 때문에 검색하거나 쇼핑할 때 회사의 모바일 사이트를 사용할 가능성이 더 큽니다
- 앱을 제거하는 가장 큰 이유 중 하나는 제한된 저장 공간입니다 (설치된 PWA는 일반적으로 1MB 미만 사용).
- 스마트폰 사용자는 제품에 대한 관련 추천을 제공하는 모바일 사이트에서 구매할 가능성이 더 높으며 스마트폰 사용자의 85%는 모바일 알림이 유용하다고 말합니다.
PWA를 충족시키는 조건
beforeinstallprompt 이벤트를 실행하고 브라우저 내 설치 프로모션을 표시하기 전에 다음 기준을 충족해야 합니다.
- 웹 앱이 아직 설치되지 않음
- HTTPS를 통해 제공
또한 다음을 포함하는 웹 앱 매니페스트가 있어야 합니다.
- short_name 또는 name
- icons - 192px 및 512px 아이콘을 포함해야 합니다.
- start_url
- display fullscreen , standalone 또는 minimal-ui 중 하나여야 합니다.
- prefer_related_applications이 존재하거나 false여서는 안 됩니다.
앱 설치를 유도하는 디자인 종류
보투게더에서 사용한 설치를 묻는 디자인
IOS에서 보이는 설치 화면
안드로이드에서 보이는 설치 화면
데스크톱 브라우저 상단에서 설치할 수 있는 버튼
PWA를 지원하는 브라우저 및 디바이스 종류
데스크톱에서:
- Firefox와 Safari는 어떤 데스크톱 운영 체제에서도 PWA를 설치하는 것을 지원하지 않습니다.
- Chrome과 Edge는 Linux, Windows, macOS 및 Chromebook에서 PWA를 설치하는 것을 지원합니다.
모바일에서:
- Android에서는 Firefox, Chrome, Edge, Opera 및 Samsung Internet Browser가 모두 PWA를 설치하는 것을 지원합니다.
- iOS 16.3 이전 버전에서는 PWA를 Safari만을 통해서만 설치할 수 있습니다.
- iOS 16.4 이후 버전에서는 PWA를 Safari, Chrome, Edge, Firefox 및 Orion의 공유 메뉴를 통해 설치할 수 있습니다.
홈 화면에 추가하는 것을 묻는 코드 예시
manifest.json 설정
- short_name 또는 name
- icons - 192px 및 512px 아이콘을 포함해야 합니다.
- start_url
- display fullscreen , standalone 또는 minimal-ui 중 하나여야 합니다.
- prefer_related_applications이 존재하거나 false여서는 안 됩니다.
{
"name": "VoTogether",
"short_name": "VoTogether",
"icons": [
{
"src": "./android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75",
"purpose": "any maskable"
},
{
"src": "./android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0",
"purpose": "any maskable"
},
{
"src": "./android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5",
"purpose": "any maskable"
},
{
"src": "./android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0",
"purpose": "any maskable"
},
{
"src": "./android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0",
"purpose": "any maskable"
},
{
"src": "./android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0",
"purpose": "any maskable"
},
{
"src": "/images/android-icon-512x512.png",
"type": "image/png",
"sizes": "512x512",
"density": "5.0",
"purpose": "any maskable"
}
],
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff"
}
구글의 LightHouse에서 PWA을 얼마나 충족하고 있는지 확인해볼수 있습니다
beforeinstallprompt 이벤트를 이용해서 홈 화면에서 설치하는 것을 물어볼 수 있습니다
beforeinstallprompt는 Progressive Web App (PWA)를 설치하기 전에 사용자에게 설치 프롬프트를 제공하기 위한 이벤트입니다. 이 이벤트는 웹 앱이 PWA로서 설치 가능한지 여부를 확인한 후, 설치할 것인지 묻는 팝업 창을 사용자에게 보여줄 수 있습니다. 사용자가 설치 프롬프트를 수락하면 매니페스트 파일에서 정의한 설치 프로세스가 시작되며, 웹 앱이 사용자의 기기에 설치됩니다.
beforeinstallprompt의 브라우저 호환성
beforeinstallprompt은 실험적인 기술이며, 특정 브라우저에서 예상대로 작동하지 않을 수 있습니다.
beforeinstallprompt은 IOS에서는 사용할 수 없습니다
예시 코드
- ios에서 사이트를 이용하고 있다면 safari 브라우저에서 책갈피를 통해 홈 화면에 추가하라고 알려야 하기에 조건문으로 컴포넌트가 보이도록 설정합니다
isDeviceIOS가 true라면 defaultBeforeInstallPromptEvent을 deferredPrompt의 초기값으로 설정하여 컴포넌트가 보이도록 합니다. 사용자가 프롬프트를 보고 닫기를 눌렀을 경우 로컬스토리지에 localStorage.setItem('iosInstalled', 'false');을 설정해주고 사용자가 페이지 이동 후 다시 보여질수도 있기 때문에 const isActive = JSON.parse(localStorage.getItem('iosInstalled') || 'true');와 같은 코드로 보이지 않도록 설정해줍니다.
const defaultBeforeInstallPromptEvent: BeforeInstallPromptEvent = {
platforms: [],
userChoice: Promise.resolve({ outcome: 'dismissed', platform: '' }),
prompt: () => Promise.resolve(),
preventDefault: () => {},
};
const isIOSPromptActive = () => {
const isActive = JSON.parse(localStorage.getItem('iosInstalled') || 'true');
if (isActive) {
return defaultBeforeInstallPromptEvent;
}
return null;
};
export default function AppInstallPrompt() {
const isDeviceIOS = /iPad|iPhone|iPod/.test(window.navigator.userAgent);
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(
isDeviceIOS ? isIOSPromptActive() : null
);
const handleCancelClick = () => {
localStorage.setItem('iosInstalled', 'false');
setDeferredPrompt(null);
};
- beforeinstallprompt 이벤트가 발생한다면 setDeferredPrompt에 beforeinstallprompt이벤트를 설정합니다.
const handleBeforeInstallPrompt = (event: BeforeInstallPromptEvent) => {
event.preventDefault();
setDeferredPrompt(event);
};
- deferredPrompt가 존재한다면 사용자에게 보여질 컴포넌트를 렌더해줍니다.
{deferredPrompt && (
<MobileInstallPrompt
handleInstallClick={handleInstallClick}
handleCancelClick={handleCancelClick}
platform={isDeviceIOS ? 'ios' : 'android'}
/>
)}
- 사용자가 선택했다면 setDeferredPrompt을 null로 바꿔줘서 사용자에게 보이지 않도록 합니다.
const handleInstallClick = () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(() => {
setDeferredPrompt(null);
});
}
};
전체 코드
import { Fragment, useEffect, useState } from 'react';
import { BeforeInstallPromptEvent } from '../../../../window';
import MobileInstallPrompt from './MobileInstallPrompt';
const defaultBeforeInstallPromptEvent: BeforeInstallPromptEvent = {
platforms: [],
userChoice: Promise.resolve({ outcome: 'dismissed', platform: '' }),
prompt: () => Promise.resolve(),
preventDefault: () => {},
};
const isIOSPromptActive = () => {
const isActive = JSON.parse(localStorage.getItem('iosInstalled') || 'true');
if (isActive) {
return defaultBeforeInstallPromptEvent;
}
return null;
};
export default function AppInstallPrompt() {
const isDeviceIOS = /iPad|iPhone|iPod/.test(window.navigator.userAgent);
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(
isDeviceIOS ? isIOSPromptActive() : null
);
const handleInstallClick = () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(() => {
setDeferredPrompt(null);
});
}
};
const handleCancelClick = () => {
localStorage.setItem('iosInstalled', 'false');
setDeferredPrompt(null);
};
const handleBeforeInstallPrompt = (event: BeforeInstallPromptEvent) => {
event.preventDefault();
setDeferredPrompt(event);
};
useEffect(() => {
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}, []);
return (
<Fragment>
{deferredPrompt && (
<MobileInstallPrompt
handleInstallClick={handleInstallClick}
handleCancelClick={handleCancelClick}
platform={isDeviceIOS ? 'ios' : 'android'}
/>
)}
</Fragment>
);
}
타입스크립트 beforeinstallprompt 이벤트 타입 적용
global로 선언해주어서 any가 아닌 beforeinstallprompt의 타입을 적용할 수 있습니다.
window.d.ts
export interface BeforeInstallPromptEvent {
readonly platforms: string[];
readonly userChoice: Promise<{
outcome: 'accepted' | 'dismissed';
platform: string;
}>;
preventDefault(): void;
prompt(): Promise<void>;
}
declare global {
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
}
}
참고 자료
https://web.dev/what-are-pwas/ (PWA란)
https://web.dev/drive-business-success/ (PWA의 이점)
https://web.dev/customize-install/ (자신만의 인앱 설치 경험을 제공하는 방법)
https://web.dev/promote-install/ (PWA 설치 촉진 디자인 패턴)
https://web.dev/install-criteria/ (PWA 설치가 가능하기 위한 조건)
https://web.dev/codelab-make-installable/ (PWA 설치 코드 예제)
https://kagrin97-blog.vercel.app/react/pwa-beforeInstallPrompt (PWA 설치 코드 예제 2)
https://stackoverflow.com/questions/51503754/typescript-type-beforeinstallpromptevent (beforeinstallprompt 이벤트 타입스크립트 적용하기)
https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable (지원하는 브라우저 종류)
https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent (beforeinstallprompt MDN)
https://wonsss.github.io/PWA/before-install-prompt/
보투게더 기술 블로그에서 작성한 글을 가져왔습니다