ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Chrome Extension] 크롬 확장 프로그램 Text Highlighter
    Project 2023. 6. 9. 16:39

     

    만들게 된 계기

    트위터 API 이슈로 특정 서비스가 중단되자 어느 사용자분이 대체제로 크롬 확장 프로그램으로 만든 것을 보고서 크롬 확장 프로그램은 어떻게 만들지? 라는 생각이 들어 찾아보게 되었다. 나도 한 번 만들어 보고 싶은 마음에 무엇을 만들까 생각을 하다가 내가 자주 사용할 것 같은 형광펜 기능을 만들게 되었다.

     

     


     

    크롬 확장 프로그램을 개발하기 위해서는 어떤 구조와 흐름으로 구성되어 있는지 알아야 했다.

    Service Worker, Backgournd scripts

    - 브라우저가 백그라운드에서 실행하는 이벤트 기반의 스크립트이다.

    - ex) 처음 설치되거나  새 탭, 새 책갈피가 추가되고,  아이콘이 클릭 되는 등의 경우 이벤트를 수신하고 대응할 수 있다.

    Content scripts

    - 페이지에 코드를 주입한다. 즉 브라우저에서 페이지와 상호 작용하고 수정할 수 있다.

    - ex) 페이지에 새 요소를 삽입하거나 웹 사이트의 스타일을 변경하거나 DOM 요소를 수정할 수 있다.

    - 동일한 DOM 트리에 대한 접근을 공유하지만 별도의 JavaScript 환경에서 실행

    Popup

    - 팝업을 사용하여 탭 목록 표시 또는 현재 탭에 대한 추가 정보 표시와 같은 기능을 제공한다.

    - 확장 프로그램 아이콘을 클릭하면 나오는 팝업창이다. 사용자가 이동하면 자동으로 닫힌다.

    Options page

    - 사용자가 Chrome 브라우저를 맞춤설정할 수 있는 것처럼 옵션 페이지에서 확장 프로그램을 맞춤 설정할 수 있습니다. 

    - 옵션을 사용하여 기능을 활성화하고 사용자가 필요에 맞는 기능을 선택할 수 있도록 합니다.

    Side panels

    - 사이드 패널을 사용하여 사용자의 탐색 과정을 지원할 수 있다.

    - 사용자는 사이드 패널 UI로 이동하거나 확장 도구 모음 아이콘을 클릭하여 사이드 패널을 찾을 수 있다.

     

     

     

    Content scripts의 경우 페이지의 DOM을 조작할 순 있지만 일부의 API에만 접근할 수 있다. 그래서 Service Worker에서 작업한 뒤 Content scripts와 통신을 통해 데이터를 전해주는 방식을 사용한다. 이는 '메시지'를 보내는 방법을 통해 통신하게 된다.

     

     

     

    Chrome Extensions architecture overview - Chrome Developers

    A high-level explanation of the architecture of Chrome Extensions.

    developer.chrome.com

    해당 페이지에서 더 상세히 알아볼 수 있다.


     

    manifest.json 작성

    {
      "manifest_version": 3,
      "name": "Text Highlighter",
      "version": "1.0",
      "description": "The text highlighter feature allows you to apply a highlighting style to selected text.",
      "background": {
        "service_worker": "background.js"
      },
      "content_scripts": [
        {
          "matches": ["<all_urls>"],
          "css": ["style.css"],
          "js": [
            "content.js",
            "node_modules/@webcomponents/custom-elements/custom-elements.min.js",
            "highlighter.js",
            "draggble.js"
          ]
        }
      ],
      "icons": {
        "16": "images/icon16.png",
        "32": "images/icon32.png",
        "48": "images/icon48.png",
        "128": "images/icon128.png"
      },
      "action": {
        "default_popup": "popup.html"
      },
      "permissions": ["tabs", "storage"]
    }

    크롬 확장 프로그램을 위해서는 해당 프로그램의 메타데이터를 담고 있는 json 형식의 manifest.json을 필수로 작성해야 한다.

    참고로 manifest version 3으로 작성하였다. 이전에 나온 version 2와는 작성하는 방법이 다르다.

    Manifest 파일로 이름, 버전, 아이콘, 권한, 배경 페이지, 내용 스크립트 등을 정의할 수 있다.

     


     

     

    웹 컴포넌트(Web Components)의 사용

    이 프로젝트를 하면서 새롭게 알게 되거나 사용하게 된 개념이 많은데 그중 하나가 웹 컴포넌트였다. 컴포넌트 기반의 UI 작성 개념은 이미 리액트를 접했기 때문에 개념 자체는 어렵지 않았다.

    웹 컴포넌트는 웹 애플리케이션을 구성하는 독립적이고 재사용 가능한 UI 요소로, 컴포넌트 기반의 개발 방식을 통해 코드의 재사용성과 유지보수성을 향상할 수 있다. Custom Elements, Shadow DOM, HTML Templates 등의 기술로 구성되어 있다.

     

    Custom Elements:  사용자가 직접 새로운 HTML 요소를 정의하고 HTML에서 사용
    Shadow DOM: 컴포넌트의 내부 구조와 스타일이 외부로부터 격리되어 독립적으로 관리
    HTML Templates: 재사용 가능한 HTML 코드 조각을 정의하는 기술

     

     

    나는 드래그 앤 드롭을 할 수 있는 엘리먼트, 텍스트 하이라이터 기능이 있는 엘리먼트를 만들었다.

     

    class Draggble extends HTMLElement {
      constructor() {
        super();
        // 초기화 코드 작성
      }
    
      // 메서드와 속성을 정의
    }
    
    // 커스텀 엘리먼트 등록
    customElements.define('custom-element', Draggble);

     

    커스텀 엘리먼트를 만들기 위해서는 HTMLElement를 상속받는 새로운 클래스를 생성해야 한다.

     

    constructor() {
        super()
    
        this.handle = null
        this.render()
      }
    
     render() {
        this.attachShadow({ mode: "open" })
        const style = document.createElement("style")
    
        this.shadowRoot.appendChild(style)
        this.shadowRoot.innerHTML += `
          <div id="container">
            <slot></slot>
          </div>
        `
        this.handle = this.shadowRoot
          .querySelector("slot")
          .assignedNodes()[0]
          .nextElementSibling.querySelector(this.getAttribute("handle"))
        this.handle.addEventListener("mousedown", this.handleMousedown.bind(this))
        this.handle.addEventListener("dragstart", () => false)
      }

     

    커스텀 엘리먼트에 attachShadow 메소드를 사용하여 Shadow DOM을 적용하여 엘리먼트 내부의 스타일과 구조를 캡슐화할 수 있다. <slot> 태그는 사용자가 <custom-element> 요소를 생성하고 그 내부에 컨텐츠를 작성하면, 해당 컨텐츠가 <slot> 위치에 삽입되어 표시된다.

     

    // highlighter.js
    
    const template = `
      <draggble-element handle=".drag">
        <div id="selectedHighlighter">
          <div class="color">
            <button type="button" title="yellow"/>
          </div>
          <div class="drag">
           <svg />
          </div>
        </div>
      </draggble-element>
    `

     

     

    <draggble-element> 엘리먼트를 <selected-highlighter> 엘리먼트에 감쌌다. 즉 <draggble-element> 태그 안의 컨텐츠들은 <slot> 태그에 삽입이 된다는 뜻이다.  <selected-highlighter> 엘리먼트 안에 드래그가 가능한 요소를 만들고 handle로 받아 드래그 앱 드롭 기능을 하는 이벤트 핸들러를 설정해주었다.

     

    드래그 앤 드롭 기능을 구현할 때 아래 글을 참고하였다.

     

    드래그 앤 드롭과 마우스 이벤트

     

    ko.javascript.info

     

    라이프 사이클 콜백

    웹 컴포넌트에도 라이프 사이클 콜백 함수가 존재한다.

     

    connectedCallback: 커스텀 엘리먼트가 처음으로 다큐먼트의 DOM 에 연결되었을 때 호출
    disconnectedCallback: 커스텀 엘리먼트가 다큐먼트의 DOM 으로부터 연결 해제되었을 때 호출
    adoptedCallback: 커스텀 엘리먼트가 새로운 다큐먼트로 이동되었을 때 호출
    attributeChangedCallback: 커스텀 엘리먼트의 어트리뷰트가 추가, 제거 또는 변경되었을 때 호출

     

     

    // highlighter.js
    
    static get observedAttributes() {
        return ["onoff"]
     }
    
    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "onoff") {
          const display = newValue === "on" ? "flex" : "none"
          const style = this.shadowRoot.querySelector("style")
          style.textContent = styledHighlighter({
            display,
          })
        }
     }

     

    나는 <selected-highlighter> 엘리먼트에서 'onoff' 어트리뷰트에 따라 보여주거나 숨길 수 있도록 스타일을 변경할 수 있도록 하였다. 이를 attributeChangedCallback을 사용하였다.이때 해당 어트리뷰트를 관찰할 수 있는 static get observedAttributes()를 사용할 수 있다. 관찰하기 위해 원하는 대상들의 이름을 포함하는 배열을 리턴해야 한다.

     

    // content.js
    
    const highlighter = document.createElement("selected-highlighter")
    document.body.appendChild(highlighter)
    
    chrome.runtime.onMessage.addListener((onoff) => {
      highlighter.setAttribute("onoff", onoff ? "on" : "off")
    })

     

    content.js에서 chrome.runtime.onMessage.addListener를 통해 onoff 상태를 담은 메시지를 받아 setAttribute를 통해 어튜리뷰트를 새로 정의해주었다.

     

    // popup.js
    
    const checkbox = document.querySelector("input")
    
    chrome.storage.local
      .get(["onoff"])
      .then(({ onoff }) => (checkbox.checked = onoff))
    
    checkbox.addEventListener("change", (e) => {
      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        chrome.tabs.sendMessage(tabs[0].id, e.target.checked)
      })
      chrome.storage.local.set({ onoff: e.target.checked })
    })

     

    popup에서 엘리먼트를 켜기/끄기 기능을 할 수 있도록 버튼을 만들어 popup에서 content로 메시지를 전달하였다.

    만약 체크박스가 변경이 되면 chrome.tabs.query를 사용하여 현재 활성화된 탭에 chrome.tabs.sendMessage를 이용해 메시지를 보낼 수 있다. 이때 크롬 확장에서도 스토리지를 지원을 하여 onoff 상태를 저장해두었다. 또한 하이라이터 요소의 드래그 위치를 저장해두어 새로고침해도 똑같은 위치에 있도록 하였다.

     

    앞에서 얘기했듯이 통신이 메시지를 통해 이루어진다고 했는데 chrome.tabs.sendMessage로 메시지를 보내고 chrome.tabs.onMessage.addListener로 메시지를 받는 모습을 볼 수 있다.

     

     


     

    Text Highlighter 기능 구현

    getSelection()

    window 객체의 getSelection 메소드를 사용하면 쉽사용자가 선택된 텍스트의 영역을 쉽게 구할 수 있다.

    this.shadowRoot.querySelector(".color")addEventListener("click", this.highlightSelection.bind(this))
    
    highlightSelection() {
        if (window.getSelection().toString().length === 0) return
        this.range = [...this.range, window.getSelection().getRangeAt(0)]
        const userHighlight = new Highlight(...this.range)
        CSS.highlights.set("highlight", userHighlight)
        window.getSelection().empty()
     }

    getRangAt으로 영역을 구하여 range 배열에 담아 Highlight API를 통하여 하이라이트 객체를 만들어 스타일을 지정할 수 있다. Highlight API은 실험적 기능으로 일부 브라우저에서는 지원이 안되는 점을 유의해야 한다.!

    만든 객체를 CSS.highlights.set에 등록하면 css에서 등록한 이름에 따라 텍스트 스타일을 지정할 수 있다. 

    ::highlight(highlight) {
      background-color: #fffbb0;
    }

     

    간단하고 쉽게 구현하기 위해서 Highlight API를 사용하게 되었지만 여러 색상 사용 등의 추가적인 기능을 구현하기엔 한계가 있다고 느꼈다.

     


     

     

    이렇게 하여 Text Highlighter을 완성하였다.

     크롬 확장 프로그램을 만드는 구조와 흐름을 알 수 있었고 웹 컴포넌트와 여러 API를 사용해 볼 수 있었다.

     

     

    https://github.com/DDD120/text-highlighter

     

    반응형

    'Project' 카테고리의 다른 글

    마피아 G 마스터 회고  (0) 2023.11.02
    ideal idea 회고  (0) 2023.04.03
    MOVIE ROOM 회고  (0) 2023.02.12
    인터랙티브 웹툰 Turn Off 회고  (0) 2023.02.06

    댓글

Designed by Tistory.