본문 바로가기
IT/개발

Service Worker 적용해 보기

by aloveu 2024. 1. 13.
반응형

 

서비스워커는 기본 브라우저 스레드와 별도로 실행되어 네트워크 요청을 가로채거나 리소스를 캐싱하거나 푸시 메시지를 전송하는 js 파일로 되어있는 하나의 워커입니다.


보통 서비스워커는 앱과 서버 간의 프락시 역할을 하고 메인 스레드에서 오는 모든 네트워크 요청이 여기를 통과합니다.

따라서 캐시 된 데이터들이 있을 경우 네트워크 요청을 중지하거나 수정하거나 하는 권한을 가집니다. 

물론 캐시를 쓸 수 있도록 등록을 해야 합니다. 등록과정은 아래 서비스워커 등록하는 방법을 알려드릴 때 캐시 부분도 같이 기술하겠습니다.


| Service Worker 적용하기

LifeCycle

서비스 워커의 라이프 사이클은 아래처럼 동작을 하고 이건 등록하는 과정을 설명하는 과정에서 다시 한번 언급하겠습니다.

 

서비스워커 등록

보통 서비스의 진입점인 페이지에서 서비스가 시작할 때 등록을 해줍니다.

여기서는 index.html파일에 등록을 할 건데 브라우저가 지원하는 경우 register 메서드를 통해 등록을 할 수 있습니다. 

보통 서비스 워커의 스코프는 해당 파일의 위치에 따라 달라지는데 도메인 내 모든 파일에 대한 요청을 제어하기 위해서 보통 루트 디렉터리에 둡니다.

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('./service-worker.js')
        .then((res) => {
            console.log('서비스 워커가 등록됨!', res);
        })
        .catch((e) => console.log('서비스 워커가 등록이 안됨!', e))
}

 

1. Install

방금 전 index.html에서 등록한 service-worker.js안에서 서비스워커 이벤트들을 리스너 할 수 있습니다.

const CACHE_NAME = 'aloveu';
const OFFLINE_URL = "./offline.html";
const ASSETS = [
    './roboto-black.woff',
    './icon-192x192.png',
];

self.addEventListener('install', e => {
    console.log('서비스워커 설치(install)함!');
    e.waitUntil((async () => {
        try{
            const cache = await caches.open(CACHE_NAME);
            await cache.addAll([new Request(OFFLINE_URL, {cache: 'reload'}), ...ASSETS]);
        }catch(err){
            console.log('install error', err);
        }
    })());


    self.skipWaiting();
});

 

2. Activate

서비스워커가 동작, 변경됐을 때 발생하는 이벤트입니다.

self.addEventListener('activate', e => {
    console.log('서비스워커 활성화(activation)');
});


3. Fetch

데이터 요청(네트워크, 캐시)이 발생했을 때는 fetch 이벤트에 항상 걸리게 되는데 여기에서 캐싱된 파일 검증하거나 header에 다른 정보를 심거나 할 수 있습니다.

const OFFLINE_URL = "./offline.html";
self.addEventListener('fetch', e => {
    e.respondWith((async () => {
        try {
            const r = await caches.match(e.request);
            if (r) { 
                console.log('캐싱된 파일', e.request);
                return r; 
            }
            console.log("네트워크 요청", e.request)
            
            const networkResponse = await fetch(e.request);
            return networkResponse;
        }catch (err) {
          console.log("Fetch failed;", err);

          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(OFFLINE_URL);
          console.log(cachedResponse);
          return cachedResponse;
        }
    })());
});

 

4. Idle, Terminated

서비스워커가 활성화된 이후 이벤트들을 받지 못하면 유휴 상태가 되고, 일정 시간 동안 유휴 상태를 유지한 후에 종료상태가 됩니다. 

서비스워커로 작업을 하다 보게 되면 이 유휴상태관리하는 하는 부분을 놓쳤다가 생기는 오류들이 많습니다.

실제 프로젝트에서 서비스워커를 통해 서버로 데이터를 항상 보내고 있었는데 가끔 누락되는 경우들이 있어서 한참을 헤맸던 경험이 있어요.

 

| 기능

1. 설치 커스텀

PWA를 설치할 수 있음을 표시하고 설치할 수 있는 방법 제시할 수 있는 방법입니다.

// html
<div class="install-banner">
    <button id="buttonInstall">설치</button>
</div>
    
// js - 앱 설치 커스텀
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    console.log(`설치 전`);

    // 페이지내 설치 배너 노출
    document.getElementsByClassName('install-banner')[0].style.display = 'block';
});

document.getElementById('buttonInstall').addEventListener('click', async () => {
    //페이지 내 설치 배너 감춤
    document.getElementsByClassName('install-banner')[0].style.display = 'none';
    
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log(`User response to the install prompt: ${outcome}`);
    deferredPrompt = null;
});

 

2. 알림 권한 요청(html)

Notification.requestPermission()이라는 Web api의 메서드를 호출

//html
<button id="notifications">알림 권한 요청</button>
    
// js - 알림에 대한 권한 요청
document.getElementById('notifications').addEventListener('click', () => {
    Notification.requestPermission().then((result) => {
        if (result === 'granted') {
            console.log('알림설정 ok');
            randomNotification();
        }else{
            console.log('알림설정 거부');
        }
    });
})


 

3. 알림 클릭 이벤트 (service-worker.js)

알림이 트레이아이콘 위에 떴을 때 해당 알림에 대한 클릭이벤트 처리를 할 수 있습니다.

//notification
self.addEventListener('notificationclick', (event) => {
    let url = 'https://aloveu.tistory.com';
    event.notification.close();
    event.waitUntil(
        clients.matchAll({type: 'window'}).then( windowClients => {
            for (var i = 0; i < windowClients.length; i++) {
                var client = windowClients[i];
                if (client.url === url && 'focus' in client) {
                    return client.focus();
                }
            }

            if (clients.openWindow) {
                return clients.openWindow(url);
            }
        })
    );
});

  

4. 푸시 서비스 

html에서 푸시 서비스를 구독하는 작업을 해야 합니다.

//푸시 메시지 서비스 구독
navigator.serviceWorker.ready.then(function(registration) {
  return registration.pushManager.getSubscription()
    .then(async (subscription) => {
        if (subscription) {
            return subscription;
        }
      // registration part
    });
}).then(function(subscription){
  fetch('./register', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      subscription: subscription
    }),
  });
});


(service-worker.js)에서는 push 이벤트를 등록할 수 있습니다.

self.addEventListener('push', function(event) {
  const payload = event.data ? event.data.text() : 'no payload';
  event.waitUntil(
    self.registration.showNotification('PWA test', {
        body: payload,
    })
  );
});

 

5. 배지

배지는 설치한 pwa 아이콘 위에 숫자를 띄워 줄 수 있어 안 읽은 메시지 개수등의 표시를 해줄 수 있습니다. 

//적용
navigator.setAppBadge(24).catch((error) => {  // 읽지 않은 메시지 24개.
    //Do something with the error.
});

// 삭제
navigator.clearAppBadge().catch((error) => {
    // Do something with the error.
});

  

6. 오프라인 페이지 등록 (service-worker.js)

fetch 쪽에서 진행하면 됩니다.

 


7. 마켓 등록을 위한 apk 파일 생성

구글 플레이스토어에도 등록할 수 있는데 그러려면 apk 파일을 생성해야 합니다. 
직접 하기에는 손이 많이 가서 https://pwa2apk.com/?ref=steemhunt에서 툴을 사용해서 하면 간단하게 등록할 수 있습니다.


8. Style

설치된 PWA에서만 다른 스타일 적용을 할 수 있습니다.

@media all and (display-mode: standalone) {
  body {
    background-color: yellow;
  }
}

 

반응형