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

웹 워커 사용법

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


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

워커 기반 성능 개선 정리

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

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

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

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

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

목차