DEV Community

이종현
이종현

Posted on

Google API 없이 브라우저 로컬에서 캘린더 일정을 분석하는 Chrome 확장 구조

https://chromewebstore.google.com/detail/ngfbdomnejfkgmlkfcmlgmcddalalnfe?utm_source=item-share-cb

OAuth 토큰도, 백엔드 서버도, LLM API 호출도 없다. chrome.runtime.sendMessage와 IndexedDB만으로 Google Calendar 화면을 읽고, 우선순위 점수를 계산하고, 결과를 인라인 패널에 표시할 수 있다. 이 글은 그 구조가 실제로 어떻게 작동하는지 단계별로 설명한다.


content script는 어떻게 calendar.google.com 화면을 직접 읽는가?

content script는 Google Calendar 페이지가 로드될 때 자동으로 실행되는 자바스크립트 파일이다. OAuth 인증이 필요 없다. 이미 로그인된 사용자의 브라우저 탭 위에서 DOM을 직접 읽기 때문에, 구글 서버에 어떤 요청도 보내지 않는다.

구체적으로, manifest.json에 "matches": ["https://calendar.google.com/*"]를 등록하면 확장이 해당 URL에서 content script를 주입한다. 스크립트는 querySelectorAll로 이벤트 칩 요소를 순회하고, 각 칩의 텍스트 노드와 aria-label 속성에서 이벤트 제목과 시간 범위를 추출한다.

주의할 점이 하나 있다. Google Calendar의 일부 뷰는 Shadow DOM 경계 안에 이벤트 요소를 렌더링한다. 이 경우 일반 querySelector로는 접근이 안 된다. element.shadowRoot를 타고 내려가는 재귀 탐색 함수가 필요하다. 이 탐색은 단순 트리 순회이므로 성능 부담은 거의 없고, 실제로 주간 보기나 월별 보기 기준으로 DOM 노드 수는 수백 개 수준이다.

추출 대상은 다음과 같이 구분한다.

  • 이벤트 제목 텍스트
  • 시작/종료 시간 (문자열 파싱)
  • 반복 여부 (aria-label 또는 특정 CSS 클래스로 판별)
  • 색상 코드 (구글 캘린더 색상 분류와 매핑)
  • 현재 뷰 유형 (day, week, month)

원문 텍스트를 외부로 보내지 않으므로, 이 단계에서 이미 프라이버시 경계가 결정된다.


Shadow DOM 인라인 패널과 service worker 사이의 메시지 흐름

content script가 DOM에서 이벤트 데이터를 추출하면, 그것을 service worker(백그라운드 스크립트)로 전달해야 한다. 여기서 chrome.runtime.sendMessage가 역할을 맡는다.

통신 흐름은 단방향이 아니다. content script가 메시지를 보내면 service worker가 처리 결과를 응답(response)으로 돌려주고, content script는 그 결과를 Shadow DOM으로 생성한 인라인 패널에 반영한다. 전체 흐름을 단순화하면 이렇다.

[content script]
  → DOM 파싱 → 이벤트 배열 추출
  → chrome.runtime.sendMessage({ type: 'ANALYZE', events: [...] })
  → 응답 수신 → Shadow DOM 패널 업데이트

[service worker]
  → chrome.runtime.onMessage.addListener
  → IndexedDB에서 기존 메타데이터 조회
  → Delta Attention 가중치 연산
  → sendResponse({ ranked: [...] })
Enter fullscreen mode Exit fullscreen mode

비동기 처리에서 한 가지 주의할 점: chrome.runtime.onMessage의 리스너가 비동기 응답을 반환하려면 리스너 함수 내에서 return true를 명시해야 한다. 그렇지 않으면 메시지 채널이 즉시 닫히고 응답이 유실된다. 이 부분은 Chrome 확장 개발 시 자주 놓치는 지점이다.

인라인 패널은 element.attachShadow({ mode: 'open' })으로 생성한다. Shadow DOM으로 격리하면 Google Calendar 페이지의 CSS 규칙이 패널 내부에 영향을 미치지 않는다. 패널은 calendar.google.com 레이아웃 위에 float되지 않고, 특정 컨테이너 요소 하위에 삽입되어 흐름을 유지한다.


IndexedDB에 메타데이터만 저장하는 이유와 구조

일정 원문 텍스트를 저장하지 않는다. IndexedDB에 기록하는 것은 메타데이터다. 이 구분이 설계에서 가장 중요한 결정이다.

저장하는 것 저장하지 않는 것
이벤트 해시(ID) 이벤트 원문 제목
시작/종료 타임스탬프 (Unix ms) 참석자 이메일
색상 코드 인덱스 위치 텍스트
반복 여부 (boolean) 설명 본문
누적 지연 카운트
사용자 지정 우선순위 오버라이드

이렇게 하면 두 가지가 동시에 해결된다. 첫째, 개인정보 노출 위험이 없다. 누군가 IndexedDB 덤프를 확보하더라도 일정 내용을 복원할 수 없다. 둘째, 스토리지 비용이 낮다. 수백 개 이벤트의 메타데이터 합산 용량은 수십 KB 수준이다.

IndexedDB의 object store 구조는 eventId를 keyPath로 하고, 타임스탬프와 우선순위 관련 필드를 인덱스로 설정한다. service worker에서 IDBOpenDBRequest를 통해 접근하며, content script에서는 직접 IndexedDB를 건드리지 않는다. 모든 영속 데이터 접근은 service worker를 거친다. 이 레이어 분리는 이후 디버깅에서 상태 오염을 막는 데도 유리하다.

일정 우선순위 분석에 대한 전반적인 접근 방식은 구글 캘린더 우선순위 자동 정렬 원리 글에서 더 자세히 다루고 있다.


로컬 연산만으로 Delta Attention을 실행하는 방법

Delta Attention은 이 확장이 내부적으로 쓰는 가중치 연산 방식의 이름이다. LLM이 아니다. "attention"이라는 이름을 쓰는 이유는, 이벤트 집합 내에서 각 이벤트가 다른 이벤트와의 관계(시간적 근접성, 색상 동질성, 반복 패턴 차이)에 따라 가중치를 다르게 받는 구조이기 때문이다.

연산 흐름은 다음과 같다.

  1. 현재 보기에 표시된 이벤트 N개를 배열로 받는다.
  2. 각 이벤트에 대해 마감까지 남은 시간, 색상 우선순위 매핑, 반복 여부 세 개의 원시 점수를 계산한다.
  3. 인접 이벤트들과의 시간 간격 행렬을 만든다. 이것이 attention 행렬의 역할을 한다.
  4. 행렬의 각 셀 값은 두 이벤트 간 시간 gap의 역수로 정규화된다. 가까운 이벤트일수록 서로에게 큰 영향을 미친다.
  5. 원시 점수와 attention 행렬을 element-wise 곱한 뒤 행 합산을 구한다.
  6. 이 값에서 IndexedDB에 저장된 누적 지연 카운트를 반영해 최종 순위를 결정한다. Delta라는 이름은 여기서 온다. 이전 상태와의 차이(지연 누적)를 현재 연산에 반영하기 때문이다.

이 연산은 서버가 필요 없다. N이 수십 개에서 수백 개 수준이면 행렬 연산 비용은 무시할 만하다. service worker는 WebWorker API를 쓸 수 없지만, 실제 측정 없이도 수백 개 이벤트 기준으로 연산이 메인 스레드를 차단할 만큼 무겁지 않다. 단, 이벤트 수가 수천 개를 넘어가는 경우에는 청크 분할 처리가 필요하다.

이 연산 결과는 외부로 나가지 않는다. 순위 배열만 sendResponse를 통해 content script로 돌아오고, 화면에 표시된 뒤에는 메모리에서 사라진다.


자주 묻는 질문

Google Calendar 계정 로그인 정보나 OAuth 토큰이 필요한가?

필요하지 않다. content script는 이미 로그인된 브라우저 탭의 DOM을 읽을 뿐이다. Google API에 요청을 보내지 않으므로 OAuth 인증 흐름 자체가 없다. 확장이 계정 자격 증명에 접근하는 경로가 존재하지 않는다.

IndexedDB에 저장된 데이터는 언제 삭제되는가?

사용자가 확장을 제거하면 브라우저가 해당 확장의 IndexedDB 저장소를 함께 삭제한다. 확장 설정 화면에서 수동 초기화 옵션을 제공할 수도 있다. 어떤 경우에도 데이터가 외부 서버로 전송되지 않으므로, 삭제는 로컬에서 완결된다.

service worker가 비활성화(suspended)된 상태에서 메시지를 보내면 어떻게 되는가?

Chrome의 Manifest V3 service worker는 일정 시간 이후 자동으로 비활성화된다. chrome.runtime.sendMessage를 호출하면 Chrome이 service worker를 다시 깨운다. 다만, 이 재활성화에 수십 밀리초의 지연이 있을 수 있으므로, content script 측에서 타임아웃 처리를 두는 것이 안전하다.

Delta Attention 연산에 외부 모델 파일이나 wasm 바이너리가 필요한가?

필요하지 않다. 수식은 순수 자바스크립트로 구현된다. 행렬 연산 라이브러리도 없다. 이벤트 수가 수백 개 이하인 일반적인 사용 환경에서는 네이티브 배열 연산으로 충분하다.

이 구조가 Google Calendar 이외의 캘린더 서비스에도 적용되는가?

content script의 DOM 파싱 부분은 Google Calendar의 HTML 구조에 의존한다. 다른 캘린더 서비스에 적용하려면 DOM 탐색 로직을 해당 서비스 구조에 맞게 재작성해야 한다. service worker, IndexedDB, Delta Attention 연산 부분은 입력 포맷만 맞추면 그대로 재사용할 수 있다.


OAuth 없이도 캘린더 데이터를 분석할 수 있다. 필요한 것은 DOM을 읽는 content script, 메시지를 주고받는 service worker, 그리고 메타데이터를 로컬에 보관하는 IndexedDB다. 외부 서버도, LLM도, 인증 플로도 없다. WorkQueue가 이 구조로 동작한다. 소스 구조를 직접 확인하거나 확장을 설치해보고 싶다면 GitHub 리포지토리와 Chrome 웹스토어 개발자 가이드를 참고하면 된다.


더 보기: imjangnote.com

Top comments (0)