크롬 익스텐션이 어떤 식으로 작동하는지를 먼저 알아볼 필요가 있다.
아래 공식 문서를 바탕으로 기본적인 내용을 훑고 개발에 착수하였다.
https://developer.chrome.com/docs/extensions/mv3/getstarted/
Chrome Extensions getting started guides - Chrome Developers
Overview of all Chrome Extensions getting started guides.
developer.chrome.com
# 크롬 익스텐션 구조
크롬에서 제공하는 가장 기본적인 파일 목록을 살펴보면 다음과 같다.
이 중 가장 기본이 되는 파일은 maifest.json으로 해당 파일이 root에 있어야만 익스텐션이 동작한다.
해당 파일을 열어보면 마치 package.json처럼 프로젝트의 기본적인 내용을 담고 있음을 확인할 수 있다.
manifest.json에 필수로 포함되어야 할 key: "manifest_version", "name", "version"
익스텐션에서 특정 기능을 구현하기 위해서 필요한 key: "action", "contents_scripts", "background"
기타 자세한 내용은 아래 페이지에서 확인하자.
https://developer.chrome.com/docs/extensions/mv3/manifest/
action: extension에 사용할 icon, tooltip, badge, popup에 대한 설정을 할 수 있다.
background: user interaction이나 extension lifecycle, navigation 등 익스텐션의 기본 이벤트들에 대한 컨트롤을 수행한다. 단, DOM에 접근할 수 없다.
contents_scripts: 각각의 페이지에 inject되어 다양한 동작을 수행한다. DOM에 접근하여 정보를 읽고 수정할 수 있다.
# 구현
처음엔 contents_scripts만 사용하여 각 페이지마다 화질 설정 동작을 수행하도록 하였지만, 해당 방법은 YouTube에 적합하지 않았다.
contents_scripts는 페이지 로드 시에 inject되는데, YouTube는 routing 시 전체 페이지를 다시 로드하는 것이 아니라 부분적으로 필요한 부분만 리렌더 하는 방식으로 동작하기 때문에 최초로 영상 페이지에 접근하는 경우가 아니라면 contents_script가 재작동하지 않는다.
물론, setInerval을 사용하여 URL이 바뀌는지 검사하여 원하는 동작을 하게끔 할 수는 있겠으나, 이는 불필요한 이벤트를 너무 많이 발생시킨다.
따라서 background에서 URL이 바뀌는 이벤트마다 contents_script에 message를 보내 화질 변경 동작을 하도록 설계하였다.
간단히 테스트 코드를 작성하여 테스트를 수행하였다.
// service_worker.js
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo && changeInfo.status == "complete"){
chrome.tabs.sendMessage(tabId, "change the quality setting");
}
});
// content_script.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
setTimeout(() => { selectHQ(0, 0) }, 500);
});
// mode 0: find settings button and click
// mode 1: find Quality menu from settings and click
// mode 2: find highest quality and click
function selectHQ(mode, count) {
// constraint of the recursive function
if (count >= 20) {
// console.log(`Failed to find ${mode === 0 ? 'settings' : mode === 1 ? 'quality menu' : 'highest quality'}`);
return;
}
const target = mode === 0
? document.getElementsByClassName('ytp-settings-button')[0]
: mode === 1
? document.getElementById('ytp-id-18')?.lastChild?.lastChild?.lastChild
: document.getElementById('ytp-id-18')?.firstChild?.firstChild?.nextSibling?.firstChild;
if (validateTarget(mode, target)) {
// found
target.click();
if (mode !== 2) setTimeout(() => {selectHQ(mode + 1, 0)}, 200);
// else console.log('Success');
} else {
// cannot find (maybe loading the HTML data)
setTimeout(() => {selectHQ(mode, count + 1)}, 500);
}
}
selectHQ()에서는 setting 버튼 클릭, 화질 메뉴 버튼 클릭, 최상위 화질 클릭을 순차적으로 수행하도록 하였다.
기존에는 setting 버튼 클릭 동작을 생략하려 하였으나 setting 버튼을 클릭하지 않으면 화질 메뉴가 로딩되지 않는 케이스가 있어 추가하였다.
페이지 로드 중에 아직 화질 메뉴가 로딩되지 않은 경우가 있어 setTimeout을 이용해 recursive하게 함수를 호출하여 원하는 동작을 반복 수행하도록 하였다.
결과:
다행히 잘 동작한다.
마지막으로 익스텐션 popup에 on/off 토글 스위치를 추가하였으며 익스텐션 on/off 상태를 service_walker에서 가지고 있도록 하였다.
토글 버튼 조작 시 popup.js에서 service_walker로 조작 결과에 대한 메세지를 전달하며, 이 결과에 따라 service_walker에서 content_script로 메세지를 전달하여 페이지를 조작하도록 하였다.
// popup.html
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h2 class="title">YoutubeHQ</h2>
<label class="switch">
<input type="checkbox" id="ythq_switch">
<span class="slider round"></span>
</label>
<script src="/scripts/popup.js"></script>
</div>
</body>
</html>
// popup.js
const toggleSwitch = document.getElementById('ythq_switch');
// initial on/off setting
(async () => {
const response = await chrome.runtime.sendMessage('is_ythq_on');
if (response === 'ythq_on') {
toggleSwitch.checked = true;
} else {
toggleSwitch.checked = false;
}
})();
// control on/off when toggled
toggleSwitch.addEventListener('change', () => {
// send message
if (toggleSwitch.checked) {
chrome.runtime.sendMessage('ythq_on');
} else {
chrome.runtime.sendMessage('ythq_off');
}
});
// service_walker.js
// extension on/off state 및 on/off toggle switch에 대한 listener 추가
let isExtensionOn = true;
// Listen for when extension on/off
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message === 'ythq_on') {
isExtensionOn = true;
(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
chrome.tabs.sendMessage(tab.id, 'ythq_on');
})();
} else if (message === 'ythq_off') {
isExtensionOn = false;
} else if (message === 'is_ythq_on') {
sendResponse(`ythq_${isExtensionOn ? 'on' : 'off'}`);
return true;
}
});
마지막으로 몇 가지 케이스에 대해 디버깅을 한 후 마무리하였다.
다음으론 이 익스텐션을 구글 웹 스토어에 등록해보려 한다.
'Projects > Chrome extension' 카테고리의 다른 글
Youtube web 화질 고정 Chrome Extension 개발기 - 3 (完) (1) | 2023.07.04 |
---|---|
Youtube web 화질 고정 Chrome Extension 개발기 - 1 (0) | 2023.06.27 |