구글 캘린더 자동 집중 시간 차단, 일정 기반 크롬 확장 개발

발행: (2026년 6월 17일 AM 03:29 GMT+9)
7 분 소요
원문: Dev.to

출처: Dev.to

Google 캘린더용 대부분의 집중 시간 도구는 OAuth 인증이 필요하고 캘린더 데이터를 서버에 동기화합니다. Clockwise는 무료 티어를 종료했습니다. Reclaim은 비쌉니다. 저는 더 간단한 것을 원했습니다: 로컬에 조용한 시간을 정의하고 확장 프로그램이 캘린더에 해당 시간을 시각적으로 차단하도록 했어요 — 클라우드도, 계정도, 브라우저 데이터도 나가지 않죠.

이게 Quiethours입니다. 작동 방식은 다음과 같습니다.

Quiethours는 Google Calendar API를 호출하지 않습니다. calendar.google.com에 콘텐츠 스크립트를 주입하고 캘린더 그리드 위에 색상 오버레이 블록을 직접 그립니다.

각 집중 규칙은 다음과 같이 저장됩니다:

interface FocusRule {
  id: string;
  dayOfWeek: DayOfWeek[];   // [1,2,3,4,5] = Mon–Fri
  startTime: string;         // 'HH:MM' 24h
  endTime: string;
  label: string;
  color: string;             // hex overlay color
  enabled: boolean;
}

규칙은 chrome.storage.local에 저장됩니다. 서버도, OAuth도, 캘린더 권한도 없습니다.

Google 캘린더는 일일 및 주간 뷰에 시간 슬롯 그리드를 렌더링합니다. 콘텐츠 스크립트는 그리드 컨테이너를 쿼리하고 올바른 시간 범위에 맞게 오버레이 요소를 삽입합니다.

올바른 컨테이너를 찾는 것이 가장 까다로운 부분입니다. Google 캘린더는 일일, 주간, 월간, 스케줄 등 다양한 뷰에서 서로 다른 컨테이너를 가지고 있어, setInterval을 사용해 컨테이너가 나타날 때까지 폴링합니다 — Google 캘린더는 페이지 이동 후 비동기적으로 DOM을 수화합니다:

const waitForCalendar = setInterval(() => {
  const root = document.querySelector('[data-view-toggle]') ??
               document.querySelector('div[role="main"]');
  if (root) {
    clearInterval(waitForCalendar);
    observer.observe(root, { childList: true, subtree: false });
    injectOverlays();
  }
}, 500);

subtree: false가 여기서 성능에 중요합니다 — 캘린더 루트의 직계 자식만 감시하면 그리드 내 이벤트 카드 렌더링마다 매번 트리거가 발생하지 않아요.

Google 캘린더는 SPA(단일 페이지 애플리케이션)입니다. 주간 뷰에서 일일 뷰로 이동하면 페이지가 재로드되지 않고 DOM이 교체됩니다. MutationObserver는 이러한 변화를 감지하고 오버레이를 다시 삽입합니다:

const observer = new MutationObserver(() => {
  removeOverlays();
  injectOverlays();
});

removeOverlays()는 재삽입 전 확장 프로그램의 데이터 속성이 붙은 모든 요소를 삭제합니다. 캘린더가 기존 시간 슬롯을 다시 렌더링할 때 중복 오버레이가 생기는 것을 방지합니다.

chrome.alarms을 이용한 주기적 규칙 확인

백그라운드 서비스 워커는 집중 블록이 현재 활성화되어 있는지 확인하기 위해 1분 간격으로 반복 알람을 실행합니다:

chrome.alarms.create(ALARM_NAME, { periodInMinutes: 1 });

chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name !== ALARM_NAME) return; const [rules, settings] = await Promise.all([getRules(), getSettings()]); const active = getActiveRule(rules);

if (active && _prevActiveRuleId !== active.id) { // Focus block just started _prevActiveRuleId = active.id; if (settings.notificationsEnabled) { chrome.notifications.create({ type: ‘basic’, iconUrl: chrome.runtime.getURL(‘icons/icon128.png’), title: 🌙 Focus time started, message: ${active.label} — ${active.startTime} to ${active.endTime}, }); } await incrementUsageCount(); // for review‑prompt threshold } else if (!active && _prevActiveRuleId) { // Focus block just ended _prevActiveRuleId = null; } });

_prevActiveRuleId는 서비스 워커 메모리 변수입니다. MV3 서비스 워커는 언제든지 종료되고 다시 시작될 수 있어 이 변수는 각 SW 시작 시 초기화됩니다.

실제로는 SW가 재시작된 후에도 이미 활성화된 집중 블록이 있을 경우 “시작되었습니다” 알림이 다시 표시될 수 있으며, 이는 1분 간격 알람 루프에 대한 허용 가능한 동작입니다.

가장 흔한 질문은 “왜 실제 캘린더 이벤트를 읽지 않나요?” 입니다.

오버레이 방식은 다음과 같습니다:

  • 스토리지와 알람 권한 외에 추가 권한이 필요하지 않습니다.
  • https://www.googleapis.com/auth/calendar 스코프가 필요하지 않습니다.
  • API 접근 없이도 모든 Google 캘린더 레이아웃과 호환됩니다.
  • 트레이드오프는 다음과 같습니다: 집중 블록 중에 실제 회의가 있으면 오버레이가 그 위에 표시됩니다. 이는 의도적인 것이며, 블록이 반투명하여 회의가 뒤에 여전히 보이도록 설계되었습니다.

MV3 서비스 워커는 공闲 상태에서 종료됩니다. chrome.alarms은 필요한 시점에 서비스가 다시 부팅될 수 있도록 신뢰할 만하는 API 중 하나입니다. 1분 간격은 이 용도에 충분합니다 — 집중 블록이 9:00에 시작되면 알람이 마지막으로Ticks한 시점에 따라 9:00 또는 9:01에 알림이 표시됩니다.

정확한 분 단위 정밀도가 필요하면 서비스가 살아있어야 하거나 콘텐츠 스크립트 폴링 방식을 사용해야 합니다. 집중 시간 알림은 ±1분 정도면 충분합니다.

Quiethours를 Chrome 웹 스토어에서 설치하세요 →

0 조회
Back to Blog

관련 글

더 보기 »