본문으로 건너뛰기

안동민 개발노트

본문 시작

웹 워커와 서비스 워커

Web Worker로 무거운 계산을 메인 스레드에서 분리하고 Service Worker의 생명주기와 캐시·오프라인 응답을 익힙니다.

우리는 9장에서 Fetch API로 서버 통신, 로컬/세션 스토리지로 클라이언트 데이터 저장, History API로 SPA 라우팅 구현, Canvas API로 웹 그래픽 처리 등 다양한 웹 API와 브라우저 기능들을 학습했습니다.

이제 웹 애플리케이션의 성능과 사용자 경험을 한 단계 더 끌어올릴 수 있는 고급 기술인 웹 워커(Web Workers)서비스 워커(Service Workers)에 대해 알아볼 차례입니다.

자바스크립트는 기본적으로 단일 스레드(Single-threaded) 언어입니다.

이는 웹 페이지의 UI 렌더링, 이벤트 처리, DOM 조작 등 모든 작업이 하나의 메인 스레드에서 순차적으로 실행된다는 의미입니다.

만약 이 메인 스레드에서 복잡하거나 시간이 오래 걸리는 연산(예: 대용량 데이터 처리, 이미지 필터링, 복잡한 애니메이션 계산)을 수행하게 되면, 해당 작업이 끝날 때까지 UI가 멈추거나 반응이 없어지는 블로킹(Blocking) 현상이 발생하여 사용자 경험을 크게 저해합니다.

웹 워커는 메인 스레드 블로킹 문제를 줄이기 위해 자바스크립트 코드를 별도 스레드에서 실행합니다.

이를 통해 계산 비용이 큰 작업을 UI 렌더링 흐름과 분리할 수 있습니다.

서비스 워커는 네트워크 요청 가로채기, 캐싱, 오프라인 응답 같은 기능을 담당하는 브라우저의 백그라운드 실행 단위입니다.

이 절에서는 웹 워커와 서비스 워커의 동작 원리, 기본 사용법, 그리고 활용 시나리오를 정리합니다.

두 기술은 Progressive Web Apps (PWA)의 핵심 구성 요소이기도 합니다.

Web Worker와 Service Worker 선택 기준

이름은 비슷하지만 해결하는 병목이 다르다. UI가 멈추는지, 네트워크와 오프라인 응답을 제어해야 하는지부터 구분하면 선택이 단순해진다.

  1. Web Worker

    Service Worker 선택 기준 CPU 계산은 Web Worker로, 요청·캐시는 Service Worker로 분리한다

  2. 이름

    비슷하지만 해결하는 병목이 다르다.

  3. UI

    멈추는지, 네트워크와 오프라인 응답을 제어해야 하는지부터 구분하면 선택이 단순해진다.

  4. 계산 병목 긴 연산

    메인 스레드를 막는다


웹 워커 (Web Workers)

웹 워커는 웹 페이지의 메인 실행 스크립트와 독립된 백그라운드 스레드에서 스크립트를 실행할 수 있도록 해줍니다.

이를 통해 메인 스레드가 무거운 연산으로 인해 블로킹되는 것을 방지하고, 웹 페이지의 UI가 항상 반응성을 유지하도록 할 수 있습니다.

웹 워커의 특징

  • 독립적인 스레드: 웹 워커는 메인 스레드와 별도의 실행 환경을 가집니다.
  • DOM 접근 불가: 워커 스레드는 HTML 문서의 DOM에 직접 접근할 수 없습니다. 이는 워커가 UI 업데이트를 직접 수행할 수 없다는 의미입니다.
  • 통신 방식: 메인 스레드와 워커 스레드는 postMessage() 메서드를 통한 메시지 기반 통신(onmessage 이벤트 리스너)으로 데이터를 주고받습니다. 데이터는 복사되어 전달됩니다 (Serialization).
  • 제한된 API: 워커는 alert(), confirm()과 같은 브라우저 UI 관련 API에 접근할 수 없습니다. 대신 XMLHttpRequest, fetch, IndexedDB, setTimeout, setInterval 등은 사용할 수 있습니다.
  • 동일 출처 정책: 워커 스크립트는 메인 스크립트와 동일한 출처(origin)에 있어야 합니다.

웹 워커 사용법

워커 스크립트 파일 생성 (worker.js)

메인 스크립트와 독립적으로 실행될 자바스크립트 파일을 생성합니다.

// worker.js
// 이 스크립트는 웹 워커 스레드에서 실행됩니다.

// 메인 스크립트로부터 메시지를 받으면 실행되는 이벤트 핸들러
self.onmessage = function(event) {
    const number = event.data; // 메인 스크립트로부터 전달받은 데이터

    console.log(`워커: ${number}까지의 소수 계산을 시작합니다.`);

    // 복잡하고 시간이 오래 걸리는 연산 (예시: 소수 계산)
    let primes = [];
    for (let i = 2; i <= number; i++) {
        let isPrime = true;
        for (let j = 2; j * j <= i; j++) {
            if (i % j === 0) {
                isPrime = false;
                break;
            }
        }
        if (isPrime) {
            primes.push(i);
        }
    }

    console.log(`워커: ${number}까지의 소수 계산 완료.`);

    // 계산 결과를 다시 메인 스크립트로 전송
    self.postMessage(primes);
};
메인 스크립트에서 워커 생성 및 통신 (main.js 또는 HTML <script>)

메인 페이지 스크립트에서 Worker 객체를 생성하고, postMessage()로 데이터를 주고받습니다.

// main.js (또는 HTML <script>)
const startButton = document.getElementById('startButton');
const resultDiv = document.getElementById('result');
const statusDiv = document.getElementById('status');

let myWorker;

startButton.addEventListener('click', () => {
    statusDiv.textContent = "워커 시작 중... 잠시 기다려 주세요.";
    // 이전에 워커가 있다면 종료
    if (myWorker) {
        myWorker.terminate();
        myWorker = null;
    }

    // 새 워커 생성 (워커 스크립트 파일 경로 지정)
    myWorker = new Worker('worker.js');

    // 워커로부터 메시지를 받을 때 실행될 함수
    myWorker.onmessage = function(event) {
        const primes = event.data; // 워커로부터 전달받은 소수 배열
        resultDiv.textContent = `계산된 소수 개수: ${primes.length}`;
        statusDiv.textContent = "계산 완료! (메인 스레드는 블로킹되지 않았습니다.)";
        myWorker.terminate(); // 작업이 끝나면 워커 종료
        myWorker = null;
    };

    // 워커에서 에러 발생 시
    myWorker.onerror = function(error) {
        statusDiv.textContent = `워커 오류: ${error.message}`;
        console.error("워커 오류:", error);
    };

    // 워커에게 계산을 시작하도록 메시지 전송
    const numberToCalculate = 500000; // 큰 숫자
    myWorker.postMessage(numberToCalculate);

    statusDiv.textContent = `워커에게 ${numberToCalculate}까지의 소수 계산 명령을 보냈습니다. (UI는 반응 중)`;
});

// HTML에 버튼과 결과 표시 영역 추가
/*
<button id="startButton">복잡한 계산 시작 (워커 사용)</button>
<div id="status"></div>
<div id="result"></div>
*/

이 예시에서 복잡한 소수 계산은 백그라운드 워커 스레드에서 실행되므로, 메인 스레드는 UI 업데이트나 사용자 이벤트 처리에 전혀 지장받지 않습니다.

계산 중에도 다른 UI 요소들을 조작할 수 있습니다.

워커 종료: worker.terminate()

워커 인스턴스의 terminate() 메서드를 호출하여 워커 스레드를 즉시 종료할 수 있습니다.

이는 더 이상 워커가 필요하지 않을 때 리소스 누수를 방지하는 데 중요합니다.

웹 워커는 계산을 분리해 주지만, 메인 스레드와 데이터를 주고받는 비용도 함께 생깁니다.

아래 다이어그램은 postMessage, 구조화 복제, Transferable 객체, 작업 종료 기준을 한 번에 점검하는 흐름입니다.

웹 워커 메시지 비용 설계

무거운 연산을 워커로 옮겨도 큰 데이터를 계속 복사하면 이득이 줄어듭니다. 메시지 크기와 종료 조건까지 설계해야 합니다.

  1. clone
    작은 값 복사

    숫자, 문자열, 작은 객체는 구조화 복제로 보내도 부담이 작습니다.

  2. transfer
    큰 버퍼 이동

    ArrayBuffer처럼 큰 바이너리는 Transferable로 소유권을 넘기면 복사 비용을 줄입니다.

  3. chunk
    진행률 나누기

    긴 작업은 중간 결과와 진행률을 보내 사용자가 멈춘 화면으로 느끼지 않게 합니다.

  4. stop
    워커 종료 조건

    작업이 끝나거나 화면을 떠나면 terminate로 정리해 메모리와 CPU를 회수합니다.


서비스 워커 (Service Workers)

서비스 워커는 웹 워커의 한 종류로, 웹 페이지와 네트워크 사이에서 프록시(Proxy) 역할을 하는 자바스크립트 파일입니다.

웹 페이지와 독립된 백그라운드에서 실행되며, 주로 다음 기능을 제공합니다.

  • 오프라인 경험: 네트워크 연결 없이도 웹 애플리케이션을 동작하게 합니다.
  • 캐싱: 네트워크 요청을 가로채서 캐시에서 리소스를 반환하여 웹 페이지 로딩 속도를 향상시킵니다.
  • 푸시 알림: 사용자가 브라우저를 닫았거나 오프라인 상태일 때도 서버로부터 푸시 알림을 받을 수 있게 합니다.
  • 백그라운드 동기화: 백그라운드에서 서버와 데이터를 동기화합니다.

서비스 워커는 Progressive Web Apps (PWA)의 핵심 기술이며, 웹 사이트를 모바일 앱처럼 동작하게 만드는 데 필수적입니다.

서비스 워커의 생명주기

서비스 워커는 요청 앞의 별도 실행 계층이다

서비스 워커는 페이지와 독립적으로 설치·활성화된 뒤 fetch를 가로채 캐시와 네트워크 중 어떤 응답을 줄지 결정한다.

수명 단계역할확인할 것실패 신호
register페이지가 SW 파일 등록scope와 HTTPS 조건등록은 됐지만 요청을 못 가로챔
install초기 캐시 준비필수 자산 캐시 성공install 실패로 활성화 안 됨
activate오래된 캐시 정리버전 정책예전 캐시가 계속 응답
fetch요청별 cache/network 선택전략이 자원 성격과 맞는가오프라인/갱신 정책 혼란

서비스 워커는 메인 스크립트와는 다른 복잡한 생명주기를 가집니다.

등록 (Registration): 웹 페이지에서 서비스 워커 파일을 등록합니다.

설치 (Installation): 서비스 워커 파일이 다운로드되고, install 이벤트가 발생합니다.

여기서 보통 필요한 애셋(asset)들을 캐시에 저장합니다.

활성화 (Activation): activate 이벤트가 발생하며, 이전 버전의 서비스 워커를 정리하고 새 서비스 워커가 제어권을 가져옵니다.

제어 (Controlling): 활성화된 서비스 워커는 이제 해당 출원의 네트워크 요청을 가로채고 처리할 수 있습니다.

종료 (Termination): 사용되지 않을 때는 메모리를 절약하기 위해 종료될 수 있으며, 필요할 때 다시 시작됩니다.

아래 다이어그램은 서비스 워커가 등록된 뒤 install, activate, fetch 단계로 넘어가며 실제 요청을 캐시와 네트워크 중 어디로 보낼지 결정하는 흐름을 한 장으로 묶어 보여줍니다.

서비스 워커 사용법 (기초)

서비스 워커 스크립트 파일 생성 (sw.js)
// sw.js
// 이 스크립트는 서비스 워커 스레드에서 실행됩니다.

const CACHE_NAME = 'my-pwa-cache-v1'; // 캐시 이름 정의
const urlsToCache = [ // 캐시할 파일 목록
    '/',
    '/index.html',
    '/styles.css',
    '/main.js',
    '/logo.png' // 예시 이미지
];

// install 이벤트: 서비스 워커 설치 시점에 발생
self.addEventListener('install', (event) => {
    console.log('[Service Worker] 설치 중...', event);
    // 캐싱 작업 수행 (비동기)
    event.waitUntil(
        caches.open(CACHE_NAME) // 캐시 저장소 열기
            .then((cache) => {
                console.log('[Service Worker] 모든 리소스를 캐시합니다.');
                return cache.addAll(urlsToCache); // 파일들을 캐시에 추가
            })
    );
});

// activate 이벤트: 서비스 워커 활성화 시점에 발생 (이전 워커 제거 등)
self.addEventListener('activate', (event) => {
    console.log('[Service Worker] 활성화 중...', event);
    event.waitUntil(
        // 이전 버전의 캐시를 삭제하여 업데이트 처리
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('[Service Worker] 오래된 캐시 삭제:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    // 서비스 워커가 페이지를 즉시 제어하도록 설정
    return self.clients.claim();
});

// fetch 이벤트: 네트워크 요청을 가로챔 (가장 중요!)
self.addEventListener('fetch', (event) => {
    console.log('[Service Worker] Fetch 요청 가로챔:', event.request.url);
    event.respondWith(
        caches.match(event.request) // 요청이 캐시에 있는지 확인
            .then((response) => {
                // 캐시에 응답이 있으면 캐시된 응답 반환
                if (response) {
                    console.log('[Service Worker] 캐시에서 응답 반환:', event.request.url);
                    return response;
                }
                // 캐시에 없으면 네트워크 요청
                console.log('[Service Worker] 네트워크 요청:', event.request.url);
                return fetch(event.request);
            })
    );
});
메인 스크립트에서 서비스 워커 등록 (main.js 또는 HTML <script>)

메인 페이지 스크립트에서 서비스 워커를 등록합니다.

일반적으로 DOMContentLoaded 이벤트 이후에 등록합니다.

// main.js (또는 HTML <script>)
if ('serviceWorker' in navigator) { // 브라우저가 서비스 워커를 지원하는지 확인
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js') // 서비스 워커 파일 경로 지정
            .then((registration) => {
                console.log('Service Worker 등록 성공:', registration.scope);
            })
            .catch((error) => {
                console.error('Service Worker 등록 실패:', error);
            });
    });
} else {
    console.warn('이 브라우저는 Service Worker를 지원하지 않습니다.');
}

이 코드를 배포하면, 사용자가 처음 웹사이트에 접속할 때 서비스 워커가 설치되고, 이후에는 네트워크 요청을 서비스 워커가 가로채 캐시에서 응답을 제공하거나 네트워크 요청을 수행할 수 있습니다.

이를 통해 웹 페이지 로딩 속도를 비약적으로 향상시키고, 심지어 네트워크 연결이 없어도 일부 기능을 사용할 수 있는 오프라인 경험을 제공할 수 있습니다.

서비스 워커의 활용 분야

  • 오프라인 웹 애플리케이션: PWA의 핵심으로, 네트워크 연결 유무에 상관없이 동작하는 앱 구현.
  • 빠른 로딩: 캐싱 전략을 통해 반복 방문 시 페이지 로딩 속도 극대화.
  • 푸시 알림: 사용자에게 서버에서 보낸 푸시 메시지 전달.
  • 백그라운드 데이터 동기화: 네트워크가 재연결될 때 자동으로 서버와 데이터 동기화.
  • 리소스 미리 가져오기(Pre-caching): 사용자가 방문할 가능성이 있는 페이지의 리소스를 미리 캐싱.

웹 워커와 서비스 워커는 이름이 비슷하지만 적용 기준이 다릅니다.

아래 다이어그램은 CPU 계산, 네트워크 캐싱, DOM 상호작용 중 어느 문제를 해결하려는지에 따라 선택 지점을 나누어 보여줍니다.

웹 워커와 서비스 워커 선택 기준

메인 스레드가 바쁜지, 네트워크 요청을 제어해야 하는지, 화면을 즉시 바꿔야 하는지에 따라 선택지가 달라집니다.

  1. Main thread 화면

    DOM 반응 클릭, 입력, 포커스처럼 사용자와 바로 연결된 작업을 처리합니다.

  2. DOM

    직접 읽고 바꾸는 코드는 메인 스레드에서 실행합니다.

  3. 오래 걸리

    계산을 넣으면 렌더링과 입력 반응이 함께 느려집니다.


워커 기반 성능 개선 정리

핵심은 메인 스레드 반응성을 유지하면서 백그라운드 작업을 어떤 기준으로 분리할지 판단하는 것입니다.

이번 절에서는 웹 애플리케이션의 성능과 사용자 경험을 개선하는 두 가지 백그라운드 스크립트 기술인 웹 워커(Web Workers)서비스 워커(Service Workers)를 살펴봤습니다.

웹 워커는 복잡한 연산을 메인 스레드 밖에서 처리해 UI 반응성을 유지하도록 돕습니다.

postMessage() 기반 통신과 DOM 접근 불가라는 제약을 함께 기억해야 합니다.

이어서, 서비스 워커가 웹 워커의 개념을 확장하여 웹 페이지와 네트워크 사이에서 프록시 역할을 하며, 오프라인 지원, 캐싱, 푸시 알림 등 PWA의 핵심 기능을 구현하는 데 필수적임을 배웠습니다.

서비스 워커의 생명주기(등록, 설치, 활성화, 제어)와 install, activate, fetch 이벤트를 통한 캐싱 및 네트워크 요청 가로채기 방식의 기초를 알아보았습니다.

웹 워커와 서비스 워커는 PWA와 고성능 웹 애플리케이션에서 자주 쓰입니다.

서비스 워커는 실제 배포 환경에서 HTTPS를 요구하고 디버깅이 복잡할 수 있으므로, Chrome 개발자 도구의 Application 탭에서 상태와 캐시를 확인해야 합니다.

이로써 9장 웹 API와 브라우저 기능의 주요 내용을 마무리합니다.

최신 자바스크립트 기능과 브라우저 API를 함께 이해하면, UI 반응성, 저장, 통신, 백그라운드 처리까지 한 흐름으로 점검할 수 있습니다.


웹 워커와 서비스 워커는 모두 메인 스레드 밖에서 동작하지만, 해결하는 문제가 계산 분리와 네트워크 중간자라는 점에서 다릅니다.

웹 워커는 계산, 서비스 워커는 네트워크를 분리한다

둘 다 메인 스레드 밖에서 실행되지만 목적이 다르다. UI 멈춤을 줄일지, fetch/cache 경로를 제어할지부터 나눈다.

선택쓸 때통신 방식주의점
Dedicated Worker큰 계산·파싱·압축 분리postMessageDOM 직접 접근 불가
Transferable큰 ArrayBuffer 전달소유권 이동원본 buffer 사용 불가
Service Worker캐시·오프라인·요청 제어fetch event, Cache APIHTTPS와 scope 필요
Lifecycle설치·활성화·업데이트 관리install/activate새 버전 적용 지연

웹 워커와 서비스 워커 절은 웹 워커, 웹 워커의 특징, 웹 워커 사용법 중심으로 웹 워커의 역할, 사용법, 적용 전 확인할 제약을 정리합니다.

Web Worker와 Service Worker는 책임 경계가 다르다

Web Worker는 현재 페이지의 무거운 일을 덜어 주고, Service Worker는 origin 단위 요청을 가로채 앱 수준 캐시 정책을 담당한다.

구분책임수명대표 위험
Web WorkerCPU 작업을 UI 밖에서 처리페이지와 함께 생성·종료복사 비용이 계산 이득보다 큼
Service Workerfetch/cache/push 같은 앱 기능페이지와 독립된 lifecycle업데이트가 즉시 반영되지 않음
공통 제한DOM 직접 접근 불가메시지 기반 협업UI를 직접 만진다고 기대
선택 기준계산 분리 vs 네트워크 제어작업 성격이 기준이름이 비슷해 혼용

웹 워커와 서비스 워커를 작업 위치, 생명주기, 캐시 책임 기준으로 정리합니다.

워커의 실행 위치를 보면 용도가 갈린다

Web Worker는 페이지가 만든 계산 보조자이고, Service Worker는 페이지 밖에서 요청을 중재하는 네트워크 계층이다.

위치주 역할데이터 교환확인할 것
main threadDOM과 UI 갱신worker 결과를 화면에 반영긴 작업으로 UI를 막지 않는가
Web Worker계산·파싱·변환 수행postMessage/transfer직렬화 비용이 큰가
Service Workerfetch와 cache 전략 적용event.respondWithscope와 HTTPS가 맞는가
Cache boundary오프라인과 갱신 정책Cache APIstale 응답 정리 정책