이전 포스팅에서 크롬 브라우저 익스텐션의 구조를 다시 살펴보고 정리해보았다.
2024.02.20 - [웹 프로그래밍 고급 주제들] - 2.4. 크롬 익스텐션 만들어보기 Part I - 구조 및 예제 정리
2.4. 크롬 익스텐션 만들어보기 Part I - 구조 및 예제 정리
이전 세 포스팅들을 통해 크롬 익스텐션을 구성하는 요소들을 하나씩 살펴보고 각각 예제를 작성해보았다. 크롬 익스텐션은 크게 아래 세 가지 요소들로 구성된다. · 익스텐션의 UI: popup 스크립
youngbyte.tistory.com
이제 브라우저 익스텐션의 구조와 그 구성 요소들에 대해 어느정도 그림이 그려졌으리라 생각한다. 이번 포스팅에서는 이전 예제들보다 조금 더 복잡한 익스텐션 예제를 작성해보면서 서비스 워커의 작성 방법과 동작, 그리고 popup 스크립트, content 스크립트와의 다양한 상호작용에 대해 살펴볼 것이다.
이전 포스팅의 가장 마지막 예제에서 크롬 익스텐션의 서비스 워커(service worker)에 대해 살펴보고 관련된 예제(Focus mode)를 작성해 보았다. 서비스 워커에 대해 이야기한 김에 좀 더 살펴보기로 하자.
크롬 웹브라우저에서 서비스 워커는 일정 시간 호출되지 않는 경우와 같이 당장 필요하지 않은 경우에 종료 상태에 있다. 그러다가 서비스 워커로 이벤트 정보 등이 전달되면 서비스 워커가 깨어난다. 서비스 워커는 이와 같이 실행 상태와 종료 상태를 반복하기 때문에 어떤 상태를 지속적으로 관리해야 하는 경우 chrome storage API를 이용해서 웹 브라우저의 캐시 영역에 데이터를 저장하거나 불러올 필요가 있다. 이번 포스팅에서는 서비스 워커의 동작 중 데이터 관리 관련해서 좀 더 살펴보도록 하겠다.
이번 포스팅을 통해 살펴볼 내용들은 아래와 같다.
· 서비스 워커(백그라운드 서비스) 작성 방법
· 포어그라운드 서비스(content 스크립트, popup 스크립트)와 백그라운드 서비스 사이의 동작 연계
· chrome 브라우저에서 제공하는 API 사용 방법
코드를 통해 개념과 실제 사용 방법을 살펴보기 위해 구글에서 제공하는 익스텐션 예제 중 Quick API reference라는 예제를 작성해 볼 것이다. Quick API reference 익스텐션은 크롬 브라우저의 주소 입력 창(옴니박스omni box라고 불린다)에 'api'라는 키워드를 입력하면 크롬 익스텐션이 사용할 수 있는 브라우저 API들을 찾을 수 있도록 도와준다.
먼저 익스텐션이 제공하는(또는 제공할) 기능들을 정리해보면 아래와 같다.
ⓐ 크롬 브라우저의 주소 입력 창(omnibox라고 부른다)에 api라는 키워드를 입력하면 chrome API를 추천해준다.
ⓑ 크롬 익스텐션 참조 페이지(https://developer.chrome.com/docs/extensions/reference/)로 접근하면 매일의 팁(Tip of today)을 제공한다.
기능들을 살펴보면 ⓐ의 기능을 제공하기 위해 주소 입력창을 컨트롤 하는 크롬 익스텐션 API를 사용할 필요가 있다. 이를 처리하는 서비스 워커를 작성할 것이다. ⓑ기능은 크롬 익스텐션 참조 페이지에 접근할 때 만 활성화된다. 즉, 타겟 페이지에 삽입되어 동작하는 content 스크립트를 작성할 필요가 있다. 두 기능을 충족시킬 수 있는 크롬 익스텐션 구조를 구상해보면 서비스 워커 스크립트, content 스크립트, 그리고 manifest 파일들이 필요함을 알 수 있다(익스텐션 자체 UI는 필요하지 않다).
먼저 익스텐션 프로젝트 디렉토리를 만들고(예, quick_api_reference) 그 아래에 익스텐션의 전체 형태를 기술하는 manifest.json 파일을 작성하면서 익스텐션의 구조를 구체적으로 기술해보자.
{
"manifest_version": 3
, "name": "Open extension API reference"
, "version": "1.0.0"
, "icons": {
"16": "images/icon-16.png"
, "128": "images/icon-128.png"
}
, "background": {
"service_worker": "service_worker.js"
, "type": "module"
}
, "permissions": ["storage", "alarms"]
, "host_permissions": ["https://extension-tips.glitch.me/*"]
, "minimum_chrome_version": "102"
, "omnibox": {
"keyword": "api"
}
, "content_scripts": [
{
"matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
"js": ["content.js"]
}
]
}
<quick_api_reference/manifest.json>
manifest 파일에서 manifest version(manifest 파일이 사용하는 문법 버전), name, version(익스텐션의 버전), icons 등은 기본적으로 들어가는 내용들이고 이전 포스팅들에서 살펴보았기 때문에 따로 설명하지 않을 것이다. 공통된 항목들 외에 눈여겨 볼 항목들을 정리해보면 아래와 같다.
항목 | 내용 | 필수 여부 |
background | 서비스 워커(=백그라운드 스크립트)에 대해 기술한다. | Y |
permissions | 익스텐션의 동작에 필요한 권한들을 기술한다. · storage: 크롬 브라우저의 캐시cache(저장소)를 사용하도록 허용한다. · alarm: 익스텐션이 시간을 확인하고 주기적으로 정해진 동작을 할 수 있도록 허용한다. |
Y |
host_permissions | 서비스 워커가 접근할 수 있는 웹 서버 도메인을 지정한다. | Y(외부 서버 접근이 필요한 경우) |
minimum_chrome_version | 익스텐션이 원활히 동작하는 크롬의 최소 버전을 지정한다. | N |
omnibox | 크롬 웹 브라우저의 주소 입력창(이를 omnibox라 부른다)에 지정한 키워드 "api"를 입력하면 서비스 워커가 깨어나고 주소 입력창과 상호작용하도록 설정한다. | Y |
content_scripts | 타겟 웹 페이지에 삽입되어 동작할 content 스크립트의 속성들을 지정한다. · matches: 타겟 페이지(들) · js: content 스크립트 이름 |
Y |
이제 본격적으로 서비스 워커 스크립트를 작성해보자. 서비스 워커가 해야할 일들은 아래와 같다.
· 크롬 브라우저의 주소 입력창에 키워드 'api'가 입력되면 깨어나 API 추천을 도와준다. - 기능 ⓐ
· 매일 매일의 익스텐션 작성 팁(tip)을 구글 서버로부터 가져와 content 스크립트에 제공한다. - 기능 ⓑ
위 두 기능은 겹치는 부분이 없으므로 각각 별도의 파일로 작성할 것이다. 먼저 크롬 웹 브라우저의 주소 입력창과 상호작용하는 기능 ⓐ 부분을 아래와 같이 sw_omnibox.js 파일로 작성하였다. 동작과 관련한 설명들은 주석으로 추가하였다.
// 서비스 워커가 동작할 때 파일 이름인 'sw_omnibox.js'가 웹 브라우저의 디버그 창에 표시된다.
console.log("sw_omnibox.js");
// chrome.runtime.onInstalled 이벤트는 익스텐션이 처음 설치될 때 발생한다.
// 크롬의 캐시 영역에 {apiSuggestions: ['tabs', 'storage', 'scripting']} 객체를 저장한다.
chrome.runtime.onInstalled.addListener(({reason}) => {
if(reason === 'install') {
chrome.storage.local.set({
apiSuggestions: ['tabs', 'storage', 'scripting']
});
}
});
const URL_CHROME_EXTENSIONS_DOC = 'https://developer.chrome.com/docs/extensions/reference/';
const NUMBER_OF_PREVIOUS_SEARCHES = 4; // 검색 목록에 저장할 키워드들의 갯수
// chrome.omnibox API는 manifest 파일의 omnibox 항목에 지정한 keyword가 입력되면 활성화된다.
// API 활성화 이후에 추가되는 입력을 처리하는 이벤트 처리 함수를 기술하였다.
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
// omnibox에 기본으로 보여줄 내용을 설정한다.
await chrome.omnibox.setDefaultSuggestion({
description: 'Enter a Chrome API or choose from past searches'
});
// 브라우저에 저장되어 있는 기본 키워드들, 그리고 가장 최근에 검색한 키워드들을 읽어온다.
const { apiSuggestions } = await chrome.storage.local.get("apiSuggestions");
// apiSggestions 객체 안에는 저장된 키워드와 가장 최근 API 검색 키워드가 들어있다.
// 이 키워드들을 {content: 키워드, description: `Open chrome.${키워드} API`} 형태의 객체로 만든다.
const suggestions = apiSuggestions.map((api) => {
return { content: api, description: `Open chrome.${api} API`};
});
// omnibox에 키워드들이 들어있는 객체를 전달하여 웹 브라우저 주소창에 표시되도록 한다.
suggest(suggestions);
});
// 주소창에 API 키워드를 입력하고 엔터키를 누르면 onInputEntered 이벤트가 발생한다.
chrome.omnibox.onInputEntered.addListener((input) => {
// 새 탭을 열고 입력한 키워드에 해당하는 API 문서 페이지를 보여준다.
chrome.tabs.create({url: URL_CHROME_EXTENSIONS_DOC + input});
// 입력한 키워드를 크롬 브라우저의 저장영역에 저장한다.
updateHistory(input);
});
// 전달하는 검색키워드(input)를 목록에 포함시키고 브라우저의 저장영역에 저장하는 함수이다.
async function updateHistory(input) {
// 브라우저에 저장되어 있는 API 검색 키워드들을 읽어온다.
const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
// [input(검색키워드), 이전 검색 키워드들]과 같이 갱신된다.
apiSuggestions.unshift(input);
// 오래된 키워드를 삭제하여 NUMBER_OF_PREVIOUS_SEARCHES 수 만큼만 남긴다.
apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
// 갱신된 키워드 목록을 웹 브라우저에 저장한다
return chrome.storage.local.set({apiSuggestions});
}
<quick_api_reference/sw_omnibox.js>
서비스 워커가 제공하는 또 다른 기능은 매일 익스텐션 작성 팁을 구글 서버로부터 가져와 사용자에게 보여주는 기능이다. 이 기능은 omnibox를 통해 키워드를 입력하고 관련 API를 검색하는 기능과 겹치지 않으므로 별도의 역시 파일에 작성하자. 나중에 이 두 기능을 기술한 별도의 서비스 워커 스크립트들을 하나의 파일로 합칠것이다.
// 이 스크립트가 호출되면 브라우저 디버그 창에 스크립트 이름인 'sw-tips.js'를 출력한다.
console.log("sw-tips.js");
// updateTip은 그날 그날 팁(tip)을 익스텐션 팁 서버에서 가져오는 함수다.
const updateTip = async () => {
const response = await fetch('https://extension-tips.glitch.me/tips.json');
const tips = await response.json();
// JSON 형태로 되어있는 tips 객체는 여러 tip들을 포함하고 있다.
// 이 중 무작위로 하나의 팁을 뽑아서 크롬 브라우저 저장 영역에 저장한다.
const randomIndex = Math.floor(Math.random()*tips.length);
return chrome.storage.local.set({tip: tips[randomIndex]});
};
const ALARM_NAME = 'tip'; // 알람 이름
// 매일(매 1,440분 = 하루) 마다 이벤트를 발생시키는 알람을 만드는 함수다.
async function createAlarm() {
const alarm = await chrome.alarms.get(ALARM_NAME);
if(typeof alarm === 'undefined'){
chrome.alarms.create(ALARM_NAME, {
delayInMinutes: 1
, periodInMinutes: 1440
});
updateTip();
}
}
// 알람을 생성한다.
createAlarm();
// 알람 이벤트가 발생하면 오늘의 팁을 업데이트한다.
chrome.alarms.onAlarm.addListener(updateTip);
// 익스텐션의 구성 요소들은 메시지 패싱과 저장소 공유 두 방식으로 데이터를 주고 받는다.
// 구성 요소들 중 하나에서 메시지가 발생하면 runtime.onMessage 이벤트가 발생한다.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 서비스 워커가 처리해야 하는지 결정하기 위해 메시지의 이름을 확인한다.
if(message.greeting === 'tip'){
// 저장소에 저장된 오늘의 tip을 가져와 메시지 발신자에게 보낸다.
chrome.storage.local.get('tip').then(sendResponse);
return true;
}
});
<quick_api_reference/sw_tips.js>
manifest 파일의 background 항목을 보면 서비스 워커 스크립트로 service_worker.js 파일을 지정하였다. 위에서 작성한 두 파일들을 아래와 같이 service_worker.js 하나의 서비스 워커 파일로 합친다.
import './sw_omnibox.js';
import './sw_tips.js';
<quick_api_reference/service_worker.js>
이제 익스텐션 API 문서에 접속할 때 마다 페이지에 '오늘의 팁(tip of today)'을 보여주기 위해 API 문서에 삽입되어 동작하는 content 스크립트를 작성해보자.
content 스크립트가 해야할 일은 사용자가 구글 익스텐션 API 참조 페이지(https://developer.chrome.com/docs/ extensions/reference/)에 접속했을 때 마다 이 API 참조 페이지 안에 '오늘의 팁'을 보여주는 것이다. 그러기 위해 우선 '오늘의 팁'을 서비스 워커에 요청하고 받아온다. 이를 서비스 워커와 주고 받아야 할 데이터(메시지) 측면에서 그림으로 나타내보면 아래 그림의 왼쪽(content.js ↔ service_worker.js)과 같다.
content.js가 활성화되면, 즉 사용자가 익스텐션에서 설정한 구글 익스텐션 API 참조 페이지에 접속하면 conten.js는 chrome에서 제공하는 API를 이용하여 {greeting: tip} JSON 객체를 메세지로 서비스 워커 service_worker.js에 보낸다. 서비스 워커는 메시지들을 감시하고 있다가 자신이 처리해야 할 메시지인 {greeting: tip} 메시지가 나타나면 저장하고 있던 오늘의 팁을 메시지의 발신자인 content 스크립트에 전달한다(sw_tips.js에 작성한 내용).
content.js가 해야할 작업인 '오늘의 팁 가져오기'와 '타겟 페이지를 통해 보여주기'를 코드로 작성해보자.
// content.js 가 호출죄면 아래 함수가 자동으로 실행된다.
(async () => {
// chrome.runtime.sendMessage API를 이용해 서비스 워키에 메시지를 보낸다.
const {tip} = await chrome.runtime.sendMessage({greeting: 'tip'});
// 구글 제공 예제에서 찾는 아래 요소는 더 이상 존재하지 않는다(2023.12.20 기준).
// const nav = document.querySelector('.navigation-rail__links');
// 대신 페이지의 네비게이션 바를 가져왓다. 여기에 오늘의 팁을 표시해 줄 것이다.
const nav = document.querySelector('.devsite-tabs-wrapper[aria-label="Lower tabs"]');
// 오늘의 팁 버튼을 하나 만든다. SVG 이미지가 포함되어 있어 조금 복잡해 보일 뿐이다.
const tipWidget = createDomElement(`
<button class="navigation-rail__link" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0; border: none; background: none;>
<div class="navigation-rail__icon">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d='M15 16H9M14.5 9C14.5 7.61929 13.3807 6.5 12 6.5M6 9C6 11.2208 7.2066 13.1599 9 14.1973V18.5C9 19.8807 10.1193 21 11.5 21H12.5C13.8807 21 15 19.8807 15 18.5V14.1973C16.7934 13.1599 18 11.2208 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9Z'"></path>
</svg>
</div>
<span>Tip</span>
</button>
`);
// popover 속성을 가진 요소를 하나 생성한다. popover 속성의 요소는 페이지 상단에 별도 창으로 표시된다.
const popover = createDomElement(
`<div id='tip-popover' popover>${tip}</div>`
);
// popover 속성 요소를 페이지에 넣는다(숨긴다).
document.body.append(popover);
// 오늘의 팁 버튼을 페이지의 네비게이션 바에 포함시킨다.
if (nav !== undefined) { // added to prevent error
nav.append(tipWidget);
}
})();
// HTML 문서의 객체(HTML Document Object)를 생성하는 함수
function createDomElement(html) {
const dom = new DOMParser().parseFromString(html, 'text/html');
return dom.body.firstElementChild;
}
<quick_api_reference/content.js>
content 스크립트까지 작성하면 익스텐션을 구성하는 서비스 워커(service_worker.js = sw_omnibox.js + sw_tips.js), 타겟 페이지 삽입 스크립트(content.js) 그리고 익스텐션을 설정하는 manifest.json들을 모두 작성한 것이다. 여기에 더해 익스텐션을 위한 아이콘들이 필요하다. 익스텐션 아이콘은 구글에서 제공하는 아래 아이콘들을 다운로드 한 후 익스텐션 프로젝트 디렉토리인 quick_api_reference 아래에 images 디렉토리를 생성하고 그 안에 저장한다.
https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/tutorial.quick-api-reference/images
이제 아래와 같이 quick_api_reference 디렉토리 안에 익스텐션 파일들이 갖추어졌을 것이다.
프로젝트를 저장한 후 크롬의 익스텐션 관리자를 통해 작성한 익스텐션을 등록하자. 작성한 익스텐션 등록 방법이 잘 기억나지 않는다면 이전 포스팅인 아래 내용을 참조한다.
2024.01.30 - [웹 프로그래밍 고급 주제들] - 2.1 크롬 익스텐션 만들어 보기 Part I - 기본 구조
2.1 크롬 익스텐션 만들어 보기 Part I - 기본 구조
글을 쓰고 있는 나는 개발자이기 때문에 일단 만드는 것에 관심이 많다. 먼저 간단한 익스텐션들을 몇 개 만들어 보고 익스텐션의 구조를 살펴보면 코드와 개념이 서로 더 잘 연결될 것 같다. 익
youngbyte.tistory.com
익스텐션이 설치된 상태에서 크롬 익스텐션 API 문저 페이지를 방문한 후 브라우저의 주소창에 'api'를 입력하고 스페이스바를 하나 입력하면 아래와 같이 익스텐션의 API 검색 기능이 제공될 것이다.
API를 하나 검색한 후(예, runtime API) 해당 API의 참조 페이지로 이동하면 아래 그림과 같이(하단 오른쪽) Tip 버튼이 표시된다.
그리고 이 Tip 버튼을 클릭하면 오늘의 팁이 페이지에 표시될것이다.
이번 포스팅에서는 서비스 워커와 데이터를 주고 받는 방법, 서비스 워커에서 외부 서버에 데이터를 요청하는 방법, 그리고 이러한 기능을 갖는 서비스 워커를 작성하는 방법과 동작에 대해 살펴보았다. 이를 그림으로 요약해보면 아래와 같다.
그림을 통해 포스팅에서 작성했던 content.js, service_worker.js(sw_omnibox.js, sw_tips.js)의 역할을 익스텐션 관점에서 정리해보았으면 좋겠다.
이번 포스팅에서 작성했던 서비스 워커의 내용과 동작이 그리 간단하지만은 않다. 첫 술에 배 부를수야 없지 않은가. 여러 번 차분히 살펴보면 점점 더 많은 부분들을 알게 될 것이라고 생각한다. 다음 포스팅에서는 익스텐션의 구성 요소 중 익스텐션의 자체 UI에 해당하는 popup 스크립트에 대해 좀 더 살펴보도록 하겠다. 수고하셨습니다!
■
'웹 프로그래밍 고급 주제들' 카테고리의 다른 글
4. 익스텐션 그 너머 - PWA: Progressive Web Application 1/2 (2) | 2024.03.25 |
---|---|
3.2. 크롬 익스텐션 만들어보기 Part II - Chrome API 사용하기 (2) | 2024.03.18 |
2.4. 크롬 익스텐션 만들어보기 Part I - 구조 및 예제 정리 (0) | 2024.02.20 |
2.3 크롬 익스텐션 만들어 보기 Part I - 서비스 워커 (0) | 2024.02.08 |
2.2 크롬 익스텐션 만들어 보기 Part I - 웹 페이지 삽입 스크립트 (0) | 2024.02.02 |
댓글