웹 워커와 서비스 워커 (3)

Caesars·2023년 3월 5일
1

JS

목록 보기
7/8

서비스 워커를 이용한 웹 푸시 서비스를 조금 더 고도화 하는 과정에서 기존 포스트에 빠진 내용이 많았습니다. 서비스 워커에 대해 새로 알게 된 부분이 있어 추가로 글을 작성합니다.


푸시 권한 설정

Notification.requestPermission().then((result)=>{
    if(result === 'granted'){
        console.log(result);
    } else if(result === 'denied'){
        console.log(result);
    }
})

위 코드를 실행하면 권한 요청창이 실행됩니다. 알림을 허용하면 result가 granted로 반환되고 서비스 워커를 등록할 수 있습니다. 만약 사용자가 알림을 거부할 경우 푸시 알림을 받을 수 없으며 직접 권한을 설정하지 않는 이상 코드상에서 다시 알림 설정을 요청할 수도 없습니다.

서비스 워커 등록

스코프 설정

하나의 도메인에 여러 개의 서비스 워커가 등록 가능하지만 동일한 스코프 내에서는 하나의 서비스 워커만 등록할 수 있습니다.

  • 등록 가능 :
    domain.com/firebase-messaging-sw.js
    domain.com/js/worker/firebase-messaging-sw2.js

  • 등록 불가능 :
    domain.com/firebase-messaging-sw.js
    domain.com/firebase-messaging-sw2.js

여러개의 서비스 워커를 등록하는 경우도 있을 수 있으므로 스코프가 겹치지 않게 주의해야 합니다.

최상위 윈도우에서만 등록

만약 iFrame 안에서 등록시 다음과 같은 에러가 발생합니다.

An error occurred while retrieving token. FirebaseError: Messaging: We are unable to register the default service worker. Failed to register a ServiceWorker: The document is in an invalid state.

firebase 스크립트에서 최상위 윈도우의 document에 서비스 워커를 등록하는 방식으로 보입니다.

서비스 워커 수명 주기

등록(Registering)

서비스 워커는 웹 애플리케이션에서 등록되어야 합니다. 등록은 웹 페이지의 js코드에서 이루어집니다.

설치(Installing)

등록된 서비스 워커는 설치 과정을 거쳐야 합니다. 설치는 등록 후 최초로 웹 페이지를 로드할 때 이루어집니다. 서비스 워커는 이 단계에서 웹 애플리케이션의 캐시를 초기화하고, 필요한 파일을 다운로드하여 캐시에 저장합니다. 또한 설치 과정에서 self.skipWiting() 메소드가 필요합니다.

self.addEventListener('install', function (event) {
    event.waitUntil(self.skipWaiting());
})

self.skipWaiting() 메소드는 새 버전의 서비스 워커가 페이지를 인수하고 즉시 활성화되도록 합니다. 서비스 워커를 처음 등록시 서비스 워커가 설치되고 즉시 활성화됩니다.

그러나 페이지에 이미 활성화된 워커가 있는 경우 상황이 복잡해집니다. self.skipWaiting()이 없으면 대기 중인 서비스 워커는 현재 활성 서비스 워커가 해제된 후에만 활성화됩니다. 서비스 워커가 해제되려면 브라우저를 닫거나 지정된 시간이 지나야 합니다. 따라서 사용자가 페이지를 새로 고침 해도 새 버전의 서비스 워커가 활성화 되지 못하고 영원히 기다리게 될 수 있습니다.

ServiceWorker.skipWaiting() 메서드는 새로 설치된 서비스 워커에게 대기 상태를 건너뛰고 바로 활성화 상태로 이동하도록 지시하여 이 문제를 해결합니다.

활성화(Activating)

서비스 워커는 설치 후 활성화되어야 합니다. 이 단계에서 이전 버전의 서비스 워커를 대체하고, 새로운 캐시를 활성화합니다.
하지만 서비스 워커가 처음 등록되면 다음에 로드 될 때까지 사용되지 않습니다. 이때 claim() 메소드를 쓰면 그 페이지를 바로 제어 되도록 합니다.

self.addEventListener('activate', event => {
  	event.waitUntil(self.clients.claim());
});

동작(Operating)

서비스 워커가 활성화되면, 웹 페이지와는 별개로 백그라운드에서 동작합니다. 푸시 알림을 처리하거나, 백그라운드 동기화 등의 추가적인 작업을 수행할 수 있습니다.

해제(Unregistering)

서비스 워커는 등록 해제할 수 있습니다. 이 경우, 서비스 워커는 더 이상 백그라운드에서 동작하지 않습니다.

Service Worker와 스크립트 간의 통신

푸시 수신과 클릭시 통계 서버로 유저 행동 관련 데이터를 보내야 할 때가 있습니다. 이 때 관련 데이터는 브라우저나 디바이스, userAgent 등 window 객체에서 제공한다고 가정합니다.

하지만 서비스 워커에서 window 객체나 쿠키, 세션 스토리지에 접근할 수가 없기에 필요 데이터를 메인 스크립트로부터 받아와야 합니다.

Service Worker와 스크립트 간의 통신 방법은 크게 두 가지로 나뉩니다.

postMessage API를 사용

스크립트에서 postMessage() API를 사용하여 Service Worker에게 메시지를 보냅니다. Service Worker에서는 message 이벤트를 수신하여 이를 처리합니다.
이 방법으로 Service Worker와 스크립트 간의 양방향 통신이 가능합니다.

문제는 상태값을 저장하려고 할 경우 postMessage API를 사용할 수 없습니다. 서비스 워커 스펙 을 보면 다음과 같이 설명이 되어 있습니다.

서비스 워커의 수명은 서비스 워커 클라이언트가 ServiceWorker 개체에 대해 보유한 참조가 아니라 이벤트의 실행 수명과 연결되어 있습니다.

처리할 이벤트가 없거나 비정상적인 작업을 감지하는 경우 언제든지 서비스 작업자를 종료할 수 있습니다.

요지는 푸시를 받는 컨텍스트와 스크립트에서 postMessage 로 받는 컨텍스트가 다르기 때문에 스크립트에서 받아와야 할 필수 값은 저장할 수 없다는 것입니다.

Indexed DB

IndexedDB는 사용자의 브라우저에 데이터를 영구적으로 저장할 수 있는 방법 중 하나입니다. 메인 스크립트와 서비스 워커 간의 직접적인 통신은 아니지만 스크립트에서 전달하고자 하는 값을 받을 수 있습니다.

서비스 워커에서 쿠키, 세션 스토리지는 사용할 수 없지만 Indexed DB는 사용이 가능하기에 일반적으로 많이 쓰입니다.

IndexedDB가 권장하는 기본 패턴은 다음과 같습니다:

  • 데이터베이스를 엽니다.
  • 객체 저장소(Object store)를 생성합니다.
  • 트랜젝션(Transaction)을 시작하고, 데이터를 추가하거나 읽어들이는 등의 데이터베이스 작업을 요청합니다.
  • 요청 결과를 가지고 어떤 동작

1. open 함수로 dataBase 열기

  • 브라우저에서 여러 개의 데이터베이스를 만들 수 있습니다.
  • 데이터베이스는 버전 정보를 가지고 있으며, 여러 개의 ObjectStore(객체 저장소)를 가질 수 있습니다.

2. 객체 저장소 생성

  • ObjectStore의 이름은 고유해야 합니다.
  • 여러 개의 레코드(key-value)를 가집니다.
  • 레코드에 유일성을 부여하기 위해 keyPath를 정의해야 한다.
  • createObjectStore를 사용해 특정 DB에 ObjectStore를 추가할 수 있습니다.
  • objectStore는 테이블, keyPath는 기본키 와 유사합니다.

3. 트랜잭션 시작

  • open 성공시 transaction을 사용해 데이터를 입출력 할 수 있습니다.
  • 트랜잭션은 데이터베이스 객체 단위로 작동하기에 사용할 객체 저장소를 지정해줘야 합니다.
  • 특정 데이터값을 조회하려면 해당 데이터의 key값을 사용하면 됩니다.

코드

// main script

let db;
// 데이터 베이스를 열도록 요청
let request = indexedDB.open("testDatabase");
          
request.onerror = function(event) {
  console.log("Why didn't you allow my web app to use IndexedDB?!");
};

request.onsuccess = function(event) {
  db = event.target.result;
};

// 새로운 데이터베이스를 만들거나 기존 데이터베이스의 버전 번호를 높일 때          
request.onupgradeneeded = function(event) {
  db = event.target.result;
  let objectStore = db.createObjectStore("testObj", { keyPath: "testKey" });
              
  objectStore.transaction.oncomplete = function(e) {
    let userObjectStore = db.transaction("testInfo", "readwrite").objectStore("testInfo");
    userObjectStore.add(userObj);
  };
};
  • 데이터베이스가 아직 존재하지 않으면, open 에 의해 생성되고, 그 다음 onupgradeneeded 이벤트가 트리거 됩니다.
  • 데이터베이스가 존재하지만 업그레이드 된 버전 번호를 지정하는 경우에도 onupgradeneeded 이벤트가 트리거 됩니다.
// Service Worker

// 백그라운드에서 메시지 수신시
messaging.onBackgroundMessage((payload) => {

  link = payload.data.link;
  
  const notificationOptions = {
    body: payload,
    icon: payload.data.image,
  };
  
  let db;
  let request = self.indexedDB.open("testDatabase");
  
  request.onerror = (() => {});
  request.onsuccess = ((event) => {
	db = event.target.result;
	let transaction = db.transaction(["testInfo"], "readonly");
	let objectStore = transaction.objectStore("testInfo");
	let getUserRequest  = objectStore.get(testKey);
	
	getUserRequest.onsuccess = function(event) {
					
		let fetchData = {
			method: 'POST',
			body: JSON.stringify(defaultBody),
			headers: {'Content-Type': 'application/json'}
		}
		
		fetch("serverUrl", fetchData)
		.then(function() {
          self.registration.showNotification(payload.data.title, notificationOptions);
		})
		.catch(function(error) {
          console.log(error);
		});

  	};
  });
});
  • 메인 스크립트에서 트랜잭션을 readwrite 모드를 사용하고 서비스 워커에서 readonly 모드를 사용했습니다. readonly 트랜잭션은 동시에 실행될 수 있지만, readwrite 트랜잭션은 객체 저장소에 오직 한 개만 작동할 수 있습니다.
  • 서비스 워커에서 xmlHttpRequest가 deprecated 되었기에 fetch API를 사용했습니다.
  • event.waitUntil() 을 사용해서 작업이 완료될 때까지 서비스 작업자가 종료되지 않도록 합니다. 함수 내부의 작업이 비동기식 이므로 완료하는 데 시간이 걸릴 수 있기 때문입니다.

참조

https://stackoverflow.com/questions/34775105/what-causes-the-global-context-of-a-service-worker-to-be-reset
https://developer.mozilla.org/ko/docs/Web/API/IndexedDB_API/Using_IndexedDB
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
https://dev.to/gelopfalcon/service-worker-and-its-self-skipwaiting-44o5
https://web.dev/service-worker-lifecycle/

profile
잊기전에 저장

0개의 댓글