[태그:] 생명주기

  • Service Worker, 그리고 캐싱 전략

    Service Worker?

    Service Worker는 브라우저 백그라운드 스레드(background thread)로 동작한다.

    즉, 메인 스레드(UI 렌더링)와는 별도로 작동.

    웹페이지가 닫혀 있어도 네트워크 요청을 가로채거나 캐싱 제어를 할 수 있는 권한을 갖는다.

    클라이언트와 네트워크 사이에서 프록시(Proxy) 역할을 수행한다.


    Service Worker의 동작 구조

    다음 3단계 생명주기를 갖는다.

    install → activate → fetch

    • install : 처음 등록될 때, 필요한 리소스를 캐시에 저장
    • activate: 이전 버전의 캐시를 정리하고 새 워커 활성화
    • fetch : 모든 네트워크 요청을 가로채 캐시 또는 네트워크로 응답

    이 구조로 인해 한 번 방문한 페이지는 오프라인에서도 작동할 수 있는 로컬 앱처럼 작동한다.

    등록(Registration)

    먼저 클라이언트 측에서 서비스 워커를 등록해야 한다.

    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker
          .register('/sw.js')
          .then(() => console.log('Service Worker registered'))
          .catch(console.error);
      });
    }
    

    /sw.js 파일은 루트 경로에 위치해야 전체 사이트를 제어할 수 있다.

    (하위 폴더에 있으면 해당 스코프 내에서만 동작)

    install 단계 : 캐시 준비

    Service Worker가 처음 설치될 때, 주요 정적 파일을 캐시에 미리 저장한다.

    self.addEventListener('install', (event) => {
      event.waitUntil(
        caches.open('v1').then((cache) => {
          return cache.addAll([
            '/',
            '/index.html',
            '/styles.css',
            '/main.js',
            '/logo.png',
          ]);
        })
      );
    });
    
    • caches.open() : 캐시 스토리지에 새로운 버전 생성
    • cache.addAll() : 지정한 자원들을 미리 다운로드 후 저장
    • waitUntil() : 설치 완료 전 종료되지 않도록 대기

    이 단계는 앱 설치 시 초기 리소스 다운로드와 같은 과정으로 볼 수 있다.

    activate 단계 : 버전 관리 및 정리

    Service Worker가 새로 설치되면 이전 캐시와 교체해야 한다.

    이때 activate 이벤트 실행,

    self.addEventListener('activate', (event) => {
      const cacheWhitelist = ['v1'];
    
      event.waitUntil(
        caches.keys().then((keys) =>
          Promise.all(
            keys
              .filter((key) => !cacheWhitelist.includes(key))
              .map((key) => caches.delete(key))
          )
        )
      );
    });
    

    이 과정들을 통해 이전 버전 캐시가 정리되고 업데이트 된 파일만 남는다.

    fetch 단계 : 요청 가로채기

    fetch 이벤트는 사용자가 페이지를 요청할 때마다 호출된다.

    Service Worker는 이 요청을 가로채 캐시나 네트워크에서 응답을 선택할 수 있다.

    e.g. Cache First + Network Fallback 전략

    self.addEventListener('fetch', (event) => {
      event.respondWith(
        caches.match(event.request).then((cached) => {
          return (
            cached ||
            fetch(event.request).then((response) => {
              return caches.open('v1').then((cache) => {
                cache.put(event.request, response.clone());
                return response;
              });
            })
          );
        })
      );
    });
    

    캐싱 전략

    PWA의 성능과 오프라인 대응을 좌우하는 건 캐싱 전략이다.

    전략은 상황에 따라 다르게 적용한다.

    Cache First(캐시 우선)

    한 번 저장한 건 그대로 쓴다. 네트워크는 보조, 즉시 로딩이 최우선

    • 동작 흐름
      1. 캐시에 해당 요청이 있는지 확인
      2. 있으면 즉시 반환(네트워크 요청 안 함)
      3. 없으면 네트워크 요청 후 캐시에 저장
    • 장점
      • 가장 빠른 응답 속도(특히 이미지, 폰트, 정적 JS/CSS)
      • 네트워크 불안정 시 강력함
    • 장점
      • 캐시 갱신이 어려움(오래된 리소스가 남을 수 있다)
    • 사용 예시
      • 앱 셸(App Shell) 구조의 JS/CSS
      • 로고, 아이콘 등 변하지 않는 정적 자산

    Network First(네트워크 우선)

    항상 최신 데이터를 원한다. 네트워크가 실패하면, 캐시에서 백업한다.

    • 동작 흐름
      1. 네트워크로 요청 시도
      2. 실패 시 캐시에서 가져온다.
    • 장점
      • 항상 최신 데이터 유지
      • API 요청, 뉴스 피드 등 실시간 콘텐츠에 적합
    • 단점
      • 네트워크 지연이 크면 느려짐
      • 오프라인에서는 첫 로드 실패
    • 예시
      • REST API, GraphQL 요청
      • JSON 데이터, 동적 요청

    Stale-While-Revalidate(캐시 우선 + 백그라운드 갱신)

    사용자에겐 캐시를 즉시 보여주고, 뒤에서는 최신 데이터를 받아 업데이트.

    • 동작 흐름
      1. 캐시에 있으면 즉시 반환
      2. 동시에 네트워크 요청으로 최신 데이터 가져오기
      3. 응답을 캐시에 덮어쓰기
    • 장점
      • 빠른 로딩 + 최신성 확보
      • UX적으로 로딩 없이 즉시 뜨는 화면.
    • 단점
      • 캐시와 최신 데이터 불일치 시 순간적 차이 발생 가능.
    • 사용 예시
      • 블로그, 상품 리스트, 피드
      • 자주 바뀌지만 실시간이 아닌 콘텐츠

    Cache Only(캐시 전용)

    네트워크는 무시하고, 오직 캐시만 본다.

    • 동작 흐름
      1. 캐시에서만 자원을 찾음
      2. 없으면 실패 응답 반환
    • 장점
      • 완전한 오프라인 대응 가능
      • 네트워크 불필요
    • 단점
      • 캐시 미스 시 실패(데이터 불일치 가능성 높음)
    • 사용 예시
      • 완전 오프라인 앱(설치 후 고정 콘텐츠)
      • 내부 도움말, 튜토리얼 페이지)

    Network Only(네트워크 전용)

    항상 실시간, 캐시는 전혀 사용하지 않는다.

    • 동작 흐름
      • 요청을 그대로 네트워크로 보낸다.
      • 캐시 관련 로직 없음
    • 장점
      • 항상 최신 데이터
      • 캐시 동기화 불필요
    • 단점
      • 오프라인/느린 네트워크 환경에 매우 취약
    • 사용 예시
      • 로그인, 결제 인증 요청
      • 민감 데이터(POST 요청, 토큰 등)

    어떤 전략을 언제 쓸까?

    상황추천 전략이유
    정적 파일(JS/CSS/폰트)Cache First변경 거의 없음, 속도 최우선
    API 요청, JSON 데이터Network First최신성 중요
    콘텐츠 페이지(블로그, 피드)Stale-While-Revalidate빠른 UX + 자동 업데이트
    완전 오프라인 앱Cache Only네트워크 불필요
    로그인/결제/POST 요청Network Only항상 실시간 통신 필요

    하나의 전략만 사용하기 어려운 상황에서는?

    실무에서는 하나의 전략만을 사용하지 않는다.

    상황에 따라서, 또는 자원 유형별로 다르게 적용하는 것이 필요하다.

    혼합 전략 사용.

    실무에서는?

    캐시 버전 관리

    캐시 이름에 버전을 붙여야 업데이트가 명확히 구분되고 관리하기 용이하다.

    const CACHE_VERSION = 'v3';
    const CACHE_NAME = `static-${CACHE_VERSION}`;
    

    캐시 청소 주기 설정

    이전 버전 캐시는 activate 이벤트에서 정리한다.

    self.addEventListener('activate', (event) => {
      event.waitUntil(
        caches.keys().then((keys) =>
          Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
        )
      );
    });
    

    Workbox 적극 활용

    Google이 만든 Workbox는 캐싱 전략, 버전 관리, 프리캐싱을 자동화한다.

    importScripts('<https://storage.googleapis.com/workbox-cdn/releases/6.5.3/workbox-sw.js>');