원문 : https://levelup.gitconnected.com/build-a-pwa-using-only-vanilla-javascript-bdf1eee6f37a
"점진적 웹 앱(PWA)"은 네이티브 앱과 같은 경험을 제공하기 위한 모던 웹 기술을 사용한 웹 애플리케이션이다. 이 앱들은 특정 요구 사항을 충족한다. 서버에 배포되고, URL을 통해서 접근 가능하고, 검색 엔진으로 색인된다.
PWA는 일반적인 앱처럼 동작하지만 많은 기능이 추가되었고 훨씬 덜 번거롭다. PWA는 빠르며 신뢰할 수 있고 오프라인 환경에서 완벽하게 동작한다.
PWA는 다음과 같은 것들로 사용자에게 풍부한 경험을 제공한다.
PWA는 데스크탑 브라우저, 모바일 기기, TV 화면 등 인터넷에 접속할 수 있고 브라우저를 지원하는 모든 제품에 맞게 만들 수 있다.
서비스 워커(Service Worker) 라는 기술을 사용해서 사용자의 환경에서 PWA를 즉시 로드할 수 있게 해준다. PWA는 애플리케이션을 위해 오프라인 지원을 제공할 수 있으며 사용자가 네트워크와 관련된 문제를 겪지 않는다.
사용자들은 PWA를 다운로드하기 위해 앱 스토어를 방문할 필요가 없다. PWA는 브라우저를 통해서 바로 설치할 수 있다. 대기 시간이 없기 때문에 굉장히 빠르고 네이티브 애플리케이션과 같은 시뮬레이션 환경을 제공한다.
개발자들은 매니페스트 파일(manifest file)로 수많은 기능들을 추가하고 가지고 놀 수 있다. 가장 잘 알려진 기능 중 하나는 PWA에서 활성화 된 푸시 알림을 사용하여 사용자들을 다시 참여시키는 것이다.
PWA는 당신의 친구 또는 동료에게 굉장히 쉽게 공유할 수 있다. 사용자가 웹 사이트 또는 앱 URL만 공유하면 된다. 사용자는 설치 가능한 apk를 공유하거나 수많은 파일을 다운로드한 후에 검증 과정을 거칠 필요가 없다. 간단하게 클릭만 하면 된다.
PWA에 대해서 자세히 알고 싶다면 Google Developers 사이트를 방문하면 된다.
이번 튜토리얼에서는 바닐라 자바스크립트를 사용하여 PWA를 구축해 볼 것이다. 하지만 이를 하기 위해 먼저 일반적인 웹 앱을 만들어야 한다. 진행하기 전에 우리가 만들 최종 UI와 구현할 기능들을 보자.
최종 UI 미리보기
UI는 여러 색상의 박스들이 중앙에 보여지고 박스들을 클릭하면 짧은 음원 클립이 재생될 것이다.
비슷하게 각 박스들은 서로 다른 뮤지컬 클립을 만든다. 이 웹 사이트의 컨셉은 여러 음원을 섞고 재생하면서 당신의 믹싱된 음원을 만드는 것이다.
이 프로젝트와 관련된 모든 파일은 여기에 있다.
튜토리얼을 따라하는 동안, 깃헙 저장소에서 사용 가능한 주요 에셋을 다운로드해야 할 것이다. 당신이 무언가를 만들고 변경 또는 수정하려고 한다면 튜토리얼이 끝난 다음에 하는 것을 추천한다.
이 프로젝트의 HTML은 굉장히 단순하다. 각 색상 패드를 보여주기 위한 디비전(<div>
)과 박스에서 재생될 오디오가 필요하다.
다음 코드 스니펫을 살펴보자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!--Style is imported but not created right now-->
<link rel="stylesheet" href="./style.css">
<title>MixCentro</title>
</head>
<body>
<div class="app">
<header>
<h1>MixCentro</h1>
<p>Make music with only one tap</p>
</header>
<div class="pads">
<div class="pad-top">
<div class="pad1">
<audio class="sound" src="./sounds/bubbles.mp3"></audio>
</div>
<div class="pad2">
<audio class="sound" src="./sounds/clay.mp3"></audio>
</div>
<div class="pad3">
<audio class="sound" src="./sounds/confetti.mp3"></audio>
</div>
</div>
<div class="pad-bottom">
<div class="pad4">
<audio class="sound" src="./sounds/glimmer.mp3"></audio>
</div>
<div class="pad5">
<audio class="sound" src="./sounds/moon.mp3"></audio>
</div>
<div class="pad6">
<audio class="sound" src="./sounds/ufo.mp3"></audio>
</div>
</div>
</div>
</div>
<!--Script is imported here but not used right now-->
<script src="./index.js"></script>
</body>
</html>
이 웹 사이트를 위해 선택한 이름은 MixCentro로, 타이틀과 헤더에 추가하는 것이다.
여기서 우리가 할 일은 타이틀(<title>
)과 헤더(<header>
)에 이 웹 사이트의 이름인 MixCentro로 지정하는 것이다. (당신 것으로 자유롭게 변경해도 된다)
이 프로젝트가 동작하기 위해서 음원이 필요할 것이다. 모든 음원 파일은 위에서 언급한 깃헙 저장소에 가서 다운로드 받는다.
우리는 주요 디비전인 "pads"를 만들었고 이 디비전은 "pad-top"과 "pad-bottom"을 포함한다. "pads" 디비전은 UI에서 본 패드들을 만드는 역할만 한다. 그리고 두 개로 나뉜 부분은 각각 3개의 패드를 포함한다.
상단 패드는 서로 다른 음원으로 구성된 패드가 있는 "pad-top"이다. 하단 패드는 상단 패드처럼 구성된 "pad-bottom"이다.
style.css와 index.js를 가져오고 있지만 현재는 사용하지 않는다.
우리는 디렉토리의 루트에 스타일시트 파일을 구성할 수 있다. style.css
에 해당한다.
기본적으로 화면의 양 사이드에는 마진과 패딩 값을 가지는데 우리 예제에서는 필요하지 않다. 그래서 기본적으로 추가된 패딩과 마진 값을 수동으로 제거해보겠다.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
HTML 파일에 헤더를 추가하면 배경이 없기 때문에 우리는 웹 사이트가 예쁘게 보이도록 만들어야 한다. 폰트 스타일링을 업그레이드하고 테마에 잘 맞는 배경을 추가하여 웹 사이트를 보기 좋게 만든다.
위에서 언급한 깃헙 저장소에는 이미 당신을 위해 준비된 이미지가 있고 images/bg/background.jpg
경로에 있다. 이 이미지는 UI 미리보기에서 사용된 것과 같다.
더 나아가려면 웹 사이트에 적합한 멋진 폰트 선택이 필요하다. 다양한 폰트 중에서 선택하기 위해 구글 폰트를 사용하기로 한다.
모든 사람의 선택과 취향이 다르기 때문에 시간이 좀 걸릴 수 있다. 폰트를 하나 선택하고 선택된 폰트의 오른쪽 상단에 있는 '+' 버튼을 클릭한다.
클릭하면 화면에 "1 Family Selected" 메시지와 함께 검은색 바(bar)가 나타난다. 그리고 이 검은색 바를 클릭하면 확장되고 위 이미지와 같은 내용을 볼 수 있다.
세부 사항들은 선택한 글꼴에 따라 약간 다를 수 있지만 나머지는 동일하다. 여기서는 폰트를 가져오기 위해 표준 방법을 사용할 것이다. 위 이미지에서 회색 박스 안의 <link href … >
전체를 복사한다.
이 폰트를 사용하기 위해 index.html
파일을 열고 <head>
태그 사이에 이 링크를 붙여넣는다.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!--Importing the font here-->
<link href="https://fonts.googleapis.com/css?family=Lexend+Exa&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./style.css">
<title>MixCentro</title>
</head>
폰트 추가
폰트를 가져온 다음, 브라우저에서 잘 보이도록 반영해야 한다.
body {
font-family: 'Lexend Exa', sans-serif;
background: url('./images/bg/background.jpg');
background-size: cover;
background-repeat: no-repeat;
color: wheat;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
header h1 {
margin: 50px 0px 20px 0px;
text-align: center;
padding: 2rem;
background: black;
font-size: 40px;
}
header p {
font-size: 25px;
color: black;
}
.pads {
background: lightblue;
width: 60%;
box-shadow: 10px 10px 20px black;
margin-bottom: 150px;
}
.pad-top{
display: flex;
}
.pad-bottom{
display: flex;
}
.pad-top > div {
height: 100px;
width: 100px;
flex: 1;
}
.pad-bottom > div {
height: 100px;
width: 100px;
flex: 1;
}
.pad1 {
background: #60d394;
}
.pad2 {
background: #d36060;
}
.pad3 {
background: #c060d3;
}
.pad4 {
background: #d3d160;
}
.pad5 {
background: #606bd3;
}
.pad6 {
background: #60c2d3;
}
필자의 경우에는 "Lexend Exa" 폰트 패밀리를 사용했고 이전에 말했던 배경 이미지를 추가했다. 플렉스박스(flexbox)를 사용한 균등 간격 레이아웃은 justify-content: space-between
으로 설정했다.
또한 "pads" 클래스는 너비를 60%로 설정해, 요소가 화면의 절반 이상을 차지했을 때 화면이 너무 커지지 않고 타이트하면서 부드럽게 보이도록 했다.
"pad-top"과 "pad-bottom" 클래스 안에 포함된 디비전들이 동일한 너비와 높이 값을 가지도록 설정하고 flex: 1
로 설정하여 균등하게 보여지도록 했다.
마지막으로 모든 패드에 서로 다른 색상을 설정했다.
마지막으로, 반응형 애플리케이션을 위해 미디어 쿼리를 추가한다.
@media (max-width: 480px){
header h1{
font-size: 35px;
}
header p{
font-size: 16px;
}
.pads{
margin-bottom: 100px;
}
}
이 코드에서 화면 크기가 480 픽셀 이하일 때 설정된 폰트 크기가 줄어들도록 했다. 그리고 하단 여백이 잘 보이도록 margin-bottom
값을 조정했다.
모바일 호환 구축
이 지점에서 우리는 멋진 UI 설정을 마쳤지만 패드들을 클릭하면 어떤 소리도 나지 않는다. 왜 그럴까?
패드에 해당하는 디비전들은 <audio>
를 포함하고 있지만 사용자가 클릭할 때 특정 음원을 재생하려면 play()
함수를 호출해야 한다. 이것이 자바스크립트가 필요한 부분이다.
우리의 자바스크립트 코드는 굉장히 간단하고 11줄로만 구성된다.
window.addEventListener('load', ()=>{
const sounds=document.querySelectorAll(".sound");
const pads=document.querySelectorAll(".pads div");
});
먼저 querySelectorAll
API를 통해 .sounds
와 .pads
클래스를 가지는 요소들을 찾아서 각각 sounds
와 pads
변수에 할당한다.
그러나 이 동작은 윈도우가 처음 로드될 때만 수행해야 한다. 그래서 위에 코드를 window.addEventListener('load')
로 감싼다.
pads.forEach((pad,index) => {
pad.addEventListener('click', ()=> {
sounds[index].currentTime = 0;
sounds[index].play();
});
});
다음은 모든 패드 디비전들을 탐색하기 위해 forEach
를 추가한다. 2개의 파라미터가 넘어오며 첫 번째는 각 패드 요소이고, 두 번째는 특정 패드의 음원을 재생하기 위해 필요한 패드의 인덱스 값이다.
각 패드에 할당된 음원 파일을 재생하기 위해 sounds
변수에서 해당 인덱스의 음원을 찾고 play()
함수를 사용해 재생한다.
그리고 currentTime = 0
과 같이 사용하는데, 재생할 때마다 시간으로 0으로 초기화하여 하나의 패드를 여러 번 클릭했을 때 여러 번 재생될 수 있도록 하기 위해서이다.
축하한다. 바닐라 자바스크립트로 웹 앱을 구축했다. 당신은 이 웹 앱과 함께 놀 수 있고 다른 사람들이 사용할 수 있도록 온라인에 배포할 수 있다. 하지만 기다려라! 웹 앱을 PWA로 변환할 일이 남았다. 들어가보자.
웹 앱 매니페스트(Web App Manifest)는 JSON 파일이다. 이것은 PWA의 세부 사항을 포함하고 사용자 기기에 PWA가 설치될 때 동작하는 방법을 브라우저에 알려준다.
매니페스트는 애플리케이션 이름(전체 또는 단축 이름), 앱 아이콘, 앱이 시작되는 열리는 URL, 테마 색상 제어 등과 같은 정보를 포함한다.
PWA 매니페스트 파일을 만드는 것은 사용자의 기기에 PWA를 설치할 때 브라우저의 동작을 제어하기 때문에 필수적이다.
매니페스트 파일
당신의 웹 앱 매니페스트를 만들기 위해, 직접 manifest.json
파일을 만들고 JSON 형식으로 세부 사항을 추가할 수 있다. 그러나 온라인으로 이미 제공된 도구를 사용하는 더 좋은 방법이 있다.
이 시대의 인터넷은 manifest.json
파일을 만들 때 시간을 절약할 수 있는 많은 유용한 옵션들을 제공한다.
모든 키-값을 JSON 형식으로 입력하는 대신 이 사이트로 이동하면 된다.
이 웹 사이트는 웹 앱 매니페스트 제너레이터로, 특정 필드에 값을 입력하기만 하면 자동으로 매니페스트 파일이 생성된다.
특정 필드에 값을 입력할 때 고려할 사항은 다음과 같다.
'standalone'
으로 변경한다.'.'
로 설정한다.아이콘 이미지는 디바이스에 맞게 크기가 조정되기 때문에 크기가 512x512를 초과하면 안된다.
모든 과정이 끝나면 'GENERATE .ZIP'을 클릭하고 프로젝트 폴더의 파일들을 zip 파일로 추출한다.
이 시점에서, 프로젝트에 매니페스트 파일을 추가했지만 아직 사용되고 있지 않다.
프로젝트에 매니페스트 파일을 반영하려면, <head>
태그 사이에 index.html
파일 링크를 추가해야 한다.
<head>
<link rel=”manifest” href=”manifest.json”>
</head>
이제 당신의 프로젝트도 일반적인 매니페스트 파일을 갖게 되었다!
이제 Yarn을 사용하여 새로운 디펜던시를 추가할 것이다.
Yarn은 NPM 레지스트리와의 호환성을 유지하면서 NPM 클라이언트 또는 다른 패키지 매니저의 작업 흐름을 대체하는 새로운 패키지 매니저이다. 패키지 매니저는 특정 용도로 사용되는 일부 패키지를 설치할 목적으로 서버를 제공한다.
물론 다른 패키지 매니저들도 있지만 이 프로젝트에서는 몇 가지 이유에서 Yarn을 사용한다.
yarn.lock
파일을 사용하여 다른 시스템에서 동일한 패키지/디펜던시가 설치되도록 한다.Yarn 설치는 굉장히 단순하다. 사이트에서 "Install Yarn" 버튼을 클릭하면 자동으로 다운로드가 시작된다.
일반적인 설치 과정을 따르면, Yarn 설치가 시작되고 경로는 환경 변수에서 자동으로 수정될 것이다.
또 다른 방법으로 npm을 사용할 수 있고 아주 쉽다. 이미 npm을 사용하고 있다면 터미널을 열고 다음 커맨드를 입력한다.
Yarn 설치가 잘 되었는지 확인하기 위해서 터미널에 다음 커맨드를 입력한다.
1.16.0 또는 비슷한 버전을 확인했다면 Yarn이 정상적으로 설치된 것이다.
새 Yarn 프로젝트를 생성하는 첫 번째 단계로, 터미널에서 프로젝트 폴더로 이동한 후 다음 커맨드를 입력한다.
터미널에서 yarn init
을 입력하면 몇 가지 질문을 받게 될 것이다. 질문에 답할 때마다 package.json
파일에 답한 결과가 저장된다.
먼저, 패키지 이름을 물을 것이다. 원하는 프로젝트 이름을 입력한다. 다른 필드의 경우, 비워두거나 특정 값을 선택적으로 입력할 수 있다.
entry point를 지정하는 질문은 중요하기 때문에 건너뛰면 안된다. 프로젝트에서 진입점을 결정한다. 앞에서 생성한 index.js로 초기화한다.
이 단계를 마치면 프로젝트에 새 폴더와 파일이 많이 생성된 것을 확인할 수 있다. 당신의 첫 번째 Yarn 프로젝트가 성공적으로 초기화되었다면 등을 두드리며 칭찬해줘라.
yarn init는 프로젝트 폴더 안에서 해야 한다. 그렇게해야 index.js 파일이 진입점으로 접근할 수 있기 때문이다.
PWA를 라이브 서버로 실행해야하므로 애플리케이션을 테스트하기 위해서는 로컬호스트가 필요하다. 그래서 우리는 serve
패키지를 설치해야 한다.
serve
패키지는 서버를 사용해 정적 사이트를 테스트할 때 좋다. 로컬호스트에서 serve
패키지가 실행되는 방법을 확인하고 이후 배포 과정에 추가할 수 있다.
프로젝트에 serve
패키지를 설치하기 위해 터미널에서 프로젝트로 폴더로 이동한 후 다음과 같이 입력한다.
이 커맨드는 프로젝트에 디펜던시를 추가하고 추가된 디펜던시는 package.json
파일에서 볼 수 있다.
로컬 서버에서 정적 페이지를 실행하기 위해서 디펜던시를 사용해 서버를 처음으로 실행한다. 터미널에 yarn serve
커맨드를 입력해서 서버를 실행한다.
서버가 실행되면 로컬호스트와 네트워크 주소에 대한 정보가 다음과 같이 보여질 것이다. 이 중 하나에 접속하면 로컬 서버에서 실행중인 정적 웹 사이트를 볼 수 있다.
서비스 워커(service worker)는 웹 워커(web worker)의 한 종류다. 서비스 워커는 기본 브라우저 스레드와 별도로 실행되어 네트워크 요청을 가로채고 캐시에서 리소스를 캐싱하거나 푸시 메시지를 전송하는 자바스크립트 파일이다.
기본적으로 서비스 워커는 기본 스레드와 별도로 실행되며 현재 연관된 애플리케이션과 완전히 독립적이다.
서비스 워커는 네트워크 요청을 제어하고, 캐싱을 처리할 수 있으며 캐시를 통해 오프라인 리소스를 지원할 수도 있다.
서비스 워커는 다음과 같이 3단계의 생명 주기를 갖는다.
서비스 워커를 설치하려면 먼저 기본 자바스크립트 파일에 등록해야 한다. 우리의 경우 index.js 파일에 해당한다. 그 전에 서비스 워커 파일인 serviceWorker.js
를 만들자.
서비스 워커의 생명주기 중 첫 번째 단계는 등록이다.
if('serviceWorker' in navigator){
try {
navigator.serviceWorker.register('serviceWorker.js');
console.log("Service Worker Registered");
} catch (error) {
console.log("Service Worker Registration Failed");
}
}
먼저 브라우저가 서비스 워커를 지원하는지 여부를 체크하고 이 로직은 window.addEventListener()
안에 추가되어야 한다. 서비스 워커를 지원한다면 navigator.serviceWorker.register
로 서비스 워커를 등록한다. 이 메서드는 서비스 워커가 성공적으로 등록되었을 때 리졸브(resolve) 된 프라미스를 반환한다.
서비스 워커의 스코프는 서비스 워커가 제어하는 파일들을 결정하기 때문에 굉장히 중요하다. 즉, 서비스 워커의 스코프는 서비스 워커가 요청을 가로 챌 경로를 말한다.
그래서 도메인 내 모든 파일에 대한 요청을 제어하기 위해 서비스 워커는 루트 디렉토리에 두는 것을 선호한다.
serviceWorker.js
파일을 생성하고 브라우저에 등록했으므로, 크롬 개발자 도구의 Application 메뉴에서 서비스 워커를 확인할 수 있다.
서비스 워커 등록에 성공했다면 콘솔에서 로그 메시지를 확인할 수 있을 것이다.
로컬 캐시에서 모든 에셋을 사용할 수 있게 하려면 모든 정적 에셋의 경로를 동적으로 선언해줘야 한다.
const staticAssets=[
'./',
'./style.css',
'./index.js',
'./sounds/bubbles.mp3',
'./sounds/clay.mp3',
'./sounds/confetti.mp3',
'./sounds/glimmer.mp3',
'./sounds/moon.mp3',
'./sounds/ufo.mp3'
];
서비스 워커는 이벤트 드리븐(event-driven) 방식으로, 브라우저 측에서 이벤트를 수행하려면 install
및 fetch
와 같은 이벤트를 추가해야 한다.
install
이벤트는 브라우저가 새로운 서비스 워커를 감지할 때마다 호출된다. 우리의 목표는 모든 정적 에셋을 검색하기 위해 캐시 API를 호출하는 것이다.
self.addEventListener('install', async event=>{
const cache = await caches.open('static-cache');
cache.addAll(staticAssets);
});
이 경우에는 'static-cache'
라는 이름으로 캐시를 호출한다. 다른 이름으로 지정할 수도 있지만 단순하게 "static-cache"로 사용하는 것을 선호한다.
서비스 워커는 저수준의 API이므로, 항상 무엇을 해야하는지 알려줘야 한다. 개발자 도구의 Application로 다시 가서 오프라인 환경을 시뮬레이트 하면, 여전히 아무 일도 일어나지 않은 것을 확인하게 될 것이다.
이 문제를 해결하기 위해 fetch API를 사용해보자.
서비스 워커에서는 등록된 이벤트에 응답하는 방법을 결정할 수 있다. 이를 위해서 respondWith()
메서드를 호출한다.
우리의 경우, 캐시가 처음 생성된 것인지 여부를 체크한 다음 네트워크에서 캐시를 패치할 것이다.
self.addEventListener('fetch', event => {
const req = event.request;
const url = new URL(req.url);
if(url.origin === location.url){
event.respondWith(cacheFirst(req));
} else {
event.respondWith(newtorkFirst(req));
}
});
캐시 첫 접근(cache-first approach)을 생성하기 위해서는 캐시 자체에 있는 파일과 요청을 일치시키는 함수(cacheFirst
)를 만든다. 요청은 고유한 키(key)처럼 동작한다.
이 함수는 캐시에 아무 것도 없을 경우 undefined
를 반환하거나 캐시 요청 자체를 반환한다.
async function cacheFirst(req){
const cachedResponse = caches.match(req);
return cachedResponse || fetch(req);
}
네트워크 첫 접근(network-first approach)을 생성하기 위해서는, 필요할 경우에 모든 네트워크 에셋이 위치한 곳에 동적 캐시를 만들어야 한다. 패치가 진행되고 불가능한 경우 정적 캐시가 반환된다.
async function newtorkFirst(req){
const cache = await caches.open('dynamic-cache');
try {
const res = await fetch(req);
cache.put(req, res.clone());
return res;
} catch (error) {
return await cache.match(req);
}
}
브라우저에서 필요한 경우 온라인으로 요청을 가져오고 put()
메서드를 사용하여 동적 캐시에 추가한다.
이제 Application를 방문하면 방금 생성한 정적 및 동적, 두 개의 캐시 저장소를 확인할 수 있다.
우리가 만든 서비스 워커로 돌아가서 "Offline" 체크박스를 클릭한 다음 오프라인 환경을 시뮬레이트할 경우, 인터넷 문제가 발생하지 않으며 모든 캐시 저장소로 인해 애플리케이션이 잘 실행될 것이다.
축하한다. 이제 당신은 모든 플랫폼에서 사용하고 설치할 수 있는 PWA를 갖게 되었다.
당신이 만든 PWA를 설치하기 위해서 온라인 서버에 프로젝트를 배포하기만 하면 된다(무료 호스팅도 잘 동작한다). 웹 사이트가 완전히 로드되면 주소 표시 줄의 오른쪽에 작은 +
표시가 나타난다.
Install 버튼을 클릭해 당신의 멋진 PWA를 설치하고 로컬에서 실행되는 것을 확인해본다.