본문 바로가기
source-code/FrontEnd

[chrome extension] 익스텐션 설치 여부 감지하기

by mattew4483 2024. 1. 18.
728x90
반응형

chrome extension 서비스를 제공할 경우,
웹 페이지에서 해당 extension이 브라우저에 설치되었는지 감지해야 하는 경우가 발생한다.
(익스텐션이 설치되지 않았다면, chrome extension store로 이동해 설치 유도 등)
 
구글링을 해보면, 일반적으로는 아래와 같은 방법들이 사용된다.

1. extension에서 DOM을 조작해 감지

extension에서 문서를 조작하고, 웹 페이지에서는 해당 문서가 조작되었는지로 판단하는 방식!
 
extension이 설치된 경우, manifest.json의 content_scripts 속 파일이 실행된다.

 // manifest.json
 {
     "content_scripts": [
        {
          "js": ["content.js"],
        }
      ]
  }

 
js 파일이 실행되므로, 당연히 현재 페이지 DOM을 조작할 수 있으니...
content.js에서 문서의 id나 attribute에 고유한 식별자를 부여한 뒤,
웹 페이지에서는 식별자에 해당하는 요소가 존재하는지로 설치 여부를 판단할 수 있을 테다.

// content.js
const myElement = document.createElement('div')
myElement.id = 'my-extension-element'


// webpage.js
const extensionElement = document.getElementById('my-extension-element')
// 해당 element가 존재하면, extension이 설치된 것으로 간주
const isExtensionInstalled = Boolean(extensionElement)

2. extension에서 특정 파일을 업로드

extension에서 정적 파일을 호스팅 하고, 웹 페이지에서는 해당 파일에 접근할 수 있는지로 판단하는 방식!

 // manifest.json
 "web_accessible_resources": [
    "test.png"
  ],

manifest에서 접근 가능 자원을 설정한 뒤,

// Code from https://groups.google.com/a/chromium.org/d/msg/chromium-extensions/8ArcsWMBaM4/2GKwVOZm1qMJ
function detectExtension(extensionId, callback) { 
  var img; 
  img = new Image(); 
  img.src = "chrome-extension://" + extensionId + "/test.png"; 
  img.onload = function() { 
    callback(true); 
  }; 
  img.onerror = function() { 
    callback(false); 
  };
}

웹 페이지에서 chrome-extension://[확장프로그램ID]/[resouce 경로] 로 해당 파일이 존재하는지 확인할 수 있다.
(존재하면 → 익스텐션이 설치되었다고 판단하는 로직)
 

문제 상황

하지만 두 방법 다 적절한 해결책이라 느껴지지 않았다.
 
왜냐하면... 우리가 알고자 하는 건 'extension의 설치 여부'이지
내 웹 페이지의 dom에 특정 값이 있는지, 혹은 특정 파일이 호스팅 되고 있는지가 아니기 때문!
 
또한 extension을 통한 문서 조작을 설치 여부의 flag로 판단할 경우
웹 페이지에서 조작 여부를 조회하는 시점과,
extension의 content.js가 실행되어 dom이 변경되는 시점의 순서를 보장할 수 없다는 치명적인 문제도 존재했다.
(실제로 해당 문제를 겪어, 익스텐션이 설치되어 있음에도 불구하고 감지되지 않았다)
 

해결책

그렇다면 어떻게 할 수 있을까?
extension의 설치 여부를 판단해야 하므로, 이를 정확히 구현하면 될테다!
따라서

1) 웹 페이지에서 extension에 message 전송
2) 전송한 message에 대해 정상적인 응답이 반환된 경우 → extension 설치로 판단!
3) 그 이외의 경우 → extension 미설치로 판단

의 논리가 가장 적절하다고 생각했다.
 
여기서 중요한 점은,
웹 페이지에서 extension에 전송한 message 응답이 올 때까지는,
설치 여부에 대한 판단이 대기(pending)되어야 한다는 것!
→ promise를 이용한 비동기 제어가 필요함을 예상할 수 있었다.
 

1. extension 외부 통신 허용

manifest.json의 externally_connectable 옵션을 통해,
해당 확장 프로그램이 통신할 수 있는 확장 프로그램 ID나 URL을 명시할 수 있다.
→ 익스텐션 설치 여부를 판단할 웹 페이지에서 message를 전송할 예정으로, 해당 페이지 URL을 지정해 주면 될 테다!

// menifest.json
"externally_connectable": {
    "matches": ["<all_urls>"]
  }

matches로 지정한 URL 패턴과 일치하는 모든 페이지에서 message api를 사용할 수 있게 된다.
이때 URL 패턴에는 최소한 2차 도메인이 포함되어야 하며,
따라서 '*', '*.com', '*.co.uk', '*.appspot.com'과 같은 호스트 이름 패턴은 지원되지 않는다.
 
단 Chrome 107부터 를 사용하여 모든 도메인에 액세스 할 수 있지만, 이 경우 취약점이 모든 host에 영향을 미치므로 Chrome 웹 스토어 심사 시간이 더 소요될 수 있다고 한다.
 

2. 웹 페이지에서 message 전송

chrome.runtime.sendMessage를 통해 extension에 message를 송신할 수 있다.
https://developer.chrome.com/docs/extensions/develop/concepts/messaging?hl=ko

메시지 전달  |  Extensions  |  Chrome for Developers

확장 프로그램과 콘텐츠 스크립트 간에 메시지를 전달하는 방법

developer.chrome.com

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

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

메시지를 보낼 extension id와 message 객체, 그리고 response callback을 넘겨줄 수 있다.
 

3. extension에서 message 수신

extension에서 해당 message와 관련된 이벤트를 처리해 주면 될 테다.
이때, 해당 로직은 브라우저가 열려있는 동안 계속 동작해야 하므로 → background.js에 작성!

// background.ts
chrome.runtime.onMessageExternal.addListener(function (
  request,
  sender,
  sendResponse
) {
    sendResponse({
        success: true,
    });
});

 onMessageExternal은 다른 확장 프로그램/앱에서 메시지를 보내면 실행된다.

chrome.runtime.onMessageExternal.addListener(
  callback:
  function,
)

 위와 같이 동작하며,

(message:any,sender:MessageSender,sendResponse:function)=>boolean|undefined

callback의 타입은 다음과 같다!
 
이를 통해 
- externally_connectable에 허용한 URL(우리의 웹 페이지)에서
- 해당 extension에 message 발신 시
- onMessageExternal 이벤트가 동작하면서
- callback에 작성한 sendResponse가 실행되어
- 우리의 웹 페이지에서 sendMessage에 대한 응답을 확인할 수 있을 테다!
 

비동기 제어

사실 우리가 궁극적으로 원하는 건
→ 익스텐션이 설치된 경우 페이지 이용, 익스텐션이 설치되지 않았으면 모달을 띄우거나 스토어 페이지로 redirect 하는 것!
 
따라서 sendMessage에 대한 응답이 올 때까지 대기한 뒤
정상적으로 resolve 되면 익스텐션이 설치된 것으로 판단하여 페이지를 이용할 수 있게 하고,
그 외의 경우에는 설치되지 않은 것으로 판단하여 스토어 페이지로 보내면 되겠다.

const isInstalled = async () => {
    if (!chrome.runtime) throw "not chrome browser"; // catch문 실행
    const response = await chrome.runtime.sendMessage(extensionId, message);
    return response.success
}

const handleByExtensionInstalled = () => {
    isInstalled().then(() => {
    // 설치 로직
    }).catch(()=> {
    //미설치 로직
    })
};

sendMessage가 Promise를 반환하므로, async-await를 통해 비동기로 제어했다.
이때 Promise에는 onMessageExternal의 sendResponse 함수에 전달된 인자가 반환된다!
 
그런데 sendMessage 공식 문서를 읽어보면...

manifest V3 이상에서만 Promise가 반환된다! (그 이하는 void)
 
만약 V3 미만에서도 위와 같이 비동기 제어가 필요하다면...

const isInstalled = async () => {
  if (!chrome.runtime) throw "not chrome browser";
  
  return new Promise<boolean>((resolve, reject) => {
    sendMessage(
      extensionId,
      {openUrlInEditor: url},
      (response: any) => {
        const isSuccess = response?.["success"];
        isSuccess ? resolve(true) : reject("fail to handshake");
      }
    );
  });
};

const handleByExtensionInstalled = () => {
    isInstalled().then(() => {
    // 설치 로직
    }).catch(()=> {
    //미설치 로직
    })
};

다음과 같이, Promise 반환 함수를 만든 후
sendMessage의 callback에 따라 resolve와 reject을 실행해 주면 된다!

728x90
반응형