SLA 개념을 이메일함에 적용해 본 결과 — 크롬 확장 프로그램 제작에서 배운 점
출처: Dev.to
문제
저는 B2B SaaS 고객 지원팀에서 일했을 때, 모든 들어오는 이메일에 SLA 타이머가 붙어 있었습니다. 초록색은 아직 시간이 남았다는 뜻, 주황색은 마감이 가까워지고 있다는 뜻, 빨간색은 이미 고객이 불만을 품고 있다는 뜻이었죠. 이 시스템은 일 처리가 누락되는 일을 거의 완전히 차단할 정도로 가혹하게 효과적이었습니다.
직장을 옮기고 나니 그 SLA 도구들이 사라졌습니다. 이제는 평범한 메일함만 남았고, 긴급성을 나타내는 신호도 없으며, 어느 스레드가 가장 오래 기다리고 있는지 한눈에 파악할 방법도 없었습니다.
해결책: InboxSLA Chrome 확장 프로그램
InboxSLA는 Gmail에 응답 시간 마감 기한을 표시합니다.
-
이메일 도메인별로 클라이언트를 정의하고 각 클라이언트마다 SLA(시간)를 지정합니다.
-
확장 프로그램은 받은 편지함을 스캔하고 스레드 행에 색깔 배지를 삽입합니다:
- 🟢 초록색 – SLA 내에 있음(남은 시간이 표시됨)
- 🟠 주황색 – 마감이 다가옴(조정 가능한 임계값, 기본값 2 시간)
- 🔴 빨간색 “OVERDUE”(지연) – 시간이 초과됨
SLA 값은 클라이언트마다 완전히 독립적입니다. 프리랜서는 장기 계약에 대해 48 시간을, 빠른 대응이 필요한 지원을 제공하는 에이전시는 4 시간을 설정할 수 있습니다.
구현 세부 사항
DOM 변경 감지
Gmail은 라벨을 이동하거나 스레드를 열 때 페이지를 새로 고치지 않고 DOM을 제자리에서 업데이트합니다. 따라서 DOMContentLoaded는 최초 로드 시에만 발생합니다. 해결 방법은 리스트 변화를 감시하는 MutationObserver를 사용하는 것입니다:
const observer = new MutationObserver(() => {
scheduleRefresh(); // 300 ms 디바운스
});
observer.observe(document.body, { childList: true, subtree: true });
디바운스가 중요한 이유는 Gmail이 타이핑, 호버, 자동 저장 초안 등 동안 지속적으로 작은 DOM 변화를 일으키기 때문입니다. throttling 없이 하면 옵저버가 초당 수백 번 호출됩니다. 구현에서는 300 ms로 디바운스하고, 스레드‑리스트 컨테이너 해시가 변하지 않으면 재스캔을 건너뜁니다.
견고한 발신자 추출
Gmail의 CSS 클래스 이름은 자동 생성되며 A/B 테스트 버전에 따라 달라집니다. 단일 셀렉터에 의존하기보다 다중 전략 폴백을 사용합니다:
const senderEl =
row.querySelector('[email]') ??
row.querySelector('.yW span[email]') ??
row.querySelector('.bA4 span[email]') ??
row.querySelector('[data-hovercard-id]');
[email] 속성 셀렉터가 가장 안정적이며, Google이 여러 Gmail 빌드에서 내부적으로 사용합니다. 추가 폴백은 변형된 클래스 이름을 처리합니다.
타임스탬프 파싱
메시지 타임스탬프를 얻는 가장 신뢰할 수 있는 위치는 <span> 요소의 title 속성입니다:
const timeEl =
row.querySelector('.xW.xY span[title]') ??
row.querySelector('td.xW span[title]');
const title = timeEl?.getAttribute('title') ?? '';
const parsed = new Date(title).getTime();
Gmail은 이를 전체 날짜 문자열(예: "Mon, Apr 21, 2026, 9:34 AM") 형태로 제공합니다. 파싱에 실패하면 코드는 Date.now()를 폴백으로 사용해 스레드가 방금 도착한 것으로 보수적으로 가정합니다. 인박스 보기, 전체 메일, 검색 결과 등에서 6가지 서로 다른 문자열 포맷을 처리해야 했습니다.
삽입 실패 방지
다른 사람이 만든 SPA에 요소를 삽입할 때는 두 가지 실패 모드를 대비해야 합니다:
-
중복 배지 – Gmail이 리스트 항목을 다시 렌더링하면서 기존 배지를 완전히 제거하지 않을 수 있습니다. 각 배지는
data-inboxsla-badge속성으로 태그하고 삽입 전 존재 여부를 확인합니다:const BADGE_ATTR = 'data-inboxsla-badge'; let badge = row.querySelector(`[${BADGE_ATTR}]`) as HTMLElement | null; if (!badge) { badge = document.createElement('span'); badge.setAttribute(BADGE_ATTR, '1'); row.appendChild(badge); } // 존재 여부와 관계없이 인‑플레이스 업데이트 badge.style.background = bg; badge.textContent = text; -
오래된 상태 – 스레드를 열었다가 리스트로 돌아오면 경과 시간이 변합니다. 배지 상태는 매 옵저버 사이클마다 다시 계산되어 인‑플레이스로 업데이트되며, Gmail 자체 이벤트 리스너는 그대로 유지됩니다.
클라이언트 설정 저장
클라이언트 설정(도메인 및 SLA 시간)은 chrome.storage.local에 저장됩니다. 콘텐츠 스크립트는 로드 시 백그라운드 서비스 워커에 요청해 설정을 받아 로컬에 캐시합니다:
// Content script
const clients = await chrome.runtime.sendMessage({ type: 'GET_CLIENTS' });
// Background service worker
chrome.runtime.onMessage.addListener((msg, _sender, respond) => {
if (msg.type === 'GET_CLIENTS') {
chrome.storage.local.get('clients').then(({ clients }) => {
respond(clients ?? []);
});
return true; // 비동기 응답 신호
}
});
콘텐츠 스크립트는 5분 쿨다운 이후에만 다시 가져와, DOM 활동이 많은 상황에서 과도한 라운드 트립을 방지합니다.
향후 고려 사항
Gmail DOM이 가장 큰 운영 리스크입니다—Google이 업데이트를 배포하면 셀렉터가 깨질 수 있습니다. 지금까지 한 번 패치를 적용했으며, 장기적으로는 Gmail Add‑ons API가 스레드 감지에 더 안정적이지만, OAuth 스코프 승인과 서버‑사이드 컴포넌트가 필요해 MVP에는 과도한 부담이었습니다.
현재는 저장된 Gmail HTML 스냅샷에 대한 통합 테스트를 실행해, 각 릴리즈 전에 셀렉터 회귀를 잡아냅니다.
설치
클라이언트 이메일을 관리하면서 스레드가 의도보다 오래 머무른 적이 있다면, 이 확장 프로그램이 바로 당신을 위한 것입니다.