서비스 워커와의 양방향 통신

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

경우에 따라 웹 앱이 페이지와 서비스 워커 간에 양방향 통신 채널을 설정해야 할 수 있습니다.

예를 들어 팟캐스트 PWA에서 사용자가 오프라인 소비를 위해 에피소드를 오프라인 저장하고 서비스 워커가 진행 상황에 관해 페이지에 정기적으로 정보를 제공하도록 허용하는 기능을 빌드하면 기본 스레드가 UI를 업데이트할 수 있습니다.

이 가이드에서는 다양한 API, Workbox 라이브러리, 몇 가지 고급 사례를 살펴봄으로써 Window서비스 워커 컨텍스트 간에 양방향 통신을 구현하는 다양한 방법을 살펴봅니다.

서비스 워커와 메시지를 교환하는 페이지를 보여주는 다이어그램

Workbox 사용

workbox-window는 창 컨텍스트에서 실행되도록 설계된 Workbox 라이브러리의 모듈 집합입니다. Workbox 클래스는 인스턴스의 등록된 서비스 워커에 메시지를 전송하고 응답을 기다리는 messageSW() 메서드를 제공합니다.

다음 페이지 코드는 새 Workbox 인스턴스를 만들고 서비스 워커에 메시지를 전송하여 버전을 가져옵니다.

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

서비스 워커는 다른 쪽에서 메시지 리스너를 구현하고 등록된 서비스 워커에 응답합니다.

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

라이브러리는 내부적으로 다음 섹션인 메시지 채널에서 검토할 브라우저 API를 사용하지만, 많은 구현 세부정보를 추상화하여 더 쉽게 사용할 수 있도록 하고 이 API의 광범위한 브라우저 지원을 활용합니다.

Workbox Window를 사용하여 페이지와 서비스 워커 간의 양방향 통신을 보여주는 다이어그램

Browser API 사용

Workbox 라이브러리가 요구사항을 충족하지 못하는 경우 페이지와 서비스 워커 간에 '양방향' 통신을 구현하는 데 사용할 수 있는 여러 하위 수준 API가 있습니다. 두 가지 방법에는 몇 가지 유사점과 차이점이 있습니다.

유사점:

  • 모든 경우에 통신은 한쪽 끝에서 postMessage() 인터페이스를 통해 시작되고 message 핸들러를 구현하여 다른 쪽 끝에서 수신됩니다.
  • 실제로 사용 가능한 모든 API를 사용하여 동일한 사용 사례를 구현할 수 있지만, 일부 시나리오에서는 일부 API를 사용하면 개발이 간소화될 수 있습니다.

차이점:

  • 통신의 다른 측면을 식별하는 방법은 다양합니다. 일부는 다른 컨텍스트에 대한 명시적 참조를 사용하는 반면, 다른 측면은 각 측면에 인스턴스화된 프록시 객체를 통해 암시적으로 통신할 수 있습니다.
  • 브라우저 지원은 각 스크립트마다 다릅니다.
페이지와 서비스 워커 간의 양방향 통신과 사용 가능한 브라우저 API를 보여주는 다이어그램

Broadcast Channel API

브라우저 지원

  • Chrome: 54.
  • Edge: 79
  • Firefox: 38.
  • Safari: 15.4

소스

Broadcast Channel API를 사용하면 BroadcastChannel 객체를 통해 탐색 컨텍스트 간에 기본 통신을 할 수 있습니다.

이를 구현하려면 먼저 각 컨텍스트가 동일한 ID로 BroadcastChannel 객체를 인스턴스화하고 이 객체에서 메시지를 주고받아야 합니다.

const broadcast = new BroadcastChannel('channel-123');

BroadcastChannel 객체는 postMessage() 인터페이스를 노출하여 리슨 컨텍스트에 메시지를 전송합니다.

//send message
broadcast.postMessage({ type: 'MSG_ID', });

모든 브라우저 컨텍스트는 BroadcastChannel 객체의 onmessage 메서드를 통해 메시지를 수신 대기할 수 있습니다.

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

보시다시피 특정 컨텍스트에 대한 명시적인 참조가 없으므로 먼저 서비스 워커 또는 특정 클라이언트에 대한 참조를 가져올 필요가 없습니다.

Broadcast Channel 객체를 사용하여 페이지와 서비스 워커 간의 양방향 통신을 보여주는 다이어그램

단점은 이 글을 작성하는 시점에서 API는 Chrome, Firefox, Edge에서 지원되지만 Safari와 같은 다른 브라우저는 아직 지원하지 않습니다.

Client API

브라우저 지원

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

소스

Client API를 사용하면 서비스 워커가 제어하는 활성 탭을 나타내는 모든 WindowClient 객체에 대한 참조를 가져올 수 있습니다.

페이지는 단일 서비스 워커에 의해 제어되므로 serviceWorker 인터페이스를 통해 활성 서비스 워커를 직접 리슨하고 메시지를 전송합니다.

//send message
navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
});

//listen to messages
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process response
  }
};

마찬가지로 서비스 워커는 onmessage 리스너를 구현하여 메시지를 수신 대기합니다.

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

서비스 워커는 클라이언트와 다시 통신하기 위해 Clients.matchAll()Clients.get()와 같은 메서드를 실행하여 WindowClient 객체 배열을 가져옵니다. 그런 다음 다음 중 하나를 postMessage()할 수 있습니다.

//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    //Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});
서비스 워커가 클라이언트 배열과 통신하는 모습을 보여주는 다이어그램

Client API는 서비스 워커의 모든 활성 탭과 비교적 간단한 방법으로 쉽게 통신할 수 있는 좋은 옵션입니다. 이 API는 모든 주요 브라우저에서 지원되지만 일부 메서드는 사용할 수 없을 수 있으므로 사이트에 구현하기 전에 브라우저 지원을 확인하세요.

메시지 채널

브라우저 지원

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41
  • Safari: 5.

소스

메시지 채널양방향 통신 채널을 설정하기 위해 한 컨텍스트에서 다른 컨텍스트로 포트를 정의하고 전달해야 합니다.

채널을 초기화하기 위해 페이지는 MessageChannel 객체를 인스턴스화하고 이를 사용하여 등록된 서비스 워커에 포트를 전송합니다. 또한 페이지는 다른 컨텍스트에서 메시지를 수신하기 위해 onmessage 리스너를 구현합니다.

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
양방향 통신을 설정하기 위해 포트를 서비스 워커에 전달하는 페이지를 보여주는 다이어그램

서비스 워커는 포트를 수신하고 포트 참조를 저장한 다음 이를 사용하여 다른 측에 메시지를 전송합니다.

let communicationPort;

//Save reference to port
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

//Send messages
communicationPort.postMessage({type: 'MSG_ID'});

MessageChannel는 현재 모든 주요 브라우저에서 지원됩니다.

고급 API: 백그라운드 동기화 및 백그라운드 가져오기

이 가이드에서는 실행할 작업을 설명하는 문자열 메시지 또는 한 컨텍스트에서 다른 컨텍스트로 캐시할 URL 목록을 전달하는 것과 같이 비교적 간단한 경우에 양방향 통신 기법을 구현하는 방법을 살펴봤습니다. 이 섹션에서는 연결 부족 및 긴 다운로드와 같은 특정 시나리오를 처리하는 두 가지 API를 살펴봅니다.

백그라운드 동기화

브라우저 지원

  • Chrome: 49.
  • Edge: 79
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

소스

채팅 앱은 연결 상태가 좋지 않아 메시지가 손실되지 않도록 할 수 있습니다. Background Sync API를 사용하면 사용자가 안정적인 연결 상태일 때 다시 시도할 작업을 지연할 수 있습니다. 이는 사용자가 전송하려는 항목이 실제로 전송되도록 하는 데 유용합니다.

페이지는 postMessage() 인터페이스 대신 sync를 등록합니다.

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

그러면 서비스 워커가 sync 이벤트를 리슨하여 메시지를 처리합니다.

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

doSomeStuff() 함수는 시도하는 작업의 성공/실패를 나타내는 약속을 반환해야 합니다. 약속이 처리되면 동기화가 완료됩니다. 실패하면 다시 시도할 동기화가 예약됩니다. 동기화 재시도도 연결을 기다리고 지수 백오프를 사용합니다.

작업이 완료되면 서비스 워커는 앞에서 살펴본 통신 API 중 하나를 사용하여 페이지와 다시 통신하여 UI를 업데이트할 수 있습니다.

Google 검색은 백그라운드 동기화를 사용하여 연결 상태가 좋지 않아 실패한 쿼리를 유지하고 나중에 사용자가 온라인 상태일 때 다시 시도합니다. 작업이 완료되면 웹 푸시 알림을 통해 결과를 사용자에게 전달합니다.

양방향 통신을 설정하기 위해 포트를 서비스 워커에 전달하는 페이지를 보여주는 다이어그램

백그라운드 가져오기

브라우저 지원

  • Chrome: 74.
  • Edge: 79
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

소스

메시지 전송이나 캐시할 URL 목록과 같이 비교적 짧은 작업의 경우 지금까지 살펴본 옵션이 적합합니다. 작업이 너무 오래 걸리면 브라우저가 서비스 작업자를 종료합니다. 그렇지 않으면 사용자의 개인 정보 보호 및 배터리가 위험해집니다.

Background Fetch API를 사용하면 영화, 팟캐스트, 게임 레벨 다운로드와 같은 긴 작업을 서비스 워커로 오프로드할 수 있습니다.

페이지에서 서비스 워커와 통신하려면 postMessage() 대신 backgroundFetch.fetch를 사용하세요.

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch(
    'my-fetch',
    ['/ep-5.mp3', 'ep-5-artwork.jpg'],
    {
      title: 'Episode 5: Interesting things.',
      icons: [
        {
          sizes: '300x300',
          src: '/ep-5-icon.png',
          type: 'image/png',
        },
      ],
      downloadTotal: 60 * 1024 * 1024,
    },
  );
});

BackgroundFetchRegistration 객체를 사용하면 페이지에서 progress 이벤트를 수신 대기하여 다운로드 진행률을 확인할 수 있습니다.

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(
    (bgFetch.downloaded / bgFetch.downloadTotal) * 100,
  );
  console.log(`Download progress: ${percent}%`);
});
양방향 통신을 설정하기 위해 포트를 서비스 워커에 전달하는 페이지를 보여주는 다이어그램
다운로드 진행률을 나타내도록 UI가 업데이트됩니다 (왼쪽). 서비스 워커 덕분에 모든 탭이 닫혀도 작업이 계속 실행될 수 있습니다 (오른쪽).

다음 단계

이 가이드에서는 페이지와 서비스 워커 간의 가장 일반적인 통신 사례(양방향 통신)를 살펴봤습니다.

응답을 받지 않고도 한 쪽에서 다른 쪽과 통신하는 데 하나의 컨텍스트만 필요한 경우가 많습니다. 페이지에서 서비스 워커로의 단방향 기술을 구현하는 방법과 사용 사례 및 프로덕션 예시를 확인하려면 다음 가이드를 참고하세요.

  • 명령형 캐싱 가이드: 페이지에서 서비스 워커를 호출하여 리소스를 미리 캐시합니다 (예: 미리 가져오기 시나리오).
  • 업데이트 브로드캐스트: 서비스 워커에서 페이지를 호출하여 중요한 업데이트 (예: 새 버전의 웹 앱을 사용할 수 있음)를 알립니다.