애플리케이션 셸 아키텍처를 사용한 웹 앱 즉시 로드

Addy Osmani
Addy Osmani

애플리케이션 셸은 사용자 인터페이스를 지원하는 최소한의 HTML, CSS, JavaScript입니다. 애플리케이션 셸은 다음과 같아야 합니다.

  • 빠르게 로드됨
  • 캐시되어야 함
  • 동적으로 콘텐츠 표시

안정적으로 우수한 성능을 얻기 위해서는 애플리케이션 셸이 필수입니다. 앱의 셸은 네이티브 앱을 빌드할 때 앱 스토어에 게시하는 코드 번들과 같다고 생각해 보세요. 앱 셸은 시작하는 데 필요한 부하이지만 전체 내용은 아닐 수 있습니다. UI를 로컬로 유지하고 API를 통해 콘텐츠를 동적으로 가져옵니다.

앱 셸의 HTML, JS 및 CSS 셸과 HTML 콘텐츠 분리

배경

Alex Russell의 프로그레시브 웹 앱 자료에서는 사용 및 사용자 동의를 통해 웹 앱이 점진적으로 변하여 오프라인 지원, 푸시 알림, 홈 화면에 추가할 수 있는 기능을 통해 네이티브 앱과 유사한 환경을 제공하는 방법을 설명합니다. 서비스 워커의 기능 및 성능 이점과 캐싱 능력에 따라 크게 달라집니다. 이렇게 하면 기본 애플리케이션에서 보던 것과 동일한 즉시 로드 및 정기적인 업데이트를 웹 앱에 제공할 수 있으므로 속도에 집중할 수 있습니다.

이러한 기능을 최대한 활용하려면 웹사이트에 대한 새로운 사고방식, 즉 애플리케이션 셸 아키텍처가 필요합니다.

서비스 워커 증강 애플리케이션 셸 아키텍처를 사용하여 앱을 구성하는 방법을 자세히 알아보겠습니다. 클라이언트 측 렌더링과 서버 측 렌더링을 모두 살펴보고 오늘 시도해 볼 수 있는 엔드 투 엔드 샘플을 공유해 드리겠습니다.

핵심을 강조하기 위해 아래 예에서는 이 아키텍처를 사용하는 앱의 첫 번째 로드를 보여줍니다. 화면 하단에 '앱을 오프라인에서 사용할 준비가 되었습니다.'라는 토스트 메시지가 표시됩니다. 나중에 셸 업데이트를 사용할 수 있게 되면 사용자에게 새 버전으로 새로고침하라고 알릴 수 있습니다.

애플리케이션 셸용 DevTools에서 실행 중인 서비스 워커의 이미지

서비스 워커란 무엇일까요?

서비스 워커는 웹페이지와는 별개로 백그라운드에서 실행되는 스크립트입니다. 이 백도어는 제공하는 페이지의 네트워크 요청, 서버의 푸시 알림 등의 이벤트에 응답합니다. 서비스 워커의 수명은 의도적으로 짧습니다. 이 백도어는 이벤트를 수신하면 절전 모드에서 해제되고, 이벤트를 처리해야 하는 기간 동안만 실행됩니다.

또한 서비스 워커는 일반적인 브라우징 컨텍스트에서 JavaScript와 비교할 때 제한된 API 세트를 갖습니다. 이는 웹의 직원을 위한 표준입니다. 서비스 워커는 DOM에 액세스할 수 없지만 Cache API와 같은 항목에 액세스할 수 있으며, Fetch API를 사용하여 네트워크 요청을 할 수 있습니다. 또한 IndexedDB APIpostMessage()를 사용하여 서비스 워커와 서비스 워커가 제어하는 페이지 간의 데이터 지속성과 메시징에 사용할 수 있습니다. 서버에서 전송된 푸시 이벤트Notification API를 호출하여 사용자 참여도를 높일 수 있습니다.

서비스 워커는 페이지에서 생성된 네트워크 요청을 가로채서 (서비스 워커에서 가져오기 이벤트를 트리거) 네트워크에서 검색된 응답 또는 로컬 캐시에서 검색된 응답 또는 프로그래매틱 방식으로 생성되는 응답을 반환할 수 있습니다. 사실상 브라우저에서 프로그래밍 가능한 프록시입니다. 멋진 부분은 응답의 출처에 상관없이, 웹페이지는 서비스 워커와 전혀 관여하지 않는 것처럼 보인다는 점입니다.

서비스 워커에 대해 자세히 알아보려면 서비스 워커 소개를 읽어보세요.

성능 이점

서비스 워커는 오프라인 캐싱에 강력하지만, 사이트나 웹 앱을 반복해서 방문할 때 즉각적인 로드의 형태로 상당한 성능상 이점을 얻을 수도 있습니다. 애플리케이션 셸을 캐시하여 오프라인에서 작동하도록 하고 JavaScript를 사용하여 콘텐츠를 채울 수 있습니다.

이렇게 하면 재방문 시 네트워크 없이 화면에 의미 있는 픽셀이 표시될 수 있습니다. 이는 결국 콘텐츠가 네트워크에서 전송되더라도 마찬가지입니다. 툴바와 카드를 즉시 표시한 다음 나머지 콘텐츠를 점진적으로 로드한다고 생각하면 됩니다.

실제 기기에서 이 아키텍처를 테스트하기 위해 WebPageTest.org에서 애플리케이션 셸 샘플을 실행하고 그 결과를 아래와 같이 표시했습니다.

테스트 1: Chrome 개발자로 Nexus 5 케이블에서 테스트

앱의 첫 번째 뷰는 네트워크에서 모든 리소스를 가져와야 하며 1.2초에야 유의미한 페인트를 얻을 수 있습니다. 서비스 워커 캐싱 덕분에 반복 방문 시 유의미한 페인트를 달성하고 0.5초 안에 로드를 완전히 완료합니다.

케이블 연결용 웹페이지 테스트 페인트 다이어그램

테스트 2: Chrome 개발자로 Nexus 5로 3G에서 테스트

약간 느린 3G 연결로 샘플을 테스트할 수도 있습니다. 이번에는 첫 방문 시 유의미한 첫 페인트를 얻는 데 2.5초가 걸립니다. 페이지를 완전히 로드하는 데 7.1초가 걸립니다. 서비스 워커 캐싱을 사용하면 반복 방문 시 유의미한 페인트를 달성하고 0.8초 내에 로드를 완전히 완료합니다.

3G 연결을 위한 웹페이지 테스트 페인트 다이어그램

다른 견해에서도 비슷한 이야기를 합니다. 애플리케이션 셸에서 유의미한 첫 페인트를 달성하는 데 걸리는 3초를 비교해 보세요.

웹페이지 테스트의 첫 번째 보기 페인트 타임라인

서비스 워커 캐시에서 같은 페이지가 로드될 때 걸리는 0.9초까지 단축됩니다. 최종 사용자가 사용할 시간을 2초 이상 절약할 수 있습니다.

웹 페이지 테스트의 반복 보기를 위한 페인트 타임라인

애플리케이션 셸 아키텍처를 사용하면 여러분의 애플리케이션에서 유사하고 안정적인 성능을 얻을 수 있습니다.

서비스 워커를 사용하려면 앱 구조화 방식을 재고해야 하나요?

서비스 워커는 애플리케이션 아키텍처에 약간의 변화를 줍니다. 모든 애플리케이션을 HTML 문자열로 찌그러뜨리는 대신 AJAX 스타일로 작업하는 것이 도움이 될 수 있습니다. 여기에는 셸 (항상 캐시되고 네트워크 없이 항상 부팅될 수 있음)과 정기적으로 새로고침되고 별도로 관리되는 콘텐츠가 있습니다.

이러한 분할은 큰 영향을 미칩니다. 처음 방문할 때 서버의 콘텐츠를 렌더링하고 클라이언트에 서비스 워커를 설치할 수 있습니다. 이후 방문에서는 데이터만 요청하면 됩니다.

점진적 개선은 어떨까요?

현재 서비스 워커가 모든 브라우저에서 지원되지는 않지만, 애플리케이션 콘텐츠 셸 아키텍처에서는 점진적 개선을 통해 모든 사용자가 콘텐츠에 액세스할 수 있도록 합니다. 샘플 프로젝트를 예로 들어보겠습니다.

아래에서 Chrome, Firefox Nightly 및 Safari에서 렌더링된 전체 버전을 볼 수 있습니다. 맨 왼쪽에서 서비스 워커를 사용하지 않고 서버에 콘텐츠가 렌더링되는 Safari 버전을 볼 수 있습니다. 오른쪽에는 서비스 워커로 구동되는 Chrome 및 Firefox Nightly 버전이 나와 있습니다.

Safari, Chrome, Firefox에 로드된 애플리케이션 셸의 이미지

언제 이 아키텍처를 사용하는 것이 적절한가요?

애플리케이션 셸 아키텍처는 동적인 앱과 사이트에 가장 적합합니다. 사이트가 작고 정적인 경우 애플리케이션 셸이 필요하지 않으며 서비스 워커 oninstall 단계에서 전체 사이트를 캐시하기만 하면 됩니다. 프로젝트에 가장 적합한 접근 방식을 사용하세요. 많은 JavaScript 프레임워크에서는 이미 콘텐츠에서 애플리케이션 로직을 분할하도록 권장하고 있으므로 이 패턴을 더 쉽게 적용할 수 있습니다.

아직 이 패턴을 사용하는 프로덕션 앱이 있나요?

애플리케이션 셸 아키텍처는 전체 애플리케이션 UI를 약간만 변경해도 가능하며, Google의 I/O 2015 프로그레시브 웹 앱 및 Google 받은편지함과 같은 대규모 사이트에서 잘 작동해 왔습니다.

Google 받은편지함 로드 중 이미지입니다. 서비스 워커를 사용하는 받은편지함을 보여줍니다.

오프라인 애플리케이션 셸은 성능을 크게 향상시키는 요소이며 Jake Archibald의 오프라인 Wikipedia 앱Flipkart Lite의 프로그레시브 웹 앱에서도 잘 드러납니다.

Jake Archibald의 위키백과 데모 스크린샷

아키텍처 설명

첫 로드 경험의 목표는 의미 있는 콘텐츠를 사용자 화면에 최대한 빨리 표시하는 것입니다.

첫 번째 로드 및 다른 페이지 로드

앱 셸을 사용한 첫 번째 로드 다이어그램

일반적으로 애플리케이션 셸 아키텍처는 다음을 수행합니다.

  • 초기 로드의 우선 순위를 정하되, 서비스 워커가 애플리케이션 셸을 캐시하도록 하여 재방문 시 네트워크에서 셸을 다시 가져올 필요가 없도록 합니다.

  • 그 외 모든 것은 지연 로드 또는 백그라운드 로드 한 가지 좋은 옵션은 동적 콘텐츠에 리드스루 캐싱을 사용하는 것입니다.

  • 예를 들어 sw-precache와 같은 서비스 워커 도구를 사용하여 정적 콘텐츠를 관리하는 서비스 워커를 안정적으로 캐시하고 업데이트하세요. (sw-precache에 관해서는 나중에 자세히 알아봅니다.)

목표를 달성하려면 다음과 같이 하세요.

  • 서버는 클라이언트가 렌더링할 수 있는 HTML 콘텐츠를 전송하고, 서비스 워커가 지원되지 않는 브라우저를 처리하기 위해 먼 미래의 HTTP 캐시 만료 헤더를 사용합니다. 애플리케이션 수명 주기 중 나중에 '버전 관리'와 간편한 업데이트를 모두 사용할 수 있도록 해시를 사용하여 파일 이름을 제공합니다.

  • 페이지<head> 문서 내 <style> 태그에 인라인 CSS 스타일을 포함하여 애플리케이션 셸의 첫 페인트를 빠르게 제공합니다. 각 페이지는 현재 보기에 필요한 자바스크립트를 비동기식으로 로드합니다. CSS는 비동기식으로 로드할 수 없으므로 JavaScript를 사용하여 스타일을 요청할 수 있습니다. JavaScript는 파서 기반 및 동기식이 아닌 비동기식이기 때문입니다. 또한 requestAnimationFrame()를 활용하면 빠른 캐시 적중이 발생하여 스타일이 실수로 주요 렌더링 경로의 일부가 되는 경우를 피할 수 있습니다. requestAnimationFrame()는 스타일이 로드되기 전에 첫 번째 프레임을 강제로 칠합니다. 또 다른 옵션은 Filament Group의 loadCSS와 같은 프로젝트를 사용하여 자바스크립트를 사용하여 CSS를 비동기식으로 요청하는 것입니다.

  • 서비스 워커는 애플리케이션 셸의 캐시된 항목을 저장하므로, 네트워크에서 업데이트를 사용할 수 없는 경우 반복 방문 시 셸이 서비스 워커 캐시에서 완전히 로드될 수 있습니다.

콘텐츠용 앱 셸

실용적인 구현

애플리케이션 셸 아키텍처, 클라이언트용 vanilla ES2015 JavaScript, 서버용 Express.js를 사용하여 완벽하게 작동하는 샘플을 작성했습니다. 물론 클라이언트 또는 서버 부분 (예: PHP, Ruby, Python)에 자체 스택을 사용하는 것을 막을 수 있는 것은 없습니다.

서비스 워커 수명 주기

애플리케이션 셸 프로젝트의 경우 다음과 같은 서비스 워커 수명 주기를 제공하는 sw-precache를 사용합니다.

이벤트 작업
설치 애플리케이션 셸 및 기타 단일 페이지 앱 리소스를 캐시합니다.
활성화 이전 캐시를 삭제합니다.
가져오기 URL의 단일 페이지 웹 앱을 제공하고 애셋 및 사전 정의된 부분의 캐시를 사용합니다. 기타 요청에는 네트워크 사용

서버 비트

이 아키텍처에서는 서버 측 구성요소 (이 경우에는 Express로 작성됨)가 콘텐츠와 프레젠테이션을 별도로 처리할 수 있어야 합니다. 콘텐츠는 페이지의 정적 렌더링을 생성하는 HTML 레이아웃에 추가될 수도 있고, 별도로 동적으로 로드될 수도 있습니다.

서버 측 설정은 데모 앱에 사용하는 것과 크게 다를 수 있습니다. 이러한 웹 앱 패턴은 대부분의 서버 설정에서 달성할 수 있지만, 재설계가 필요하긴 하지만 그러합니다. 다음 모델이 매우 효과적인 것으로 확인되었습니다.

앱 셸 아키텍처 다이어그램
  • 엔드포인트는 애플리케이션의 세 부분으로 정의됩니다. 즉, 사용자에게 표시되는 URL (색인/와일드 카드), 애플리케이션 셸 (서비스 워커), HTML 부분 등이 있습니다.

  • 각 엔드포인트에는 handlebars 레이아웃을 가져오는 컨트롤러가 있습니다. 이 컨트롤러는 핸들바 부분과 뷰를 가져올 수 있습니다. 간단히 말해, 부분 뷰는 최종 페이지에 복사된 HTML 청크인 뷰입니다. 참고: 고급 데이터 동기화를 수행하는 JavaScript 프레임워크는 애플리케이션 셸 아키텍처로 포팅하기 훨씬 쉬운 경우가 많습니다. 부분보다는 데이터 결합 및 동기화를 사용하는 경향이 있습니다.

  • 처음에는 사용자에게 콘텐츠가 있는 정적 페이지가 게재됩니다. 이 페이지에서는 지원되는 경우 애플리케이션 셸과 애플리케이션 셸과 애플리케이션 셸에 의존하는 모든 항목 (CSS, JS 등)을 캐시하는 서비스 워커를 등록합니다.

  • 그러면 앱 셸이 특정 URL의 콘텐츠에서 JavaScript를 사용하여 XHR에 대한 단일 페이지 웹 앱 역할을 합니다. XHR 호출은 /partials* 엔드포인트에 대해 이루어지며, 이 엔드포인트는 해당 콘텐츠를 표시하는 데 필요한 HTML, CSS 및 JS의 작은 청크를 반환합니다. 참고: 여기에 접근하는 방법에는 여러 가지가 있으며 XHR은 그 중 하나에 불과합니다. 일부 애플리케이션은 초기 렌더링을 위해 데이터를 인라인 (JSON 사용)하므로 평면화된 HTML 의미에서 '정적'이 아닙니다.

  • 서비스 워커를 지원하지 않는 브라우저에는 항상 대체 환경이 제공되어야 합니다. 여기서는 기본적인 정적 서버 측 렌더링으로 돌아가 보겠습니다. 그러나 이는 여러 옵션 중 하나일 뿐입니다. 서비스 워커 관점은 캐시된 애플리케이션 셸을 사용하여 단일 페이지 애플리케이션 스타일 앱의 성능을 향상할 수 있는 새로운 기회를 제공합니다.

파일 버전 관리

파일 버전 관리 및 업데이트의 처리 방법에 대한 질문이 제기될 수 있습니다. 이는 애플리케이션에 따라 다르며 다음과 같은 옵션이 있습니다.

  • 먼저 네트워크에 연결하고, 그렇지 않으면 캐시된 버전을 사용합니다.

  • 네트워크 전용이며 오프라인에서는 실패합니다.

  • 이전 버전을 캐시하고 나중에 업데이트합니다.

애플리케이션 셸의 경우 서비스 워커 설정에 캐시 우선 접근 방식을 취해야 합니다. 애플리케이션 셸을 캐시하지 않는다면 아키텍처를 적절히 채택하지 않은 것입니다.

도구

Google은 다양한 서비스 워커 도우미 라이브러리를 유지관리하여 애플리케이션의 셸을 사전 캐시하거나 공통 캐싱 패턴을 처리하는 프로세스를 쉽게 설정할 수 있도록 합니다.

Web Fundamentals의 서비스 워커 라이브러리 사이트 스크린샷

애플리케이션 셸에 sw-precache 사용

sw-precache를 사용하여 애플리케이션 셸을 캐시하면 파일 버전, 설치/활성화 질문, 앱 셸에 대한 가져오기 시나리오와 관련된 문제를 처리할 수 있습니다. 애플리케이션의 빌드 프로세스에 sw-precache를 드롭하고 구성 가능한 와일드 카드를 사용하여 정적 리소스를 선택하세요. 서비스 워커 스크립트를 수동으로 작성하는 대신, sw-precache가 캐시 우선 가져오기 핸들러를 사용하여 안전하고 효율적인 방식으로 캐시를 관리하는 스크립트를 생성하도록 하세요.

앱을 처음 방문하면 필요한 리소스의 전체 세트 사전 캐싱이 트리거됩니다. 이는 앱 스토어에서 네이티브 앱을 설치하는 환경과 유사합니다. 사용자가 앱으로 돌아오면 업데이트된 리소스만 다운로드됩니다. 우리의 데모에서는 새 셸을 사용할 수 있을 때 "앱 업데이트. 새로고침하여 새 버전으로 업데이트하세요.' 이 패턴은 사용자에게 최신 버전으로 새로고침할 수 있음을 알리는 간편한 방법입니다.

런타임 캐싱에 sw-toolbox 사용

리소스에 따라 다양한 전략으로 런타임 캐싱에 sw-toolbox를 사용하세요.

  • 이미지의 cacheFirst 및 커스텀 만료 정책이 N maxEntries인 이름이 지정된 전용 캐시가 포함됩니다.

  • 원하는 콘텐츠 최신 상태에 따라 API 요청의 경우 networkFirst 또는 가장 빠릅니다. 가장 빠른 것도 좋지만 자주 업데이트되는 특정 API 피드가 있는 경우 networkFirst를 사용합니다.

결론

애플리케이션 셸 아키텍처에는 여러 가지 이점이 있지만 일부 애플리케이션 클래스에만 적합합니다. 모델은 아직 완성되지 않았으므로 이 아키텍처의 노력과 전반적인 성능 이점을 평가할 가치가 있습니다.

이 실험에서는 클라이언트와 서버 간의 템플릿 공유를 활용하여 두 개의 애플리케이션 계층을 빌드하는 작업을 최소화했습니다. 이를 통해 점진적 개선이 여전히 핵심 기능이 되도록 할 수 있습니다.

이미 앱에서 서비스 워커의 사용을 고려하고 있다면 아키텍처를 살펴보고 자체 프로젝트에 적합한지 평가해 보세요.

제프 포스닉, 폴 루이스, 알렉스 러셀, 세스 톰슨, 롭 도슨, 테일러 새비지, 조 메들리와 같은 검토자의 감사의 말을 전합니다.