메시지 교환

확장앱은 콘텐츠 스크립트, 백그라운드 페이지, 확장 페이지 등 다양한 맥락에서 서로 분리되어 동작합니다. 예를 들어 어떤 외국어 문장을 선택하여 콘텍스트 메뉴를 클릭하면 사이드바에 번역된 결과를 보여주는 번역기 확장앱을 만들려면, 콘텍스트 메뉴를 만들고 동작하게 만드는 부분은 백그라운드 페이지에서 구현해야 하고 사이드바에 표시되는 페이지는 확장앱 페이지의 일종입니다. 이렇게 서로 다른 맥락 사이에 필요한 정보를 주고받기 위해 메시지 교환 (Message Passing) 이라는 방법을 사용합니다.

매니페스트

whale.runtime API는 별도의 권한 설정없이 콘텐츠 스크립트를 포함한 확장앱의 모든 맥락에서 사용할 수 있습니다.

확장앱 내부

콘텐츠 스크립트는 대부분의 확장앱 API를 직접 사용할 수 없습니다. 이 경우 필요한 정보는 백그라운드 페이지와 메시지 교환을 통해 간접적으로 얻을 수 있습니다. 또, 콘텐츠 스크립트에서 처리하기에 시간이 오래 걸릴 것 같은 작업은 백그라운드 페이지에 요청하여 대신 수행하게 한 뒤 그 결과를 수신하는 경우도 있을 수 있습니다.

단순 일회성 메시지

간단하게 .sendMessage() 로 보내고 .onMessage 이벤트로 받습니다.

콘텐츠 스크립트에서 백그라운드(확장) 페이지 에서
보내기 whale.runtime.sendMessage() whale.tabs.sendMessage()
받기 whale.runtime.onMessage whale.runtime.onMessage

아래 예제는 콘텐츠 스크립트와 백그라운드 페이지가 인사 메시지를 반복하여 주고받는 코드입니다.

background.js
whale.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message === `How are you?`) {
sendResponse(`I'm fine thank you and you?`);
}
});
contentScript.js
for (let i = 0; i < 100; i++) {
console.log(`How are you?`);

whale.runtime.sendMessage(`How are you?`, response => {
console.log(response); // = I'm fine thank you and you?
});
}
  • 만약 서로 다른 여러 페이지에 onMessage 이벤트 핸들러가 있는 경우, 맨 처음 수행되는 sendResponse() 응답만 유효합니다.
    그 이후의 응답은 모두 무시됩니다.
  • onMessage 이벤트 핸들러 함수 내에서 sendResponse() 함수를 비동기적으로 사용하려면 핸들러 함수가 명시적으로 true를 반환해야 합니다. 어떤 이벤트 핸들러도 true를 반환하지 않으면 sendResponse() 함수는 자동으로 실행됩니다. 예를 들어 아래와 같이 코드를 작성하면 콘텐트 스크립트에 의도한 메시지를 응답할 수 없습니다.

    background.js
    whale.runtime.onMessage.addListener((message, sender, sendResponse) => {
    setTimeout(() => {
    sendResponse(`I'm fine thank you and you?`);
    }, 1000);
    // 여기에 return true 가 필요합니다
    });
    contentScript.js
        whale.runtime.sendMessage(`How are you?`, response => {
    console.log(response); // === undefined
    });
    }

오래가는 연결

지속적으로 재사용할 수 있는 메시지 채널이 필요한 경우도 있습니다. 이 경우 runtime.sendMessage()runtime.onMessage 를 사용하는 대신 whale.runtime.connect() API를 이용해 메시지 포트(Port)를 생성한 뒤 포트가 제공하는 .postMessage().onMessage를 활용할 수 있습니다. 특히 .connect() 단계에서 채널마다 이름을 부여할 수 있으므로 메시지 채널을 구분하여 관리, 사용할 수 있습니다.

아래 예제는 앞에서 소개한 일회성 메시지 교환 예제를 메시지 포트를 이용하도록 변경한 예제입니다.

contentScript.js
const port = whale.runtime.connect({name: `greetings`});

port.onMessage.addListener(message => {
console.log(message);
});

for (let i = 0; i < 100; i++) {
console.log(`How are you?`)
port.postMessage(`How are you?`);
}
background.js
whale.runtime.onConnect.addListener(port => {
if (port.name === `greetings`) {
port.onMessage.addListener(message => {
if (message === `How are you?`) {
port.postMessage(`I'm fine thank you and you?`);
}
});
}
});

확장앱 외부

확장앱은 외부의 다른 확장앱이나 웹 사이트 주소를 지정하여 확장앱 외부와도 메시지를 주고 받을 수 있습니다. 거듭 강조하지만 외부의 연결을, 특히 웹 페이지와의 연결을 허용할 때에는 여러분이 만드는 확장앱이 언제든 예상하지 못한 위협에 노출될 수 있음을 떠올리셔야 합니다.

매니페스트

매니페스트에 externally_connectable 항목을 지정하지 않으면 기본적으로 모든 다른 확장앱과 연결할 수 있습니다. 그러나 만약 externally_connectable 항목을 지정하면서 "ids": ["*"] 를 추가하지 않는다면 모든 다른 확장앱의 연결을 거부하게 됩니다.

서로 다른 확장앱 사이에서

확장앱 ID를 알면 서로 다른 확장앱 사이에서도 메시지를 주고 받을 수 있습니다.
메시지를 보낼 때는 여전히 runtime.sendMessage() API를 사용합니다. 그저 첫 번째 인자에 메시지를 받을 상대 확장앱 ID를 포함하면 됩니다.
메시지를 받을 확장앱에서는 runtime.onMessage 대신 runtime.onMessageExternal을, runtime.onConnect 대신 runtime.onConnectExternal 을 사용하면 앞에서 소개한 확장앱 내부의 메시지 교환과 같이 서로 메시지를 주고 받을 수 있습니다.

확장앱ID: KJ
whale.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
if (sender.id === `SJ`) {
sendResponse(`너는 노크할 필요가 없단다`);
}
});
확장앱ID: SJ
whale.runtime.sendMessage(`KJ`, `똑똑똑`, response => {
console.log(response); // === 너는 노크할 필요가 없단다
});

일반 웹 페이지와 확장앱 사이에서

매니페스트에 externally_connectable 항목으로 메시지 교환을 허용할 웹 주소 패턴을 지정하면 해당 웹 페이지에서 메시지 관련 API를 사용할 수 있습니다. URL 패턴은 유효한 최상위 혹은 2단계 도메인 형태여야 하며 도메인이나 서브도메인에 와일드카드를 포함할 수 없습니다.

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

사용할 수 없는 패턴의 예: <all_urls>, *, *.com, *.co.kr, *://*.com/*

웹사이트의 페이지에서 메시지를 보내려면 아래 API를 사용합니다. 서로 다른 확장앱 사이에서 메시지를 주고 받는 것과 비슷합니다.

웹 페이지 : 보내기 확장앱 페이지 : 받기
API whale.runtime.sendMessage() whale.runtime.onMessageExternal.addListener()

메시지의 자료형

메시지를 주고 받을 때 내부적으로 JSON.stringify()JSON.parse() 하는 것과 비슷한 변환 과정을 거칩니다. Number, String, Object, Boolean 등 JSON 문자열로 표현할 수 있는 (JSON-ifiable) 정보만 메시지로 교환할 수 있습니다. 클래스 인스턴스, 정규식 인스턴스, 함수, DOM 요소 등은 교환할 수 없으며 [Object object] 등 문자열화 된 상태로 전달됩니다.

스토리지 API 활용

일반적인 메시지 교환 방법은 아니지만 whale.storage API를 이용하여 비슷한 효과를 얻을 수 있는 방법을 소개합니다. 대부분 위에서 설명한 whale.runtime.sendMessage() 등 메시지 API를 사용하는 것으로 충분합니다. 그러나 이것으로 해결되지 않는 경우가 있습니다.

예를 들어, 콘텐츠 스크립트에서 사이드바에 확장 페이지를 whale.sidebarAction.show() API로 열고 어떤 정보를 메시지로 전달하기 위해 아래와 같이 구현하면, 실제로는 사이드바에 확장 페이지가 로딩되어 준비되기까지의 시간차로 인해 메시지가 전달되지 않습니다.

contentScript.js
whale.sidebarAction.show(() => {
whale.runtime.sendMessage(`Hello Sidebar!`);
});
sidebar.js
window.addEventListener(`DOMContentLoaded`, () => {
// 이 구문이 실행되기 전 이미 contentScript.js 의 sendMessage() 가 끝난 상태.
// 그러므로 sidebarAction.show() 의 콜백에서 보내는 메시지는 이곳에 도달하지 않습니다.
whale.runtime.onMessage.addListener(message => {
console.log(message);
});
});

이 문제를 해결하기 위해 사이드바에서 확장앱 페이지 로딩이 완료되었는지를 확인하고 메시지를 보내는대신, 아래와 같이 콘텐츠 스크립트에서는 전달하고픈 메시지를 스토리지에 저장하고 확장앱 페이지에서는 스토리지에서 해당 데이터를 확인하는 우회적인 방법을 사용할 수 있습니다.

contentScript.js
whale.storage.local.set({
message: `Hello`
}, () => {
if (!whale.runtime.lastError) {
whale.sidebarAction.show();
}
});
sidebar.js
window.addEventListener(`DOMContentLoaded`, () => {
// 처음 로딩 될 때: 메시지가 있는지 확인하고 삭제
whale.storage.local.get(`message`, storage => {
console.log(storage.message); // = Hello
whale.storage.local.remove(`message`);
});

// 로딩 이후의 변화 대응
whale.storage.onChanged.addListener((changes, areaName) => {
if (areaName === `local` && `message` in changes) {
console.log(changes.message.newValue);
}
});
});

이 방법을 사용하기 위해서는 확장앱이 storage 권한을 가지도록 매니페스트를 설정해야 합니다.

더 알아보기