서비스 워커 및 애플리케이션 셸 모델

단일 페이지 웹 애플리케이션 (SPA)의 일반적인 아키텍처 기능은 애플리케이션의 전역 기능을 구동하는 데 필요한 최소한의 HTML, CSS, 자바스크립트 집합입니다. 실제로는 모든 페이지에서 유지되는 헤더, 탐색 및 기타 일반적인 사용자 인터페이스 요소입니다. 서비스 워커가 이 최소 UI의 HTML 및 종속 애셋을 사전 캐시하면 이를 애플리케이션 셸이라고 합니다.

애플리케이션 셸의 다이어그램 상단에 헤더가 있고 하단에 콘텐츠 영역이 있는 웹페이지의 스크린샷입니다. 헤더의 라벨은 'Application Shell'이고 하단에는 'Content'라는 라벨이 지정되어 있습니다.

애플리케이션 셸은 웹 애플리케이션에서 인지되는 성능에서 중요한 역할을 합니다. 콘텐츠가 가장 먼저 로드되므로 사용자가 콘텐츠가 사용자 인터페이스에 채워질 때까지 기다리는 동안 가장 먼저 표시됩니다.

네트워크를 사용할 수 있고 최소한 어느 정도 빠른 경우 애플리케이션 셸은 빠르게 로드되지만, 애플리케이션 셸 및 관련 애셋을 사전 캐시하는 서비스 워커는 애플리케이션 셸 모델에 다음과 같은 추가적인 이점을 제공합니다.

  • 재방문에 대해 안정적이고 일관된 실적을 제공합니다. 서비스 워커가 설치되지 않은 앱을 처음 방문할 때 애플리케이션의 마크업과 관련 자산을 네트워크에서 로드해야 서비스 워커가 캐시에 저장할 수 있습니다. 그러나 재방문 시 캐시에서 애플리케이션 셸을 가져오므로 로드와 렌더링이 즉시 수행됩니다.
  • 오프라인 시나리오에서도 기능에 안정적으로 액세스. 때때로 인터넷 접속이 불안정하거나 아예 없을 때 "해당 웹사이트를 찾을 수 없습니다"라는 화면 때문에 걱정스러울 때가 있습니다. 애플리케이션 셸 모델은 캐시에서 애플리케이션 셸 마크업으로 모든 탐색 요청에 응답하여 이를 해결합니다. 누군가 이전에 방문한 적이 없는 웹 앱의 URL을 방문하더라도 애플리케이션 셸은 캐시에서 제공되며 유용한 콘텐츠로 채워질 수 있습니다.

애플리케이션 셸 모델을 사용해야 하는 경우

애플리케이션 셸은 경로마다 변경되지 않지만 콘텐츠는 변경되는 공통 사용자 인터페이스 요소가 있을 때 가장 적합합니다. 대부분의 SPA는 실제로 애플리케이션 셸 모델을 이미 사용할 가능성이 높습니다.

이것이 프로젝트에 설명되어 있고 서비스 워커를 추가하여 안정성과 성능을 향상시키려는 경우 애플리케이션 셸은 다음을 수행해야 합니다.

  • 빠르게 로드됩니다.
  • Cache 인스턴스의 정적 애셋을 사용합니다.
  • 헤더 및 사이드바와 같은 일반적인 인터페이스 요소를 페이지 콘텐츠와 별도로 포함합니다.
  • 페이지별 콘텐츠를 검색하고 표시합니다.
  • 필요한 경우 오프라인에서 볼 수 있도록 동적 콘텐츠를 캐시합니다(선택사항).

애플리케이션 셸은 API 또는 자바스크립트에 번들된 콘텐츠를 통해 페이지별 콘텐츠를 동적으로 로드합니다. 또한, 애플리케이션 셸의 마크업이 변경되면 서비스 워커 업데이트가 새 애플리케이션 셸을 선택해 자동으로 캐시해야 한다는 점에서 자동 업데이트되어야 합니다.

애플리케이션 셸 빌드

애플리케이션 셸은 콘텐츠와는 별개로 존재해야 하지만 콘텐츠 내에 채워질 콘텐츠의 기반을 제공해야 합니다. 되도록이면 슬림하지만 초기 다운로드에 사용자 환경이 빠르게 로드되고 있음을 사용자가 이해할 수 있도록 의미 있는 콘텐츠를 충분히 포함하는 것이 이상적입니다.

적절한 균형은 앱에 따라 다릅니다. 제이크 아치볼드의 Trained To Thrill 앱용 애플리케이션 셸에는 Flickr에서 새 콘텐츠를 가져올 수 있는 새로고침 버튼이 있는 헤더가 포함되어 있습니다.

두 가지 상태에 있는 Trained to Thrill 웹 앱의 스크린샷 왼쪽에는 캐시된 애플리케이션 셸만 표시되며 콘텐츠는 채워지지 않습니다. 오른쪽에서는 콘텐츠 (일부 기차의 사진 몇 장)가 애플리케이션 셸의 콘텐츠 영역에 동적으로 로드됩니다.

애플리케이션 셸 마크업은 프로젝트마다 다르지만, 다음은 애플리케이션 상용구를 제공하는 index.html 파일의 한 가지 예입니다.

​​<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      Application Shell Example
    </title>
    <link rel="manifest" href="/manifest.json">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="styles/global.css">
  </head>
  <body>
    <header class="header">
      <!-- Application header -->
      <h1 class="header__title">Application Shell Example</h1>
    </header>

    <nav class="nav">
      <!-- Navigation items -->
    </nav>

    <main id="app">
      <!-- Where the application content populates -->
    </main>

    <div class="loader">
      <!-- Spinner/content placeholders -->
    </div>

    <!-- Critical application shell logic -->
    <script src="app.js"></script>

    <!-- Service worker registration script -->
    <script>
      if ('serviceWorker' in navigator) {
        // Register a service worker after the load event
        window.addEventListener('load', () => {
          navigator.serviceWorker.register('/sw.js');
        });
      }
    </script>
  </body>
</html>

그러나 프로젝트에서 애플리케이션 셸을 생성할 때 다음과 같은 특성을 지녀야 합니다.

  • HTML은 개별 사용자 인터페이스 요소에 대해 명확하게 분리된 영역이 있어야 합니다. 위의 예에서는 애플리케이션의 헤더, 탐색, 기본 콘텐츠 영역, 콘텐츠를 로드할 때만 표시되는 로딩 '스피너' 공간이 포함됩니다.
  • 애플리케이션 셸을 위해 로드되는 초기 자바스크립트 및 CSS는 최소화되어야 하며 콘텐츠가 아닌 애플리케이션 셸 자체의 기능과만 관련이 있어야 합니다. 이렇게 하면 애플리케이션이 셸을 최대한 빠르게 렌더링하고 콘텐츠가 표시될 때까지 기본 스레드 작업이 최소화됩니다.
  • 서비스 워커를 등록하는 인라인 스크립트

애플리케이션 셸이 빌드되면 서비스 워커와 서비스 워커를 모두 캐시하는 서비스 워커를 빌드할 수 있습니다.

애플리케이션 셸 캐싱

애플리케이션 셸과 필요한 애셋은 서비스 워커가 설치 시 즉시 사전 캐시해야 하는 대상입니다. 위의 예시와 같은 애플리케이션 셸을 가정하고 workbox-build을 사용하여 기본 Workbox 예시에서 이를 실행하는 방법을 살펴보겠습니다.

// build-sw.js
import {generateSW} from 'workbox-build';

// Where the generated service worker will be written to:
const swDest = './dist/sw.js';

generateSW({
  swDest,
  globDirectory: './dist',
  globPatterns: [
    // The necessary CSS and JS for the app shell
    '**/*.js',
    '**/*.css',
    // The app shell itself
    'shell.html'
  ],
  // All navigations for URLs not precached will use this HTML
  navigateFallback: 'shell.html'
}).then(({count, size}) => {
  console.log(`Generated ${swDest}, which precaches ${count} assets totaling ${size} bytes.`);
});

build-sw.js에 저장된 이 구성은 shell.html에 포함된 애플리케이션 셸 마크업 파일을 포함하여 앱의 CSS 및 자바스크립트를 가져옵니다. 스크립트는 다음과 같이 Node를 사용하여 실행됩니다.

node build-sw.js

생성된 서비스 워커는 ./dist/sw.js에 기록되며 완료되면 다음 메시지가 로깅됩니다.

Generated ./dist/sw.js, which precaches 5 assets totaling 44375 bytes.

페이지가 로드되면 서비스 워커가 애플리케이션 셸 마크업과 해당 종속 항목을 사전 캐시합니다.

네트워크에서 다운로드한 애셋 목록을 보여주는 Chrome DevTools의 네트워크 패널 스크린샷 서비스 워커에 의해 사전 캐시된 애셋은 행의 왼쪽에 톱니바퀴가 있는 다른 애셋과 구분됩니다. 여러 JavaScript 및 CSS 파일은 설치 시 서비스 워커에 의해 사전 캐시됩니다.
서비스 워커는 설치 시 애플리케이션 셸의 종속 항목을 사전 캐시합니다. 사전 캐싱 요청은 마지막 두 행이며, 요청 옆에 있는 톱니바퀴 아이콘은 서비스 워커가 요청을 처리했음을 나타냅니다.

번들러를 사용하는 프로젝트를 비롯한 거의 모든 워크플로에서 애플리케이션 셸의 HTML, CSS 및 JavaScript를 미리 캐시할 수 있습니다. 이 문서를 살펴보다 보면 Workbox를 직접 사용하여 도구 모음을 설정하여 SPA이든 프로젝트에 가장 적합한 서비스 워커를 빌드하는 방법을 배우게 됩니다.

결론

애플리케이션 셸 모델을 서비스 워커와 결합하면 오프라인 캐싱에 효과적이며, 특히 마크업 또는 API 응답을 위한 사전 캐싱 기능을 네트워크 우선, 캐시로 대체 전략과 결합하는 경우에 특히 유용합니다. 그 결과, 오프라인 상태에서도 재방문 시 애플리케이션 셸을 즉시 렌더링하는 안정적이고 빠른 환경이 구축됩니다.