안동민 개발노트 아이콘

안동민 개발노트

9장 : 웹 API와 브라우저 기능

웹 워커와 서비스 워커

우리는 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 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 객체, 작업 종료 기준을 한 번에 점검하는 흐름입니다.


서비스 워커 (Service Workers)

서비스 워커는 웹 워커의 한 종류로, 웹 페이지와 네트워크 사이에서 프록시(Proxy) 역할을 하는 자바스크립트 파일입니다. 웹 페이지와 독립된 백그라운드에서 실행되며, 주로 다음 기능을 제공합니다.

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

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

서비스 워커의 생명주기

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

등록 (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 상호작용 중 어느 문제를 해결하려는지에 따라 선택 지점을 나누어 보여줍니다.


워커 기반 성능 개선 정리

핵심은 메인 스레드 반응성을 유지하면서 백그라운드 작업을 어떤 기준으로 분리할지 판단하는 것입니다. 이번 절에서는 웹 애플리케이션의 성능과 사용자 경험을 개선하는 두 가지 백그라운드 스크립트 기술인 웹 워커(Web Workers)서비스 워커(Service Workers)를 살펴봤습니다.

웹 워커는 복잡한 연산을 메인 스레드 밖에서 처리해 UI 반응성을 유지하도록 돕습니다. postMessage() 기반 통신과 DOM 접근 불가라는 제약을 함께 기억해야 합니다.

이어서, 서비스 워커가 웹 워커의 개념을 확장하여 웹 페이지와 네트워크 사이에서 프록시 역할을 하며, 오프라인 지원, 캐싱, 푸시 알림 등 PWA의 핵심 기능을 구현하는 데 필수적임을 배웠습니다. 서비스 워커의 생명주기(등록, 설치, 활성화, 제어)와 install, activate, fetch 이벤트를 통한 캐싱 및 네트워크 요청 가로채기 방식의 기초를 알아보았습니다.

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

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


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

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

아래 다이어그램은 웹 워커 (Web Workers)에서 요청 흐름, 화면 전환, 작업 분리 지점을 확인합니다.

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

다음 학습으로 넘어가기 전, 웹 워커와 서비스 워커에서 남은 개념 경계와 실습 확인 포인트를 점검합니다.