Google에서 PWA 구축, 1부

게시판팀이 PWA를 개발하면서 서비스 워커에 관해 알아낸 점

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

이 글은 외부용 PWA를 빌드하면서 Google 게시판팀이 얻은 교훈에 관한 블로그 게시물 시리즈 중 첫 번째 글입니다. 이 게시물에서는 Google이 직면한 몇 가지 문제와 이를 극복하기 위해 취한 접근 방식, 함정을 피하기 위한 일반적인 조언을 공유합니다. 이는 PWA에 대한 완전한 개요가 아닙니다. Google 팀의 경험을 바탕으로 얻은 교훈을 공유하는 것이 목표입니다.

이 첫 번째 게시물에서는 먼저 약간의 배경 정보를 다룬 후 서비스 워커에 관해 알아본 모든 내용을 자세히 살펴봅니다.

배경

게시판은 2017년 중반부터 2019년 중반까지 활발하게 개발되었습니다.

PWA를 빌드하기로 한 이유

개발 프로세스를 자세히 살펴보기 전에 이 프로젝트에 PWA를 빌드하는 것이 매력적인 옵션인 이유를 살펴보겠습니다.

  • 빠른 반복 기능 게시판이 여러 시장에서 파일럿으로 진행될 예정이므로 특히 유용합니다.
  • 단일 코드베이스. 사용자는 Android와 iOS 간에 거의 동등하게 분포되어 있었습니다. PWA를 사용하면 두 플랫폼에서 모두 작동하는 단일 웹 앱을 빌드할 수 있습니다. 덕분에 팀의 속도와 영향력이 향상되었습니다.
  • 사용자 행동과 관계없이 빠르게 업데이트됩니다. PWA는 자동으로 업데이트할 수 있으므로 오래된 클라이언트가 줄어듭니다. 고객의 이전 시간을 최소화하면서 중대한 백엔드 변경사항을 출시할 수 있었습니다.
  • 퍼스트 파티 및 서드 파티 앱과 쉽게 통합됩니다. 이러한 통합은 앱의 요구사항이었습니다. PWA의 경우 URL을 열면 되는 경우가 많았습니다.
  • 앱 설치의 불편함을 없앴습니다.

Google의 프레임워크

게시판의 경우 Polymer를 사용했지만, 잘 지원되는 최신 프레임워크라면 무엇이든 사용할 수 있습니다.

서비스 워커에 관해 알게 된 점

서비스 워커가 없으면 PWA를 사용할 수 없습니다. 서비스 워커는 고급 캐싱 전략, 오프라인 기능, 백그라운드 동기화 등 다양한 기능을 제공합니다. 서비스 워커는 약간의 복잡성을 추가하지만 그 이점이 추가된 복잡성보다 큽니다.

가능한 경우 생성

서비스 워커 스크립트를 직접 작성하지 마세요. 서비스 워커를 직접 작성하려면 캐시된 리소스를 수동으로 관리하고 Workbox와 같이 대부분의 서비스 워커 라이브러리에 공통적인 로직을 다시 작성해야 합니다.

하지만 내부 기술 스택으로 인해 라이브러리를 사용하여 서비스 워커를 생성하고 관리할 수 없었습니다. 아래의 학습 내용은 때때로 이러한 점을 반영합니다. 자세한 내용은 생성되지 않은 서비스 워커의 함정을 참고하세요.

일부 라이브러리는 서비스 워커와 호환되지 않음

일부 JS 라이브러리는 서비스 워커에서 실행될 때 예상대로 작동하지 않는 가정을 합니다. 예를 들어 window 또는 document를 사용할 수 있다고 가정하거나 서비스 작업자가 사용할 수 없는 API (XMLHttpRequest, 로컬 저장소 등)를 사용하는 경우입니다. 애플리케이션에 필요한 모든 중요한 라이브러리가 서비스 워커와 호환되는지 확인합니다. 이 특정 PWA의 경우 인증에 gapi.js를 사용하고 싶었지만 서비스 워커를 지원하지 않아 사용할 수 없었습니다. 라이브러리 작성자는 서비스 워커 사용 사례를 지원하기 위해 가능한 경우 JavaScript 컨텍스트에 관한 불필요한 가정을 줄이거나 삭제해야 합니다(예: 서비스 워커와 호환되지 않는 API를 피하고 전역 상태를 피함).

초기화 중에 IndexedDB에 액세스하지 않음

서비스 워커 스크립트를 초기화할 때 IndexedDB를 읽지 마세요. 그렇지 않으면 원치 않는 상황이 발생할 수 있습니다.

  1. 사용자에게 IndexedDB (IDB) 버전 N이 포함된 웹 앱이 있음
  2. 새 웹 앱이 IDB 버전 N+1로 푸시됨
  3. 사용자가 PWA를 방문하여 새 서비스 워커 다운로드를 트리거합니다.
  4. 새 서비스 워커가 install 이벤트 핸들러를 등록하기 전에 IDB에서 읽어서 N에서 N+1로 전환하는 IDB 업그레이드 주기를 트리거합니다.
  5. 사용자에게 버전 N의 이전 클라이언트가 있으므로 이전 버전의 데이터베이스에 활성 연결이 계속 열려 있으므로 서비스 워커 업그레이드 프로세스가 중단됩니다.
  6. 서비스 워커가 중단되고 설치되지 않음

이 경우 서비스 워커 설치 시 캐시가 무효화되었으므로 서비스 워커가 설치되지 않으면 사용자는 업데이트된 앱을 받지 못했습니다.

복원력 강화

서비스 워커 스크립트는 백그라운드에서 실행되지만 I/O 작업 (네트워크, IDB 등) 중에 있더라도 언제든지 종료될 수 있습니다. 장기 실행 프로세스는 언제든지 다시 시작할 수 있어야 합니다.

대용량 파일을 서버에 업로드하고 IDB에 저장하는 동기화 프로세스의 경우 중단된 부분 업로드에 대한 Google의 솔루션은 내부 업로드 라이브러리의 재개 가능한 시스템을 활용하여 업로드하기 전에 재개 가능한 업로드 URL을 IDB에 저장하고, 첫 번째로 완료되지 않은 경우 이 URL을 사용하여 업로드를 재개하는 것이었습니다. 또한 장기 실행 I/O 작업 전에 상태가 IDB에 저장되어 각 레코드의 프로세스 위치를 나타냈습니다.

전역 상태에 의존하지 않음

서비스 워커는 다른 컨텍스트에 있으므로 예상되는 많은 기호가 표시되지 않습니다. 많은 코드가 window 컨텍스트와 서비스 워커 컨텍스트 (예: 로깅, 플래그, 동기화 등) 모두에서 실행되었습니다. 코드는 로컬 저장소나 쿠키와 같이 사용하는 서비스에 대해 방어적이어야 합니다. globalThis를 사용하여 모든 컨텍스트에서 작동하는 방식으로 전역 객체를 참조할 수 있습니다. 또한 스크립트가 종료되고 상태가 제거되는 시점을 보장할 수 없으므로 전역 변수에 저장된 데이터는 가급적 사용하지 마세요.

로컬 개발

서비스 워커의 주요 구성요소는 로컬에 리소스를 캐시하는 것입니다. 그러나 개발 중에는 특히 업데이트가 지연된 경우 원하는 것과 정확히 반대입니다. 서버 워커를 설치하면 문제를 디버그하거나 백그라운드 동기화나 알림과 같은 다른 API를 사용할 수 있습니다. Chrome에서는 Chrome DevTools를 통해 네트워크 우회 확인란 (애플리케이션 패널 > 서비스 워커 창)을 사용 설정하고 네트워크 패널에서 캐시 사용 중지 확인란을 사용 설정하여 메모리 캐시도 사용 중지할 수 있습니다. 더 많은 브라우저를 지원하기 위해 개발자 빌드에서 기본적으로 사용 설정된 서비스 워커에 캐싱을 사용 중지하는 플래그를 포함하는 다른 솔루션을 선택했습니다. 이렇게 하면 개발자가 캐싱 문제 없이 항상 최신 변경사항을 가져올 수 있습니다. 브라우저가 애셋을 캐시하지 못하도록 Cache-Control: no-cache 헤더도 포함하는 것이 중요합니다.

등대

Lighthouse는 PWA에 유용한 여러 디버깅 도구를 제공합니다. 사이트를 스캔하고 PWA, 성능, 접근성, SEO, 기타 권장사항을 다루는 보고서를 생성합니다. PWA의 기준 중 하나를 위반하는 경우 알림을 받으려면 지속적 통합에서 Lighthouse를 실행하는 것이 좋습니다. 실제로 서비스 워커가 설치되지 않았는데 프로덕션 푸시 전에 이를 인지하지 못한 적이 한 번 있었습니다. Lighthouse를 CI의 일부로 포함했다면 이러한 문제가 방지되었을 것입니다.

지속적 배포 수용

서비스 워커는 자동으로 업데이트할 수 있으므로 사용자는 업그레이드를 제한할 수 없습니다. 이렇게 하면 사용 중인 오래된 클라이언트의 양이 크게 줄어듭니다. 사용자가 앱을 열면 서비스 워커는 새 클라이언트를 지연 로드하는 동안 이전 클라이언트를 제공합니다. 새 클라이언트가 다운로드되면 사용자에게 새 기능에 액세스하려면 페이지를 새로고침하라는 메시지가 표시됩니다. 사용자가 이 요청을 무시하더라도 다음에 페이지를 새로고침하면 새 버전의 클라이언트가 수신됩니다. 따라서 사용자가 iOS/Android 앱에서와 동일한 방식으로 업데이트를 거부하기는 매우 어렵습니다.

클라이언트의 이전 시간을 매우 짧게 유지하면서 중대한 백엔드 변경사항을 푸시할 수 있었습니다. 일반적으로 Google에서는 사용자가 최신 클라이언트로 업데이트할 수 있도록 한 달의 기간을 둔 후 중대한 변경사항을 적용합니다. 앱이 비활성 상태일 때도 계속 제공되므로 사용자가 앱을 오랫동안 열지 않은 경우 이전 클라이언트가 실제로 존재할 수 있었습니다. iOS에서는 서비스 워커가 몇 주 후에 제거되므로 이 경우는 발생하지 않습니다. Android의 경우 오래된 상태에서 게재하지 않거나 몇 주 후에 콘텐츠의 만료를 수동으로 설정하여 이 문제를 완화할 수 있습니다. 실제로는 비활성 클라이언트로 인한 문제가 발생하지 않았습니다. 특정 팀에서 얼마나 엄격하게 적용할지는 구체적인 사용 사례에 따라 다르지만, PWA는 iOS/Android 앱보다 훨씬 더 유연합니다.

서비스 워커에서 쿠키 값 가져오기

서비스 워커 컨텍스트에서 쿠키 값에 액세스해야 하는 경우가 있습니다. 이 경우 퍼스트 파티 API 요청을 인증할 토큰을 생성하기 위해 쿠키 값에 액세스해야 했습니다. 서비스 워커에서는 document.cookies와 같은 동기 API를 사용할 수 없습니다. 서비스 워커에서 언제든지 활성 (창이 있는) 클라이언트로 메시지를 전송하여 쿠키 값을 요청할 수 있지만, 백그라운드 동기화 중에와 같이 사용 가능한 창이 있는 클라이언트 없이 서비스 워커가 백그라운드에서 실행될 수 있습니다. 이 문제를 해결하기 위해 쿠키 값을 클라이언트로 다시 에코하는 엔드포인트를 프런트엔드 서버에 만들었습니다. 서비스 워커가 이 엔드포인트에 네트워크 요청을 하고 응답을 읽어 쿠키 값을 가져왔습니다.

Cookie Store API가 출시됨에 따라 이를 지원하는 브라우저에는 더 이상 이 해결 방법이 필요하지 않습니다. 이 API는 브라우저 쿠키에 대한 비동기 액세스를 제공하고 서비스 워커에서 직접 사용할 수 있기 때문입니다.

생성되지 않은 서비스 워커의 함정

정적 캐시된 파일이 변경되면 서비스 워커 스크립트가 변경되는지 확인

일반적인 PWA 패턴은 서비스 워커가 install 단계 중에 모든 정적 애플리케이션 파일을 설치하는 것입니다. 이렇게 하면 클라이언트가 후속 방문 시 Cache Storage API 캐시를 직접 히트할 수 있습니다. 서비스 워커는 브라우저에서 서비스 워커 스크립트가 어떤 방식으로든 변경되었다고 감지하는 경우에만 설치되므로 캐시된 파일이 변경될 때 서비스 워커 스크립트 파일 자체가 어떤 방식으로든 변경되었는지 확인해야 했습니다. 서비스 워커 스크립트 내에 정적 리소스 파일 세트의 해시를 삽입하여 수동으로 이를 실행했기 때문에 모든 출시에서 고유한 서비스 워커 JavaScript 파일이 생성되었습니다. Workbox와 같은 서비스 워커 라이브러리는 이 프로세스를 자동화합니다.

단위 테스트

서비스 워커 API는 전역 객체에 이벤트 리스너를 추가하여 작동합니다. 예를 들면 다음과 같습니다.

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

이벤트 트리거와 이벤트 객체를 모의 처리하고 respondWith() 콜백을 기다린 다음 약속을 기다린 후 결과를 어설션해야 하므로 테스트하기가 번거로울 수 있습니다. 이를 구성하는 더 쉬운 방법은 모든 구현을 더 쉽게 테스트할 수 있는 다른 파일에 위임하는 것입니다.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

서비스 워커 스크립트를 단위 테스트하기가 어렵기 때문에 핵심 서비스 워커 스크립트를 최대한 간단하게 유지하고 대부분의 구현을 다른 모듈로 분할했습니다. 이러한 파일은 표준 JS 모듈이므로 표준 테스트 라이브러리를 사용하여 더 쉽게 단위 테스트할 수 있습니다.

2부와 3부를 기대해 주세요.

이 시리즈의 2부와 3부에서는 미디어 관리 및 iOS 관련 문제에 대해 알아봅니다. Google에서 PWA를 빌드하는 방법에 관해 자세히 알아보려면 저자 프로필을 방문하여 문의 방법을 알아보세요.