드디어 PWA의 마지막 기둥, 서비스 워커(service worker)예요. 앞에서 설치 조건을 얘기할 때마다 "서비스 워커가 등록돼 있어야 한다"고 미뤄뒀던 그 일꾼이죠. 이름이 좀 거창한데, 정체는 페이지 뒤에서 몰래 도는 자바스크립트 한 장이에요. 이 녀석이 있어야 인터넷이 끊겨도 앱이 열리는 마법이 가능해져요. 처음엔 동작 방식이 낯설 수 있는데, 오늘 등록 → 생명주기 → 오프라인 대응 순서로 하나씩 밟아가면 "아, 이래서 오프라인이 되는구나"하고 감이 올 거예요.

32.1 서비스 워커가 뭐예요?

서비스 워커는 우리 웹페이지와 네트워크 사이에 앉아 있는 중간 일꾼이에요. 페이지가 서버로 뭔가를 요청하면, 그 요청이 서버로 곧장 가지 않고 서비스 워커를 먼저 거쳐요. 그래서 서비스 워커가 "이건 내가 저장해둔 게 있으니 그걸 줄게"라고 가로챌 수 있는 거예요. 오프라인에서도 화면이 뜨는 비밀이 바로 여기 있어요.


중요한 특징이 몇 개 있어요. 서비스 워커는 페이지와 별도로 돌아서, 페이지를 닫아도 잠깐씩 살아 있을 수 있어요. 대신 페이지의 화면 요소(document)에는 직접 손을 못 대요. 화면을 그리는 게 아니라 요청을 중계하는 게 일이거든요. 그리고 HTTPS에서만 동작해요. 남의 요청을 가로챌 수 있는 강력한 힘이라, 안전한 연결에서만 허락하는 거죠. 다행히 localhost는 연습용으로 예외를 줘요.

32.2 서비스 워커는 어떻게 등록해요?

서비스 워커를 쓰려면 두 개의 파일이 필요해요. 뒤에서 돌 sw.js(서비스 워커 본체)와, 그걸 등록하는 메인 페이지 스크립트요. 등록은 메인 쪽에서 이렇게 해요.


if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then((reg) => {
console.log("등록 성공", reg.scope);
})
.catch((err) => {
console.log("등록 실패", err);
});
}


맨 앞의 if ("serviceWorker" in navigator)이 브라우저가 서비스 워커를 지원하는지 먼저 확인하는 거예요. 지원 안 하는 옛 브라우저에서 오류가 나지 않게 막아주는 안전장치죠. 지원하면 navigator.serviceWorker.registersw.js 파일을 등록해요. 이 함수는 약속(Promise)을 돌려줘서, 성공하면 then, 실패하면 catch로 이어져요.


여기서 꼭 알아둘 게 scope(범위)예요. sw.js를 사이트 맨 위 경로에 두면 서비스 워커가 사이트 전체를 관리해요. 만약 /app/sw.js처럼 하위 폴더에 두면 /app 아래만 관리하게 되고요. 그래서 사이트 전체를 오프라인 대응하고 싶으면 보통 sw.js를 최상위에 둬요. 이걸 모르면 "왜 일부 페이지만 오프라인이 되지?"하고 한참 헤매게 돼요.

32.3 install이랑 activate가 뭐예요?

서비스 워커는 등록되고 나서 정해진 단계를 거쳐 살아나요. 이걸 생명주기라고 부르는데, 우리가 신경 쓸 건 두 단계예요. install(설치)과 activate(활성화)죠.


install은 서비스 워커가 처음 등록될 때 딱 한 번 불려요. 보통 이 순간에 오프라인에 대비해 미리 저장해둘 파일들을 챙겨요. sw.js 안에 이렇게 써요.


const CACHE = "app-v1";

self.addEventListener("install", (e) => {
e.waitUntil(
caches.open(CACHE).then((cache) => {
return cache.addAll(["/", "/style.css", "/app.js"]);
})
);
});


caches.open(CACHE)app-v1이라는 이름의 저장 창고(캐시)를 열고, cache.addAll로 목록에 적은 파일들을 미리 받아 담아둬요. 홈 화면, 스타일, 스크립트를 저장해두는 거죠. e.waitUntil은 "이 저장 작업이 끝날 때까지 install 단계를 끝내지 마"라고 붙잡아 두는 거예요. 저장이 다 끝나야 다음 단계로 넘어가게요.


activate는 서비스 워커가 실제로 일을 시작할 때 불려요. 주로 옛 버전 창고를 청소하는 데 써요. 새 버전을 배포하면서 창고 이름을 app-v2로 바꾸면, 이 단계에서 app-v1 같은 낡은 창고를 지워 용량을 정리하는 식이에요.

32.4 fetch 이벤트로 오프라인을 어떻게 해요?

이제 오늘의 하이라이트예요. 페이지가 뭔가를 요청할 때마다 서비스 워커의 fetch(가져오기) 이벤트가 불려요. 여기서 요청을 가로채서 저장해둔 걸 먼저 내주면 오프라인에서도 화면이 떠요.


self.addEventListener("fetch", (e) => {
e.respondWith(
caches.match(e.request).then((cached) => {
return cached || fetch(e.request);
})
);
});


흐름을 따라가 볼게요. 페이지가 style.css를 달라고 하면 fetch 이벤트가 불려요. caches.match(e.request)저장 창고에 그 파일이 있는지 찾아봐요. 있으면(cached) 그걸 바로 돌려주고, 없으면 fetch(e.request)진짜 네트워크에 요청해요. e.respondWith는 "이 응답으로 답해줘"라고 브라우저에게 건네는 부분이고요.


이 방식을 캐시 우선(cache first)이라고 불러요. 저장된 게 있으면 네트워크를 안 거치니 빠르고, 인터넷이 끊겨도 저장해둔 파일로 화면이 열려요. 실제로 인터넷을 끊고 앱을 새로 열어보면, 예전 같으면 "연결 없음" 화면이 떴을 자리에 우리가 install 때 저장해둔 홈 화면이 그대로 떠요. 이게 오프라인 대응의 가장 기본 형태예요. 다만 캐시 우선은 파일이 바뀌어도 옛 버전을 계속 줄 수 있어서, 새 버전을 내보낼 땐 창고 이름을 바꿔(app-v2) 새로 저장하게 만드는 게 짝꿍처럼 따라와요.

32.5 오늘 정리

서비스 워커의 기초를 훑었어요. 서비스 워커는 페이지와 네트워크 사이에서 요청을 중계하는 일꾼이고, HTTPS에서만 돌아요. navigator.serviceWorker.register로 등록하는데, sw.js를 최상위에 둬야 사이트 전체를 관리한다는 scope 규칙이 중요했죠. 생명주기에서는 installcaches.openaddAll로 파일을 미리 저장하고, activate 때 낡은 창고를 청소해요. 그리고 fetch 이벤트에서 caches.match로 저장본을 먼저 내주는 캐시 우선 방식으로 오프라인을 구현했어요. 이 세 이벤트만 손에 익으면, 인터넷이 끊겨도 열리는 앱이라는 PWA의 핵심을 여러분 손으로 만들 수 있어요.