크롬 익스텐션 연구소,, (가천대학교 학식 메뉴 조회 익스텐션)

김현중·2025년 2월 26일

연구소

목록 보기
11/34
post-thumbnail

이번에 가천대학교 홈페이지와 가천대학교 사이버 캠퍼스에 진입하면 활성화 되는 이번 주 교육 대학원 학식 점심 메뉴를 알려주는 익스텐션을 제작했습니다.

평소에 사이버 캠퍼스에 들어갈 일은 많았지만, 학식 메뉴를 보려면 가천대학교 홈페이지 -> 대학 생활 -> 학생 식당 -> 교육대학원 페이지로 이동해야하는 번거로움이 있었습니다. 그래서 사이버 캠퍼스에 들어가기만 해도 이번 주의 점심 메뉴를 보여주는 익스텐션이 있다면 정말 편하겠다고 생각했습니다.

그리하여 html, css, javascript + python, SQLlite를 사용해 익스텐션을 제작했습니다.
python으로 교육대학원 페이지를 크롤링하고, 데이터를 menu.db에 저장했습니다. API는 fastAPI를 사용했고, onRender를 사용해 배포를 진행했습니다.
백엔드 파트는 claude를 사용해 제작했습니다.



manifest

매니페스트는 Chrome 확장 프로그램의 설게도입니다. JSON 형식의 파일(manifest.json)로, 확장 프로그램에 관한 중요한 정보와 설정을 가지고 있습니다.

  1. 기본 정보 : 이름, 버전, 설명 등 확장 프로그램의 기본 정보
  2. 권한 선언 : 확장 프로그램이 필요로 하는 브라우저 API 접근 권한
  3. 리소스 위치 : 아이콘, 팝업 창, 백그라운드 스크립트 등의 파일 경로
  4. 동작 방식 : 확장 프로그램이 어떻게 작동할지에 대한 설정

Chrome은 이 매니페스트 파일을 읽고 확장 프로그램을 로드하고 실행합니다.

아래는 전체 코드입니다.

{
  "manifest_version": 3, // 확장 프로그램의 매니페스트 버전(Chrome 확장 프로그램 API 버전 3)
  "name": "givebob",
  "description": "가천대학교 교육대학원 점심 식단",
  "version": "1.0", // 확장 프로그램 버전
  "action": {   // 확장 프로그램 기본 동작
    "default_title": "밥줘",
    "default_popup": "/index.html",  // 확장 프로그램 아이콘 클릭 시 열리는 팝업 HTML 파일 경로
    "default_icon": { // 툴바 아이콘
      "16": "icons/icon.png",
      "32": "icons/icon.png",
      "48": "icons/icon.png",
      "128": "icons/icon.png"
    }
  },
  "icons": { // 웹스토어 아이콘
    "16": "icons/icon.png",
    "32": "icons/icon.png",
    "48": "icons/icon.png",
    "128": "icons/icon.png"
  },
  // 확장 프로그램이 필요로 하는 권한 목록(현재 탭 접근, 네트워크 요청 규칙 설정, 탭 관리 권한)
  "permissions": ["activeTab", "declarativeNetRequest", "tabs"],
  // 확장 프로그램이 접근할 수 있는 호스트(도메인) 목록
  "host_permissions": [
    "*://*.gachon.ac.kr/*",
    "https://givebob.onrender.com/*"
  ],
  "background": { // 백그라운드에서 실행되는 스크립트 설정
    "service_worker": "background.js" // 백그라운드에서 실행될 서비스 워커 스크립트 파일 경로
  }
}

주석으로 설명을 보충했습니다.



서비스 워커?

서비스 워커는 웹 페이지와 별개로 백그라운드에서 실행되는 javascript 파일입니다.

  1. 백그라운드 작업 처리 : 사용자가 확장 프로그램의 UI와 상호작용하지 않을 때도 작업을 수행
  2. 이벤트 리스닝 : 브라우저 이벤트(탭 열기/닫기, 웹 요청)를 감지하고 반응
  3. 상태 유지 : 확장 프로그램의 상태를 관리하고 필요할 때 저장
  4. 네트워크 요청 관리 : 웹 요청을 가로채거나 수정할 수 있음

매니페스트 V3에서는 이전 버전의 background scripts나 pages 대신 service_worker를 사용합니다.
서비스 워커는 필요할 때만 활성화되고 사용하지 않을 때는 비활성화되어 메모리 사용을 최적화 합니다.

그런데 javascript코드로 script.js를 작성했는데, background.js도 작성했습니다. 엥?



background.js vs script.js

backgound.js (서비스 워커)

  • 실행 환경: 브라우저 백그라운드에서 실행됨
  • 생명 주기: 확장 프로그램이 설치되어 있는 동안 지속적으로 활성화될 수 있음
  • 주요 기능:
    • 브라우저 이벤트 리스닝(탭 변경, 브라우저 시작 등)
    • 글로벌 상태 관리
    • API 요청 처리
    • 다른 부분의 확장 프로그램과 통신
    • 확장 프로그램의 '두뇌'

script.js (콘텐츠 스크립트)

  • 실행 환경: 웹 페이지의 컨텍스트 내에서 실행됨
  • 생명 주기: 특정 페이지가 로드될 때만 활성화
  • 주요 기능:
    • 웹 페이지의 DOM 조작
    • 웹 페이지에서 데이터 추출
    • 웹 페이지에 UI 요소 추가
    • UI와 상호작용
    • 확장 프로그램의 '손과 눈'

즉, 이 두 파일은 다른 역할을 하며, 관심사 분리 원칙에 따라 파일을 분리했습니다.

익스텐션 팝업은 다음과 같이 생겼습니다.

아래는 script.js와 background.js 전체 파일 코드입니다.

script.js

// 메뉴 데이터를 가져오는 비동기 함수
async function fetchMenuData() {
  const loadingEl = document.getElementById("education-loading");
  const errorEl = document.getElementById("education-error");
  const contentEl = document.getElementById("education-content");

  // loading 표시 보이기
  loadingEl.classList.add("show");
  errorEl.classList.remove("show");
  contentEl.textContent = "";

  try {
    const response = await fetch(
      "https://givebob.onrender.com/api/menu/education"
    );
    const data = await response.json(); // fetch를 사용했으므로 JSON 형식으로 파싱 필요

    if (data.status === "success") {
      const menuElements = []; // 메뉴를 담을 배열

      // 각 날짜와 해당 날짜의 식사 메뉴 처리
      for (const [date, meals] of Object.entries(data.data.menus)) {
        const dateDiv = document.createElement("div");
        dateDiv.className = "menu-date";
        dateDiv.textContent = date;
        menuElements.push(dateDiv);

        // 각 날짜의 식사 유형(아침, 점심, 저녁 등)과 메뉴 항목을 처리
        for (const [type, items] of Object.entries(meals)) {
          const typeDiv = document.createElement("div");
          typeDiv.className = "menu-type";
          typeDiv.textContent = type;
          menuElements.push(typeDiv);

          const ul = document.createElement("ul");
          ul.className = "menu-items";

          items.forEach((item) => {
            const li = document.createElement("li");
            li.textContent = item;
            ul.appendChild(li);
          });
          // 완성된 ul 요소를 menuElements 배열에 추가
          menuElements.push(ul);
        }
      }
      // 모든 메뉴 요소들을 콘텐츠 영역에 추가
      contentEl.append(...menuElements);
    } else {
      throw new Error("메뉴를 불러오는데 실패했습니다.");
    }
  } catch (error) {
    errorEl.textContent = "메뉴를 불러오는데 실패했습니다.";
    errorEl.classList.add("show");
  } finally {
    // loading 표시 숨기기
    loadingEl.classList.remove("show");
  }
}

// DOM이 완전히 로드되었을 때 실행되는 이벤트 리스너를 등록
document.addEventListener("DOMContentLoaded", () => {
  fetchMenuData(); // 페이지 로드 시 메뉴 데이터 가져옴
});

background.js

// 익스텐션이 설치되거나 업데이트 될 때
chrome.runtime.onInstalled.addListener(() => {
  // 기본적으로 익스텐션 활성화
  chrome.action.enable();
});

// 가천대학교 도메인인지 확인하는 함수
function isGachonDomain(url) {
  return url && url.match(/^https?:\/\/([^\/]+\.)?gachon\.ac\.kr/);
}

// URL이 변경될 때마다 확인
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // 탭이 로딩되는 동안 여러 번 호출될 수 있으므로 URL 변경 시에만 확인
  if (changeInfo.url) {
    // 변경된 URL이 가천대학교 도메인인지 확인
    if (isGachonDomain(tab.url)) {
      // 가천대학교 도메인이면 익스텐션 활성화
      chrome.action.enable(tabId); // 팝업은 자동으로 열지 않음
    } else {
      // 다른 사이트에서는 (아이콘)비활성화
      chrome.action.disable(tabId);
    }
  }
});

// 새 탭이 활성화될 때마다 확인
chrome.tabs.onActivated.addListener(async (activeInfo) => {
  // 현재 활성화된 탭의 정보 가져옴
  const tab = await chrome.tabs.get(activeInfo.tabId);
  if (isGachonDomain(tab.url)) {
    // 가천대학교 사이트면 활성화
    chrome.action.enable(activeInfo.tabId);
  } else {
    chrome.action.disable(activeInfo.tabId);
  }
});


✋!잠깐 이렇게 하면 사용자가 Chrome에서 탭을 열 때 마다 isGachonDomain 함수가 실행되지 않을까?

다음 트러블 슈팅에서 만나요

profile
진짜 성실한 사람

0개의 댓글