서비스 워커로 마이그레이션

배경 또는 이벤트 페이지를 서비스 워커로 대체

서비스 워커는 백그라운드 코드가 기본 스레드에서 실행되지 않도록 확장 프로그램의 백그라운드 또는 이벤트 페이지를 대체합니다. 이렇게 하면 확장 프로그램이 필요할 때만 실행되므로 리소스를 절약할 수 있습니다.

배경 페이지는 도입 이후 확장 프로그램의 기본 구성요소였습니다. 간단히 말해 백그라운드 페이지는 다른 창이나 탭과는 별개로 존재하는 환경을 제공합니다. 이를 통해 확장 프로그램은 이벤트에 응답하여 관찰하고 작업할 수 있습니다.

이 페이지에서는 백그라운드 페이지를 확장 프로그램 서비스 워커로 변환하는 작업을 설명합니다. 확장 프로그램 서비스 워커에 관한 일반적인 자세한 내용은 서비스 워커로 이벤트 처리 튜토리얼과 확장 프로그램 서비스 워커 정보 섹션을 참고하세요.

백그라운드 스크립트와 확장 프로그램 서비스 워커의 차이점

일부 컨텍스트에서는 '백그라운드 스크립트'라는 확장 프로그램 서비스 작업자가 표시됩니다. 확장 프로그램 서비스 워커는 백그라운드에서 실행되지만 백그라운드 스크립트라고 부르는 것은 동일한 기능을 암시하여 다소 혼동을 줄 수 있습니다. 그 차이는 아래에 설명되어 있습니다.

백그라운드 페이지의 변경사항

서비스 워커는 백그라운드 페이지와 여러 가지 차이점이 있습니다.

  • 기본 스레드 외부에서 작동하므로 확장 프로그램 콘텐츠를 방해하지 않습니다.
  • 확장 프로그램의 출처에서 가져오기 이벤트를 가로채는 기능(예: 툴바 팝업에서 가져오는 이벤트)과 같은 특수 기능이 있습니다.
  • 클라이언트 인터페이스를 통해 다른 컨텍스트와 통신하고 상호작용할 수 있습니다.

변경해야 할 사항

백그라운드 스크립트와 서비스 워커의 작동 방식 간의 차이를 고려하여 몇 가지 코드를 조정해야 합니다. 우선, 매니페스트 파일에 서비스 워커가 지정되는 방식은 백그라운드 스크립트가 지정되는 방식과 다릅니다. 추가 조치:

  • 이러한 호출은 DOM 또는 window 인터페이스에 액세스할 수 없으므로 이러한 호출을 다른 API 또는 오프스크린 문서로 이동해야 합니다.
  • 이벤트 리스너는 반환된 약속의 응답으로 또는 이벤트 콜백 내부에서 등록되어서는 안 됩니다.
  • XMLHttpRequest()와 하위 호환되지 않으므로 이 인터페이스에 대한 호출을 fetch() 호출로 대체해야 합니다.
  • 사용하지 않을 때는 종료되므로 전역 변수를 사용하는 대신 애플리케이션 상태를 유지해야 합니다. 서비스 워커를 종료하면 타이머가 완료되기 전에 종료할 수도 있습니다. 알람으로 교체해야 합니다.

이 페이지에서는 이러한 작업을 자세히 설명합니다.

매니페스트의 'background' 필드 업데이트

매니페스트 V3에서는 백그라운드 페이지가 서비스 워커로 대체됩니다. 매니페스트 변경사항은 다음과 같습니다.

  • manifest.json에서 "background.scripts""background.service_worker"로 바꿉니다. "service_worker" 필드는 문자열 배열이 아닌 문자열을 사용합니다.
  • manifest.json에서 "background.persistent"를 삭제합니다.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

"service_worker" 필드는 단일 문자열을 사용합니다. ES 모듈 (import 키워드 사용)을 사용하는 경우에만 "type" 필드가 필요합니다. 값은 항상 "module"입니다. 자세한 내용은 확장 프로그램 서비스 워커 기본사항을 참고하세요.

DOM 및 창 호출을 오프스크린 문서로 이동

일부 확장 프로그램은 새 창이나 탭을 시각적으로 열지 않고도 DOM 및 창 객체에 액세스해야 합니다. Offscreen API는 사용자 환경을 방해하지 않고 확장 프로그램과 함께 패키징된 표시되지 않는 문서를 열고 닫아 이러한 사용 사례를 지원합니다. 오프스크린 문서는 메시지 전달을 제외하고 다른 확장 프로그램 컨텍스트와 API를 공유하지 않지만 확장 프로그램이 상호작용할 수 있는 전체 웹페이지로 작동합니다.

Offscreen API를 사용하려면 서비스 워커에서 오프스크린 문서를 만듭니다.

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

화면 밖 문서에서는 이전에 백그라운드 스크립트에서 실행했을 모든 작업을 수행합니다. 예를 들어 호스트 페이지에서 선택한 텍스트를 복사할 수 있습니다.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

메시지 전달을 사용하여 오프스크린 문서와 확장 프로그램 서비스 작업자 간에 통신합니다.

localStorage를 다른 유형으로 변환

웹 플랫폼의 Storage 인터페이스 (window.localStorage에서 액세스 가능)는 서비스 워커에서 사용할 수 없습니다. 이 문제를 해결하려면 다음 두 가지 중 하나를 수행하세요. 먼저 다른 저장소 메커니즘 호출로 대체할 수 있습니다. chrome.storage.local 네임스페이스는 대부분의 사용 사례에 적합하지만 다른 옵션도 사용할 수 있습니다.

호출을 오프스크린 문서로 이동할 수도 있습니다. 예를 들어 이전에 localStorage에 저장된 데이터를 다른 메커니즘으로 이전하려면 다음을 실행합니다.

  1. 변환 루틴과 runtime.onMessage 핸들러를 사용하여 오프스크린 문서를 만듭니다.
  2. 오프스크린 문서에 변환 루틴을 추가합니다.
  3. 확장 프로그램 서비스 워커에서 chrome.storage에서 데이터를 확인합니다.
  4. 데이터를 찾을 수 없는 경우 오프스크린 문서를 create runtime.sendMessage()를 호출하여 변환 루틴을 시작합니다.
  5. 오프스크린 문서에 추가한 runtime.onMessage 핸들러에서 변환 루틴을 호출합니다.

또한 확장 프로그램에서 Web Storage API가 작동하는 방식에도 약간의 차이가 있습니다. 저장소 및 쿠키에서 자세히 알아보세요.

리스너 동기식 등록

리스너를 비동기식으로 (예: 프로미스 또는 콜백 내부) 등록해도 Manifest V3에서 작동하지 않을 수 있습니다. 다음 코드를 살펴보세요.

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

페이지가 계속 실행되고 다시 초기화되지 않으므로 영구 백그라운드 페이지에서 작동합니다. Manifest V3에서는 이벤트가 전달될 때 서비스 워커가 다시 초기화됩니다. 즉, 이벤트가 실행될 때 리스너가 비동기식으로 추가되므로 등록되지 않으며 이벤트가 누락됩니다.

대신 이벤트 리스너 등록을 스크립트의 최상위 수준으로 이동합니다. 이렇게 하면 확장 프로그램이 시작 로직 실행을 완료하지 않았더라도 Chrome에서 작업의 클릭 핸들러를 즉시 찾아 호출할 수 있습니다.

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

XMLHttpRequest()를 전역 fetch()로 대체

XMLHttpRequest()는 서비스 워커, 확장 프로그램 등에서 호출할 수 없습니다. 백그라운드 스크립트의 XMLHttpRequest() 호출을 글로벌 fetch() 호출로 바꿉니다.

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

상태 유지

서비스 워커는 일시적이므로 사용자의 브라우저 세션 중에 반복적으로 시작, 실행, 종료될 가능성이 높습니다. 또한 이전 컨텍스트가 해제되었으므로 전역 변수에서 데이터를 즉시 사용할 수 없습니다. 이 문제를 해결하려면 Storage API를 정보 소스로 사용하세요. 예를 통해 이 방법을 알아보겠습니다.

다음 예에서는 전역 변수를 사용하여 이름을 저장합니다. 서비스 워커에서 이 변수는 사용자의 브라우저 세션이 진행되는 동안 여러 번 재설정될 수 있습니다.

Manifest V2 백그라운드 스크립트
let savedName = undefined;

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.sendMessage(tab.id, { name: savedName });
});

매니페스트 V3의 경우 전역 변수를 Storage API 호출로 바꿉니다.

Manifest V3 서비스 워커
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

타이머를 알람으로 변환

setTimeout() 또는 setInterval() 메서드를 사용하여 지연된 작업이나 주기적인 작업을 사용하는 것이 일반적입니다. 하지만 서비스 워커가 종료될 때마다 타이머가 취소되므로 이러한 API는 서비스 워커에서 실패할 수 있습니다.

Manifest V2 백그라운드 스크립트
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

대신 Alarms API를 사용하세요. 다른 리스너와 마찬가지로 알람 리스너는 스크립트의 최상위 수준에 등록해야 합니다.

Manifest V3 서비스 워커
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

서비스 워커 유지

서비스 워커는 정의상 이벤트 기반이며 비활성 상태에서 종료됩니다. 이렇게 하면 Chrome에서 확장 프로그램의 성능과 메모리 소비를 최적화할 수 있습니다. 서비스 워커 수명 주기 문서에서 자세히 알아보세요. 예외적인 경우에는 서비스 워커가 더 오래 유지되도록 추가 조치를 취해야 할 수 있습니다.

장기 실행 작업이 완료될 때까지 서비스 워커를 활성화 상태로 유지

확장 프로그램 API를 호출하지 않는 장기 실행 서비스 워커 작업 중에 서비스 워커가 작업 중간에 종료될 수 있습니다. 예를 들면 다음과 같습니다.

  • 5분 이상 걸릴 수 있는 fetch() 요청 (예: 연결 상태가 좋지 않은 경우 대용량 다운로드)
  • 복잡한 비동기 계산은 30초 이상 걸립니다.

이러한 경우 서비스 워커 전체 기간을 연장하려면 주기적으로 사소한 확장 API를 호출하여 제한 시간 카운터를 재설정하면 됩니다. 이는 예외적인 경우에만 예약되어 있으며 대부분의 경우 동일한 결과를 얻는 더 나은 플랫폼 관용적인 방법이 있습니다.

다음 예는 지정된 프로미스가 확인될 때까지 서비스 워커를 활성 상태로 유지하는 waitUntil() 도우미 함수를 보여줍니다.

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

서비스 워커를 지속적으로 유지

드물지만 전체 기간을 무기한 연장해야 하는 경우도 있습니다. Google은 엔터프라이즈 및 교육을 가장 큰 사용 사례로 파악했으며, 이러한 사용 사례에서는 이를 허용하지만 일반적으로는 지원하지 않습니다. 이러한 예외적인 상황에서는 사소한 확장 프로그램 API를 주기적으로 호출하여 서비스 워커를 유지할 수 있습니다. 이 권장사항은 기업 또는 교육 사용 사례를 위해 관리 기기에서 실행되는 확장 프로그램에만 적용됩니다. 다른 경우에는 허용되지 않으며 Chrome 확장 프로그램팀은 향후 이러한 확장 프로그램에 대해 조치를 취할 권리를 보유합니다.

다음 코드 스니펫을 사용하여 서비스 워커를 유지합니다.

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}