Chrome Alarms API로 자동 잠금 타이머 구현하기
Service Worker가 종료되면 setTimeout도 사라집니다. chrome.alarms API로 Service Worker 수명과 독립적인 자동 잠금 타이머를 구현했습니다.
1. 문제 상황
요구사항: 비활성 시 자동 잠금
PIN 보호 기능이 있는 크롬 확장을 개발 중입니다. 보안을 위해 일정 시간 비활성 시 자동으로 잠금되어야 합니다.
[사용자가 PIN 입력하여 잠금 해제]
|
| 15분 비활성
v
[자동으로 다시 잠금됨]
첫 번째 시도: setTimeout
가장 직관적인 방법으로 setTimeout을 사용했습니다:
// background.js (Service Worker)
let autoLockTimer = null;
function startAutoLockTimer(minutes) {
clearTimeout(autoLockTimer);
autoLockTimer = setTimeout(() => {
lockPin();
console.log('Auto-locked due to inactivity');
}, minutes * 60 * 1000);
}
// PIN 해제 시
async function handleUnlockPin(pin) {
if (await verifyPin(pin)) {
setPinUnlocked(true);
startAutoLockTimer(15); // 15분 후 자동 잠금
return { ok: true };
}
return { ok: false };
}
문제: Service Worker가 종료됨
Manifest V3에서 Background는 Service Worker로 동작합니다. Service Worker는 비활성 상태가 되면 30초~5분 후 종료됩니다.
[PIN 잠금 해제]
|
setTimeout(15분) 설정
|
(30초 후)
|
Service Worker 종료됨! 💥
|
setTimeout 콜백 사라짐
|
15분 후에도 잠금 안 됨 😱
결과:
- setTimeout이 실행되기 전에 Service Worker가 종료
- 타이머가 사라지고 자동 잠금이 동작하지 않음
Manifest V2 vs V3 차이
| 항목 | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | 지속적 페이지 | Service Worker |
| 수명 | 항상 활성 | 비활성 시 종료 |
| setTimeout | 안정적 | 신뢰할 수 없음 |
| 해결책 | - | chrome.alarms |
2. 해결 방법: chrome.alarms API
핵심 아이디어
chrome.alarms API는 Service Worker 수명과 독립적으로 동작합니다. 설정된 시간이 되면 Chrome이 Service Worker를 깨워서 알람 이벤트를 전달합니다.
[알람 설정]
|
chrome.alarms.create('myAlarm', { delayInMinutes: 15 })
|
(Service Worker 종료됨)
|
(15분 후)
|
Chrome이 Service Worker를 다시 시작
|
chrome.alarms.onAlarm 이벤트 발생 ✓
권한 추가
// manifest.json
{
"manifest_version": 3,
"permissions": [
"alarms" // ← 추가
]
}
기본 API
// 알람 생성
chrome.alarms.create('alarmName', {
delayInMinutes: 15, // 15분 후 1회 실행
// 또는
periodInMinutes: 60, // 60분마다 반복
// 또는
when: Date.now() + 900000 // 특정 시각에 실행 (Unix timestamp ms)
});
// 알람 삭제
chrome.alarms.clear('alarmName');
// 모든 알람 삭제
chrome.alarms.clearAll();
// 알람 조회
chrome.alarms.get('alarmName', (alarm) => {
console.log(alarm); // { name, scheduledTime, periodInMinutes }
});
// 모든 알람 조회
chrome.alarms.getAll((alarms) => {
console.log(alarms);
});
// 알람 이벤트 리스너
chrome.alarms.onAlarm.addListener((alarm) => {
console.log('Alarm fired:', alarm.name);
});
3. 자동 잠금 구현
3.1 상수 및 설정 정의
// background.js
const AUTO_LOCK_ALARM_NAME = 'pinAutoLock';
const AUTO_LOCK_TIMEOUT_KEY = 'autoLockTimeout';
const DEFAULT_AUTO_LOCK_MINUTES = 15;
// 자동 잠금 설정 조회
async function getAutoLockSettings() {
const data = await chrome.storage.local.get(AUTO_LOCK_TIMEOUT_KEY);
return data[AUTO_LOCK_TIMEOUT_KEY] ?? {
enabled: true,
minutes: DEFAULT_AUTO_LOCK_MINUTES
};
}
// 자동 잠금 설정 저장
async function setAutoLockSettings(settings) {
await chrome.storage.local.set({ [AUTO_LOCK_TIMEOUT_KEY]: settings });
}
3.2 알람 타이머 관리
// 자동 잠금 타이머 시작/재설정
async function resetAutoLockTimer() {
// 1. 기존 알람 삭제
await chrome.alarms.clear(AUTO_LOCK_ALARM_NAME);
// 2. PIN이 활성화되어 있는지 확인
const settings = await getSettings();
if (!settings.pinEnabled) return;
// 3. PIN이 해제된 상태인지 확인
const unlocked = await isPinUnlocked();
if (!unlocked) return;
// 4. 자동 잠금 설정 확인
const autoLock = await getAutoLockSettings();
if (!autoLock.enabled || autoLock.minutes <= 0) return;
// 5. 새 알람 생성
await chrome.alarms.create(AUTO_LOCK_ALARM_NAME, {
delayInMinutes: autoLock.minutes
});
console.debug(`[AutoLock] Timer set for ${autoLock.minutes} minutes`);
}
// 자동 잠금 타이머 취소
async function clearAutoLockTimer() {
await chrome.alarms.clear(AUTO_LOCK_ALARM_NAME);
console.debug('[AutoLock] Timer cleared');
}
3.3 알람 이벤트 핸들러
// 알람 이벤트 리스너 (Service Worker 시작 시 등록)
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === AUTO_LOCK_ALARM_NAME) {
const settings = await getSettings();
// PIN이 여전히 활성화되어 있으면 잠금
if (settings.pinEnabled) {
await setPinUnlocked(false);
console.debug('[AutoLock] Locked due to inactivity');
}
}
});
3.4 PIN 관련 함수에 통합
// PIN 잠금 해제 시
async function handleUnlockPin(pin) {
const isValid = await verifyPin(pin);
if (!isValid) {
return { ok: false, reason: 'invalid-pin' };
}
await setPinUnlocked(true);
await resetAutoLockTimer(); // ← 타이머 시작
return { ok: true };
}
// PIN 수동 잠금 시
async function handleLockPin() {
await setPinUnlocked(false);
await clearAutoLockTimer(); // ← 타이머 취소
return { ok: true };
}
// PIN 비활성화 시
async function handleDisablePin() {
await setPinUnlocked(false);
await clearAutoLockTimer(); // ← 타이머 취소
await chrome.storage.local.remove('pinMeta');
return { ok: true };
}
3.5 활동 감지 시 타이머 리셋
사용자가 확장을 사용할 때마다 타이머를 리셋하면 더 좋은 UX를 제공할 수 있습니다:
// Debounced 활동 감지 (빈번한 리셋 방지)
let activityResetTimer = null;
const ACTIVITY_DEBOUNCE_MS = 5000; // 5초 디바운스
async function onUserActivity() {
const settings = await getSettings();
if (!settings.pinEnabled) return;
const unlocked = await isPinUnlocked();
if (!unlocked) return;
// 디바운스: 기존 타이머 취소하고 새로 설정
if (activityResetTimer) {
clearTimeout(activityResetTimer);
}
activityResetTimer = setTimeout(async () => {
await resetAutoLockTimer();
console.debug('[AutoLock] Timer reset due to user activity');
}, ACTIVITY_DEBOUNCE_MS);
}
// 메시지 핸들러에서 활동 감지
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
(async () => {
const activityMessages = [
'getState', 'listBookmarks', 'addBookmark', 'removeBookmark',
'listRecent', 'removeFromRecent', 'getSettings'
];
if (activityMessages.includes(msg.type)) {
onUserActivity();
}
// 실제 메시지 처리...
})();
return true;
});
// 시크릿 모드 브라우징 활동 감지
chrome.tabs.onCreated.addListener((tab) => {
if (tab.incognito) onUserActivity();
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab.incognito && changeInfo.status === 'complete') {
onUserActivity();
}
});
chrome.tabs.onActivated.addListener(async (activeInfo) => {
const tab = await chrome.tabs.get(activeInfo.tabId);
if (tab?.incognito) onUserActivity();
});
chrome.windows.onFocusChanged.addListener(async (windowId) => {
if (windowId === chrome.windows.WINDOW_ID_NONE) return;
const window = await chrome.windows.get(windowId);
if (window?.incognito) onUserActivity();
});
4. 설정 UI 구현
4.1 Options 페이지 HTML
<!-- options.html -->
<div class="setting-row">
<div class="setting-info">
<span class="setting-title" data-i18n="settingAutoLockTitle">Auto-Lock</span>
<span class="setting-desc" data-i18n="settingAutoLockDesc">
Automatically lock after inactivity
</span>
</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="autoLockToggle">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
</label>
<select id="autoLockMinutes" class="select-sm">
<option value="5">5 min</option>
<option value="10">10 min</option>
<option value="15" selected>15 min</option>
<option value="30">30 min</option>
<option value="60">60 min</option>
</select>
</div>
</div>
4.2 Options 페이지 JavaScript
// options.js
const els = {
autoLockToggle: document.getElementById('autoLockToggle'),
autoLockMinutes: document.getElementById('autoLockMinutes'),
};
// 초기 설정 로드
async function loadAutoLockSettings() {
const res = await sendMessage({ type: 'getAutoLockSettings' });
const settings = res.settings || { enabled: true, minutes: 15 };
els.autoLockToggle.checked = settings.enabled;
els.autoLockMinutes.value = settings.minutes;
els.autoLockMinutes.disabled = !settings.enabled;
}
// 토글 변경 핸들러
els.autoLockToggle.addEventListener('change', async () => {
const enabled = els.autoLockToggle.checked;
const minutes = parseInt(els.autoLockMinutes.value, 10);
await sendMessage({
type: 'setAutoLockSettings',
enabled,
minutes
});
els.autoLockMinutes.disabled = !enabled;
showToast(i18n.t('toastSaved'));
});
// 시간 변경 핸들러
els.autoLockMinutes.addEventListener('change', async () => {
const enabled = els.autoLockToggle.checked;
const minutes = parseInt(els.autoLockMinutes.value, 10);
await sendMessage({
type: 'setAutoLockSettings',
enabled,
minutes
});
showToast(i18n.t('toastSaved'));
});
4.3 Background 메시지 핸들러
// background.js - 메시지 핸들러에 추가
if (msg.type === 'getAutoLockSettings') {
const autoLock = await getAutoLockSettings();
sendResponse({ settings: autoLock });
}
else if (msg.type === 'setAutoLockSettings') {
const enabled = msg.enabled !== undefined ? !!msg.enabled : true;
const minutes = Math.max(1, Math.min(60, Number(msg.minutes) || DEFAULT_AUTO_LOCK_MINUTES));
await setAutoLockSettings({ enabled, minutes });
// 설정 변경 후 타이머 재설정
if (enabled && await isPinUnlocked()) {
await resetAutoLockTimer();
} else {
await clearAutoLockTimer();
}
sendResponse({ ok: true });
}
5. 고급 패턴
5.1 반복 알람
// 매 시간마다 실행되는 알람
chrome.alarms.create('hourlyCheck', {
periodInMinutes: 60
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'hourlyCheck') {
performHourlyTask();
}
});
5.2 특정 시각에 알람
// 다음 날 오전 9시에 알람
function scheduleAt9AM() {
const now = new Date();
const target = new Date();
target.setHours(9, 0, 0, 0);
// 이미 9시가 지났으면 내일로
if (target <= now) {
target.setDate(target.getDate() + 1);
}
chrome.alarms.create('dailyReminder', {
when: target.getTime()
});
}
5.3 알람 상태 확인
// 현재 활성화된 알람 확인
async function getActiveAlarms() {
return new Promise((resolve) => {
chrome.alarms.getAll((alarms) => {
resolve(alarms);
});
});
}
// 특정 알람이 설정되어 있는지 확인
async function isAlarmSet(name) {
return new Promise((resolve) => {
chrome.alarms.get(name, (alarm) => {
resolve(!!alarm);
});
});
}
// 사용
const alarms = await getActiveAlarms();
console.log('Active alarms:', alarms);
if (await isAlarmSet('pinAutoLock')) {
console.log('Auto-lock is scheduled');
}
5.4 알람 남은 시간 계산
async function getRemainingTime(alarmName) {
return new Promise((resolve) => {
chrome.alarms.get(alarmName, (alarm) => {
if (!alarm) {
resolve(null);
return;
}
const remaining = alarm.scheduledTime - Date.now();
resolve({
totalMs: remaining,
minutes: Math.ceil(remaining / 60000),
seconds: Math.ceil(remaining / 1000)
});
});
});
}
// 사용
const remaining = await getRemainingTime('pinAutoLock');
if (remaining) {
console.log(`Auto-lock in ${remaining.minutes} minutes`);
}
6. Before/After 비교
Before: setTimeout (불안정)
// ❌ Service Worker 종료 시 타이머 사라짐
let timer = null;
function startAutoLock(minutes) {
clearTimeout(timer);
timer = setTimeout(() => {
lockPin();
}, minutes * 60 * 1000);
}
function handleUnlock() {
setPinUnlocked(true);
startAutoLock(15); // Service Worker 종료되면 동작 안 함
}
After: chrome.alarms (안정)
// ✅ Service Worker 수명과 독립적
const ALARM_NAME = 'pinAutoLock';
async function startAutoLock(minutes) {
await chrome.alarms.clear(ALARM_NAME);
await chrome.alarms.create(ALARM_NAME, {
delayInMinutes: minutes
});
}
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === ALARM_NAME) {
await lockPin(); // Service Worker가 종료되어도 Chrome이 깨워서 실행
}
});
async function handleUnlock() {
await setPinUnlocked(true);
await startAutoLock(15); // 항상 안정적으로 동작
}
7. 주의사항
7.1 최소 시간 제한
// chrome.alarms의 최소 지연 시간은 약 1분
// 1분 미만으로 설정하면 1분으로 자동 조정됨
chrome.alarms.create('test', {
delayInMinutes: 0.1 // 6초 → 실제로는 ~1분 후 실행
});
// 개발 모드에서는 더 짧은 시간 가능 (약 30초)
7.2 알람 이름 유일성
// 같은 이름의 알람은 덮어씀
chrome.alarms.create('myAlarm', { delayInMinutes: 5 });
chrome.alarms.create('myAlarm', { delayInMinutes: 10 });
// → 10분 후 1회만 실행
// 여러 알람이 필요하면 다른 이름 사용
chrome.alarms.create('alarm1', { delayInMinutes: 5 });
chrome.alarms.create('alarm2', { delayInMinutes: 10 });
7.3 Service Worker 시작 시 리스너 등록
// ⚠️ 중요: 알람 리스너는 Service Worker 시작 시 즉시 등록해야 함
// 조건문 안에 넣으면 알람이 발생해도 처리되지 않을 수 있음
// ❌ 잘못된 예
async function init() {
const settings = await getSettings();
if (settings.pinEnabled) {
chrome.alarms.onAlarm.addListener(handleAlarm); // 조건부 등록
}
}
// ✅ 올바른 예
// 파일 최상위 레벨에서 무조건 등록
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'pinAutoLock') {
const settings = await getSettings();
if (settings.pinEnabled) { // 핸들러 내부에서 조건 체크
await lockPin();
}
}
});
7.4 브라우저 재시작 후 알람 복원
// 알람은 브라우저 종료 시 사라질 수 있음
// Service Worker 시작 시 필요한 알람이 있는지 확인하고 재생성
chrome.runtime.onStartup.addListener(async () => {
const unlocked = await isPinUnlocked();
const settings = await getSettings();
if (unlocked && settings.pinEnabled) {
const alarm = await new Promise(r => chrome.alarms.get('pinAutoLock', r));
if (!alarm) {
// 알람이 없으면 재생성
await resetAutoLockTimer();
}
}
});
8. 완전한 구현 코드
// background.js - 자동 잠금 관련 전체 코드
const AUTO_LOCK_ALARM_NAME = 'pinAutoLock';
const AUTO_LOCK_TIMEOUT_KEY = 'autoLockTimeout';
const DEFAULT_AUTO_LOCK_MINUTES = 15;
// ============================================
// 설정 관리
// ============================================
async function getAutoLockSettings() {
const data = await chrome.storage.local.get(AUTO_LOCK_TIMEOUT_KEY);
return data[AUTO_LOCK_TIMEOUT_KEY] ?? {
enabled: true,
minutes: DEFAULT_AUTO_LOCK_MINUTES
};
}
async function setAutoLockSettings(settings) {
await chrome.storage.local.set({ [AUTO_LOCK_TIMEOUT_KEY]: settings });
}
// ============================================
// 타이머 관리
// ============================================
async function resetAutoLockTimer() {
await chrome.alarms.clear(AUTO_LOCK_ALARM_NAME);
const settings = await getSettings();
if (!settings.pinEnabled) return;
const unlocked = await isPinUnlocked();
if (!unlocked) return;
const autoLock = await getAutoLockSettings();
if (!autoLock.enabled || autoLock.minutes <= 0) return;
await chrome.alarms.create(AUTO_LOCK_ALARM_NAME, {
delayInMinutes: autoLock.minutes
});
console.debug(`[AutoLock] Timer set: ${autoLock.minutes} min`);
}
async function clearAutoLockTimer() {
await chrome.alarms.clear(AUTO_LOCK_ALARM_NAME);
console.debug('[AutoLock] Timer cleared');
}
// ============================================
// 알람 이벤트 핸들러 (최상위 레벨에서 등록)
// ============================================
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === AUTO_LOCK_ALARM_NAME) {
const settings = await getSettings();
if (settings.pinEnabled) {
await setPinUnlocked(false);
console.debug('[AutoLock] Locked due to inactivity');
}
}
});
// ============================================
// 브라우저 시작 시 알람 복원
// ============================================
chrome.runtime.onStartup.addListener(async () => {
const unlocked = await isPinUnlocked();
if (unlocked) {
await resetAutoLockTimer();
}
});
// ============================================
// 메시지 핸들러 (일부)
// ============================================
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
(async () => {
if (msg.type === 'unlockPin') {
const result = await handleUnlockPin(msg.pin);
if (result.ok) {
await resetAutoLockTimer(); // ← 잠금 해제 시 타이머 시작
}
sendResponse(result);
}
else if (msg.type === 'lockPin') {
await setPinUnlocked(false);
await clearAutoLockTimer(); // ← 잠금 시 타이머 취소
sendResponse({ ok: true });
}
else if (msg.type === 'getAutoLockSettings') {
const settings = await getAutoLockSettings();
sendResponse({ settings });
}
else if (msg.type === 'setAutoLockSettings') {
const enabled = !!msg.enabled;
const minutes = Math.max(1, Math.min(60, Number(msg.minutes) || 15));
await setAutoLockSettings({ enabled, minutes });
if (enabled && await isPinUnlocked()) {
await resetAutoLockTimer();
} else {
await clearAutoLockTimer();
}
sendResponse({ ok: true });
}
})();
return true;
});
9. 베스트 프랙티스
DO (해야 할 것)
- [x] manifest.json에
alarms권한 추가 - [x] 알람 리스너는 Service Worker 최상위에서 등록
- [x] 알람 이름은 상수로 관리
- [x] 알람 생성 전 기존 알람 삭제
- [x] 브라우저 재시작 시 알람 복원 로직
DON'T (하지 말아야 할 것)
- [ ] Service Worker에서 setTimeout으로 장시간 타이머
- [ ] 알람 리스너를 조건부로 등록
- [ ] 1분 미만의 정밀한 타이밍 기대
- [ ] 알람 이름 하드코딩 (상수 사용 권장)
10. FAQ
Q: setTimeout 대신 항상 chrome.alarms를 사용해야 하나요?
A: 아닙니다. 짧은 시간(수 초)의 타이머는 setTimeout이 더 정확합니다. chrome.alarms는 최소 ~1분의 제한이 있고, 정확한 시간을 보장하지 않습니다. Service Worker 종료 이후에도 동작해야 하는 장시간 타이머에만 chrome.alarms를 사용하세요.
Q: 알람이 정확히 설정된 시간에 발생하나요?
A: 아닙니다. Chrome은 배터리와 성능을 위해 알람을 약간 지연시킬 수 있습니다. 정확한 시간이 중요한 경우에는 적합하지 않습니다.
Q: 여러 알람을 사용할 때 성능 문제가 있나요?
A: 일반적인 사용에서는 문제없습니다. 하지만 수백 개의 알람을 생성하면 성능에 영향을 줄 수 있습니다. 필요 없는 알람은 정리하세요.
Q: content script에서 chrome.alarms를 사용할 수 있나요?
A: 아닙니다. chrome.alarms는 background (Service Worker)에서만 사용 가능합니다. content script에서 타이머가 필요하면 background에 메시지를 보내서 처리하세요.
11. 참고 자료
시리즈 목차
- JavaScript URL 비교와 정규화
- Web Crypto API로 안전한 해싱 구현하기
- CSS 변수와 다크 모드 구현하기
- 크롬 확장 프로젝트 구조 정리하기
- 크롬 확장 다국어(i18n) 구현하기
- 크롬 확장 공유 모듈 설계
- Chrome Storage로 실시간 상태 동기화
- Chrome Alarms API로 자동 잠금 타이머 ← 현재 글