💻 개발
[기술부채] Service Worker(서비스 워커)
연관 키워드
PWA, postMessage, MSW(Mock Service Worker)
등장 배경
오프라인 환경에서의 사용성 저하
웹 애플리케이션은 기본적으로 네트워크에 의존하는 실행 모델을 가진다. 페이지 로딩과 데이터 조회, 리소스 다운로드까지 대부분의 동작이 네트워크 상태에 영향을 받기 때문에 저속 네트워크나 오프라인 환경에서는 응답 지연이 길어지거나 애플리케이션이 정상적으로 동작하지 않는 문제가 발생한다. 이러한 특성은 사용자 경험을 저하시켰기 때문에 웹 플랫폼이 해결해야 할 과제로 남아있었다.
웹 브라우저의 서비스 워커는 이러한 배경 속에서 등장한 기술이다.
사전 지식
워커 프로세스(운영체제에서의 관점)
: 메인 실행 흐름과 분리되어 독립적으로 작업을 수행하는 실행 단위
주로 병렬 처리, 메모리 격리, 비동기 작업 처리를 위해 사용된다.
운영체제는 프로그램을 프로세스 단위로 실행한다. 워커 프로세스는 이 중에서 특정 작업만 담당하도록 분리된 보조 프로세스이다.
- 특징
- 독립 메모리 공간 → 메인 프로세스와 메모리 공유 없음
- 병렬 실행 가능 → 멀티코어 활용
- IPC로 통신 → 메시지, 파이프, 소켓 등
- 장애 격리 → 워커에 장애가 발생하더라도 메인 프로세스에 영향이 없음
웹 브라우저의 워커
웹 브라우저는 UI 스레드를 보호하기 위해 다양한 워커 실행 모델을 제공한다.
타입 | 특징 |
Web Worker | 일반 백그라운드 계산 |
Service Worker | 네트워크 프록시 |
Shared Worker | 여러 탭 공유 |
Worklet | 오디오/렌더링 전용 |
이러한 워커들은 공통적으로 DOM에 접근할 수 없고, 메시지 기반 통신(지난 포스트에서 다루었던 postMessage가 사용됨)이 이루어지며, 메인 스레드와 완전히 독립적으로 동작한다.
브라우저의 메인 스레드
: 웹 페이지의 렌더링과 사용자의 상호작용을 처리하는 실행 흐름.
HTML 파싱, 스타일 계산, 레이아웃, 페인팅, JS 실행 등 대부분의 작업이 이 스레드에서 실행된다.
- 특징
- 메인 스레드는 기본적으로 싱글 스레드 실행 모델을 따른다.
- 메인 스레드는 이벤트 루프 위에서 동작한다.
- 메인 스레드는 UI 렌더링을 수행하기 때문에, 무거운 작업을 수행하면 사용자 경험이 저하된다. ⇒ 워커의 필요성
때문에 메인 스레드에서의 모든 작업은 순차적으로 한 번에 하나씩 실행된다.
한 가지 짚고 넘어가자면, 앞서 설명했던 워커 프로세스와 달리, 브라우저의 워커는 ‘모델’로서 논리적인 개념이고, 실제 구현에 따라 스레드가 될 수도 있고 프로세스가 될 수도 있다.
Service Worker(서비스 워커)
: 웹 브라우저와 웹 서버 사이에서 프록시처럼 동작하는 백그라운드 스크립트
페이지와 별개 스레드에서 실행되며, 네트워크 요청을 가로채고 캐시를 관리하여 오프라인 지원, 빠른 로딩, 푸시 알림 같은 기능을 구현할 수 있도록 해준다.
특징
1. 이벤트 기반 워커이다
서비스 워커는 항상 실행되는 백그라운드 스레드가 아니라, 브라우저가 특정 이벤트를 전달할 때만 로드되어 실행되는 이벤트 기반 런타임이다. 작업이 끝나면 다시 종료되기 때문에 상주 프로세스에 비해 메모리&배터리 사용량 측면에서 효율이 좋다.
- 활용 사례: 푸시 알림, 백그라운드 동기화
: 웹 푸시는 운영체제(OS) 수준의 푸시 인프라가 먼저 메시지를 수신하고, 브라우저가 해당 웹 페이지(Origin)의 서비스 워커를 일시적으로 기동하여 push 또는 sync 이벤트를 전달하는 방식으로 동작한다. 때문에 탭이나 브라우저 UI가 열려 있지 않아도(브라우저 프로그램이 실행 중이 아니더라도) 이벤트를 처리할 수 있다.
2. 네트워크 프록시의 역할을 수행한다
브라우저와 네트워크 사이에서 모든 네트워크 요청을 가로채고 응답을 직접 구성할 수 있다. 이를 통해 요청을 캐시로 대체하거나, 네트워크와 캐시를 조합한 전략을 수행할 수 있다.
- 활용 사례: 오프라인 모드, 리소스 캐싱
: 브라우저의 모든 fetch 요청을 가로채서, 네트워크가 없으면 Cache Storage에서 응답을 반환하고, 네트워크 상태가 유효하다면 서버 응답을 반환하면서 캐시를 갱신할 수 있다.
3. 메인 스레드와 분리된 실행 환경에서 동작한다
페이지의 메인 JavaScript 실행 흐름과 분리된 컨텍스트에서 실행되기 때문에 UI 렌더링과 사용자 입력 처리에 직접적인 블로킹을 일으키지 않는다.
4. DOM에 접근할 수 없다
페이지와 격리된 실행 컨텍스트를 가지므로 DOM, window, 동기적 Web Storage 등에 직접 접근할 수 없다. 페이지와의 데이터 교환은
postMessage기반 메시징을 통해 이루어진다.대신 네트워크 제어, 캐시 관리, 알림 표시 등 브라우저가 제공하는 API에 접근할 수 있다.
5. HTTPS에서만 동작한다
서비스 워커는 보안 상의 이유로 로컬 호스트(
localhost)를 제외하고 HTTPS에서만 동작한다. 네트워크 요청을 수정할 수 있다는 점에서 중간자 공격에 굉장히 취약하기 때문이다.서비스 워커의 생명주기
서비스 워커는 네이티브 앱과 유사한 생명주기를 가진다.
일반 스크립트처럼 업데이트 즉시 교체되지 않고, 기존 버전과 공존하면서 점진적으로 전환되는 버전 관리 모델을 가진다. 이는 실행 중인 페이지의 안정성을 보장하기 위한 것이다.

1. 등록(Registration)
페이지가 서비스 워커의 스크립트 파일을 브라우저에 등록한다. 이 단계에서 브라우저는 스크립트 파일을 다운로드하고, 업데이트 여부를 확인한다.
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/' }); }
2. 설치(Install)
새로운 서비스 워커가 처음 실행되는 단계.
핵심 리소스를 사전에 캐시하는(precache) 작업을 수행한다.
self.addEventListener('install', event => { event.waitUntil(precacheAssets()); });
3. 대기(Waiting)
기존 워커가 아직 페이지를 제어 중인 상태일 때, 새 워커는 활성화되지 않고 대기한다.
*실행 중인 웹 페이지가 갑자기 다른 버전으로 바뀌면 캐시 또는 리소스 버전이 불일치해 오류가 발생할 수 있기 때문에, 안전한 교체를 위해 대기한다.
*강제 활성화
self.skipWaiting();
4. 활성화(Activate)
새 서비스 워커의 설치가 완료된 상테에서 기존 서비스 워커의 제어 대상이 없어지면, 새 서비스 워커가 제어권을 획득한다.
이 때 이전 버전의 캐시를 삭제하고, 데이터를 마이그레이션한다.
self.addEventListener('activate', event => { event.waitUntil(cleanOldCaches()); });
*강제 제어상태로 진입
clients.claim();
5. 제어 상태(Controlling)
해당 스코프의 페이지 요청을 가로채고 처리한다.
fetch 인터셉트, 푸시 이벤트 처리, 백그라운드 동작 등 서비스 워커의 핵심 동작이 이 생명주기에서 실행된다.
스코프
: 서비스 워커가 페이지를 제어할 수 있는 URL의 범위
기본적으로 서비스 워커 스크립트 파일의 위치를 기준으로 결정된다.
/sw.js → 스코프: / /app/sw.js → 스코프: /app/ /shop/sw.js → 스코프: /shop/
서비스 워커를 등록할 때 옵션으로 스코프를 직접 지정할 수도 있다.
navigator.serviceWorker.register('/sw.js', { scope: '/app/' });
6. 중복/폐기됨(Redundant)
서비스 워커의 설치 혹은 활성화 작업 도중 문제가 발생하거나 기존에 제어 상태에 있던 서비스 워커가 새로운 서비스 워커로 대체되었을 때 기존 서비스 워커는 폐기되고 이후 소멸된다.
실습: 오프라인 환경에서 모킹된 응답을 반환하는 예제
네트워크 요청이 실패했을 때 서비스 워커가 대체 응답을 반환하도록 해보자.
// index.html <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>Service Worker 오프라인 모킹 실습</title> <style> body { font-family: sans-serif; padding: 40px; } .result { margin-top: 20px; font-size: 18px; } </style> </head> <body> <h1>오프라인 모킹 실습</h1> <p>온라인 → 실제 API 응답</p> <p>오프라인 → 서비스 워커 모킹 응답</p> <button onclick="requestUser()">사용자 정보 요청</button> <div id="result" class="result"></div> <script> const resultEl = document.getElementById('result'); async function requestUser() { resultEl.textContent = '요청 중...'; try { const res = await fetch('https://jsonplaceholder.typicode.com/users/1'); const data = await res.json(); resultEl.textContent = `이름: ${data.name} / source: ${data.source || 'network'}`; } catch (err) { resultEl.textContent = '요청 실패'; } } if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('서비스 워커 등록 성공:', reg.scope)) .catch(err => console.log('서비스 워커 등록 실패:', err)); }); } </script> </body> </html>
// sw.js self.addEventListener('install', () => { console.log('서비스 워커 설치됨'); }); self.addEventListener('activate', () => { console.log('서비스 워커 활성화됨'); }); self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 특정 API 요청만 제어 if (url.hostname === 'jsonplaceholder.typicode.com') { event.respondWith( fetch(event.request).catch(() => { console.log('오프라인 → 모킹 데이터 반환'); const mockData = { name: '오프라인 사용자', source: 'service worker mock' }; return new Response(JSON.stringify(mockData), { headers: { 'Content-Type': 'application/json' } }); }) ); return; } // 나머지 요청은 그대로 네트워크 event.respondWith(fetch(event.request)); });
localhost로 html 파일을 띄우면 다음과 같이 콘솔을 통해 서비스 워커 스크립트가 성공적으로 등록/설치/활성화되는 것을 확인할 수 있다.

이 상태에서 새로고침을 하면 동일한 서비스 워커가 이미 활성화된 상태이기 때문에, 생명주기 이벤트가 다시 실행되지 않는다. 다음 이미지를 보면 서비스 워커의 생명주기 이벤트에 정의해둔 콘솔이 출력되지 않는 것을 확인할 수 있다.
self.addEventListener('install', () => { console.log('서비스 워커 설치됨'); }); self.addEventListener('activate', () => { console.log('서비스 워커 활성화됨'); }); ...

온라인 상태에서 ‘사용자 정보 요청’ 버튼을 클릭하면 아래와 같이 API의 응답(name)이 화면에 표시된다.

오프라인 모드로 전환한 후

다시 ‘사용자 정보 요청’ 버튼을 클릭하면 모킹해둔 정보가 표시되는 것을 확인할 수 있다.
event.respondWith( fetch(event.request).catch(() => { console.log('오프라인 → 모킹 데이터 반환'); const mockData = { name: '오프라인 사용자', source: 'service worker mock' }; return new Response(JSON.stringify(mockData), { headers: { 'Content-Type': 'application/json' } }); }) );

TIPS: 크롬 브라우저 기준, 주소창에
chrome://serviceworker-internals/를 입력하면 실행 중인 서비스 워커의 목록을 볼 수 있다.
예제에서 설치한 서비스 워커도 목록에 있는 것을 확인할 수 있다.

API Mocking 도구인 MSW(Mock Service Worker)는 서비스 워커가 네트워크 요청을 가로채고 응답을 대체할 수 있다는 특성을 활용해 만들어진 라이브러리이다.