김서버의 프론트엔드 일기

Service Worker란? (with PWA) 본문

공부하는거/Web

Service Worker란? (with PWA)

kimSerVer 2022. 2. 7. 22:10

개요

 Service Worker 는 우리가 사용하는 브라우저의 캐시 중에 하나입니다. 이 Service Worker는 요즘 웹 푸시 메시지 보내기와, PWA의 핵심 기술로 사용이 되고 있습니다.

 이 Service Worker와의 첫 인연은 그렇게 좋지 않았습니다. 왜냐하면 회사에서 웹 클라이언트에서 버그를 수정하거나, 기능을 추가하고 마스터에 버전을 올려도 계속해서 과거의 버전의 웹이 사용이 되는 현상이 있었습니다.

 그래서 추후의 업데이트 때도, 이런 증상이 나타나면 곤란해지기 때문에 반드시 해결을 해야하는 문제였었고, 그때 당시 저는 제일 먼저

주로 캐시가 원인일 것으로 생각을 하고 원인을 분석을 했으나, 오랜 삽질 끝에 밝혀진 원인은 Service Worker가 설정이 됨으로 나타나는 현상이었습니다. 

 이 때 공부한 내용을 정리하기 위한 글로 Service Worker의 기본적인 구조와 함께, 어떤 원인으로 위 문제가 발생했는지에 대해서 알아보겠습니다.

웹 브라우저에서 서버까지 캐시를 할 수 있는 모든 과정

이론

우선 Service Worker의 작동 방식은 Web Worker를 기반으로 하고 있기 때문에, 최신 웹 브라우저에서 사용이 가능한 Web Worker의 기본 작동 방식을 이해를 먼저 하는 것이 좋습니다. Web Worker는 기존에 Single Thread 방식이 아닌 웹에서 Multi Thread 방식으로 웹을 돌아가게 하는 방식입니다. Web Worker는 Main Thread와는 다른 Thread를 만들어서 Main Thread와 postMessage로 통신하면서 작동을 시킬 수 있는 방식입니다.

다음의 예제 코드를 실행해보면서 결과의 차이를 알아보겠습니다.

<!-- index.html --> 
<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h1>인덱스</h1>
    <script src="/index.js"></script>
  </body>
</html>
// index.js

const function1 = () => {
  const myWorker = new Worker("/worker.js");
  console.time("function1");

  myWorker.onmessage = function (event) {
    console.log("받음");
    console.log(event.data);
  };
  myWorker.postMessage("실행하거라");
  console.timeEnd("function1");

  console.log("다른 함수를 실행하러 바로 갑니다.");
};
function1();

const function2 = () => {
  let result = 0;
  console.time("function2");
  for (let i = 0; i <= 1e10; i++) {
    result += 1;
  }
  console.log(result);
  console.timeEnd("function2");

  console.log("for 문에서 block이 걸려서 다른 함수를 이제 실행하러 갑니다.");
};

function2();
// worker.js
self.onmessage = (e) => {
  let result = 0;
  for (let i = 0; i <= 1e10; i++) {
    result += 1;
  }
  self.postMessage(result);
};

위 코드에서 function1 과 function2의 실행 부분을 각각 비교하면 다음과 같습니다.

function1의 실행결과 (with worker)
function2의 실행 결과 (without worker)

콘솔 탭에서 볼 경우, 단순히 동기 비동기 함수의 차이로 보일 수도 있습니다. 하지만 이 부분은 performance탭을 통해 보면 다음과 같은 큰 차이가 있음을 알 수 있습니다. 

Function1 (with worker)
function2 (without worker)

Web Worker 를 통해서 함수를 실행할 경우, 새로운 thread를 생성해서 실행을 시키는 것과, 모든 로직을 오로지 main thread에서만 실행한다는 차이가 있습니다.

 이 차이는 엄청 오래 걸리는 로직을 처리 하게 될 때, 브라우저의 해당 탭이 정지하고 로직을 처리하느냐, 그렇지 않고 처리를 하느냐라는 차이를 불러올 수가 있습니다.

 Web Worker를 통해서 로직을 처리하게 될 경우, 메인 스레드와는 독립적으로 실행하기 때문에 마치 멀티 스레드를 사용하는 것처럼  사용할 수 있게 됩니다. 하지만 DOM을 직접 조작할 수 없고, window의 일부 메서드와 속성은 사용할 수 없는 단점은 있으나 WebSocket과 IndexedDB를 포함한 많은 수의 항목은 사용 가능합니다.

Service Worker

 ServiceWorker는 웹에서 정적 파일들(html, js, css, img, etc)을 최신 브라우저들이 제공하는 Cache Storage를 통해서 서빙할 수 있게 해주는 기능입니다. 그래서 인터넷이 오프라인이 되어도 정적 파일들이 캐싱되어있으면, 해당 파일 들을 클라이언트에 제공해서, 사이트를 이용할 수 있게 합니다.

 그리고 Service Worker가 실행되는 환경은 Web Worker의 환경에서 돌아가게 됩니다. 즉 메인 쓰레드를 차지하지 않고 별도의 스레드를 만들어서 백그라운드로 실행을 하게 됩니다.

Service worker를 지원하는 브라우저들

위 사진은 serviceWorker.register()를 하면 저장이 되는 것을 확인 하는 곳으로 프로토콜 부분을 보면 https가 아닌 로컬의 chrome으로부터 가져오는 것을 확인할 수 있습니다. (chrome://inspect/#service-workers)

실습

Service Worker에서 정말 간단한 PWA를 만들어보면서 실습을 해보도록 하겠습니다.

// main.js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("./sw.js")
}
// sw.js
const CACHE_NAME = "example-cache-v1.10.24"; // 현재 버전을 명시해주는 것이 좋습니다.

const FILES_TO_CACHE = []; // 캐싱할 정적파일 주소

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("service worker pre-caching offline page");
      return cache.addAll(FILES_TO_CACHE);
    })
  );
  // console.log("서비스워커 설치 (install)함");
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(
        keyList.map((key) => {
          if (key !== CACHE_NAME) {
            return caches.delete(key);
          }
        })
      );
    })
  );

  // console.log("서비스 워커 동작(activate)함");
});

self.addEventListener("fetch", (event) => {
  // console.log("데이터 (fetch) 요청!", event.request);
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((res) => {
        return res || fetch(event.request);
      });
    })
  );
});

Life Cycle

install (서비스워커 최초 설치, 혹은 새로운 버전 파일들을 설치)  
activate (서비스워커 동작, 변경) 
fetch (데이터 요청)
  • install: Service Worker의 최신 파일을 받아옵니다. 이때 이미 Service Worker에 active상태인 버전이 있을 경우, 받아온 파일들은 wait 상태가 됩니다.
  • activate: 현재 실행하고 있는 Service Worker의 실행 환경입니다.
  • fetch: Service Worker가 active상태가 될 때, 해당 목록을 캐시 서버에서 다운로드를 합니다.

문제점

 일반 적인 개발자들은 버그를 수정하거나, 새로운 기능을 넣어서 배포를 하게 되면, 바로 적용이 되는 것을 기대하게 됩니다. 하지만 Service Worker로 등록이 되어있으면, Life Cycle의 한계에서 install로 새로운 버전의 파일들을 가지고 와도, 이미 active 상태가 되어있는 Service Worker가 있으면 받아온 파일들을 바로 뿌려주지 않고 wait상태로 빠지게 됩니다.

 이렇게 wait 상태로 빠진 버전의 Service Worker는 이전 버전의 Service Worker를 사용하는 페이지가 모두 닫힌 경우 활성화되어 활성 워커가 됩니다.

 한 때, 룩핀은 Service Worker의 이런 문제점을 그대로 겪고 있었습니다. 웹으로 방문을 하는 유저들은 크롬 자체가 완전히 종료가 되어야만 새로운 버전이 업데이트가 되었고, 모바일의 경우에는 업데이트가 잘 이루어지지 않는 현상이 있었습니다.

그때 상황을 해결했던 방식 룩 핀은 PWA의 기능을 전혀 사용하지 않고 있기 때문에, Service Worker가 불필요하다고 판단이 되었고, Service Worker를 비 활성화하는 것으로 캐싱 단계를 전부 없애는 것으로 해결을 했었습니다.

캐싱 문제의 해결 방법

이후에 계속해서 조사를 하던 중, ServiceWorker 파일(sw.js)에서 skipWaiting()을 이용하면 wait 상태의 워커를 즉시 활성 상태로 옮길 수 있다라는 점을 찾아냈습니다.

하지만 그래도 이미 이전 버전의 active의 이후에 fetch로 받아온 정적 파일을 밀어내고 새로 적용하는 것은 불가능해서, 유저가 새로 고침을 하거나 skipWaiting을 감지해서 페이지를 reload를 하는 방식으로 가능합니다.

// main.js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("./sw.js")
}

navigator.serviceWorker.addEventListener("controllerchange", () => {
  // worker에서 skipWaiting을 할 경우 감지를 합니다.
  alert("새로운 업데이트를 감지했습니다. 새로고침 하겠습니다.");
  window.location.reload();
});
// sw.js
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("service worker pre-caching offline page");
      return cache.addAll(FILES_TO_CACHE);
    })
  );
  // 파일을 가져올때, 기존에 있는 캐시가 가져오는 캐시이름과 다를 경우 skipWaiting을 진행한다.
  caches.keys().then((keyList) => {
    keyList.map((key) => {
      if (key !== CACHE_NAME) {
        self.skipWaiting();
      }
    });
  });
  // console.log("서비스워커 설치 (install)함");
});

결론

 Service Worker는 아직 주로 웹 푸시와 PWA를 하기 위한 정도로만 많이 사용이 되고 있고, 아직 IE의 점유율이 높아 활용을 잘 못하고 있는 부분이 있습니다.

 하지만 계속 조사를 하다 보니, 이외에도 굉장히 많은 부분에서 활용이 가능하다고 생각이 들었습니다.

 예를 들면, 어떤 정적 파일을 서빙할 때(큰 사이즈의 이미지, 동영상 등), 그 용량이 꽤나 클 경우 처음에만 설치를 시키고 이후에 재 요청의 경우는 Service Worker에서 불러서 사용하도록 해서, 파일 서버의 과부하를 줄이고, CDN의 트래픽을 줄이는 이점이 있을 것이라고 생각이 들었습니다.

 이외에도 백그라운드 프로세스 동기화 API를 잘 이용하면 여러 가지 방면으로 사용이 가능할 것으로 기대가 되는 브라우저의 기능이라고 생각합니다.