크롬 확장 플러그인 톺아보기

Jan.08.2021 전수현

Web Frontend

크롬 확장 플러그인 톺아보기

안녕하세요. 우아한 테크캠프 3기를 거쳐서 지난 10월에 배민선물하기팀으로 합류한 신입 꼬꼬마 프론트엔드 개발자 전수현입니다.

평소에 타입스크립트를 사용하는 프론트엔드에서 API 연동 작업을 하면 API 문서에 있는 Request, Response에 대한 인터페이스를 먼저 정의하면서 작업을 시작하게 됩니다.
문제는 이 작업이 웹 브라우저 창과 개발 IDE창 사이에서 지속적으로 복사 & 붙여넣기를 주구장창 반복하게 되기도 하고(노잼) 꽤나 시간을 많이 잡아먹는 일이라는 점입니다.

마침 팀에서 진행하던 크리스마스 사전예약 프로젝트가 막 론칭한 타이밍이었고, 프로젝트 회고 시간이 다가왔습니다.

회고 중에 개발 중에는 시간이 없어 몸으로 때웠던 점들을 개선해 생산성을 높여보자는 논의가 나왔는데요, 다른 프론트엔드 동료분들도 이에 공감을 해주셔서, 해당 불편사항을 해결할 수 있는 간단한 크롬 확장 프로그램으로 만들어보자는 아이디어가 나왔습니다.

이번 글에서는 제가 크롬 확장 프로그램을 처음으로 개발하면서 배우고 정리한 내용들을 소개해드리려고 합니다!

다양한 웹 브라우저에서 경험할 수 있는 확장 프로그램

2020년 기준으로 전세계에서 많이 사용되는 브라우저들은 크로미움 기반인 경우가 많은데요, 크로미움 기반으로 만들어진 대부분의 브라우저(Google Chrome, Microsoft Edge, Opera, Vivaldi, Naver Whale)들은 확정 프로그램을 지원합니다.

Mozilla Firefox의 경우도 원래 다양한 확장프로그램 기능으로 유명했는데요, Firefox의 경우에 크로미움 브라우저에서 지원하는 Browser Extension의 구조가 비슷하기 때문에 조금의 수정만으로도 다른 브라우저로 쉽게 포팅이 가능하다는 장점이 있습니다. Safari의 경우 지난 WWDC20에서 14버전부터 사용가능한 웹 익스텐션에 대해서 공개한 바 있어, 앞으로도 다양한 브라우저에서 사용할 수 있을 것 같습니다.

본 포스팅의 경우 Google Chrome Extension을 기준으로 작성되었습니다.

크롬 익스텐션 API로 할 수 있는 것 파악하기

크롬 브라우저에서 동작하는 거의 대부분을 제어 가능합니다.

Browser Extension에 관련된 문서는 크게 크롬 개발자 공식 문서MDN 공식 문서 가 제일 잘 나와있는데요, 제가 개인적으로 공부하면서 느낀 점은 브라우저 익스텐션의 대략적인 기능 파악이나 Manifest.json 파일에 대한 설명은 MDN이, 각각의 API에서 세부적으로 어떤 함수들을 제공하는가에 대해서는 크롬 개발자 공식 문서가 도움이 되었던 것 같습니다. (Manifest.json는 바로 다음에 설명드릴게요!) 다만, MDN 문서를 참고하실때는 브라우저 별 API 구현 차이점에 관한 문서를 참고하시는게 큰 도움이 될 것 같습니다.

단순하게 웹페이지의 DOM을 조작하는 것을 넘어서 정말 많은 기능이 지원되기 때문에 GitHub에서 Google Chrome 개발 팀에서 운영하는 https://github.com/GoogleChrome/chrome-extensions-samples 에 나와있는 표를 참고하는 것만으로도 대략 어떤 기능이 구현가능한지에 대해서 쉽게 파악할 수 있는 것 같습니다.

이렇게 지원되는 다양한 기능 중에 제가 개인적으로 흥미로웠던 내용들은 이렇습니다.

  • chrome.devtools
    • 개발자 도구에 기존에 존재하는 패널들에 별도의 페이지를 확장 하거나 일부 기능에 대하여 제어가 가능합니다. 단, 해당 API는 manifest.json에서 devtools_page로 명시된 파일에서만 접근 가능합니다.
  • chrome.tabs
    • 탭 관리도 확장 플러그인으로 가능합니다.
    • 단순한 탭 이동, 제어 뿐만이 아니라 화면 캡쳐, 줌까지 제어 가능합니다.
  • chrome.webRequest
    • http 리퀘스트를 가로챌 수 있습니다. 다양한 훅 메서드를 지원하며 response, request header의 수정이 가능합니다.

확장 플러그인 내부 구조 파악하기

그럼 확장 플러그인 내부 구조에 대해 알아봐야 할 타이밍입니다.

브라우저를 불문하고 모든 확장 플러그인은 manifest.json이라는 파일을 요구합니다.

Manifest 파일이라고 하면 PWA나 안드로이드 앱을 개발하셨던 분들은 익숙하실 수도 있는데요, 이 플러그인의 대한 간단한 메타 데이터(이름, 아이콘 파일)부터 어떤 권한을 필요로 하고 어떤 파일을 포함하고 있는지 명시하는 파일입니다.

meta

Manifest 파일을 통해 적어둔 메타 데이터는 그대로 크롬에 반영된다. 한창 사이버펑크 2077의 출시를 기대하고 있을 때라 플러그인 이름도 TyperPunk로 지었습니다..

{
  "name": "TyperPunk",
  "version": "1.0.3",
  "options_page": "options.html",
  "background": {
    "page": "background.html",
    "persistent": true
  },
  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": "icon-128.png"
  },
  "permissions": [],
  "content_scripts": [
    {
      "matches": ["http://*/*"],
      "js": ["main.bundle.js"],
      "run_at": "document_end"  
    }
  ],
  "icons": {
    "128": "icon-128.png"
  },
  "manifest_version": 2,
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}

크게 요악하면 다음과 같습니다.

  • content_scripts: 페이지마다 동작할 스크립트를 정의합니다. 해당 스크립트가 언제 실행(run_at)되고, 어느 사이트에서 적용될 것(matches)인지에 대해서 설정할 수 있습니다. 파일 경로는 mainfest.json을 기준으로 하고, matches의 경우 [MDN Match Pattern Structure]를 활용하면 편리하게 작성할 수 있습니다.
  • browser_action: 우측 상단에 표시되는 아이콘과, 그 아이콘을 클릭했을 때 뜰 팝업 창에 대한 정보를 담습니다. 후술할 내용이지만, 팝업창을 굳이 사용하지 않더라도 팝업창을 이용해 개발 환경을 더 개선시킬 수 있습니다.
  • background: 백그라운드 스크립트에 대해서 정의합니다. 개별 페이지의 생애주기와 관련없이 특정 작업을 하고 싶을 때 background에서 처리합니다. 위의 예시에는 나와있지 않지만 content_scripts와 마찬가지로 "scripts": []를 추가해 자바스크립트 파일도 추가할 수 있고, persistent 옵션으로 자주 사용되지 않는 페이지의 경우 메모리 사용량을 줄일 수 있습니다.
  • permissions: 플러그인에서 사용될 권한들을 정의합니다. 저는 따로 사용하지 않았지만 주로 tabs, storage, webRequest등이 주로 사용됩니다.
  • options_page: 옵션 페이지에 대해서 정의합니다. chrome://extensions 에서 어플리케이션 세부 정보를 들어갔을 때, 확장 프로그램 옵션 에 노출될 페이지를 정의합니다. 보통은 permissions에 storage를 추가해서 storage API를 활용해 설정을 저장하고 불러옵니다.
  • content_security_policy: 기본적으로 익스텐션에서는 <script><object>에서만 리소스를 불러올 수 있고 eval() 사용을 지양하고 있지만 이 정책에 대해서 조금 더 느슨한 설정이 필요할 때 사용합니다. MDN 문서 참고

현재 manifest 명세의 버전은 3까지 나와있지만, Manifest V3 버전은 2021년 1월부터 Chrome Web Store에서도 막 지원한다고 하는 초기 단계라서 본 포스트는 V2를 기준으로 합니다. MV3(Manifest V3)에서는 background 대신 service worker를 사용하고, Promise가 지원되는 등 좀 더 현대적인 개발을 할 수 있을 것으로 기대됩니다. (Extension API MV3 Overview)

브라우저마다 다른 내용은 이 링크 의 표를 참고하시면 됩니다.

background scripts vs content scripts

제가 제작한 확장 플러그인의 워크플로우는 이렇습니다.

  1. 백엔드 개발자분이 제작한 API 문서에 접속
  2. 확장 플러그인이 자동으로 API 명세표(보통은 하나의 Request/Response당 하나의 명세표가 있습니다.) 제일 밑의 버튼과 다양한 옵션 콤보박스(세미콜론 여부, 주석포함 여부, 공백 2자 여부)가 담긴 행 추가
  3. 버튼을 누를시 해당 표 내용을 여러개의 TypeScript interface를 포함하는 TypeScript 소스코드로 변환
  4. 변환 후 클립보드에 해당 코드내용 복사

저의 경우는 DOM을 직접 제어하는 작업 이외에 별도의 권한이 크게 필요가 없었으므로, background script 없이 content scripts로도 충분히 개발이 가능했습니다.

어떤 기능을 구현하고자 할 때, background scripts에 구현할지 아니면 content scripts에 구현할 지 어떻게 구분할까요?

background scripts는

  • 대부분의 Extension API가 사용가능합니다.
  • 대신 개별 웹 페이지에 대해 접근 권한이 없습니다.
  • 따라서, 개별 웹 페이지의 생애 주기에 무관하고 장기적인 로직 처리가 필요하거나, content scripts에서 사용되지 못하는 Extension API를 실행하도록 권장됩니다.

content scripts는

  • 아주 일부의 Extension API에만 접근 가능합니다.
    • 그래서 background scripts와 메세지를 통해서 여러가지 정보를 교환하는 방식으로 프로그램을 설계해야 합니다. 확장 플러그인에서 지원하는 다양한 메시지 방법은 MDN 문서에서 자세하게 설명되어 있습니다.
  • 그러나 마운트된 페이지의 DOM에 직접 접근이 가능합니다.
    • 이걸 토대로 기존 웹 페이지에서 정보를 긁어온다던가, 버튼을 추가한다던가의 기능이 쉽게 구현 가능합니다.
    • 내가 원하는 css도 문서에 삽입할 수 있습니다!
  • window.postMessage 를 통해서 개별 웹 페이지의 스크립트와도 통신이 가능합니다. (서비스 워커에서 사용되는 방식과 비슷!)
  • content scripts에서 cross-origin XMLHttpRequest를 보내는 것은 보안상의 이유로 지양되고 있습니다. 아직 MDN 문서에는 해당 사항이 반영되어있지 않아 헷갈릴 수 있는데, 2020년 9월부로 content scripts에서 CORS를 우회하는 방법이 제한되었고 대신 background scripts에서 request를 보내고 해당 결과값을 메세지를 통해서 content scripts와 주고 받을 수 있도록 권장하고 있습니다.
// Old content script, making a cross-origin fetch:
var itemId = 12345;
var url = "https://another-site.com/price-query?itemId=" +
         encodeURIComponent(request.itemId);
fetch(url)
  .then(response => response.text())
  .then(text => parsePrice(text))
  .then(price => ...)
  .catch(error => ...)

// New content script, asking its background page to fetch the data instead:
chrome.runtime.sendMessage(
    ,
    price => ...);

// New extension background page, fetching from a known URL and relaying data:
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.contentScriptQuery == "queryPrice") {
      var url = "https://another-site.com/price-query?itemId=" +
              encodeURIComponent(request.itemId);
      fetch(url)
          .then(response => response.text())
          .then(text => parsePrice(text))
          .then(price => sendResponse(price))
          .catch(error => ...)
      return true;  // Will respond asynchronously.
    }
  });
  // [크로미움 블로그 참고](https://www.chromium.org/Home/chromium-security/extension-content-script-fetches)

웹팩을 활용해서 효율적으로 개발하기

개발을 진행하면서 자연스럽게 함수나 클래스의 기능에 따라 파일을 쪼개다 보면, 나도 모르는 사이에 아래처럼 소스 파일 수가 많아지는 것을 경험할 수 있습니다.

TMF

개발을 진행할 수록 갈수록 늘어나는 파일들

갈수록 개발하는 파일은 많아지는데, 매번 새로운 js파일을 생성할 때 마다 mainfest.json에 기록하기는 너무 번거롭기도 하고 다양한 외부 라이브러리(저 같은 경우에는 bulma와 clipboard.js를 추가로 사용했습니다.) 를 편하게 포함시키거나 좀 더 현대적인 개발환경(TypeScript, babel등을) 도입하고 싶을 수 있습니다. 이런 경우에는 webpack같은 번들러를 활용해서 개발 환경을 개선시킬 수 있습니다!

저의 경우는 깃허브에 오픈되어있는 프로젝트인 chrome-extension-webpack-boilerplate를 적극 활용하였는데요, 저의 요구사항에 따라 직접 webpack.config.js에 여러가지 플러그인이나 수정할 사항들을 추가하면서 편하게 개발할 수 있었습니다. 그 외에도 환경별로 다양한 boilerplate 코드나 webpack-plugin이 깃허브에 존재하니 한 번 검색해보시는 걸 추천 드립니다!

개발 변경사항 실시간으로 적용하기

위의 webpack-boilerplate의 경우는 webpack-dev-server를 활용하여 HMR(파일 변경시 자동으로 변경점만 빌드)을 지원하지만 큰 단점이 있습니다. 확장 프로그램을 개발하다보면 주로 핵심이 되고 가장 많이 다루게 되는 파일이 contents_script인데, contents_script를 브라우저에 적용하기 위해서는 chrome.runtime.reload()를 호출해서 강제로 플러그인을 리로드 해줘야 변경사항이 브라우저에 적용되기 때문이죠.

그래서 웹팩을 사용하는 경우에는 위의 기능을 개선한 webpack-extension-reloader를 extension에 추가하거나, 웹팩을 사용하지 않는 경우에는 popup.html에 chrome.runtime.reload()를 호출하는 버튼을 하나 추가해두어서 파일 변경사항이 있을 때마다 수동으로 버튼을 눌러 플러그인을 새로고침하는 간단한 방법도 존재합니다.

update_icon

마무리

저는 평소에도 다양한 구글 확장 플러그인을 즐겨 사용합니다.

Vimium, 네이버 한영사전(☆☆☆☆☆, 영어 문서를 달고사는 개발자들에겐 필수 플러그인!!), Notion Web Clipper등을 즐겨 사용하는 입장에서 확장 플러그인에서 어떤 기능과 권한을 가지고 어떻게 동작하는지 늘 관심이 많았습니다.

제가 개발한 이 토이 프로젝트는 비효율적인 작업을 효율적으로 개선하기 위한 프로젝트였다보니 최대한 빠르고 효율적인 개발을 목표로 1-2일만에 완성하여 팀 내에 공유드렸습니다. 다만, 평소에 확장 플러그인에 대해서 가지고 있었던 관심과 애정도 있고, 개발 경험담을 다른 분들에게 공유해드리고자 목표하던 기능을 구현하고도 계속 확장 프로그램에 대해서 공부하게 되었습니다.

공부를 하다보니 이렇게 설계하면 더 좋았을텐데 하는 아쉬움도 조금 남아있고, ‘아 다음에는 저 기능으로 이런거도 만들아봐야지’ 하는 생각에 재미있게 이 토이 프로젝트를 마무리 할 수 있었던 것 같습니다.

저처럼 확장 플러그인 개발에 관심이 많지만 여러가지 이유로 도전해보지 못하셨던 분들에겐 큰 도움이 되셨으면 좋겠고, 혹시 다른 방법으로 개발하셨거나 또 다른 방식으로 업무에 도움이 되는 확장 플러그인을 개발해본 경험이 있으신 분은 댓글로 경험담을 나누어주시면 감사하겠습니다. ☺️

긴 글 읽어주셔서 감사드리고 2021년에도 즐겁고 건강하게 개발하세요!!

기타 참고문서

  • [GitHub] samuelsimoes/chrome-extension-webpack-boilerplate
    • 제가 주로 활용한 코드지만, 마지막 개발이 이루어진지 오래되었고, 깃허브에 찾아보면 다양한 베리에이션이 존재하기 때문에 요런거 구경하는 재미도 솔솔합니다. ㅎㅎ
  • [MDN] Building a cross-browser extension
    • cross-browser extension을 개발하기 위해서 필요한 것들을 다루고 있습니다.
  • [근둥이의 블로그] Safari에서 Web Extension 개발하기
    • WWDC20에 발표된 내용을 토대로 XCode를 활용하여 Web Extension을 개발하는 글입니다.
  • [Chrome] Manifest V3 Draft
    • Browser Extension의 구조는 W3C의 커뮤니티 그룹(비공식) 중 하나인 Browser Extension Community Group에 의해서 제안되고 있으며, Draft는 이 링크를 통해서 확인이 가능합니다. 아직까지 여러 브라우저를 아우르는 공식 표준안은 존재하지 않는다고 합니다. 크롬 팀에서 새로 개발중인 Manifest V3에 관심이 가신다면 한번 둘러보시길 추천!