구글 캘린더 자동 집중 시간 차단, 일정 기반 크롬 확장 개발
출처: 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 웹 스토어에서 설치하세요 →