놀라지 않는 Service workers: 오프라인‑우선 PWAs를 위한 결정적 캐싱

발행: (2026년 2월 4일 오전 02:18 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

Series:
시작하기 · Part 1 · Part 2 · Part 3 · Part 4 · Part 5 · Part 6 · Part 7 · Part 8 · Part 9 · Part 10

이 게시물은 오픈소스 Pain Tracker 저장소를 기반으로 한 Dev.to 시리즈의 Part 3입니다.

의학적 조언이 아닙니다.
규정 준수 주장도 아닙니다.

이 글은 결정론적 동작과 진실된 경계에 관한 내용입니다.

프라이버시 우선, 감시 없이도 지속될 수 있는 오프라인 헬스 테크를 원한다면, 빌드 후원하기 → https://paintracker.ca/sponsor.

아직 Part 2를 읽지 않으셨다면:

  • Part 2: 세 가지 저장 계층 (상태 캐시 vs 오프라인 DB vs 암호화된 금고)

“놀라운” 서비스 워커가 하는 일

서비스 워커 때문에 고생한 적이 있다면 보통 다음 중 하나일 겁니다(아직 고생하지 않았다면 축하합니다—곧 겪게 될 테니까요).

  1. 배포 후 오래된 HTML이 앱을 깨뜨림

    • 브라우저가 오래된 index.html을 계속 제공합니다.
    • 모듈 그래프가 바뀌면서 청크 404 오류가 발생하거나 화면이 빈 채로 나타납니다.
  2. 베이스 경로가 실제와 일치하지 않음

    • /에서는 동작하지만 GitHub Pages의 /pain-tracker/ 아래에서는 깨집니다.
    • 잘못된 스코프를 등록해 기대한 대로 동작하지 않습니다.
  3. 캐시가 의도치 않은 데이터 보존이 됨

    • 응답을 캐시하려는 의도가 없었습니다.
    • “모두”를 캐시하면 쉬워 보이지만…
    • 사용자별 페이로드가 캐시에 남아 민감한 앱에서는 문제가 됩니다.
  4. 업데이트가 혼란스러움

    • 새 워커가 설치되지만 활성화되지 않습니다.
    • 사용자는 페이지를 새로 고치지 않습니다.
    • 현재 어떤 버전이 실행 중인지 알 수 없습니다.

핵심: 브라우저는 정확히 당신이 요구한 대로 동작하고 있지만, 요구를 충분히 신중하게 하지 않은 것입니다.

Pain Tracker의 서비스 워커 철학

  • 탐색에 대해 네트워크‑우선 (구식 HTML 방지)
  • 정적 자산만 캐시 (스크립트, 스타일, 이미지, 폰트)
  • 버전이 지정된 캐시와 활성화 시 정리
  • offline.html과 매니페스트를 위한 소규모 프리캐시

“오프라인 마법”은 없으며, 임의 API 응답에 대한 런타임 캐시도 없고, SW를 데이터 레이어로 만들려는 시도도 없습니다.

두 개의 서비스‑워커 스크립트가 존재합니다

범위URL
루트 (일반 배포)
GitHub Pages 기본 경로 (/pain-tracker/)

두 워커 모두에서 가장 중요한 라인은 개념적으로 다음과 같습니다: 탐색은 네트워크‑우선.

public/sw.js에서는 request.mode === 'navigate' 또는 Accept: text/html을 사용해 탐색 요청을 감지합니다. 그런 다음 다음과 같이 동작합니다:

try {
  return fetch(request);
} catch (_) {
  return caches.match('/offline.html');
}

그게 전부입니다.

왜 이것이 중요한가

캐시‑우선 전략으로 탐색을 캐시하면, 결국 새로운 빌드가 배포되면서 HTML이 새로운 청크 파일명을 가리키게 됩니다. 캐시된 HTML은 여전히 오래된 파일명을 가리키므로 → 청크 404가 발생하고 앱이 “무작위로 깨진” 느낌을 줍니다.

헬스‑관련 PWA에서는 이러한 종류의 실패가 깔끔한 오프라인 메시지보다 훨씬 더 나쁩니다.

무엇을 캐시하는가

Pain Tracker는 동일 출처 GET 요청 중 정적 자산처럼 보이는 것만을 캐시합니다:

  • 허용된 경로 접두사: /assets/, /icons/, /logos/, /screenshots/ (GitHub Pages 빌드용 /pain-tracker/ 접두사 포함).
  • 보수적인 확장자 허용 목록: .js, .css, .png, .svg, .woff2, …

캐시 흐름

  1. 요청이 캐시 안에 있으면 → 반환합니다.
  2. 그렇지 않으면 → fetch하고, 성공적인 200 응답을 캐시한 뒤 반환합니다.

결과적인 오프라인 전략

  • 첫 방문 이후 셸이 빠르게 로드됩니다.
  • 배포 시 캐시된 HTML 때문에 막히는 일이 없습니다.
  • 민감한 런타임 응답이 실수로 캐시되지 않습니다.

버전 관리 및 정리

두 워커 모두 버전 문자열을 가지고 있으며, 이를 기반으로 캐시 이름을 생성합니다:

const CACHE_NAME = `pain-tracker-static-v${VERSION}`;

활성화 시 SW는 pain-tracker- 접두사가 붙은 오래된 캐시를 삭제하여 다음과 같은 깨끗한 불변성을 유지합니다:

  • SW 버전을 올리면 → 오래된 캐시가 제거됩니다.
  • 추측이나 “아마 업데이트될지도” 같은 상황이 없습니다. 업데이트가 되든 안 되든 명확합니다.

GitHub‑Pages‑스타일 워커 (public/pain-tracker/sw.js)

변경되는 점:

  • 매니페스트의 프리캐시 URL이 /pain-tracker/manifest.json이 됩니다.
  • 정적 접두사에 /pain-tracker/assets/가 포함됩니다.
  • 오프라인 폴백에 /pain-tracker/가 포함됩니다.

이 중복은 “왜 한 환경에서는 오프라인이고 다른 환경에서는 아닌가” 하는 시간을 몇 시간씩 절약해 줍니다.

앱에서 SW 등록

등록 코드는 src/utils/pwa-utils.ts에 있습니다:

// https://github.com/CrisisCore-Systems/pain-tracker/blob/main/src/utils/pwa-utils.ts

핵심 동작

  1. VITE_BASE(설정된 경우) 또는 Vite의 BASE_URL에서 baseUrl을 계산하고, Vite가 상대 경로 베이스를 반환할 때는 location.pathname을 fallback으로 사용합니다.
  2. ${baseUrl}sw.jsscope: baseUrl 옵션으로 등록합니다.
  3. updateViaCache: 'none'을 설정해 업데이트 검사를 강제합니다.
  4. updatefound 리스너를 연결해 새 콘텐츠가 사용 가능할 때 앱이 사용자에게 알릴 수 있게 합니다.

서비스 워커는 활성화 시 SW_READY 메시지를 전송하고, PING에 대해 PONG으로 응답합니다. PWA 매니저는 이를 감지해 window.__pwa_sw_ready = true 로 설정합니다.

실용적인 팁

  • “SW가 아직 준비됐나요?”와 같은 불안정한 테스트를 피합니다.
  • DevTools에서 간단한 디버그 신호를 제공합니다.

하지 않는 동작 (그리고 그 이유)

  • 임의의 fetch를 캐시하지 않음.
  • API 응답을 캐시하지 않음.
  • 네비게이션을 캐시하지 않음.
  • “헬스 데이터를 클라우드에 동기화” 시스템을 구현하지 않음.

이것들은 누락된 기능이 아니라 의도된 경계입니다.

나중에 더 많은 SW 기능(백그라운드 동기화, 오프라인 처리 등)을 추가한다면 새로운 신뢰 경계로 다루세요:

  • 캐시에서 허용할 데이터 종류를 결정합니다.
  • Cache Storage에 Class A 페이로드를 저장하지 않도록 합니다.
  • “잠금 상태”와 “잠금 해제 상태”에서 발생할 수 있는 일을 명확히 정의합니다.

빠른 테스트 체크리스트

  1. 서비스 워커 스크립트 확인
  2. DevTools에서
    • Application → Service Workers – 범위와 스크립트 URL을 확인합니다.
    • Application → Cache Storagepain-tracker-static-v… 를 찾습니다.
  3. 오프라인 동작 테스트
    • Network → Offline.
    • 라우트를 새로 고칩니다.
    • 깨진 셸 대신 오프라인 폴백을 받아야 합니다.

다음은?

  • Part 4에서는 방어적 파싱(Zod + 스키마‑우선 입력)과 “offline‑first”가 “조용히 쓰레기를 받아들이는” 상황이 되지 않도록 하는 방법을 다룹니다.

Prev: Part 2 — 세 가지 저장 계층
Next: Part 4 — Zod + 방어적 파싱

프로젝트 후원 (주요)

https://paintracker.ca/sponsor

/paintracker.ca/sponsor

레포지토리 별표 달기 (보조):
https://github.com/CrisisCore-Systems/pain-tracker

시리즈 전체를 처음부터 읽기:
link

Back to Blog

관련 글

더 보기 »

CSS preprocessor는 필요 없습니다

CSS Pre‑processors: 아직도 가치가 있을까? CSS preprocessors가 모든 CSS 문제를 해결해 주는 마법의 영약처럼 보였던 시절이 있었습니다. 단지 …

React 퀴즈 앱

React Quiz App 🧠 이 프로젝트는 React 기본 개념, 컴포넌트 기반 아키텍처, 그리고 효율적인 상태 관리에 대한 실전 이해를 강조합니다. Live demo...