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

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의 폭넓은 브라우저 지원을 활용하면서 더 쉽게 사용할 수 있습니다.

작업 상자 창을 사용하여 페이지와 서비스 워커 간의 양방향 통신을 보여주는 다이어그램

브라우저 API 사용

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

유사점:

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

차이점:

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

방송 채널 API

브라우저 지원

  • 54
  • 79
  • 38
  • 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...
  }
};

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

브로드캐스트 채널 객체를 사용하여 페이지와 서비스 워커 간의 양방향 통신을 보여주는 다이어그램

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

Client API

브라우저 지원

  • 40
  • 17
  • 44
  • 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는 모든 주요 브라우저에서 지원되지만 모든 메서드를 사용할 수 있는 것은 아니므로 사이트에 구현하기 전에 브라우저 지원을 확인해야 합니다.

메시지 채널

브라우저 지원

  • 2
  • 12
  • 41
  • 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를 살펴봅니다.

백그라운드 동기화

브라우저 지원

  • 49
  • 79
  • x
  • x

소스

채팅 앱에서는 연결 상태가 좋지 않아 메시지가 손실되지 않도록 해야 할 수 있습니다. 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 검색에서는 백그라운드 동기화를 사용하여 연결 상태가 좋지 않아 실패한 쿼리를 유지하고 나중에 사용자가 온라인 상태일 때 쿼리를 다시 시도합니다. 작업이 완료되면 웹 푸시 알림을 통해 사용자에게 결과를 전달합니다.

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

백그라운드 가져오기

브라우저 지원

  • 74
  • 79
  • x
  • x

소스

메시지 전송이나 캐시할 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가 업데이트되었습니다 (왼쪽). 서비스 워커 덕분에 모든 탭이 닫혀도 (오른쪽) 작업이 계속 실행될 수 있습니다.

다음 단계

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

많은 경우 응답을 받지 않고 다른 컨텍스트와 통신하기 위해 하나의 컨텍스트만 필요할 수 있습니다. 다음 가이드에서 서비스 워커와 서비스 워커 간 페이지에서 단방향 기술을 구현 사례 및 프로덕션 예시와 함께 구현하는 방법을 알아보세요.

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