728x90

부모님이 컴퓨터에 익숙하지 않다보니, 대신 예약을 해드리는 일이 종종 있습니다.  골프 예약 같은 경우에 경쟁이 치열하다 보니 종종 실패하더군요... 그래서 관련해서 도움을 받을 수 있는 예약 도우미 확장자를 만들었습니다.

 

그 과정에 보편적으로 쓸만한 기능들 그리고 트러블 슈팅 내역을 공유하고자 하여 작성하게 되었습니다.

 

제가 생각하는 기능은

1. 단축키를 통해서 원하는 시간에 예약하기

2. 그리고 해당 페이지에서 예약 과정 처리하기입니다.

 

이 과정에서 필요하다 느낀 기능은 알람, 페이지 접근, 스크립트 사용, 저장소 사용입니다. 

(확장 프로그램의 경우에 결국 브라우저가 아니기 떄문에 웹api에서 제공하는 alert대신 를 chrome api에서 notification을 사용해야 하더군요...) 돔 요소를 제어하는 거는 기존 프론트엔드에서 하던 js를 사용하면 되지만 다른 기능들을 구현하기 위해선 추가적인 확장 프로그램에서 어떤 기능을 제공하는지를 알 필요가 있었습니다.

 

우선 공식 문서를 통해서 학습하려고 했습니다. 개인적으로 구글측 공식문서의 경우에 하나의 완성된 앱이 아닌, 부분적인 코드 위주로 보여줘서 처음부터 보면 어렵게 느껴지더군요..


그래서 블로그를 서칭해서 완성된 예시 2개를 참고했습니다.

https://yscho03.tistory.com/103

 

크롬 확장 프로그램(Chrome Extension)을 개발해보자

크롬 확장 프로그램 (Chrome Extension) 소개 Chrome Extension 이란? Chrome 브라우저의 작은 소프트웨어 프로그램이다. 활용분야 생산성 도구 웹 페이지 콘텐츠 보강 정보 집계 게임 동작방식 웹과 동일하

yscho03.tistory.com

 

https://m2kdevelopments.medium.com/19-understanding-chrome-extensions-commands-3bfa5ecd62cd

 

19 Understanding Chrome Extensions Commands

The chrome.commands API is used to enable the chrome extension run actions from keyboard shortcuts.

m2kdevelopments.medium.com

 

첫 번째 게시글은 한글로 되어있다보니, 어떻게 확장 프로그램 앱이 구성되어있는지, 그리고 등록은 어떻게 하는지에 대해서 파악하기 좋았습니다. 

그리고 두 번쨰 게시글에서는 command 입력에 대해서 전체적인 흐름을 잡기 좋았습니다.

이후 필요한 기능에 대해서 공식 문서를 확인하며 적용시켰습니다.

이제 학습 과정에 대해서 공유드렸고, 어떻게 구현했는지에 대해서 설명하고자 합니다.

 

확장프로그램 개발하기

완벽한 코드를 공유하기보다는 어떤 식으로 기능을 구현했는지를 설명하고자 합니다.


manifest.json

manifest.json은 확장 프로그램의 진입점 역할을 합니다. 어떤 기능을 하는지, 어떤 권한을 하는지를 작성할 수 있습니다.

이제 어떤 키값을 설정했고, 그에 대한 설명을 이어나가려고 합니다.

확장 프로그램을 만들려면 최소한의 키가 필요한데 이는 name, description , version ,manifest_version, icons입니다.
각각 확장 프로그램의 이름, 설명, 프로그램 버전 관리, 사용하고 있는 manifest의 버전, icon입니다.

이 중 manifest버전은 크롬에서 제공하고 있는 버전을 표시하는 것으로 가장 최신인 버전이 3입니다.

 

모든 키 값을 사용하진 않고, 설명하기엔 방대하기에 관련 내용은 상단 manifest.json링크를 통해 확인하면 됩니다.

저는 제가 사용한 키 값에 대해서만 우선 설명하려고 합니다.

// manifest.json

{
  "name": "예약 도우미",
  "description": "~~~ 설명.",
  "version": "0.01",
  "manifest_version": 3,
  "permissions": ["storage", "notifications", "scripting"],
  "host_permissions": ["https://특정사이트.co.kr/*"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "/images/icons-16.png",
      "32": "/images/icons-32.png",
      "48": "/images/icons-48.png",
      "128": "/images/icons-128.png"
    }
  },
  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Ctrl+Shift+Y"
      },
      "description": "Execute action when the extension icon is clicked"
    },
    "start_reservation": {
      "suggested_key": {
        "default": "Alt+T"
      },
      "description": "예약 시작"
    },
    "fill_cert_no": {
      "suggested_key": {
        "default": "Alt+U"
      },
      "description": "인증 번호 입력"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches":["https://특정사이트.co.kr/*"],
      "js": ["content.js"]
    }
  ],
  "icons": {
    "16": "/images/icons-16.png",
    "32": "/images/icons-32.png",
    "48": "/images/icons-48.png",
    "128": "/images/icons-128.png"
  }
}

 

permission :확장 프로그램이 특정 권한을 사용할 때 유저에게 안내를 하기 위해 사용합니다. 권한이 등록되지 않은 기능은 작동하지 않으므로 내가 어떤 기능을 구현하고 그러기 위해 필요한 권한에 대해서 확인할 필요가 있습니다.

제가 사용한 권한은 아래 3가지입니다.

  • storage: 브라우저의 저장소(local storage)에 데이터를 저장하거나 읽는 권한.
  • notifications: 알림(notification)을 생성하는 권한.
  • scripting: 실행 중인 탭에 스크립트를 주입하는 권한.

host_permissions :  확장 프로그램이 작동할 도메인을 지정합니다.  모든 url에 대해서라면 <all_urls> 을 사용하면 됩니다.

 

action :  확장 프로그램을 클릭했을 떄 열리는 페이지에 대해서 정의합니다.

commands : 키보드 단축키를 등록하기 위해서 사용합니다. 사용할 수 키등이 존재하니  공식문서를 확인하길 바랍니다.

이때 각각의 키에 대해서 입력을 하면 추후 background(service_worker)에서  사용할 수 있습니다. 유의할 점은 _execute_action의 경우에는 팝업창을 여는데 사용됩니다. 

그리고 각각의 운영체제에 따른 입력 값을 지정할 수도 있습니다.

 

backgrond :  다른 페이지를 이용중일떄도 작동하게 할 내용(백그라운드에서)을 담을 수 있습니다.  저는 commands를 통해 등록한 키가 감지되면 background.js에서 특정 함수가 작동하도록 구현하였습니다.

content  : 컨텐츠 스크립트는 지정된 URL에서 실행됩니다., matches:를 통해서 특정 페이지에서만 스크립트가 실행하도록 하였습니다.

 

popup.html

팝업창의 경우 일반적인 html과 똑같습니다. 해당 페이지에서는 브라우저 api를 사용할 수 있습니다.(ex : alert) 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <div>
      <label for="startInput">시작 시간 (예 : 11:00)</label>
      <input type="text" id="startInput" placeholder="11:00" maxlength="5" />
    </div>
    <div>
      <label for="endInput">종료 시간 (예: 13:00):</label>
      <input type="text" id="endInput" placeholder="15:00" maxlength="5" />
    </div>
    <button id="submit">시간 설정</button>
    <script src="popup.js"></script>
  </body>
</html>

 

 

이후  popup.js에서 버튼이 클릭될 경우 storage에 해당 시간을 저장하여 추후 다른 script에서도 사용할 수 있도록 하였습니다.

document.addEventListener('DOMContentLoaded', () => {
  const submitButton = document.getElementById('submit');
  const startInput = document.getElementById('startInput');
  const endInput = document.getElementById('endInput');

  submitButton.addEventListener('click', () => {
    const startTime = startInput.value.trim();
    const endTime = endInput.value.trim();

    if (!startTime || !endTime) {
      alert('시작 시간과 종료 시간을 모두 입력해주세요.');
      return;
    }

    if (!/^\d{2}:\d{2}$/.test(startTime) || !/^\d{2}:\d{2}$/.test(endTime)) {
      alert('시간 형식이 잘못되었습니다. 예: 11:00');
      return;
    }
	// chrome.storage api
    chrome.storage.local.set({ startTime, endTime }, () => {
      alert(`희망예약시간이 ${startInput} ~ ${endInput}으로 설정되었습니다`);
    });
  });
});

 

 

background.js

 

background.js에서 탭에 접근해서 돔 요소를 조종하려고 합니다. 하지만 일반적으로 tab에 접근할 경우에는 권한 문제가 발생합니다. activate_tabs를 통해서 간단하게 해결할 수도 있지만, 그럴 경우 사용자에게 불안감을 야기시킵니다.

따라서 크롬에서는 권한을 최소화 하는 방식을 권장합니다

이에 대한 대안이 content.js를 사용하는 방식입니다.

 

  • 콘텐츠 스크립트는 웹 페이지의 DOM과 상호작용할 수 있는 중간다리 역할을 합니다.
  • 백그라운드 스크립트에서 데이터를 전달받아 DOM을 수정하거나 이벤트를 실행합니다.

우선 코드를 먼저보겠습니다.

 

 

// 커맨드 감지 리스너
chrome.commands.onCommand.addListener((command) => {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (tabs.length === 0) {
      notify('활성화된 탭이 없습니다.');
      return;
    }

    const tab = tabs[0];
    const url = tab.url || '';

    // 허용된 URL에서만 명령 실행
    if (!url.startsWith('url주소')) {
      notify('이 페이지에서는 확장 프로그램을 사용할 수 없습니다.');
      return;
    }

    // 명령 처리
    if (command === 'start_reservation') {
      chrome.storage.local.get(['startTime', 'endTime'], (data) => {
        const { startTime, endTime } = data;

        if (!startTime || !endTime) {
          notify('저장된 시간이 없습니다. 시간을 먼저 설정해주세요.');
          return;
        }

        const startNumeric = parseInt(startTime.replace(':', ''), 10);
        const endNumeric = parseInt(endTime.replace(':', ''), 10);

        // 콘텐츠 스크립트로 시간 범위를 전달
        chrome.scripting.executeScript(
          {
            target: { tabId: tab.id },
            files: ['content.js'],
          },
          () => {
            chrome.tabs.sendMessage(tab.id, {
              action: 'start_reservation',
              startNumeric,
              endNumeric,
            });
          }
        );
      });
    }

    if (command === 'fill_cert_no') {
      // 콘텐츠 스크립트에 fill_cert_no 액션 전달
      chrome.scripting.executeScript(
        {
          target: { tabId: tab.id },
          files: ['content.js'],
        },
        () => {
          chrome.tabs.sendMessage(tab.id, { action: 'fill_cert_no' });
        }
      );
    }
  });
});

function notify(message) {
  chrome.notifications.create({
    type: 'basic',
    iconUrl: '/images/icons-32.png',
    title: '알림',
    message: message,
  });
}

 

chrome.scripting.executeScript :  백그라운드 스크립트에서 특정 탭에 스크립트를 삽입할 때 사용됩니다. 이를 통해 웹 페이지의 DOM에 접근하거나, 콘텐츠 스크립트를 실행할 수 있습니다.

chrome.runtime.sendMessage :  크롬 확장 프로그램 내에서 서로 다른 스크립트 간에 메시지를 주고받는 데 사용

 

// content.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'start_reservation') {
    const { startNumeric, endNumeric } = message;

    // 예약 실행 로직
    executeReservation(startNumeric, endNumeric);
  }

  if (message.action === 'fill_cert_no') {
    // 인증 번호 입력 로직
    fillCertNo();
  }
});

 

이후 content.js에서도 리스너를 통해서 해당 함수를 사용합니다.

 

 

 

느낀점

환경자체가 다르고 사용하는 기능이 다르기 때문에 낯설게 느껴졌습니다. 모든 기능을 사용하거나 파악하기는 초기에 힘들기에 필요한 기능 위주로 검색하고 사용하는 것이 효율적이라고 느껴졌습니다.

 

모든 코드를 공개하지 않은 이유는.. 모든 프로젝트가 같은 로직을 사용하진 않을것이기 때문에, 그리고 저는 특정 사이트에만 적용되도록 구현했어서 공개할 필요는 없다고 생각했습니다. 다만 기본적인 원리 위주로  공개를 할 경우 타 프로젝트에 적용시에 더 유용하다 판단하였습니다.

 

+ Recent posts