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

백그라운드 또는 이벤트 페이지를 서비스 워커로 대체

백그라운드 코드가 기본 스레드에 남아 있도록 확장 프로그램의 백그라운드 또는 이벤트 페이지를 서비스 워커가 대체합니다. 이를 통해 필요한 경우에만 확장 프로그램을 실행하여 리소스를 절약할 수 있습니다.

백그라운드 페이지는 확장 프로그램이 도입된 이후로 기본 구성요소였습니다. 간단히 말하자면, 백그라운드 페이지는 다른 창이나 탭과 상관없는 환경을 제공합니다. 이렇게 하면 확장 프로그램이 이벤트를 관찰하고 이벤트에 응답하여 작업할 수 있습니다.

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

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

어떤 컨텍스트에서는 '백그라운드 스크립트'라는 확장 프로그램 서비스 워커를 볼 수 있습니다. 확장 프로그램 서비스 워커가 백그라운드에서 실행되지만 백그라운드 스크립트를 호출하는 것은 동일한 기능을 암시하므로 다소 오해의 소지가 있습니다. 그 차이는 아래에 설명되어 있습니다.

백그라운드 페이지에서 발생한 변경사항

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

  • 기본 스레드 밖에서 작동합니다. 즉, 확장 프로그램 콘텐츠를 방해하지 않습니다.
  • 확장 프로그램 출처의 가져오기 이벤트(예: 툴바 팝업)를 가로채는 등의 특별한 기능이 있습니다.
  • Clients 인터페이스를 통해 다른 컨텍스트와 통신하고 상호작용할 수 있습니다.

필요한 변경사항

백그라운드 스크립트와 서비스 워커가 작동하는 방식의 차이를 고려하여 몇 가지 코드를 조정해야 합니다. 먼저 매니페스트 파일에서 서비스 워커를 지정하는 방식은 백그라운드 스크립트를 지정하는 방식과 다릅니다. 다음과 같은 기능이 제공됩니다.

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

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

매니페스트의 '백그라운드' 필드 업데이트

Manifest 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. 데이터를 찾을 수 없는 경우 오프스크린 문서를 만들고 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 });
});

Manifest 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를 사용하지 않는 장기 실행 서비스 워커 작업 중에는 서비스 워커가 작업 중에 종료될 수 있습니다. 예를 들면 다음과 같습니다.

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

이런 경우 서비스 워커 수명을 연장하려면 주기적으로 trivial Extension 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'];
}