이 Codelab은 Google Developers 교육팀에서 개발한 프로그레시브 웹 앱 개발 교육 과정의 일부입니다. Codelab을 순서대로 진행하면 이 과정의 학습 효과를 극대화할 수 있습니다.
과정에 관한 자세한 내용은 프로그레시브 웹 앱 개발 개요를 참고하세요.
소개
이 실습에서는 Lighthouse를 사용하여 프로그레시브 웹 앱 (PWA) 표준에 따라 웹사이트를 감사합니다. 서비스 워커 API를 사용하여 오프라인 기능도 추가합니다.
학습할 내용
- Lighthouse로 사이트를 감사하는 방법
- 애플리케이션에 오프라인 기능을 추가하는 방법
유의해야 할 사항
- 기본 HTML, CSS, JavaScript
- ES2015 Promise에 대한 지식
필요한 항목
- 터미널/셸 액세스가 가능한 컴퓨터
- 인터넷 연결
- Chrome 브라우저 (Lighthouse 사용)
- 텍스트 편집기
- 선택사항: Android 기기의 Chrome
GitHub에서 pwa-training-labs 저장소를 다운로드하거나 클론하고 필요한 경우 LTS 버전의 Node.js를 설치합니다.
offline-quickstart-lab/app/ 디렉터리로 이동하여 로컬 개발 서버를 시작합니다.
cd offline-quickstart-lab/app npm install node server.js
Ctrl-c을 사용하여 언제든지 서버를 종료할 수 있습니다.
브라우저를 열고 localhost:8081/으로 이동합니다. 사이트가 간단한 정적 웹페이지임을 확인할 수 있습니다.
참고: 서비스 워커가 실습을 방해하지 않도록 서비스 워커를 등록 취소하고 localhost의 모든 서비스 워커 캐시를 지우세요. Chrome DevTools에서는 Application(애플리케이션) 탭의 Clear storage(저장소 지우기) 섹션에서 Clear site data(사이트 데이터 지우기)를 클릭하여 이를 달성할 수 있습니다.
선호하는 텍스트 편집기에서 offline-quickstart-lab/app/ 폴더를 엽니다. app/ 폴더에서 실습을 빌드합니다.
이 폴더에는 다음이 포함됩니다.
images/폴더에 샘플 이미지가 포함되어 있습니다.styles/main.css은 기본 스타일시트입니다.index.html은 샘플 사이트의 기본 HTML 페이지입니다.package-lock.json및package.json은 앱 종속 항목을 추적합니다 (이 경우 유일한 종속 항목은 로컬 개발 서버용임).server.js는 테스트를 위한 로컬 개발 서버입니다.service-worker.js은 서비스 워커 파일입니다 (현재 비어 있음).
사이트를 변경하기 전에 Lighthouse로 감사하여 개선할 수 있는 부분을 확인해 보겠습니다.
Chrome에서 앱으로 돌아가 개발자 도구의 감사 탭을 엽니다. Lighthouse 아이콘과 구성 옵션이 표시됩니다. 기기에서 '모바일'을 선택하고, 감사를 모두 선택하고, 제한 옵션 중 하나를 선택하고, 저장소 지우기를 선택합니다.

감사 실행을 클릭합니다. 감사가 완료되는 데 잠시 시간이 걸립니다.
설명
감사가 완료되면 개발자 도구에 점수가 포함된 보고서가 표시됩니다. 다음과 같이 점수가 표시됩니다 (점수는 정확히 동일하지 않을 수 있음).
참고: Lighthouse 점수는 근사치이며 환경 (예: 브라우저 창을 많이 열어 놓은 경우)의 영향을 받을 수 있습니다. 점수는 여기에 표시된 점수와 정확히 동일하지 않을 수 있습니다.

프로그레시브 웹 앱 섹션은 다음과 유사하게 표시되어야 합니다.

보고서에는 다음 5가지 카테고리의 점수와 측정항목이 표시됩니다.
- 프로그레시브 웹 앱
- 성능
- 접근성
- 권장사항
- 검색엔진 최적화
보시다시피 앱이 프로그레시브 웹 앱 (PWA) 카테고리에서 낮은 점수를 받았습니다. 점수를 높여 봅시다.
보고서의 PWA 섹션을 잠시 살펴보고 누락된 항목이 있는지 확인하세요.
서비스 워커 등록
보고서에 나열된 실패 중 하나는 등록된 서비스 워커가 없다는 것입니다. 현재 app/service-worker.js에 빈 서비스 워커 파일이 있습니다.
닫는 </body> 태그 바로 앞의 index.html 하단에 다음 스크립트를 추가합니다.
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('service-worker.js')
.then(reg => {
console.log('Service worker registered! 😎', reg);
})
.catch(err => {
console.log('😥 Service worker registration failed: ', err);
});
});
}
</script>설명
이 코드는 페이지가 로드되면 빈 service-worker.js 서비스 워커 파일을 등록합니다. 하지만 현재 서비스 워커 파일은 비어 있으며 아무 작업도 실행하지 않습니다. 다음 단계에서 서비스 코드를 추가해 보겠습니다.
리소스 사전 캐시
보고서에 나열된 또 다른 실패는 앱이 오프라인일 때 200 상태 코드로 응답하지 않는다는 것입니다. 이 문제를 해결하려면 서비스 워커를 업데이트해야 합니다.
서비스 워커 파일 (service-worker.js)에 다음 코드를 추가합니다.
const cacheName = 'cache-v1';
const precacheResources = [
'/',
'index.html',
'styles/main.css',
'images/space1.jpg',
'images/space2.jpg',
'images/space3.jpg'
];
self.addEventListener('install', event => {
console.log('Service worker install event!');
event.waitUntil(
caches.open(cacheName)
.then(cache => {
return cache.addAll(precacheResources);
})
);
});
self.addEventListener('activate', event => {
console.log('Service worker activate event!');
});
self.addEventListener('fetch', event => {
console.log('Fetch intercepted for:', event.request.url);
event.respondWith(caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request);
})
);
});이제 브라우저로 돌아가 사이트를 새로고침합니다. 콘솔에서 서비스 워커가 다음을 충족하는지 확인합니다.
- 등록됨
- 설치됨
- 활성화됨
참고: 이전에 서비스 워커를 등록했거나 모든 이벤트가 실행되지 않는 문제가 있는 경우 서비스 워커를 등록 해제하고 페이지를 새로고침하세요. 그래도 안 되면 앱의 모든 인스턴스를 닫고 다시 엽니다.
그런 다음 명령줄에서 Ctrl + c를 실행하여 로컬 개발 서버를 종료합니다. 사이트를 다시 새로고침하면 서버가 오프라인 상태임에도 불구하고 사이트가 로드됩니다.
참고: 서비스 워커를 가져올 수 없음을 나타내는 콘솔 오류가 표시될 수 있습니다(An unknown error occurred when fetching the script. service-worker.js Failed to load resource: net::ERR_CONNECTION_REFUSED). 이 오류는 브라우저가 서비스 워커 스크립트를 가져올 수 없기 때문에 (사이트가 오프라인 상태이므로) 표시되지만, 서비스 워커를 사용하여 자체를 캐시할 수 없으므로 예상되는 동작입니다. 그렇지 않으면 사용자의 브라우저가 동일한 서비스 워커에 영원히 갇히게 됩니다.
설명
서비스 워커가 index.html의 등록 스크립트에 의해 등록되면 서비스 워커 install 이벤트가 발생합니다. 이 이벤트 중에 install 이벤트 리스너가 명명된 캐시를 열고 cache.addAll 메서드로 지정된 파일을 캐시합니다. 이는 일반적으로 사용자가 사이트를 처음 방문할 때 발생하는 install 이벤트 중에 발생하므로 '사전 캐싱'이라고 합니다.
서비스 워커가 설치된 후 현재 페이지를 제어하는 다른 서비스 워커가 없으면 새 서비스 워커가 '활성화'되고(서비스 워커에서 activate 이벤트 리스너가 트리거됨) 페이지 제어를 시작합니다.
활성화된 서비스 워커가 제어하는 페이지에서 리소스를 요청하면 요청이 네트워크 프록시처럼 서비스 워커를 통과합니다. 각 요청에 대해 fetch 이벤트가 트리거됩니다. 서비스 워커에서 fetch 이벤트 리스너는 캐시를 검색하고 캐시된 리소스가 있으면 이를 사용하여 응답합니다. 리소스가 캐시되지 않으면 리소스가 정상적으로 요청됩니다.
리소스 캐싱을 통해 앱은 네트워크 요청을 방지하여 오프라인으로 작동할 수 있습니다. 이제 앱이 오프라인일 때 200 상태 코드로 응답할 수 있습니다.
참고: 이 예에서는 로그인 외에는 활성화 이벤트가 사용되지 않습니다. 이 이벤트는 서비스 워커 수명 주기 문제를 디버그하는 데 도움이 되도록 포함되었습니다.
선택사항: 개발자 도구의 애플리케이션 탭에서 캐시 저장소 섹션을 펼쳐 캐시된 리소스를 확인할 수도 있습니다.

node server.js를 사용하여 개발 서버를 다시 시작하고 사이트를 새로고침합니다. 그런 다음 개발자 도구에서 감사 탭을 다시 열고 새 감사 (왼쪽 상단의 더하기 기호)를 선택하여 Lighthouse 감사를 다시 실행합니다. 감사가 완료되면 PWA 점수가 크게 개선되었지만 여전히 개선할 수 있음을 알 수 있습니다. 다음 섹션에서 점수를 계속 개선해 보겠습니다.
참고: 웹 앱 설치 배너 테스트는 실습 범위에 포함되지 않으므로 이 섹션은 선택사항입니다. 원격 디버깅을 사용하여 직접 시도해 볼 수 있습니다.
PWA 점수가 아직 좋지 않습니다. 보고서에 나열된 나머지 실패 중 일부는 사용자에게 웹 앱 설치 메시지가 표시되지 않고 주소 표시줄에 스플래시 화면이나 브랜드 색상이 구성되지 않았다는 것입니다. 추가 기준을 충족하여 이러한 문제를 해결하고 홈 화면에 추가를 점진적으로 구현할 수 있습니다. 가장 중요한 것은 매니페스트 파일을 만드는 것입니다.
매니페스트 파일 만들기
app/에 manifest.json라는 파일을 만들고 다음 코드를 추가합니다.
{
"name": "Space Missions",
"short_name": "Space Missions",
"lang": "en-US",
"start_url": "/index.html",
"display": "standalone",
"theme_color": "#FF9800",
"background_color": "#FF9800",
"icons": [
{
"src": "images/touch/icon-128x128.png",
"sizes": "128x128"
},
{
"src": "images/touch/icon-192x192.png",
"sizes": "192x192"
},
{
"src": "images/touch/icon-256x256.png",
"sizes": "256x256"
},
{
"src": "images/touch/icon-384x384.png",
"sizes": "384x384"
},
{
"src": "images/touch/icon-512x512.png",
"sizes": "512x512"
}
]
}매니페스트에서 참조된 이미지가 앱에 이미 제공되어 있습니다.
그런 다음 index.html의 <head> 태그 하단에 다음 HTML을 추가합니다.
<link rel="manifest" href="manifest.json">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Space Missions">
<meta name="apple-mobile-web-app-title" content="Space Missions">
<meta name="theme-color" content="#FF9800">
<meta name="msapplication-navbutton-color" content="#FF9800">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-starturl" content="/index.html">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="icon" sizes="192x192" href="icon-192x192.png">
<link rel="apple-touch-icon" sizes="192x192" href="/images/touch/icon-192x192.png">
<link rel="icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="apple-touch-icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="apple-touch-icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="icon" sizes="512x512" href="/images/touch/icon-512x512.png">
<link rel="apple-touch-icon" sizes="512x512" href="/images/touch/icon-512x512.png">사이트로 돌아갑니다. 개발자 도구의 애플리케이션 탭에서 스토리지 지우기 섹션을 선택하고 사이트 데이터 지우기를 클릭합니다. 그런 다음 페이지를 새로고침합니다. 이제 매니페스트 섹션을 선택합니다. manifest.json 파일에 구성된 아이콘과 구성 옵션이 표시됩니다. 변경사항이 표시되지 않으면 시크릿 창에서 사이트를 열고 다시 확인하세요.
설명
manifest.json 파일은 브라우저 크롬, 홈 화면 아이콘, 스플래시 화면과 같은 앱의 일부 프로그레시브 측면을 스타일 지정하고 형식을 지정하는 방법을 브라우저에 알려줍니다. 또한 네이티브 앱처럼 브라우저 외부에서 standalone 모드로 열리도록 웹 앱을 구성하는 데 사용할 수도 있습니다.
이 글을 작성하는 시점에는 일부 브라우저에서 아직 지원이 개발 중이며 <meta> 태그는 아직 완전히 지원되지 않는 특정 브라우저에 대해 이러한 기능의 하위 집합을 구성합니다.
index.html의 이전 캐시 버전을 삭제하기 위해 사이트 데이터를 삭제해야 했습니다 (해당 버전에는 매니페스트 링크가 없었기 때문). 다른 Lighthouse 감사를 실행하여 PWA 점수가 얼마나 개선되었는지 확인해 보세요.
설치 메시지 활성화
앱을 설치하는 다음 단계는 사용자에게 설치 메시지를 표시하는 것입니다. Chrome 67에서는 사용자에게 자동으로 메시지가 표시되었지만 Chrome 68부터 사용자 동작에 따라 프로그래매틱 방식으로 설치 메시지가 활성화되어야 합니다.
다음 코드를 사용하여 index.html 상단 (<main> 태그 바로 뒤)에 '앱 설치' 버튼과 배너를 추가합니다.
<section id="installBanner" class="banner">
<button id="installBtn">Install app</button>
</section>그런 다음 styles/main.css에 다음 스타일을 추가하여 배너의 스타일을 지정합니다.
.banner {
align-content: center;
display: none;
justify-content: center;
width: 100%;
}파일을 저장합니다. 마지막으로 index.html에 다음 스크립트 태그를 추가합니다.
<script>
let deferredPrompt;
window.addEventListener('beforeinstallprompt', event => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
event.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = event;
// Attach the install prompt to a user gesture
document.querySelector('#installBtn').addEventListener('click', event => {
// Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice
.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
// Update UI notify the user they can add to home screen
document.querySelector('#installBanner').style.display = 'flex';
});
</script>파일을 저장합니다. 원격 디버깅을 사용하여 Android 기기의 Chrome에서 앱을 엽니다. 페이지가 로드되면 '앱 설치' 버튼이 표시됩니다 (데스크톱에서는 표시되지 않으므로 휴대기기에서 테스트해야 함). 버튼을 클릭하면 홈 화면에 추가 메시지가 표시됩니다. 단계에 따라 기기에 앱을 설치합니다. 설치 후 새로 생성된 홈 화면 아이콘을 탭하여 브라우저 외부에서 독립형 모드로 웹 앱을 열 수 있습니다.
설명
HTML 및 CSS 코드는 사용자가 설치 프롬프트를 활성화할 수 있도록 하는 데 사용할 수 있는 숨겨진 배너와 버튼을 추가합니다.
beforeinstallprompt 이벤트가 발생하면 Chrome 67 이하에서 사용자에게 자동으로 설치를 요청하는 기본 환경을 방지하고 전역 deferredPrompt 변수에서 beforeinstallevent을 캡처합니다. 그런 다음 beforeinstallevent의 prompt() 메서드로 프롬프트를 표시하도록 '앱 설치' 버튼이 구성됩니다. 사용자가 선택 (설치 여부)하면 userChoice 약속이 사용자의 선택 (outcome)으로 처리됩니다. 마지막으로 모든 준비가 완료되면 설치 버튼이 표시됩니다.
Lighthouse로 사이트를 감사하는 방법과 오프라인 기능의 기본사항을 구현하는 방법을 알아봤습니다. 선택사항 섹션을 완료했다면 홈 화면에 웹 앱을 설치하는 방법도 배웠을 것입니다.
추가 리소스
Lighthouse는 오픈소스입니다. 포크하고 자체 테스트를 추가하고 버그를 신고할 수 있습니다. Lighthouse는 빌드 프로세스와 통합하기 위한 명령줄 도구로도 사용할 수 있습니다.
PWA 교육 과정의 모든 Codelab을 확인하려면 과정의 환영 Codelab을 참고하세요.