매니페스트로 앱 소개서를 채웠다면, 이제 사용자의 홈 화면에 실제로 아이콘을 심는 단계가 남았어요. PWA에서 이걸 설치(install)라고 불러요. 앱스토어에서 내려받는 그 설치랑 이름은 같은데, 과정은 훨씬 가벼워요. 오늘은 이 설치가 어떤 조건에서 일어나고, 우리가 직접 설치 버튼을 만들려면 어떤 코드가 필요한지 예제로 짚어볼게요. 자바스크립트 이벤트(event, 브라우저가 알려주는 사건) 하나만 잘 다루면 생각보다 간단해요.
31.1 설치가 정확히 뭔가요?
여기서 말하는 설치는 사용자가 우리 웹사이트를 홈 화면이나 앱 목록에 추가하는 걸 말해요. 설치하고 나면 아이콘이 생기고, 그걸 누르면 브라우저 주소창 없이 앱 창으로 바로 열려요. 무거운 파일을 내려받는 게 아니라, 이미 방문한 사이트를 앱 형태로 등록하는 것에 가까워요.
그래서 설치가 순식간이에요. 사용자 입장에선 "추가" 한 번 누르면 끝이고, 우리 입장에선 앱을 따로 빌드해 스토어에 올리는 수고가 없어요. 웹 주소 하나로 앱 배포가 끝나는 셈이죠.
31.2 설치되려면 뭐가 필요해요?
아무 사이트나 설치되는 건 아니에요. 브라우저가 설치할 자격을 따져요. 조건은 앞서 나온 것과 이어져요. 첫째, 유효한 매니페스트가 연결돼 있어야 해요. 그 안에 name(또는 short_name), icons, start_url, display가 제대로 채워져 있어야 하고요. 둘째, 서비스 워커가 등록돼 있어야 해요. 셋째, 사이트가 HTTPS(보안 연결)로 제공돼야 해요.
이 조건이 다 맞으면 크롬 같은 브라우저는 이 사이트를 설치 가능으로 판단하고, 주소창에 설치 아이콘을 띄우거나 화면 아래에 설치 안내를 보여줘요. 반대로 아이콘 크기가 부족하거나 서비스 워커가 없으면, 아무리 기다려도 설치 안내가 안 떠요. 설치가 안 될 때는 이 세 조건부터 점검하면 돼요.
31.3 설치 버튼은 언제 떠요?
브라우저가 "이 사이트 설치 가능하네"라고 판단하는 그 순간, 브라우저는 자바스크립트에게 beforeinstallprompt라는 이벤트를 보내줘요. 이름 그대로 "설치 안내를 띄우기 직전"이라는 신호예요. 우리는 이 신호를 받아서 원하는 대로 처리할 수 있어요.
let deferredPrompt;
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
deferredPrompt = e;
installBtn.style.display = "block";
});
한 줄씩 볼게요. e.preventDefault()는 브라우저가 기본 설치 안내를 지금 바로 띄우지 말라고 막는 거예요. 대신 이벤트 객체를 deferredPrompt라는 변수에 담아뒀어요. 나중에 우리가 원하는 타이밍에 쓰려고요. 마지막 줄에서는 숨겨뒀던 내 설치 버튼을 화면에 보이게 했어요. 결과적으로 브라우저 기본 안내 대신 우리가 디자인한 버튼이 뜨는 거죠.
31.4 내 버튼으로 설치하려면요?
이제 사용자가 우리가 띄운 버튼을 눌렀을 때 진짜 설치 안내를 부르는 코드가 필요해요. 아까 담아둔 deferredPrompt가 여기서 쓰여요.
installBtn.addEventListener("click", async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
deferredPrompt = null;
});
버튼을 누르면 deferredPrompt.prompt()가 실행되면서 브라우저의 설치 창이 그제야 떠요. "홈 화면에 추가하시겠어요?" 같은 창이죠. 사용자가 수락했는지 거절했는지는 userChoice로 알 수 있는데, outcome 값이 "accepted"면 수락, "dismissed"면 거절이에요. 한 번 쓴 프롬프트는 재사용할 수 없어서 deferredPrompt를 null로 비워둬요. 결과적으로 사용자가 버튼을 누른 순간에만, 그것도 우리가 정한 자리에서 설치 창이 뜨게 되는 거예요.
여기서 왜 굳이 deferredPrompt에 담아두는 번거로운 짓을 하는지 궁금할 수 있어요. 브라우저 규칙상 prompt()는 사용자가 직접 무언가를 클릭한 순간에만 부를 수 있게 막혀 있거든요. 페이지가 열리자마자 마음대로 설치 창을 띄우면 사용자 입장에서 짜증 나잖아요. 그래서 신호는 미리 받아두고(담아두기), 실제 창은 버튼 클릭 때 부르는 이 두 단계로 나누는 거예요. 흐름만 익히면 어렵지 않아요.
31.5 설치했는지 알 수 있어요?
사용자가 실제로 설치를 끝냈는지도 이벤트로 알 수 있어요. 설치가 완료되면 브라우저가 appinstalled라는 이벤트를 보내주거든요.
window.addEventListener("appinstalled", () => {
installBtn.style.display = "none";
console.log("설치 완료");
});
이 신호가 오면 이미 설치가 끝난 거니까, 굳이 설치 버튼을 계속 보여줄 이유가 없어요. 그래서 버튼을 숨기는 처리를 넣었어요. 이렇게 하면 이미 설치한 사용자에게 자꾸 설치를 권하는 어색함을 피할 수 있어요. 실무에서는 이 시점에 간단한 환영 안내를 띄우기도 해요.
이미 앱으로 열린 상태인지 미리 알고 싶을 때도 있어요. 그럴 땐 화면이 standalone 모드로 떠 있는지 확인하면 돼요.
const isApp = window.matchMedia("(display-mode: standalone)").matches;
if (isApp) {
installBtn.style.display = "none";
}
matchMedia로 display-mode: standalone인지 물어보면, 지금 화면이 설치된 앱 창으로 열렸는지 일반 브라우저 탭으로 열렸는지를 참과 거짓으로 알려줘요. 앱으로 열렸다면 설치 버튼을 처음부터 숨기면 되겠죠. 이렇게 앞의 appinstalled 이벤트와 함께 쓰면, 이미 설치한 사용자에게는 어떤 경우에도 설치 버튼이 안 보이게 깔끔하게 정리할 수 있어요.
31.6 아이폰에서도 되나요?
여기서 현실적인 이야기를 하나 해야 해요. 지금까지 본 beforeinstallprompt 이벤트는 크롬 계열(안드로이드, 데스크톱)에서 잘 동작해요. 그런데 아이폰 사파리는 이 이벤트를 지원하지 않아요. 그래서 아이폰에서는 우리가 만든 설치 버튼이 애초에 뜨지 않아요.
대신 아이폰에는 수동 설치 길이 있어요. 사파리 아래쪽 공유 버튼을 누르고 홈 화면에 추가를 고르면 우리 PWA가 아이콘으로 깔려요. 자동 버튼이 없으니, 아이폰 사용자에게는 화면에 "공유 버튼을 눌러 홈 화면에 추가해 주세요" 같은 안내 문구를 대신 보여주는 방식으로 처리해요. 기기를 구분하려면 사용자 환경 정보를 살펴 아이폰일 때만 이 안내를 띄우면 돼요. 브라우저마다 지원이 다르다는 걸 감안해 설계하는 게 실무의 포인트예요.
31.7 오늘 정리
오늘은 PWA의 마지막 관문인 홈 화면 설치를 다뤘어요. 설치는 사이트를 앱 아이콘으로 등록하는 가벼운 과정이고, 매니페스트, 서비스 워커, HTTPS 세 조건이 맞아야 브라우저가 설치를 허락했죠. 자동 설치의 핵심은 beforeinstallprompt 이벤트를 받아 deferredPrompt에 담아뒀다가, 사용자가 버튼을 누르면 prompt()로 설치 창을 띄우는 흐름이었어요. 설치가 끝나면 appinstalled 이벤트로 버튼을 정리하고요. 아이폰은 공유 버튼으로 홈 화면에 추가하는 수동 방식이라는 것까지 챙기면, 여러분의 웹사이트가 사용자 폰 홈 화면에 아이콘으로 자리 잡는 그림이 완성돼요.