글도비-크롬 익스텐션 개발기(NLP를 이용한 ~ 프로젝트 [2탄])

!0!·2023년 5월 15일
0

허접 졸프🥲

목록 보기
2/3

프로젝트 설명

이전 게시글에서 언급했듯이 'NLP를 이용한 글쓰기 피드백 크롬 익스텐션, 글도비'를 만들고 있다. '띄어쓰기 및 맞춤법 교정', '격식-비격식 전환', '문맥에 맞는 유의어 추천' 기능을 제공한다.

이번 게시글에서는 크롬 익스텐션을 만드는 부분을 다루려고 한다.
사실 '크롬 익스텐션 만들기' 포스팅이 우리나라 거로는 많이 없다. 있는 포스팅들은 약간 c 언어 배울 때 printf("hello, world\n") 코드 돌려보고 나 C언어 해봤다! 하는 느낌..

처음에는 나도 간단한 동작만 확인하는 게 목표였어서 그냥 '크롬 익스텐션 만들기' 검색해서 나오는 코드를 복붙하려고 했더니, 매니페스트 버전 충돌이 자꾸 생겨서 공식 문서를 많이 참고했다. 거의 다 읽었던 것 같다..! (영어 싫어.........)
https://developer.chrome.com/docs/extensions/mv3/

아무튼 그래서.. 혹시나 검색해서 들어오는 사람이 있을까봐 최대한 자세히.. 풀어보려고.. 노력은... 해볼게요..?


extension files

크롬 익스텐션을 만드려면 다음과 같은 파일들이 필수로 필요하다. 다른 게시글에서 구조를 지겹게 봤다면 이 부분은 스킵해도 된다.

  • the manifest
    "manifest.json" 파일로 만드려는 크롬 익스텐션의 메타데이터를 담고 있다. 이름, 설명, 권한 설정, 아이콘, 스크립트들 등에 대한 정보를 등록한다. 매니페스트 파일의 이름은 반드시 manifest.json이어야 하며, 루트 디렉토리에 위치해야 한다. (다른 폴더 안에 넣어두면 안 됨!)
  • the service worker(백그라운드 스크립트)
    js 파일로, 브라우저에서 발생하는 event(예: 새 탭 열기, 탭 닫기)를 listening하고 handling한다. 그리고 모든 크롬 API를 사용할 수 있다. chrome.tabs.query와 같은 크롬 API가 정의되어 있어서, 사용자의 브라우저 상태를 파악하고, 그에 맞는 코드를 작성할 수 있다. 말 그대로 "백그라운드"에서 일어나는 일을 처리하는 스크립트로, 웹 페이지의 내용과는 직접적으로 상호작용할 수 없다. 이 일은 content script에서 하게 된다.
  • content scripts
    역시 js 파일로 작성하며, DOM을 읽어오거나 수정할 수 있다. 일부 크롬 에이피아이만 사용 가능하며, 서비스 워커와 메세지를 주고 받으며 간접적으로 다른 크롬 에이피아이를 사용할 수 있다
  • the popup and other pages
    기존의 웹페이지에서 작동하는 것 말고도 html 파일을 사용하면 새 창을 띄울 수 있다.

    크롬에서 퍼즐 모양의 크롬 익스텐션 메뉴를 눌렀을 때 나오는 화면이 바로 popup.html이다. (당연한 얘기 같지만 js와 css를 등록해서 사용할 수 있다)

Warning
As Manifest V3 approaches full feature parity with V2, we will be phasing out Manifest V2 in 2023. See Manifest V2 support timeline for details.

크롬익스텐션 만들기 포스팅 보다 보면, "manifest_version": 2,라고 되어있는 포스팅이 있을 수 있는데, V3으로 작성해야 한다.


chrome://extensions/ < 에 들어가서 '압축해제된 확장 프로그램을 로드합니다.'를 클릭한 뒤 'manifest.json'과 그 파일에서 정의한 다른 스크립트들이 함께 있는 폴더를 업로드해주면 직접 만든 크롬 익스텐션을 사용할 수 있다.

다음은 우리 프로젝트의 manifest.json 파일이다.

manifest.json 예시

{
    "manifest_version": 3,
    "name": "글도비",
    "version": "1.0",
    "description": "글을 돕다.",
    "background": {
        "service_worker": "scripts/background.js"
    },
    "action": {
        "default_title": "글도비",
        "default_popup": "popup.html"
    },
    "icons": {
        "16": "images/geuldobi.png",
        "32": "images/geuldobi.png",
        "48": "images/geuldobi.png",
        "128": "images/geuldobi.png"
    },
    "permissions": ["cookies", "storage", "activeTab", "scripting", "tabs", "<all_urls>", "proxy"], 
    "host_permissions" : ["https://*/"],
    "content_scripts": [
        {
            "js": [
                "scripts/content.js", "scripts/content_gen.js", "scripts/content_word.js"
            ],
            "matches": [
                "<all_urls>"
            ],
            "exclude_matches": [
                "https://docs.google.com/document/*",
                "https://*.notion.so/*",
                "https://*.facebook.com/*"
            ],
            "css": ["styles/content.css"]
        }, 
        {
            "js": [
                "scripts/content_iframe.js"
            ],
            "matches": [
                "https://docs.google.com/document/*"
            ],
            "css": ["styles/content.css"]
        }
    ]
}

manifest file은 말그대로 "메타데이터"라서 구성 요소들(주로 스크립트, 이미지 파일)의 위치와 파일명을 명시해준다. 그리고 어떤 권한을 줄지(예를 들어 "cookies" 권한을 주지 않으면 쿠키 수집을 할 수 없음), 어느 url에서 어느 스크립트를 주입할 지도 함께 담아준다.

iframe과 크롬 익스텐션

manifest 파일에서 "content_scripts" 부분을 보면, "https<://docs.google.com/document/*", "https://*.notion.so/*", "https://*.facebook.com/*" 세 개의 링크를 제외하고("exclude_matches":) 모든 url에서 사용하는 content scripts와, "https://docs.google.com/document/*"에서만 사용하는 content script로 나누어 작성하였다.

그 이유는 iframe을 사용하는 웹 페이지 때문이다. 구글 Docs는 iframe으로 페이지를 구성하고 있다. 따라서 iframe 안에서 선택된 텍스트를 가져오는 방식이 다르기 때문에, 드래그를 명령어로 사용하는 우리 프로젝트에서는 웹 페이지에 따라 콘텐트 스크립트를 두 가지로 구분할 필요가 있었다.

iframe에서 getSelection() 사용하기

if (window.frames.length > 0) {
  const selection = window.frames[0].document.getSelection();
  if (selection.rangeCount > 0) {
    const first = selection.getRangeAt(0);
    // 코드 실행
  }
}

window.frames[0]을 사용하여 iframe 내부의 document 객체에 접근한 후, document.getSelection() 메서드를 사용하여 선택된 텍스트를 가져오는 코드이다.
사실 iframe 관련 아직 짜지는 않았다. 나머지 부분은 비슷하게 작동할 테니 조금만 수정하면 되겠지..ㅎㅎ

여러 개 content scripts의 작동

사실 크롬 웹스토어에서 서비스되고 있는 크롬익스텐션들은 웬만해서는 여러 개의 컨텐트 스크립트로 이루어져 있다. 근데 크롬 익스텐션 튜토리얼 페이지들은 대부분 하나의 컨텐트 스크립트를만을 사용하고 있어서 이 부분은 꼭 써야지 생각했었다.

    "content_scripts": [
        {
            "js": [
                "scripts/content.js", "scripts/content_gen.js", "scripts/content_word.js"
            ],
            "matches": [
                "<all_urls>"
            ]

이렇게 manifest.json 파일을 작성했을 때, 크롬 익스텐션을 켜고 아무 웹페이지에나 들어가면 세 개의 js 파일이 적혀있는 순서대로 모두 로드된다.
그리고 별다른 스크립트를 로드하는 작업 없이 다른 스크립트의 함수와 변수를 사용할 수 있다. 작업하는 스타일에 따라서는 한 스크립트에는 함수나 변수들을 모두 정의해두고, 다른 스크립트에서 호출만 하는 식으로 작성해도 편하겠다는 생각을 했었다. (c에서 함수 정의된 헤더파일을 사용하고 main.c를 분리하는 느낌으로?)

우리 크롬 익스텐션은 기본적으로 사용자가 드래그를 하면 단어인지, 문장인지를 판별하고 그에 따른 툴팁을 append해주는데, 이 기능을 하는 기본적인 뼈대 코드를 content.js에 담았다. 그리고 드래그한 내용이 각각 단어였을 때와 문장이었을 때의 코드를 각각 content_word.js와 congent_gen.js에 적어두었다.

그리고 여러 개의 콘텐트 스크립트를 사용하다보면, 정의되지 않은 변수나 함수를 사용하게 될 수도 선언문이 적힌 스크립트를 manifest 파일에 먼저 적는 것을 추천한다. (우리처럼 이벤트가 발생했을 때에만 작동하는 거면 별로 상관 없긴 하겠지만 그래도 변수 선언을 먼저 하는 게 마음이 편해서 나도 content.js 파일 상단부에 변수를 선언해두었다.)

//content.js
console.log("content is loaded");
//content_gen.js
console.log("content_gen is loaded");
//content_word.js
console.log("content_word is loaded");


각 코드의 상단에 console.log()를 넣어 로딩되는 순서를 알아보았다. manifest.json에 넣은 순서대로 로드되는 것을 알 수 있다!

content script(프론트 알고리즘)

오늘 포스팅의 마지막 부분! 사실 여기서부터는 크롬 익스텐션 개발과는 관련이 없다. 그냥 제대로 구현하기 위해 사용한 알고리즘들인데 혹시나 비슷한 기능을 위해서 검색하는 분들이 계실까봐 몇 개 적어본다

드래그한 단어가 포함된 문장 판별하기

우선 주요 기능을 구현하기 위해서는 api로 알맞은 쿼리를 전달해줘야 한다.
다른 기능은 사용자가 입력한 값이나 드래그한 값을 그대로 넘겨주면 되지만, 가장 문제는 '문맥에 맞는 유/동의어' 추천 기능이다.

"여기에 두 개의 문장이 있습니다. 하지만 적절한 문장을 선택해서 모델에 넘겨줘야 합니다."

사용자가 "문장"을 드래그했을 때 단순히 "문장"이 포함된 문장을 탐색해 가져오는 것이 아니라, 문맥과 확률을 제대로 계산할 수 있게 사용자가 선택했던 단어가 "포함되어 있는" 두 번째 문장을 가져와야 한다. (문장탈트붕괴...)
따라서 다음과 같은 알고리즘을 구현하여 문장을 하기로 했다.

function getSelectedSentence() {
  let selection = window.getSelection();
  let anchorNode = selection.anchorNode;

  // 선택된 텍스트가 없거나, anchorNode가 없는 경우
  if (!selection || !anchorNode) return "";

  let sentenceStart = anchorNode.textContent.lastIndexOf(".", selection.anchorOffset) + 1;
  let sentenceEnd = anchorNode.textContent.indexOf(".", selection.focusOffset, sentenceStart) + 1;
  sentenceEnd = sentenceEnd < sentenceStart ? anchorNode.textContent.length : sentenceEnd;

  let sentence = anchorNode.textContent.substring(sentenceStart, sentenceEnd).trim();

  // 선택한 단어를 [MASK]로 치환
  let selectedWord = selection.toString().trim();
  //sentence = sentence.replace(selectedWord, "[MASK]");
  return [selectedWord, sentence];
}

getSelection() method의 return값 Selection객체의 속성인 anchorNode를 사용해서 앞뒤로 .을 찾아 해당 단어가 포함된 문장을 추출하는 알고리즘이다. 다만... '.'이 있어야지만 sentence가 찾아진다는 점..이다. 우선 사용자가 문장마다 온점을 찍었다고 가정한 뒤에, 온점이 없으면 첫 문장 또는 마지막 문장(아직 온점을 찍지 않은)이라고 생각하고 [텍스트의 시작|끝]까지 찾아 긁어오는 방향으로 수정하려고 한다. (나는 진짜 바보다 물음표랑 느낌표도 넣어줘야겠다..)


크롬 익스텐션에서 직접 실행해본 사진. sentence, selectedWord(console에서는 MaskWord)를 API로 보내서 문맥에 맞는 관련 단어를 추천해준다.

입력창 내용 바꾸기

사용자가 입력창의 내용을 모델이 제안해준 내용으로 바꾸고자 선택했을 때 실행되는 코드이다.

    const $text = document.getElementsByTagName('div');

    for (let i = 0; i < $text.length; i++) {
      if ($text[i].innerHTML.includes(selec_text)) {
        $text[i].innerHTML = $text[i].innerHTML.replace($dragged.innerText, machine);
        break;
      }
    }

document에서 div의 태그를 가져와서 사용자가 드래그한 selec_text가 있으면 그 부분을 찾아 머신이 추천한 machine으로 바꿔주는 코드이다. 이 코드는 격식-비격식 적용에 사용한 부분으로, 똑같은 문장이 여러 개가 나올 확률이 적다고 생각해서 이렇게 작성하였다. 단어는 비교적 같은 단어가 여러 문장에 나올 수 있기 때문에 if문을 다른 식으로 작성하였다. (지금 코드는 가장 먼저 나온 것을 찾아 바꿔줌)

사이드바 추가하기

이거는 사실 매우매우매우 간단하다. body의 오른쪽에 패딩을 주고, sidebar를 추가하면 된다! 나는 div태그로 사이드바를 만들어 append하였다.

const $sidebar = document.createElement('div'); $sidebar.id = "sidebar"; 

//sidebar
$dic_more.addEventListener('mousedown', (event)=>{
  document.querySelector('body').style.paddingRight = "350px";
  $sidebar.innerText = "결과 더 보기";
  $sidebar.style.display = "block";
  document.querySelector('body').append($sidebar);
  }
})


눈에 잘 보이라고 사이드바의 스타일을 일부러 조정해본 사진이다. body의 padding을 조절하기 때문에 웹페이지가 잘리지 않고 옆으로 잘 밀리는 것을 확인할 수 있다.
(저 사진에서 padding을 350px만큼 주고 사이드바의 width는 290으로 설정했다.)


오늘 포스팅 여기서 끝!! 정리 끝나면 여기 깃허브링크도 업로드할게요~

0개의 댓글

관련 채용 정보