PWA를 최근에 연구하고 있다. 어디까지 가능한지, 웹과 달리 어떤 설정들이 필요한지 찾는 게 생각보다 너무 재밌었다.
0. PWA란?
PWA는 웹을 앱처럼 느껴지게 만드는 웹앱 어플리케이션이다. 하지만 다운받을 필요 없이 홈화면에 추가를 누르면 앱과 동일한 기능을 한다. 용량 차지가 적어 훨씬 가볍지만 웹과 앱을 함께쓸 수 있다는 점에서 요즘 지향되고 있는 기술이다.
장점:
1. 배포가 매우 간단하다. 새로운 기능이나 수정사항은 서버에 즉시 반영할 수 있다. 더불어 업데이투 버튼 없이도 사용자가 앱을 다시 로드할 때마다 자동으로 최신 버전이 업데이트 된다.
2. PWA는 플랫폼 별 별도의 코드 베이스 없이 브라우저가 있는 모든 기기에서 실행될 수 있다.
3. 웹 기술(HTML, CSS, JavaScript)을 기반이라 빠르게 프로토타입을 개발하고 테스트할 수 있다. 더불어 이미 웹 애플리케이션을 가지고 있다면 빠르게 PWA로 확장할 수 있다.
4. SEO와 접근성 면에서 훨씬 유리하다. 웹과 같이 검색 엔진에 의해 인덱싱되는 것이 가능해 검색 결과에서 노출될 수 있다. 링크를 공유하거나 북마크할 수 있다.
5. 서비스 워커(Service Worker)를 사용하면 네이티브 앱처럼 오프라인에서 동작할 수 있는 기능도 구현할 수 있어 앱과 상당히 유사하다.
단점:
1. 기능 접근에 제한이 상당히 많다. (그래서 그 한계를 찾고 있다.) 네이티브 앱과 비교하여 하드웨어 접근에 제한이 있다. 예를 들어, 고급 카메라 제어나 블루투스, 센서 등의 기능은 PWA에서 사용할 수 없다. 더불어 PWA는 푸시 알림과 백그라운드 작업이 가능은 하지만, 네이티브 앱보다는 기능적으로 제한적이다. 특히 iOS의 경우엔 동작에 제한이 있는 경우가 많다.
2. PWA는 웹 기술을 사용하기 때문에, 네이티브 앱에 비해 성능이 떨어진다. 특히 css 그래픽이 많이 요구되거나 고성능이 필요한 애플리케이션에서는 성능에 한계가 있었다.
3. PWA는 기본적으로 웹 애플리케이션이기 때문에, 앱 스토어에서 노출될 수 없다. 이를 해결책이 결국 PWA를 네이티브 앱으로 포장(wrapping)하거나, 앱 스토어에 등록할 수 있는 별도의 작업을 진행하는 것이다.
1. 프로젝트 생성
먼저, 리액트와 타입스크립트를 사용한 프로젝트를 생성한다.
create-react-app은 PWA 템플릿을 제공하기때문에 일반 리액트 추가 명령어에 typescript 템플릿만 추가했다.
npx create-react-app my-pwa-app --template typescript
2. PWA 설정
프로젝트가 생성되면, 기본적으로 PWA를 지원하기 위한 설정이 포함되어 있다.
public 폴더 안에 manifest.json 파일과 service-worker.ts 파일이 생성된다.
나는 아무리 만들어도 serviceworker.ts 파일이 없어 별도로 추가했다.
// src/serviceWorkerRegistration.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}$/
)
);
export function register(config?: any) {
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);
} else {
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: any) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing!;
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?: any) {
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.');
});
}
2.1 manifest.json 설정
public/manifest.json 파일은 PWA의 설정을 담당합니다. 애플리케이션의 이름, 아이콘, 시작 URL, 테마 색상 등을 설정할 수 있다. 여기서 진행하는 셜정은 splash 화면으로도 쓸 수 있다.
{
"short_name": "MyPWA",
"name": "My Progressive Web App",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
display의 standalone설정이 브라우저의 ui를 숨겨주는 기능이다.
2.2 서비스 워커(Service Worker) 설정
src/service-worker.ts 파일은 서비스 워커를 등록하고 관리한다. (PWA의 핵심 요소인 오프라인 기능을 지원한다.)
서비스 워커를 활성화하려면 index.tsx에서 serviceWorkerRegistration을 등록해야 한다.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// 서비스 워커 등록
serviceWorkerRegistration.register();
3. HTTPS 설정
PWA는 HTTPS 환경에서만 동작한다. 특히 사용자의 권한을 요청하려면 ssl과 함께 https설정을 해야한다. 로컬 개발 시에도 HTTPS 환경을 구성하려면 react-scripts의 SSL 옵션을 사용한다.
HTTPS=true npm start
이 명령어가 안듣는다면 nph start에 https로 시작하게 package.json을 수정해주면 된다.
4. 테스트 및 배포
로컬에서 PWA가 잘 동작하는지 확인한 후, 실제 배포 환경에서 테스트해야 한다. PWA의 배포는 일반적인 웹 애플리케이션과 같다. GitHub Pages나 Netlify, Vercel이 다 가능한데, 나는 환경변수 설정이 편한 Netlify에 배포했다.