메시지 전달

콘텐츠 스크립트는 콘텐츠 스크립트를 실행하는 확장 프로그램이 아닌 웹페이지의 컨텍스트에서 실행되므로 나머지 확장 프로그램과 통신하는 방법이 필요한 경우가 많습니다. 예를 들어 RSS 리더 확장 프로그램은 콘텐츠 스크립트를 사용하여 페이지에 RSS 피드가 있는지 감지한 다음 서비스 워커에 알림을 보내 해당 페이지의 작업 아이콘을 표시할 수 있습니다.

이 통신은 메시지 전달을 사용하므로 확장 프로그램과 콘텐츠 스크립트가 서로의 메시지를 수신 대기하고 동일한 채널에서 응답할 수 있습니다. 메시지에는 유효한 JSON 객체 (null, 불리언, 숫자, 문자열, 배열 또는 객체)가 포함될 수 있습니다. 메시지 전달 API에는 두 가지가 있습니다. 하나는 일회성 요청용이고, 다른 하나는 여러 메시지를 전송할 수 있는 장기 연결용으로 더 복잡합니다. 확장 프로그램 간에 메시지를 전송하는 방법에 관한 자세한 내용은 교차 확장 프로그램 메시지 섹션을 참고하세요.

일회성 요청

확장 프로그램의 다른 부분에 단일 메시지를 전송하고 원하는 경우 응답을 가져오려면 runtime.sendMessage() 또는 tabs.sendMessage()를 호출합니다. 이러한 메서드를 사용하면 콘텐츠 스크립트에서 확장 프로그램으로 또는 확장 프로그램에서 콘텐츠 스크립트로 일회성 JSON 직렬화 가능 메시지를 보낼 수 있습니다. 응답을 처리하려면 반환된 약속을 사용합니다. 이전 확장 프로그램과의 호환성을 위해 콜백을 마지막 인수로 전달할 수 있습니다. 동일한 호출에서 약속과 콜백을 사용할 수는 없습니다.

메시지를 전송하면 메시지를 처리하는 이벤트 리스너에 선택적 세 번째 인수인 sendResponse가 전달됩니다. 이 함수는 메시지를 전송한 함수의 반환 값으로 사용되는 JSON 직렬화 가능한 객체를 사용합니다. 기본적으로 sendResponse 콜백은 동기식으로 호출해야 합니다. sendResponse에 전달된 값을 가져오기 위해 비동기 작업을 실행하려면 이벤트 리스너에서 참 값이 아닌 리터럴 true을 반환해야 합니다. 이렇게 하면 sendResponse이 호출될 때까지 메시지 채널이 다른 쪽으로 열려 있게 됩니다.

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must send an explicit `true`
  return true;
}

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

콜백을 약속으로 변환하고 확장 프로그램에서 사용하는 방법에 관한 자세한 내용은 Manifest V3 이전 가이드를 참고하세요.

콘텐츠 스크립트에서 요청을 보내는 방법은 다음과 같습니다.

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

메시지에 동기식으로 응답하려면 응답을 받은 후 sendResponse를 호출하고 false를 반환하여 완료되었음을 나타내면 됩니다. 비동기식으로 응답하려면 true를 반환하여 사용할 준비가 될 때까지 sendResponse 콜백을 활성 상태로 유지합니다. 비동기 함수는 지원되지 않는 Promise를 반환하므로 지원되지 않습니다.

콘텐츠 스크립트에 요청을 보내려면 다음과 같이 요청이 적용되는 탭을 지정합니다. 이 예시는 서비스 워커, 팝업, 탭으로 열린 chrome-extension:// 페이지에서 작동합니다.

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

메시지를 수신하려면 runtime.onMessage 이벤트 리스너를 설정합니다. 확장 프로그램과 콘텐츠 스크립트에서 동일한 코드를 사용합니다.

content-script.js 또는 service-worker.js:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      sendResponse({farewell: "goodbye"});
  }
);

이전 예에서 sendResponse()는 동기식으로 호출되었습니다. sendResponse()를 비동기식으로 사용하려면 onMessage 이벤트 핸들러에 return true;를 추가합니다.

여러 페이지에서 onMessage 이벤트를 리슨하는 경우 특정 이벤트에 대해 sendResponse()를 가장 먼저 호출한 페이지만 응답을 전송할 수 있습니다. 해당 이벤트에 대한 다른 모든 응답은 무시됩니다.

장기 연결

재사용 가능한 장기 메시지 전달 채널을 만들려면 runtime.connect()를 호출하여 콘텐츠 스크립트에서 확장 프로그램 페이지로 메시지를 전달하거나 tabs.connect()를 호출하여 확장 프로그램 페이지에서 콘텐츠 스크립트로 메시지를 전달합니다. 채널 이름을 지정하여 다양한 유형의 연결을 구분할 수 있습니다.

장기 연결의 한 가지 사용 사례는 자동 양식 작성 확장 프로그램입니다. 콘텐츠 스크립트는 특정 로그인의 확장 프로그램 페이지에 대한 채널을 열고 페이지의 각 입력 요소에 관한 메시지를 확장 프로그램에 전송하여 작성할 양식 데이터를 요청할 수 있습니다. 공유 연결을 사용하면 확장 프로그램이 확장 프로그램 구성요소 간에 상태를 공유할 수 있습니다.

연결을 설정할 때 각 엔드에는 해당 연결을 통해 메시지를 전송하고 수신하기 위한 runtime.Port 객체가 할당됩니다.

다음 코드를 사용하여 콘텐츠 스크립트에서 채널을 열고 메시지를 전송 및 수신합니다.

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

확장 프로그램에서 콘텐츠 스크립트로 요청을 전송하려면 이전 예의 runtime.connect() 호출을 tabs.connect()로 바꿉니다.

콘텐츠 스크립트 또는 확장 프로그램 페이지의 수신 연결을 처리하려면 runtime.onConnect 이벤트 리스너를 설정합니다. 확장 프로그램의 다른 부분에서 connect()를 호출하면 이 이벤트와 runtime.Port 객체가 활성화됩니다. 수신 연결에 응답하는 코드는 다음과 같습니다.

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

포트 전체 기간

포트는 확장 프로그램의 여러 부분 간에 양방향 통신 방법으로 설계되었습니다. 최상위 프레임은 포트를 사용할 수 있는 확장 프로그램의 가장 작은 부분입니다. 확장 프로그램의 일부가 tabs.connect(), runtime.connect() 또는 runtime.connectNative()를 호출하면 postMessage()를 사용하여 즉시 메시지를 보낼 수 있는 포트가 생성됩니다.

탭에 프레임이 여러 개인 경우 tabs.connect()를 호출하면 탭의 프레임마다 runtime.onConnect 이벤트가 한 번씩 호출됩니다. 마찬가지로 runtime.connect()가 호출되면 onConnect 이벤트가 확장 프로그램 프로세스의 프레임마다 한 번씩 실행될 수 있습니다.

연결이 닫힌 시점을 확인해야 할 수도 있습니다(예: 열린 포트마다 별도의 상태를 유지하는 경우). 이렇게 하려면 runtime.Port.onDisconnect 이벤트를 수신 대기합니다. 이 이벤트는 채널의 다른 쪽 끝에 유효한 포트가 없을 때 발생하며, 다음과 같은 원인이 있을 수 있습니다.

  • 다른 쪽에 runtime.onConnect 리스너가 없습니다.
  • 포트가 포함된 탭이 로드 취소됩니다 (예: 탭이 탐색된 경우).
  • connect()가 호출된 프레임이 언로드되었습니다.
  • runtime.onConnect를 통해 포트를 수신한 모든 프레임이 언로드되었습니다.
  • runtime.Port.disconnect()다른 쪽에서 호출합니다. connect() 호출로 인해 수신기 측에 여러 포트가 생성되고 이러한 포트 중 하나에서 disconnect()가 호출되면 onDisconnect 이벤트는 다른 포트가 아닌 전송 포트에서만 발생합니다.

교차 확장 프로그램 메시지

확장 프로그램의 여러 구성요소 간에 메시지를 전송하는 것 외에도 메시지 API를 사용하여 다른 확장 프로그램과 통신할 수 있습니다. 이렇게 하면 다른 확장 프로그램에서 사용할 수 있는 공개 API를 노출할 수 있습니다.

다른 확장 프로그램의 수신 요청 및 연결을 수신 대기하려면 runtime.onMessageExternal 또는 runtime.onConnectExternal 메서드를 사용하세요. 각 예는 다음과 같습니다.

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

다른 확장 프로그램으로 메시지를 보내려면 다음과 같이 통신하려는 확장 프로그램의 ID를 전달합니다.

service-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

웹페이지에서 메시지 보내기

확장 프로그램은 다른 웹페이지의 메시지를 수신하고 응답할 수도 있지만 웹페이지에 메시지를 보낼 수는 없습니다. 웹페이지에서 확장 프로그램으로 메시지를 전송하려면 manifest.json에서 "externally_connectable" 매니페스트 키를 사용하여 통신할 웹사이트를 지정합니다. 예를 들면 다음과 같습니다.

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

이렇게 하면 지정한 URL 패턴과 일치하는 모든 페이지에 메시지 API가 노출됩니다. URL 패턴은 2차 도메인을 하나 이상 포함해야 합니다. 즉, '*', '*.com', '*.co.uk', '*.appspot.com'과 같은 호스트 이름 패턴은 지원되지 않습니다. Chrome 107부터 <all_urls>를 사용하여 모든 도메인에 액세스할 수 있습니다. 이 변경사항은 모든 호스트에 영향을 미치므로 이를 사용하는 확장 프로그램의 Chrome 웹 스토어 검토에 시간이 더 오래 걸릴 수 있습니다.

runtime.sendMessage() 또는 runtime.connect() API를 사용하여 특정 앱 또는 확장 프로그램에 메시지를 보냅니다. 예를 들면 다음과 같습니다.

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (response) => {
      if (!response.success) handleError(url);
    }
  );
}

확장 프로그램에서 교차 확장 프로그램 메시지와 같이 runtime.onMessageExternal 또는 runtime.onConnectExternal API를 사용하여 웹페이지의 메시지를 리슨합니다. 예를 들면 다음과 같습니다.

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

네이티브 메시지

확장 프로그램은 네이티브 메시지 호스트로 등록된 네이티브 애플리케이션과 메시지를 교환할 수 있습니다. 이 기능에 대한 자세한 내용은 네이티브 메시지를 참고하세요.

보안 고려사항

다음은 메시지와 관련된 몇 가지 보안 고려사항입니다.

콘텐츠 스크립트의 신뢰도가 낮음

콘텐츠 스크립트는 확장 프로그램 서비스 워커보다 신뢰성이 낮습니다. 예를 들어 악성 웹페이지가 콘텐츠 스크립트를 실행하는 렌더링 프로세스를 손상시킬 수 있습니다. 콘텐츠 스크립트의 메시지가 공격자가 조작했을 수 있다고 가정하고 모든 입력을 검사하고 정리해야 합니다. 콘텐츠 스크립트에 전송된 데이터가 웹페이지로 유출될 수 있다고 가정합니다. 콘텐츠 스크립트에서 수신된 메시지로 트리거될 수 있는 권한이 있는 작업의 범위를 제한합니다.

교차 사이트 스크립팅

교차 사이트 스크립팅으로부터 스크립트를 보호해야 합니다. 사용자 입력, 콘텐츠 스크립트를 통한 다른 웹사이트, API와 같이 신뢰할 수 없는 소스에서 데이터를 수신할 때는 이를 HTML로 해석하거나 예상치 못한 코드가 실행될 수 있는 방식으로 사용하지 않도록 주의하세요.

더 안전한 방법

가능하면 스크립트를 실행하지 않는 API를 사용하세요.

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
안전하지 않은 메서드

확장 프로그램을 취약하게 만드는 다음 메서드는 사용하지 마세요.

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});