[태그:] pwa

  • 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>');
          

        2. PWA 핵심 구성요소: Manifest


          Manifest?

          PWA가 단순한 웹에서 앱이 되는 전환점은 manifest.json에 있다.

          이 파일은 브라우저에게 이 웹이 앱처럼 설치될 수 있다는 신호를 준다.

          즉, 웹의 메타데이터(metadata) 역할을 하며

          아이콘, 이름, 시작 경로, 표시 모드(display mode) 등을 정의한다.

          Manifest 파일의 기본 구조

          {
            "name": "Lux Store",
            "short_name": "Lux",
            "start_url": "/",
            "display": "standalone",
            "background_color": "#ffffff",
            "theme_color": "#111111",
            "icons": [
              {
                "src": "/icons/icon-192.png",
                "sizes": "192x192",
                "type": "image/png"
              },
              {
                "src": "/icons/icon-512.png",
                "sizes": "512x512",
                "type": "image/png"
              }
            ]
          }
          
          필드역할
          name앱의 전체 이름, 홈 화면에 표시될 이름
          short_name아이콘 아래 짧은 이름
          start_url앱 실행 시 처음 열릴 경로
          display앱 표시 방식(standalone, fullscreen, minimal-ui, browser)
          background_color스플래시 화면의 배경색
          theme_color브라우저 툴바 색상
          icons설치 아이콘 이미지(192px, 512px 이상 권장)

          Display 모드가 바꾸는 UX

          display 속성은 앱이 실행될 때의 화면 형태를 결정한다.

          일반적으로는 PWA는 standalone을 사용,

          사용자는 홈 화면에서 실행했을 때 브라우저 UI가 사라지고 앱 전용 창을 오픈한 경험을 얻는다.

          • browser: 일반 웹처럼 주소창・탭 노출
          • minimal-ui: 주소창만 최소화된 브라우저
          • standalone: 앱처럼 동작, 주소창 없음
          • fullscreen: 완전한 전체 화면 모드



          Add to Home Screen(A2HS) 동작 과정

          브라우저는 다음 조건이 충족되면 이 웹을 설치할 수 있다는 프로그래시브 신호를 감지한다.

          다음 조건이 만족되면 Chrome, Edge 등은 자동으로 A2HS 가능 상태로 전환한다.

          • 설치 조건
            1. HTTPS 환경에서 제공될 것
            2. 유효한 manifest.json이 존재할 것
            3. Service Worker가 등록되어 있을 것



          beforeinstallprompt 이벤트로 제어하기

          브라우저가 설치 가능한 상태가 되면, beforeinstallprompt 이벤트가 발생한다.

          개발자는 사용자에게 언제, 어떻게 설치 안내를 띄울지 설정할 수 있다.

          예를 들어, 페이지 진입 10초후에 설치 배너 표시하려고 할 때:

          let deferredPrompt;
          
          window.addEventListener('beforeinstallprompt', (e) => {
            e.preventDefault();
            deferredPrompt = e;
          
            // 커스텀 설치 버튼 노출
            const installBtn = document.querySelector('#install-btn');
            installBtn.style.display = 'block';
          
            installBtn.addEventListener('click', async () => {
              deferredPrompt.prompt();
              const choice = await deferredPrompt.userChoice;
              if (choice.outcome === 'accepted') {
                console.log('사용자가 설치를 수락했습니다.');
              }
              deferredPrompt = null;
            });
          });
          

          홈 화면에 추가된 후

          설치가 완료되면, 브라우저는 manifest의 정보를 기반으로 OS 수준의 앱 아이콘을 생성한다.

          설치된 PWA는 이후 브라우저 캐시 및 서비스 워커를 통해 오프라인에서도 실행 가능.

          • Android: 런처 아이콘으로 표시, 실행 시 standalone 창에서 구동
          • iOS(Safari): 홈 화면에 추가 수동 트리거 방식(자동 배너 없음)
          • 데스크톱 Chrome: 앱 설치 시 OS 프로그램 목록에도 추가됨

          설치 UX를 CTA(Call To Action)로 디자인하면 전환율이 높아진다.

          e.g. 앱으로 더 빠르게 열기


          manifest 검증과 진단은 어떻게 할까?

          Chrome DevTools → Application → Manifest 탭에서 설치 가능 여부를 실시간으로 확인 가능.

          [Lighthouse]의 [Progressive Web App] 카테고리를 통해 다음 항목을 자동으로 점검할 수 있다.

          • manifest 유효성
          • 앱 설치 가능 여부
          • 오프라인 대응 여부
          • HTTPS 보안 연결 상태