icon
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)에 있어야 합니다.

웹 워커 사용법

1. 워커 스크립트 파일 생성 (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);
};

2. 메인 스크립트에서 워커 생성 및 통신 (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() 메서드를 호출하여 워커 스레드를 즉시 종료할 수 있습니다. 이는 더 이상 워커가 필요하지 않을 때 리소스 누수를 방지하는 데 중요합니다.


서비스 워커 (Service Workers)

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

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

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

서비스 워커의 생명주기

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

  1. 등록 (Registration): 웹 페이지에서 서비스 워커 파일을 등록합니다.
  2. 설치 (Installation): 서비스 워커 파일이 다운로드되고, install 이벤트가 발생합니다. 여기서 보통 필요한 애셋(asset)들을 캐시에 저장합니다.
  3. 활성화 (Activation): activate 이벤트가 발생하며, 이전 버전의 서비스 워커를 정리하고 새 서비스 워커가 제어권을 가져옵니다.
  4. 제어 (Controlling): 활성화된 서비스 워커는 이제 해당 출원의 네트워크 요청을 가로채고 처리할 수 있습니다.
  5. 종료 (Termination): 사용되지 않을 때는 메모리를 절약하기 위해 종료될 수 있으며, 필요할 때 다시 시작됩니다.

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

1. 서비스 워커 스크립트 파일 생성 (sw.js)

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

const CACHE_NAME = 'my-pwa-cache-v1'; // 캐시 이름 정의
const urlsToCache = [ // 캐시할 파일 목록
    '/',
    '/index.html',
    '/styles.css',
    '/main.js',
    '/images/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);
            })
    );
});

2. 메인 스크립트에서 서비스 워커 등록 (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): 사용자가 방문할 가능성이 있는 페이지의 리소스를 미리 캐싱.

마무리하며

이번 장에서는 웹 애플리케이션의 성능과 사용자 경험을 혁신적으로 개선하는 두 가지 강력한 백그라운드 스크립트 기술인 웹 워커(Web Workers)서비스 워커(Service Workers) 에 대해 심도 있게 학습했습니다.

여러분은 웹 워커가 메인 스레드를 블로킹하지 않고 복잡한 연산을 백그라운드에서 수행하여 UI의 반응성을 유지하는 방법을 이해했습니다. 또한, postMessage()를 통한 메시지 기반 통신과 DOM 접근 불가라는 주요 특징을 살펴보았습니다.

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

웹 워커와 서비스 워커는 모던 웹 애플리케이션, 특히 PWA를 개발하는 데 있어 매우 중요한 기술입니다. 이들을 통해 여러분은 사용자에게 더욱 빠르고, 안정적이며, 풍부한 경험을 제공하는 웹 애플리케이션을 만들 수 있습니다. 서비스 워커는 실제 배포 환경에서 HTTPS를 요구하고 디버깅이 다소 복잡할 수 있으므로, Chrome 개발자 도구의 'Application' 탭을 활용하여 워커의 상태와 캐시를 확인하며 연습하는 것이 중요합니다.

이로써 9장 "웹 API와 브라우저 기능"의 모든 내용이 마무리되었습니다. 여러분은 이제 자바스크립트 언어의 최신 기능뿐만 아니라, 브라우저가 제공하는 강력한 API들을 활용하여 동적이고 효율적이며 성능까지 뛰어난 웹 애플리케이션을 만들 수 있는 기반을 다졌습니다. 지속적인 실습과 탐구를 통해 여러분의 웹 개발 역량을 더욱 강화하시길 바랍니다.