게시판팀이 PWA를 개발하면서 서비스 워커에 관해 알게 된 점
이 글은 Google 게시판팀이 외부용 PWA를 빌드하는 동안 배운 내용을 다루는 블로그 게시물 시리즈 중 첫 번째 글입니다. 이 게시물에서는 Google이 직면한 몇 가지 문제점, 이를 극복하기 위한 접근 방식, 함정을 피하기 위한 일반적인 조언을 공유합니다. 이는 PWA의 전체 개요가 아닙니다. 저희 팀의 경험을 통해 알게 된 점을 공유하는 것을 목표로 하고 있습니다.
이 첫 번째 게시물에서는 약간의 배경 정보를 먼저 살펴본 후 서비스 워커에 대해 배운 모든 내용을 자세히 알아봅니다.
배경
게시판은 2017년 중반부터 2019년 중반까지 활발하게 개발 중이었습니다.
PWA를 빌드하기로 선택한 이유
개발 프로세스를 살펴보기 전에 이 프로젝트에서 PWA 빌드가 매력적인 옵션인 이유를 살펴보겠습니다.
- 신속한 반복 능력. 게시판은 여러 시장에서 파일럿으로 진행되므로 특히 유용합니다.
- 단일 코드베이스. 사용자들은 Android와 iOS가 거의 고르게 분포되어 있었습니다. PWA는 두 플랫폼 모두에서 작동하는 단일 웹 앱을 빌드할 수 있다는 것을 의미했습니다. 그 결과 팀의 속도와 영향력이 증가했습니다
- 사용자 행동과 상관없이 빠르게 업데이트됩니다. PWA는 자동으로 업데이트될 수 있어 실제 사용되는 오래된 클라이언트의 양을 줄입니다. 클라이언트를 위해 아주 짧은 시간 안에 브레이킹 체인지 백엔드 변경사항을 푸시할 수 있었습니다.
- 퍼스트 파티 및 서드 파티 앱과 쉽게 통합됩니다. 이러한 통합은 앱의 요구사항이었습니다. PWA에서는 단순히 URL을 열기만 하는 경우가 많았습니다.
- 앱 설치 시 발생하는 마찰 제거
Google의 프레임워크
게시판에는 Polymer가 사용되었지만 잘 지원되는 최신 프레임워크라면 모두 사용할 수 있습니다.
서비스 워커에 관해 알게 된 점
서비스 워커가 없으면 PWA를 사용할 수 없습니다. 서비스 워커는 고급 캐싱 전략, 오프라인 기능, 백그라운드 동기화 등과 같은 많은 기능을 제공합니다. 서비스 워커는 복잡성을 추가하지만, 추가된 복잡성보다 서비스 워커의 이점이 더 크다는 것을 발견했습니다.
가능하면 생성
서비스 워커 스크립트를 직접 작성하지 마세요. 서비스 워커를 직접 작성하려면 캐시된 리소스를 수동으로 관리하고 Workbox와 같은 대부분의 서비스 워커 라이브러리에 공통된 로직을 다시 작성해야 합니다.
하지만 내부 기술 스택으로 인해 라이브러리를 사용하여 서비스 워커를 생성하고 관리할 수는 없었습니다. 아래에서 알게 된 사실도 그것을 반영할 때가 있습니다. 자세한 내용은 생성되지 않은 서비스 워커의 문제점을 참고하세요.
일부 라이브러리는 서비스 워커와 호환되지 않음
일부 JS 라이브러리는 서비스 워커가 실행할 때 예상대로 작동하지 않는다고 가정합니다. 예를 들어 window
또는 document
를 사용할 수 있거나 서비스 워커 (XMLHttpRequest
, 로컬 스토리지 등)가 사용할 수 없는 API를 사용 중이라고 가정합니다. 애플리케이션에 필요한 중요 라이브러리가 서비스 워커와 호환되는지 확인합니다. 이 특정 PWA의 경우 인증에 gapi.js를 사용하려고 했지만 서비스 워커를 지원하지 않았으므로 사용할 수 없었습니다. 또한 라이브러리 작성자는 서비스 워커 사용 사례를 지원하기 위해 가능하면 자바스크립트 컨텍스트에 관한 불필요한 가정을 줄이거나 제거해야 합니다. 예를 들어 서비스 워커와 호환되지 않는 API를 피하고 전역 상태를 방지해야 합니다.
초기화 중 IndexedDB 액세스 방지
서비스 워커 스크립트를 초기화할 때 IndexedDB를 읽지 마세요. 그러지 않으면 다음과 같은 상황이 발생할 수 있습니다.
- 사용자에게 IndexedDB (IDB) 버전이 N인 웹 앱이 있음
- IDB 버전 N+1로 새 웹 앱이 푸시됨
- 사용자가 PWA를 방문하여 새 서비스 워커의 다운로드를 트리거합니다.
- 새 서비스 워커는
install
이벤트 핸들러를 등록하기 전에 IDB에서 읽어서 IDB 업그레이드 주기가 N에서 N+1로 전환되도록 트리거합니다. - 사용자가 버전 N의 이전 클라이언트를 사용하며, 활성 연결이 이전 버전의 데이터베이스에 여전히 열려 있기 때문에 서비스 워커 업그레이드 프로세스가 중단됩니다.
- 서비스 워커가 중단되고 설치되지 않음
여기서는 서비스 워커 설치 시 캐시가 무효화되었으므로 서비스 워커가 설치되지 않은 경우 사용자는 업데이트된 앱을 받지 못했습니다.
복원력 강화
서비스 워커 스크립트는 백그라운드에서 실행되지만 I/O 작업 (네트워크, IDB 등)이 진행되는 동안에도 언제든지 종료될 수 있습니다. 모든 장기 실행 프로세스는 언제든지 재개할 수 있어야 합니다.
대용량 파일을 서버에 업로드하고 IDB에 저장하는 동기화 프로세스의 경우 중단된 부분 업로드에 대한 솔루션은 내부 업로드 라이브러리의 재개 가능한 시스템을 활용하여 업로드 전에 재개 가능한 업로드 URL을 IDB에 저장하고 처음에 업로드가 완료되지 않으면 해당 URL을 사용하여 업로드를 재개하는 것이었습니다. 또한 장기 실행 I/O 작업 전에는 상태가 IDB에 저장되어 각 레코드의 프로세스가 어디에 있었는지 나타냅니다.
전역 상태에 의존하지 않음
서비스 워커는 다른 컨텍스트에 존재하므로 존재할 것으로 예상되는 많은 기호가 없습니다. 많은 코드가 window
컨텍스트와 서비스 워커 컨텍스트 (예: 로깅, 플래그, 동기화 등)에서 모두 실행되었습니다. 코드는 로컬 스토리지 또는 쿠키와 같이 사용하는 서비스에 대해 방어해야 합니다. globalThis
를 사용하여 모든 컨텍스트에서 작동하는 방식으로 전역 객체를 참조할 수 있습니다. 또한 스크립트가 종료되고 상태가 제거되는 시점을 보장하지 않으므로 전역 변수에 저장된 데이터는 가급적 사용하지 마세요.
로컬 개발
서비스 워커의 주요 구성요소는 리소스를 로컬에서 캐싱하는 것입니다. 그러나 개발 중에는 특히 업데이트가 느리게 실행되는 경우 개발자가 원하는 것과 반대입니다. 서버 작업자의 문제를 디버그하거나 백그라운드 동기화 또는 알림과 같은 다른 API를 사용할 수 있도록 서버 작업자를 계속 설치해야 합니다. Chrome에서는 Chrome DevTools를 통해 네트워크 우회 체크박스 (Application 패널 > Service worker 창)를 사용 설정하고 Network 패널에서 Disable cache 체크박스를 선택하여 메모리 캐시를 사용 중지할 수 있습니다. Google은 더 많은 브라우저를 지원하기 위해 개발자 빌드에서 기본적으로 사용 설정되는 서비스 워커에서 캐싱을 사용 중지하는 플래그를 포함하여 다른 솔루션을 선택했습니다. 이렇게 하면 개발자가 캐싱 문제 없이 항상 최신 변경사항을 가져올 수 있습니다. Cache-Control: no-cache
헤더를 포함하여
브라우저가 애셋을 캐시하지 못하도록
하는 것이 중요합니다.
등대
Lighthouse는 PWA에 유용한 여러 디버깅 도구를 제공합니다. 사이트를 검사하고 PWA, 성능, 접근성, 검색엔진 최적화, 기타 권장사항을 다루는 보고서를 생성합니다. 기준 중 하나를 PWA로 설정할 경우 알림을 받을 수 있도록 지속적 통합에서 Lighthouse를 실행하는 것이 좋습니다. 이전에는 서비스 워커가 설치되지 않았고 프로덕션 푸시 전에는 이를 인식하지 못했습니다. Lighthouse를 CI의 일부로 포함하는 게 불가능했을 겁니다
지속적 배포 수용
서비스 워커는 자동으로 업데이트될 수 있으므로 사용자는 업그레이드를 제한할 수 없습니다. 이렇게 하면 사용 중인 오래된 클라이언트의 양이 크게 줄어듭니다. 사용자가 앱을 열면 서비스 워커는 새 클라이언트를 느리게 다운로드하는 동안 이전 클라이언트를 제공합니다. 새 클라이언트가 다운로드되면 새 기능에 액세스하려면 페이지를 새로고침하라는 메시지가 사용자에게 표시됩니다. 사용자가 이 요청을 무시하더라도 다음에 페이지를 새로고침하면 새 버전의 클라이언트를 받게 됩니다. 따라서 사용자가 iOS/Android 앱과 동일한 방식으로 업데이트를 거부하기는 매우 어렵습니다.
클라이언트의 이전 시간을 매우 짧은 시간 내에 브레이킹 체인지 백엔드로 푸시할 수 있었습니다. 일반적으로 브레이킹 체인지를 수행하기 전에 사용자가 최신 클라이언트로 업데이트할 수 있도록 한 달을 제공합니다. 앱이 비활성 상태일 때 서비스를 제공하기 때문에 실제로 사용자가 앱을 오랫동안 열지 않았다면 이전 클라이언트가 야생에 존재할 수 있었습니다. iOS에서는 서비스 워커가 몇 주 후에 제거되므로 이러한 상황이 발생하지 않습니다. Android의 경우 오래된 상태에서 게재하지 않거나 몇 주 후에 콘텐츠를 수동으로 만료하면 이 문제를 완화할 수 있습니다. 실제로 우리는 비활성 클라이언트에서 문제가 발생하지 않았습니다. 특정 팀이 얼마나 엄격하기를 원하는지는 각자의 사용 사례에 따라 다르지만 PWA는 iOS/Android 앱보다 훨씬 더 많은 유연성을 제공합니다.
서비스 워커에서 쿠키 값 가져오기
서비스 워커 컨텍스트에서 쿠키 값에 액세스해야 하는 경우가 있습니다. 여기서는 퍼스트 파티 API 요청을 인증하기 위한 토큰을 생성하기 위해 쿠키 값에 액세스해야 했습니다. 서비스 워커에서는 document.cookies
와 같은 동기 API를 사용할 수 없습니다. 언제든지 서비스 워커에서 활성 (윈도우된) 클라이언트에 메시지를 보내 쿠키 값을 요청할 수 있습니다. 하지만 백그라운드 동기화 도중과 같이 사용 가능한 기간이 설정된 클라이언트를 사용하지 않고 백그라운드에서 서비스 워커를 실행할 수도 있습니다. 이 문제를 해결하기 위해 프런트엔드 서버에 단순히 쿠키 값을 클라이언트에 다시 에코하는 엔드포인트를 만들었습니다. 서비스 워커는 이 엔드포인트에 네트워크 요청을 하고 응답을 읽어 쿠키 값을 얻었습니다.
Cookie Store 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를 빌드하는 방법에 관해 더 궁금한 점이 있으면 작성자 프로필을 방문하여 문의 방법을 알아보세요.