Skip to main content

Service Worker

· 13 min read
이현진

서비스 워커는 브라우저가 백그라운드에서 실행하는 스크립트로, 웹 페이지와는 별개로 작동하며 웹 페이지 또는 사용자의 인터랙션이 필요하지 않은 기능만 제공하고 있다. 서비스 워커의 수명주기는 웹 페이지와는 완전히 별개이다. 웹 서비스와 브라우저 및 네트워크 사이에서 프록시 서버 역할을 하며, 오프라인에서도 서비스를 사용할 수 있도록 한다.

웹 페이지와 별개로 존재하기 때문에 다음과 같은 제약이 있다.

  1. 서비스 워커는 요청하지 않는 이상, 없는 것이나 다름이 없다.
  2. 웹 페이지 life cycle을 따르지 않는다. 서비스 워커는 웹 페이지가 닫히더라도 자동으로 비활성화되지 않는다.
  3. 웹 페이지와 별개로 존자해므로 DOM이나 window 요소에 접근할 수 없다.

캐시와 상호작용

image

fetch 이벤트의 중간자 역할로 사용할 수 있다. 이 경우 서비스 워커는 HTTP를 통해 정보를 요청하는 대신 가지고 있는 캐시에서 자료를 전달한다. 캐시가 삭제되지 않는 한 브라우저는 인터넷 연결 없이도 정보를 보여줄 수 있다.

푸쉬 알림

image

브라우저 창이 닫힌 상태에서도 동작하므로, 푸시 알림을 구현할 수 있다.

백그라운드 동기화

image

채팅 메시지 또는 사진 업로드 등의 작업 도중 컴퓨터가 오프라인 상태가 되는 경우 온라인 상태가 되었을 때 해당 작업을 마저 완료할 수 있다.

Fetch

fetch란 web resource에 접근하기 위해 행해지는 모든 request action을 의미한다. service worker는 fetch를 통해 발생하는 모든 http request를 중간에서 가로챌 수 있다. (proxy) 예를 들어 실 서버로 보내지는 request를 중간에서 가로채어 서버로 보내지 말고 대신 cache 된 데이터를 제공하는 등의 처리를 하는 것이 가능하다.

http request가 있을 때마다 fetch이벤트가 발생하는데 service worker는 fetch event listener를 등록하여 이벤트를 감지하고 있다가 fetch이벤트가 발생할 때 listener에서 request를 intercept하는 방식이다.

메시지를 통한 서비스 워커와 페이지간의 커뮤니케이션

  • 윈도우에서 해당 윈도우를 제어하는 서비스 워커로 메시지 보내기
  • 서비스 워커에서 범위 내의 모든 윈도우로 메시지 보내기
  • 서비스 워커에서 특정 윈도우로 메시지 보내기
  • 윈도우간 메시지 보내기 (서비스 워커를 통해)

페이지에서 서비스워커를 가져오려면 다음과 같이한다.

navigator.serviceWorker.controller

그 후에는 메시지 자체를 첫번째 인수로 받는 서비스 워커의 postMessage() 메서드를 활용한다.이 메시지는 무엇이든 될 수 있다.

메시지가 게시되고 나면 서비스 워커는 message 이벤트를 수신하여 해당 메시지를 받을 수 있다.

self.addEventListener("message", function (event) {
console.log(event.data)
})

위 예제는 메시지를 받아서 콘솔에 출력한다. 메시지에 포함된 콘텐츠는 이벤트 리스너에 전달된 이벤트 객체의 데이터 속성에서 찾을 수 있다. 메시지 데이터 자체를 포함하는 것 이외에도 이벤트 객체는 여러 유용한 속성을 가지고 있다. 가장 유용한 속성중 일부는 source 속성에 있다.

source 속성에는 메시지를 보낸 윈도우 정보가 들어있다. 이 정보를 활용해서 무엇을 할지 그리고 응답을 어디로 보낼지 판단할 수 있다. 다음 코드는 메시지의 source 속성을 사용하는 간단한 예제 코드이다.

self.addEventListener("message", function (event) {
console.log("Message received:", event.data)
console.log("From a window with the id:", event.source.id)
console.log("which is currently pointing at:", event.source.url)
console.log("and is", event.source.focused ? "focused" : "not focused")
console.log("and", event.source.visibilityState)
})

페이지에서 서비스워커로 해당 페이지를 캐시하라고 요청을 보낸다.

navigator.serviceWorker.controller.postMessage("cache-current-page")

사용자가 해당 페이지에 방문하면 서비스 워커에 메시지를 보냅니다. 서비스 워커는 이 메시지를 수신하고, 어느 페이지를 캐싱할지 결정하기 위해 이벤트 source 속성의 값을 사용할 수 있다.

서비스 워커에서 페이지로 메시지를 보내는 것은 페이지에서 서비스 워커로 메시지를 게시하는 것과 비슷하다. 유일한 차이점은 postMessage()를 호출하는 객체이다. 지금까지 서비스 워커에서 postMessage()를 호출했다면 이번에는 서비스 워커 클라이언트에서 호출한다.

서비스 워커내에서 서비스 워커의 글로벌 객체인 clients 객체를 사용해 현재 서비스 워커 범주안에 열려 있는 모든 윈도우(Window Clients)를 가져올 수 있다. clients 객체는 matchAll 메서드를 가지고 있다. 이 메서드는 서비스워커 범주 내에 열려 있는 모든 윈도우를 가져오는데 사용된다. matchAll() 메서드는 0개 혹은 그 이상의 WindowClient 객체를 담는 배열로 리졸브되는 프로미스를 반환한다.

self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
if (client.url.includes("/my-account")) {
client.postMessage("Hi client: " + client.id;
}
})
})

이 코드는 현재 서비스 워커가 제어하는 모든 클라이언트를 가져와 하나씩 돌며 페이지를 표시하고 있는 윈도우로 메시지를 보낸다. 이 메시지를 윈도우에서 받는 코드는 아래와 같다.

navigator.serviceWorker.addEventListener("message", function (event) {
console.log(event.data)
})

서비스 워커가 설치되고 필요한 모든 리소스 캐싱이 끝나면 앱 사용자에게 온라인이든 오프라인이든 상관없이 앱을 사용할 수 있다고 알려야한다.

self.addEventListener('install', function() {
event.waitUntil (
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(CACHED_URLS)
}).then(function() {
return self.clients.matchAll({
includeUncontrolled: true
})
}).then(function (clients) {
clients.forEach(client) {
client.postMessage('caching-complete')
}
})
)
})

위 코드에서 clients.matchAll()를 호출할 때 옵션 객체를 전달하여 제어되지 않는 클라이언트도 결과에 포함되도록 요청한다.

사용자가 페이지에 처음 방문하면 서비스 워커가 설치되고 활성화된다. 하지만 페이지는 여전히 서비스 워커에의해 제어되지 않는다. 만일 self.clients.matchAll()에 제어되지 않는 윈도우를 포함하지 않도록 알려주지 않았다면 메시지는 목적지에 도달하지 못한다.

LifeCycle

서비스 워커의 라이프사이클은 복잡하다.
서비스 워커가 무엇을 하려는지, 그리고 어떤 그 라이프사이클이 어떤 이점이 있는지 모른다면, 서비스워커와 싸우고 있는 것처럼 느껴질 수 있다. 하지만 작동 방식을 알고 나면 웹과 네이티브 패턴의 장점을 혼합하여 사용자에게 원활하고 눈에 거슬리지 않는 업데이트를 제공할 수 있다.

서비스 워커 라이프 사이클의 목적은 다음과 같다.

  • Make offline-first possible.
  • 새 서비스 워커가 현재 서비스 워커를 방해하지 않고 스스로 준비할 수 있도록 허용한다.
  • 범위 내 페이지가 전체적으로 동일한 서비스 워커에 의해 제어되는지(또는 서비스워커가 없는지) 확인한다.
  • 한 번에 하나의 사이트 버전만 실행되도록 한다.

마지막은 매우 중요하다. 서비스 워커가 없으면 사용자는 사이트에 하나의 탭을 로드한 다음 나중에 다른 탭을 열 수 있다. 이렇게 되면 사이트의 두 가지 버전이 동시에 실행될 수 있다. 가끔은 괜찮을 수도 있지만, 스토리지를 다루는 경우 공유 스토리지 관리 방식에 대해 서로 다른 의견을 가진 두 개의 탭이 쉽게 생길 수 있다. 이로 인해 오류가 발생하거나 최악의 경우 데이터가 손실될 수 있다.

라이프 사이클의 기본에 대해서 알아보자.

  • install 이벤트는 서비스 워커가 처음 받는 이벤트이며 한 번만 발생된다.
  • installEvent.waitUntil()에 전달된 프로미스는 설치의 기간과 성공 또는 실패를 알린다.
  • 서비스 워커는 성공적으로 설치를 완료하고 active가 될 때까지 가져오기 및 푸시 같은 이벤트를 수신하지 않는다.
  • 기본적으로 페이지 요청 자체가 서비스 워커를 거치지 않는 한 페이지의 fetch는 서비스 워커를 거치지 않는다. 따라서 서비스 워커의 효과를 확인하려면 페이지를 새로고침 해줘야한다.
  • clients.claim()은 기본값을 재정의하고 제어되지 않은 페이지를 제어할 수 있다.

Install

서비스 워커가 받는 첫 번째 이벤트는 install입니다. 이 이벤트는 워커가 실행되는 즉시 트리거되며 서비스 워커당 한 번만 호출됩니다. 서비스 워커 스크립트를 변경하면 브라우저는 이를 다른 서비스 워커로 간주하고 다른 설치 이벤트를 받는다.

설치 이벤트는 클라이언트를 제어하기 전에 필요한 모든 것을 캐시할 수 있는 기회이다. event.waitUntil()에 전달한 프로미스는 설치가 완료된 시점과 성공 여부를 브라우저에게 알려준다. 프로미스가 거부되면 설치에 실패했다는 신호가 되고 브라우저는 서비스 워커를 버린다.

Activate

서비스 워커가 클라이언트를 제어하고 pushsync와 같은 기능적 이벤트를 처리할 준비가 되면 활성화 이벤트를 받게 된다. 하지만 그렇다고 해서 .register()를 호출한 페이지가 제어되는 것은 아니다.

clients claim

서비스 워커가 활성화되면 서비스 워커 내에서 clients.claim()을 호출하여 제어되지 않는 클라이언트를 제어할 수 있다. 서비스 워커를 사용하여 페이지를 네트워크를 통해 로드할 때와 다르게 로드하는 경우 clients.claim() 없이 로드되는 일부 클라이언트를 서비스 워커가 제어하게 되므로 문제가 될 수 있다.(타이밍의 문제)

많은 사람들이 clients.claim()을 상용구처럼 사용하는 것을 보지만, 저는 거의 사용하지 않습니다. 처음 로드할 때만 중요하며, 점진적인 개선으로 인해 페이지가 서비스 워커 없이도 원활하게 작동하는 경우가 대부분이다.

Updating the service worker

다음 중 하나라도 발생하면 업데이트가 트리거된다.

  • 범위 내의 페이지로 이동하는 경우
  • 푸시 및 동기화와 같은 기능적 이벤트는 지난 24시간 이내에 업데이트 확인이 없는 경우이다.
  • 서비스 워커 URL이 변경된 경우에만 .register()를 호출한다. 그러나 워커 URL을 변경하지 않아야 한다.
  • Chrome 68 이상을 포함한 대부분의 브라우저는 등록된 서비스 워커 스크립트의 업데이트를 확인할 때 캐싱 헤더를 무시하도록 기본 설정되어 있다.importScripts()를 통해 서비스 워커 내부에 로드된 리소스를 가져올 떄는 여전히 캐싱 헤더를 존중한다. 서비스 워커를 등록할 때 updateViaCache 옵션을 설정하여 이 기본 동작을 재정의할 수 있다.
  • 서비스 워커가 브라우저에 이미 있는 것과 바이트 단위로 다르면 업데이트된 것으로 간주된다.
  • 업데이트된 서비스 워커는 기존 워커와 함께 시작되며 자체 설치 이벤트를 받는다.
  • 새 워커에 확인되지 않은 상태 코드가 있거나, 구문 분석에 실패하거나, 실행 중에 오류가 발생하거나, 설치 중에 거부되는 경우 새 워커는 버려지지만 현재 워커는 활성 상태로 유지된다.
  • 설치에 성공하면 업데이트된 워커는 기존 워커가 클라이언트를 0개 제어할 때까지 기다린다.
  • self.skipWaiting()은 대기를 방지하여 서비스 워커가 설치가 완료되는 즉시 활성화되도록 한다.

Install

캐시 이름을 static-v1에서 static-v2로 변경했습니다. 즉, 기존 서비스 워커가 여전히 사용 중인 현재 캐시의 내용을 덮어쓰지 않고 새 캐시를 설정할 수 있다.

이 패턴은 네이티브 앱이 실행 파일과 함께 번들로 제공하는 에셋과 유사하게 버전별 캐시를 생성한다. 아바타처럼 버전에 특정되지 않은 캐시가 있을 수도 있다.

Waiting

성공적으로 설치되면 업데이트된 서비스 워커는 기존 서비스 워커가 더 이상 클라이언트를 제어하지 않을 때까지 활성화를 지연한다. 이 상태를 "대기 중"이라고 하며, 브라우저에서 한 번에 하나의 서비스 워커 버전만 실행되도록 하는 방법이다.

데모 탭이 하나만 열려 있더라도 페이지를 새로 고치는 것만으로는 새 버전이 적용될 수 없다. 이는 브라우저 탐색이 작동하는 방식 때문이다. 탐색할 때 응답 헤더가 수신될 때까지 현재 페이지가 사라지지 않으며, 응답에 Content-Disposition 헤더가 있는 경우에도 현재 페이지가 유지될 수 있다. 이러한 중첩으로 인해 현재 현재 서비스 워커는 새로 고침 중에 항상 클라이언트를 제어해야한다.

Activate

이전 서비스 워커가 사라지고 새 서비스 워커가 클라이언트를 제어할 수 있게 되면 이 기능이 실행된다. 이때는 데이터베이스 마이그레이션, 캐시 지우기 등 이전 작업자가 사용 중 일 때는 할 수 없었던 작업을 수행하기에 이상적인 시기이다.

참고