`Service Worker` 자바스크립트에서 사용하기

`Service Worker` 자바스크립트에서 사용하기

이 글은 자바스크립트의 Service Worker 개념을 설명합니다.

Service Worker의 기초부터 실전 캐시 제어까지 단계별로 설명합니다.

YouTube Video

offline.html
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <title>Offline</title>
 6</head>
 7<body>
 8  <h1>You are offline</h1>
 9  <p>This is the offline fallback page.</p>
10</body>
11</html>
style.css
1body {
2  font-family: sans-serif;
3  background-color: #f0f0f0;
4  padding: 20px;
5}
6h1 {
7  color: #333;
8}
javascript-service-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <div class="container">
111        <h2>HTML Sample</h2>
112        <button id="fetchBtn">Fetch Test</button>
113    </div>
114
115    <script>
116        // Override console.log to display messages in the #output element
117        (function () {
118            // Override console.log
119            const originalLog = console.log;
120            console.log = function (...args) {
121                originalLog.apply(console, args);
122                const message = document.createElement('div');
123                message.textContent = args.map(String).join(' ');
124                output.appendChild(message);
125            };
126
127            // Override console.error
128            const originalError = console.error;
129            console.error = function (...args) {
130                originalError.apply(console, args);
131                const message = document.createElement('div');
132                message.textContent = args.map(String).join(' ');
133                message.style.color = 'red'; // Color error messages red
134                output.appendChild(message);
135            };
136        })();
137
138        document.getElementById('executeBtn').addEventListener('click', () => {
139            // Prevent multiple loads
140            if (document.getElementById('externalScript')) return;
141
142            const script = document.createElement('script');
143            script.src = 'javascript-service-worker.js';
144            script.id = 'externalScript';
145            //script.onload = () => console.log('javascript-service-worker.js loaded and executed.');
146            //script.onerror = () => console.log('Failed to load javascript-service-worker.js.');
147            document.body.appendChild(script);
148        });
149    </script>
150</body>
151</html>

Service Worker 자바스크립트에서 사용하기

Service Worker는 브라우저와 네트워크 사이에 위치하여 요청 캐싱 및 오프라인 지원을 가능하게 하는 자바스크립트 기능입니다. PWA(프로그레시브 웹 앱)의 핵심 기술로, 웹 애플리케이션에 네이티브 앱과 유사한 경험을 제공합니다.

Service Worker란 무엇인가요?

Service Worker는 브라우저의 백그라운드 스레드에서 동작하는 자바스크립트 파일입니다. 페이지와는 별도의 스레드에서 동작하며, UI에는 접근할 수 없지만 네트워크 요청을 가로채고, 캐시를 관리하며, 푸시 알림을 처리할 수 있습니다.

Service Worker의 주요 특징은 다음과 같습니다:.

  • 로컬호스트를 제외하면 HTTPS 환경에서만 동작합니다.
  • Promise 기반의 비동기 API를 사용합니다.
  • install, activate, fetch, push와 같은 이벤트를 사용하는 이벤트 기반 구조입니다.

Service Worker 등록하기

먼저, 브라우저에 Service Worker를 등록하는 코드를 작성해봅시다.

 1if ('serviceWorker' in navigator) {
 2    window.addEventListener('load', () => {
 3        navigator.serviceWorker.register('/sw.js')
 4        .then(registration => {
 5            console.log(
 6                'Service Worker registered with scope:',
 7                registration.scope
 8            );
 9        })
10        .catch(error => {
11            console.error('Service Worker registration failed:', error);
12        });
13    });
14}

설명

  • navigator.serviceWorker.register()를 사용하여 /sw.js(Service Worker 파일)을 등록합니다.
  • 등록 시에는 then으로 성공 처리, catch로 에러 처리를 할 수 있습니다.
  • registration.scopeService Worker의 영향을 받는 경로 범위(스코프)를 나타냅니다.
  • 기본적으로 스코프는 등록된 파일(이 경우, /sw.js)이 위치한 디렉터리와 그 하위 디렉터리입니다.

Service Worker 스코프

스코프를 제한하고 싶다면, register의 두 번째 인자를 사용하여 scope를 지정할 수 있습니다.

1navigator.serviceWorker.register('/sw.js', { scope: '/app/' })
2.then(registration => {
3    console.log(
4        'Service Worker registered with scope:',
5        registration.scope
6    );
7});

설명

  • 이 설정으로 /app/ 아래의 페이지만 Service Worker가 제어하게 됩니다.

Service Worker 파일 작성하기

다음으로 sw.js라는 파일을 만들고 기본 이벤트를 구현합니다.

1// sw.js
2const CACHE_NAME = 'my-cache-v1';
3const urlsToCache = [
4    '/',
5    '/index.html',
6    '/styles.css',
7    '/script.js',
8    '/offline.html'
9];

이 코드는 캐시될 리소스 목록을 정의합니다.

각 이벤트의 역할과 동작 원리

install

 1// Install event (initial caching)
 2self.addEventListener('install', event => {
 3    console.log('[ServiceWorker] Install');
 4    event.waitUntil(
 5        caches.open(CACHE_NAME).then(cache => {
 6            console.log('[ServiceWorker] Caching app shell');
 7            return cache.addAll(urlsToCache);
 8        })
 9    );
10});
  • self.addEventListener('install')Service Worker가 처음 등록될 때 실행됩니다. 이 단계에서는 필요한 파일들이 미리 캐시됩니다.

activate

 1// Activation event (delete old caches)
 2self.addEventListener('activate', event => {
 3    console.log('[ServiceWorker] Activate');
 4    event.waitUntil(
 5        caches.keys().then(keyList => {
 6            return Promise.all(keyList.map(key => {
 7                if (key !== CACHE_NAME) {
 8                    console.log('[ServiceWorker] Removing old cache:', key);
 9                    return caches.delete(key);
10                }
11            }));
12        })
13    );
14    return self.clients.claim();
15});
  • activate 이벤트에서는 오래된 캐시를 삭제하여 저장 공간을 최적화합니다. 새 버전의 캐시만 유지됩니다.

fetch

1// Fetch event (cache-first strategy)
2self.addEventListener('fetch', event => {
3  console.log('[ServiceWorker] Fetch', event.request.url);
4    event.respondWith(
5        caches.match(event.request).then(response => {
6            return response || fetch(event.request).catch(() => caches.match('/offline.html'));
7        })
8    );
9});

모든 HTTP 요청을 가로채서 캐시된 버전이 있으면 반환하고, 없으면 네트워크에서 불러옵니다. 오프라인일 때는 대체 페이지(예: offline.html)를 반환합니다.

작동을 확인합니다

실제로 Service Worker가 어떻게 동작하는지 확인해봅시다.

 1document.getElementById('fetchBtn').addEventListener('click', () => {
 2    fetch('/style.css')
 3        .then(response => response.text())
 4        .then(data => {
 5            console.log('Fetched data:', data);
 6        })
 7        .catch(error => {
 8            console.error('Fetch failed:', error);
 9        });
10});
  • 여기서는 Service Worker의 등록과 테스트 버튼을 클릭하여 리소스 가져오기 동작을 확인합니다.

캐싱 전략 예시

주요 캐싱 전략은 다음과 같습니다:.

캐시 우선(Cache First)

Cache First 전략 구현 예시는 다음과 같습니다:.

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • 이 코드는 캐시 우선 전략을 구현하며, 요청된 리소스가 캐시에 있으면 캐시에서 반환하고, 없으면 네트워크에서 가져옵니다.

네트워크 우선(Network First)

Network First 전략 구현 예시는 다음과 같습니다:.

 1self.addEventListener('fetch', event => {
 2    event.respondWith(
 3        fetch(event.request)
 4            .then(response => {
 5                return caches.open(CACHE_NAME).then(cache => {
 6                    cache.put(event.request, response.clone());
 7                    return response;
 8                });
 9            })
10            .catch(() => caches.match(event.request))
11    );
12});
  • 이 코드는 네트워크 우선 전략을 구현하며, 요청된 리소스를 먼저 네트워크에서 가져오고 실패하면 캐시에서 가져옵니다.

스타일과 자바스크립트만 캐시하고, API는 실시간으로 접근

스타일과 자바스크립트는 캐싱하고, API에는 실시간으로 접근하는 구현 예시는 다음과 같습니다:.

 1self.addEventListener('fetch', event => {
 2    if (event.request.url.includes('/api/')) {
 3        // Fetch API responses in real-time without caching
 4        return;
 5    }
 6
 7    // Use cache-first strategy for static files
 8    event.respondWith(
 9        caches.match(event.request).then(response => {
10            return response || fetch(event.request);
11        })
12    );
13});
  • 이 코드는 항상 API 요청을 실시간으로 접근하고, 스타일시트나 자바스크립트와 같은 정적 파일에는 캐시 우선 전략을 적용합니다.

업데이트 흐름

서비스 워커의 업데이트 흐름은 다음과 같습니다:.

  1. 새로운 sw.js가 감지됨
  2. install 이벤트가 실행됨
  3. 이전 Service Worker가 유휴 상태가 될 때까지 대기함
  4. activate 이벤트가 실행됨
  5. 새로운 Service Worker로 전환됨
  6. controllerchange 이벤트가 발생합니다.

업데이트 감지

Service Worker가 설치되면 다음 방문까지 이전 것이 계속 사용됩니다. 업데이트를 적용하기 위해, 업데이트를 감지하고 페이지를 새로 고침하는 코드를 사용하는 것이 일반적입니다.

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • controllerchange 이벤트는 서비스 워커의 컨트롤러, 즉 현재 페이지를 제어하는 서비스 워커가 변경될 때 발생합니다.
  • 이미 열려 있는 페이지는 계속 현재 서비스 워커를 사용하며, 새로 설치된 서비스 워커는 즉시 해당 페이지에 적용되지 않습니다. 따라서 controllerchange 이벤트를 이용해 새로운 컨트롤러가 활성화되었음을 감지하고, 페이지를 새로 고침하여 업데이트를 즉시 적용하는 기법이 사용됩니다.

주의사항 및 권장사항

Service Worker 사용 시 다음 사항을 유의하세요:.

  • HTTPS 필수 보안상의 이유로, localhost를 제외하면 http://에서는 동작하지 않습니다.

  • 해시 기반 파일명 사용 캐시 이름에는 파일명, URL, 버전 정보를 포함할 수 있습니다.

  • 클라이언트와의 통신 postMessage를 이용해 Service Worker와 페이지의 자바스크립트 간 통신이 가능합니다.

요약

Service Worker는 웹 앱에서 오프라인 지원 및 성능 향상을 위한 필수 기술입니다. 설치, 활성화, fetch 처리 등 기본 동작 흐름을 이해하고, 적절한 캐싱 전략을 구현하면 더 완성도 높은 웹 애플리케이션을 개발할 수 있습니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video