[태그:] service worker

  • PWA 배포와 진단, 그리고 한계

    HTTPS, PWA의 출발선

    PWA는 HTTPS 환경에서만 동작한다.

    Service Worker가 네트워크 요청을 가로채는 구조이기 때문에,

    보안되지 않은 연결(HTTP)에서는 이를 차단한다.

    단순한 보안이 아니라 신뢰 체인의 시작

    앞서 Service Worker가 네트워크 요청을 가로채는 구조라고 언급했다.

    즉 이를 통해 보안을 전제로 성립하지 않으면, 악성 스크립트가 중간에서 데이터를 변조하기에

    매우 좋다는 위험성을 내포한다는 것을 알 수 있다.

    따라서, HTTPS 환경은 PWA에서 매우 기본적 전제 조건으로 성립한다.

    (애초에 브라우저는 HTTPS 환경이 아니라면 PWA를 등록조차 하지 않는다)

    실무에서의 HTTPS 이슈

    1. 내부망(사내망) 개발 환경

    사설 인증서(Self-signed Certificate)를 사용하는 경우, Chrome은 기본적으로

    Service Worker 등록을 차단한다.

    → chrome://flags/#allow-insecure-localhost 옵션을 임시 허용하거나, mkcert / local CA 기반 개발 인증서 사용

    2. 멀티 도메인 환경

      Service Worker의 scope는 도메인 단위로 묶인다.

      리소스를 다른 서브도메인에서 캐싱하려면 CORS + HTTPS 동일 정책 필수.

      → 동일 origin 정책을 고려하여 serviceWorker.register(’/sw.js’, { scope: ‘/’ }) 구성 조정.

      3. SSL 갱신 자동화 이슈

        HTTPS 인증서가 만료되면 PWA는 즉시 installable 자격을 상실한다.

        (브라우저가 신뢰할 수 없는 컨텍스트로 판단한다.)

        따라서 CI/CD 파이프라인에서 certbot renew 또는 Cloudflare API 기반

        SSL 자동 갱신 스크립트 포함 필요.

        4. Mixed Content 차단

          HTTPS 페이지 내에서 HTTP 자원을 요청하면 Service Worker는 정상 작동하지 않는다.

          해당 요청은 네트워크 캐시에도 등록되지 않는다.



          PWA 품질 진단

          Chrome DevTools의 Lighthouse는 PWA 품질을 자동으로 분석해준다.

          평가 항목은 다음과 같다.

          PWA 품질 평가 항목

          • Installable: HTTPS, Manifest, Service Worker 등 설치 조건 충족 여부
          • Performance: LCP, FID, CLS 등 핵심 성능 지표
          • Best Practices: 보안 및 접근성 검증
          • Accessibility: 시각적, 키보드 접근성 테스트
          • SEO: 검색엔진 메타 구성 확인

          PWA 품질 진단 실행

          [DevTools] – [Lighthouse 탭] – [Progressive Web App] 선택,

          결과 리포트에서 Installable 과 Offline ready 항목이 모두 초록색이면 OK

          *고품질의 PWA

          오프라인 대응 + HTTPS + Manifest + 빠른 로딩 + 접근성 확보


          캐시 버전 관리

          PWA의 가장 큰 함정은 캐시 갱신이 느리다는 것,

          Service Worker는 안정성을 위해 자동으로 새 버전을 즉시 활성화하지 않는다.

          이로 인해 사용자가 이전 캐시를 여전히 보고 있을 가능성이 존재하고,

          새로운 코드가 배포되어도 즉시 반영되지 않게 된다.

          해결하려면

          • 캐시 버전 + 업데이트 알림
          • 클라이언트 측에서 새 버전 감지 시 알림 띄우기

          오프라인 테스트

          PWA 배포가 끝나면 오프라인 시나리오 테스트 진행이 필요하다.

          Chrome DevTools → [Application] → [Service Workers] → Offline 체크 → 새로고침

          이 과정에서 캐시된 콘텐츠가 보이면 정상 작동이다.

          • 추가로 점검해야 할 사항
            • 새로고침 시 캐시된 버전이 즉시 뜨는가?
            • fetch 실패 시 fallback 페이지(offline.html)로 전환되는가?
            • Service Worker가 업데이트될 때 새 버전 적용이 정상인가?

          PWA의 한계

          여전히 제한적인 PWA

          브라우저별로 Service Worker의 업데이트 시점이 불균일하고, 캐시 관리 복잡도 및 네트워크 동기화 지연 이슈로 현재까지는 아직 PWA에서 한계가 보인다. 또한, iOS에서 PWA에 대한 지원을 제약하기 때문에 PWA는 완성된 기술이 아닌 진화 과정의 일부 단계로 보인다.

          iOS에서의 PWA 제약

          iOS(Safari)의 PWA 지원은 여전히 제약적이고 Apple은 보안 및 배터리 이슈를 이유로 일부 기능을 막고 있다.

          기능지원 여부비고
          Push NotificationiOS 16.4+제한적 지원, 유저 승인 필요
          Background SyncX불가능
          Storage(Cache Size)약 50MB 한도자동 삭제 가능성 있음
          Install PromptX (자동 배너 없음)홈 화면에 추가 수동 유도
          Web Bluetooth / USBX미지원 API 다수

        1. 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>');