PWA

지니·2021년 5월 24일
3

대학생의 일기

목록 보기
3/17

리액트를 사용하여 진행하는 프로젝트에서 모바일 상에서도 쉽게 접근할 수 있도록 하고자 PWA를 사용하게 되었다.



PWA

PWA는 Progressive Web Application의 약자로 웹 애플리케이션과 네이티브 애플리케이션의 장점을 합친 것이라고 볼 수 있다. 또한, 오프라인에서 동작, 설치, 쉬운 동기화, 푸시 알림 등의 기능 또한 구현이 가능하도록 한다.



LightHouse

갑자기 LightHouse? LightHouse부터 적은 이유가 있다. 일단 알아보도록 하자.

LightHouse란, 웹앱의 품질을 개선하는 오픈 소스 자동화 도구로, 확인할 URL을 지정하고 페이지에 대한 테스트를 실행한 후 이에 대한 보고서를 작성하게 된다. 크롬에서는 F12(개발자 도구)로 들어간 후 LightHouse 탭을 누르고,

이 화면에서 Generate report만 눌러주면 된다.

그러면 이런 화면을 볼 수 있다. 여기서 상단 맨 오른쪽에 Progressive Web App을 볼 수 있는데, 이걸로 앱 다운로드를 제공할지 말지를 알 수 있다.

(본인은 LightHouse를 좀 늦게 알아서 PWA 사용을 위한 설정할 때 고생 좀 했다... PWA 사용할 때 뿐만 아니라 웹앱의 품질을 알아보는 지표로도 많이 사용한다고 하니 미리 알아두는 것이 좋을 것 같다.)



PWA + React

우선, PWA를 사용하기 위해 반드시 필요한 파일이 몇 가지가 있다.

  • index.html (리액트 프로젝트의 public 폴더에 이미 존재)
  • manifest.json
  • service-worker.js

manifest.json

앱에 대한 정보를 담고있는 JSON 파일이다. 파일 명은 대부분 이렇게 사용하는 듯 하다.

{
    "name": "TeamLog",
    "short_name": "TeamLog",
    "start_url": "public/index.html",
    "display": "standalone",
    "icons": [
      {
        "src": "/icons/apple-touch-icon-57x57.png",
        "sizes": "57x57",
        "type": "image/png"
      },
      {
        "src": "/icons/apple-touch-icon-60x60.png",
        "sizes": "60x60",
        "type": "image/png"
      },
      {
        "src": "/icons/apple-touch-icon-72x72.png",
        "sizes": "72x72",
        "type": "image/png"
      },
      {
        "src": "/icons/apple-touch-icon-76x76.png",
        "sizes": "76x76",
        "type": "image/png"
      },
      {
        "src": "/icons/apple-touch-icon-114x114.png",
        "sizes": "114x114",
        "type": "image/png"
      },
      {
        "src": "/icons/apple-touch-icon-120x120.png",
        "sizes": "120x120",
        "type": "image/png"
      },
  
      {
        "src": "/icons/apple-touch-icon-144x144.png",
        "sizes": "144x144",
        "type": "image/png"
      },
      {
        "src": "/icons/apple-touch-icon-152x152.png",
        "sizes": "152x152",
        "type": "image/png"
      },
      {
        "src": "/icons/favicon-196x196.png",
        "sizes": "192x192",
        "type": "image/png"
      },
      {
        "src": "/icons/mstile-310x310.png",
        "sizes": "310x310",
        "type": "image/png"
      },
      {
        "src": "/icons/favicon-512x512.png",
        "sizes": "512x512",
        "type": "image/png"
      }
    ],
    "prefer_related_applications": true,
    "theme_color": "#543971",
    "background_color": "#FFFFFF"
  }
  
  • short_name
    : 아이콘 이름
  • name
    : 웹앱의 이름
  • icons
    : 아이콘에 사용할 이미지
  • start_url
    : 웹앱 실행시 시작되는 URL 주소
  • display
    : 디스플레이 유형
    browser - 일반 브라우저와 동일
    standalone : 최상단 상태표시줄을 제외한 전체 화면
    fullscreen : 상태표시줄 제외한 전체 화면
    minimul-ui : fullscreen 상태에서 뒤로가기, 새로고침 등 제공
  • prefer_related_applications
    : 웹앱과 네이티브 앱 사이의 우선순위 결정 (false가 default이며 true로 변경하면 사용자 에이전트가 앱의 설치를 권장한다.)
  • theme_color
    : 상단 툴바의 색
  • background_color
    : 스플래시 화면(앱 실행 시 보여주는 시작 화면) 배경 색

여기서 진한 글씨로 사용한 요소들은 반드시 파일에 포함되어야 한다는 글(https://web.dev/installable-manifest/?utm_source=lighthouse&utm_medium=devtools)을 보게 되었고, 실제로 본인도 처음에 prefer_related_applications를 빼먹었을 때 앱으로 사용할 수 없다고 떴었다.

참고로,

LightHouse에 이렇게 뜨면 모바일에 설치 가능하게 되고,

이렇게 뜨면 설치 불가능한 상태라는 것을 알 수 있다. reasons 누르면 그 이유를 알 수 있고 공식 문서와 구글링을 통해 상황에 맞춰 해결하면 될 것 같다.

manifest.json을 생성하고 다 작성했으면

<link rel="manifest" href="manifest.json" crossorigin="use-credentials">

이 코드를 index.html의 헤더 부분에 넣어주면 된다.


service-worker.js

다음으로 service-worker.js라는 파일이 필요한데 그 전에 Service Worker가 무엇인지부터 알아보자.

Service Worker는 웹 브라우저가 백그라운드에서 실행하는 스크립트로 웹페이지와는 별개로 작동한다. 웹 서비스와 브라우저 및 네트워크 사이에서 프록시 서버의 역할을 하며, 오프라인에서도 서비스를 이용할 수 있도록 한다.

로컬에서는 http를 사용해도 무방하지만 서버에 배포할 때는 https를 사용해야 Service Worker를 사용할 수 있다!!!


먼저 Service Worker를 사용하기 위해 작성해야 할 코드가 있다.

  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('service-worker.js').then((reg) => console.log('Service worker registered.', reg));
    }
  </script>

이 코드를 index.html에 추가해줘야 하는데, 이는 service-worker.js 안의 서비스 워커를 실행하겠다는 뜻이다. service-worker.js는 index.html과 같은 위치에 있어야 하는 것 같다.

이제 service-worker.js 파일을 만들어보자.

const CACHE_NAME = 'cache-v2';

const FILES_TO_CACHE = [
  'offline.html',
];

self.addEventListener('install', (evt) => {
    evt.waitUntil(
        caches.open(CACHE_NAME).then((cache) => {
            console.log('install');
            return cache.addAll(FILES_TO_CACHE);
        }),
    );
});

self.addEventListener('activate', (evt) => {
    evt.waitUntil(
        caches.keys().then((keyList) => {
            return Promise.all(keyList.map((key) => {
                if (key !== CACHE_NAME) {
                    console.log('Removing old cache', key);
                    return caches.delete(key);
                }
            }));
        }),
    );
});

self.addEventListener('fetch', (evt) => {
    if (evt.request.mode !== 'navigate') {
        return;
    }
    evt.respondWith(
        fetch(evt.request)
        .catch(() => {
            return caches.open(CACHE_NAME)
            .then((cache) => {
                return cache.match('offline.html');
            });
        }),
    );
});

전체 코드는 다음과 같다. 이제 주요 코드에 대해 살펴보자.


  • install
self.addEventListener('install', (evt) => {
    evt.waitUntil(
        caches.open(CACHE_NAME).then((cache) => {
            console.log('install');
            return cache.addAll(FILES_TO_CACHE);
        }),
    );
});

캐시를 초기화하고 오프라인을 위한 파일들을 추가할 수 있다. 본인은 위에 FILES_TO_CACHE라는 변수의 배열을 상단에 만든 상태이다.

waitUntil - Service Worker는 waitUntil 안의 코드가 실행되기 전까지는 설치되지 않음.
caches - 데이터를 저장할 수 있도록 주어진 Service Worker의 범위 내에서 사용할 수 있는 특별한 객체


  • activate
self.addEventListener('activate', (evt) => {
    evt.waitUntil(
        caches.keys().then((keyList) => {
            return Promise.all(keyList.map((key) => {
                if (key !== CACHE_NAME) {
                    console.log('Removing old cache', key);
                    return caches.delete(key);
                }
            }));
        }),
    );
});

더 이상 필요하지 않은 파일을 제거하고 앱이 끝난 후 정리하는데 사용한다. 최상단에 버전명이 바뀌었을 때 작동하는 코드이다.


  • fetch
    if (evt.request.mode !== 'navigate') {
        return;
    }
    evt.respondWith(
        fetch(evt.request)
        .catch(() => {
            return caches.open(CACHE_NAME)
            .then((cache) => {
                return cache.match('offline.html');
            });
        }),
    );
});

네트워크에서 뭔가를 받아올 때 인터넷에 연결되지 않을 경우 캐시에 저장된 것들을 꺼내는 역할을 한다. 위의 코드는 인터넷 연결이 안됐을 때 offline.html의 내용을 보여주겠다는 뜻이다.



APP 다운로드

IOS에서는 manifest.js를 지원해주지 않기 때문에 모바일 앱 다운로드가 불가능하다고 한다. 즉, 안드로이드에서만 앱 다운로드가 가능하다.

위의 코드를 다 작성하고 LightHouse를 살펴보면 이렇게 뜰 것이다.

PWA Optimized는 최적의 조건? PWA가 갖춰야 할 최적의 상태? 정도로 생각된다. 그 위에 Installable이 활성화되면 된다. 공식 문서에서도 나와있지만, 모바일 앱으로 다운받을 수 있는지 확인하려면 다음 코드를 작성해보면 된다.
(본인은 모바일 화면에서 버튼을 터치했을 때 다운로드 받을 수 있도록 하기 위해서 이 코드를 리액트 함수 컴포넌트를 포함한 js파일 안에 작성하였다.)

let deferredInstallPrompt = null;

window.addEventListener('beforeinstallprompt', (e) => {
  deferredInstallPrompt = e;
  console.log("'beforeinstallprompt' event was fired.");
});

function userClickedAddToHome() {
  deferredInstallPrompt.prompt();

  deferredInstallPrompt.userChoice.then((choiceResult) => {
    if (choiceResult.outcome === 'accepted') {
      // 유저가 홈 스크린에 어플리케이션 추가에 동의
    } else {
      // 유저가 홈 스크린에 어플리케이션 추가를 거부
    }
    deferredInstallPrompt = null;
  });
}

beforeinstallprompt라는 이벤트가 발생하여 도메인이 모든 설치 조건을 충족함을 브라우저에게 알린다. 버튼을 눌렀을 때userClickedAddToHome 함수가 실행되도록 하면 브라우저에서 앱을 다운받을건지 묻고 수락하면 앱이 다운되는 방식이다. 위에서 설정이 잘못된게 있으면 이 이벤트는 발생하지 않는다.


결론적으로 본인은 manifest.js랑 service-worker.js는 index.html와 같은 위치에 두고 beforeinstallprompt 이벤트 리스너는 함수형 컴포넌트가 들어있는 .js 파일에 작성했다.

대략적인 개념을 살펴보면서 설정할 때 어려움을 겪으면서 따져봐야 할 사항을 정리해보는 시간을 갖게 되었다. 개인적으로 알아보면서 여기서 가장 정보를 많이 얻었던 것 같다.

https://chaewonkong.github.io/posts/pwa.html
https://www.youtube.com/watch?v=NMdnzvPsGu8

profile
Coding Duck

1개의 댓글

comment-user-thumbnail
2024년 7월 24일

좋은글 감사합니다.

답글 달기