CloseWatcher는 사용자가 '닫기' 의도를 표현하는 여러 방식 — 데스크톱의 Esc 키, 안드로이드의 뒤로 가기 버튼 — 을 하나의 이벤트로 받아 처리하는 브라우저 API입니다. 커스텀으로 만든 사이드바·드롭다운·바텀시트를 이 두 동작으로 자연스럽게 닫을 수 있게 해줍니다.
모바일 웹에서 골치 아팠던 지점이 바로 이겁니다. 안드로이드에서 뒤로 가기를 누르면 내가 띄운 메뉴가 닫히는 게 아니라 페이지가 통째로 이전 화면으로 넘어가 버렸거든요. 사용자는 메뉴만 닫으려던 건데 말이죠.
왜 필요한가
네이티브 <dialog>나 Popover API를 쓰면 Esc·뒤로 가기 닫힘이 공짜로 딸려옵니다. 문제는 그걸 못 쓰는 커스텀 UI입니다. 디자인 요구 때문에 직접 만든 오버레이가 대표적이죠. 이런 요소에 표준 닫기 동작을 붙이려면 예전엔 keydown으로 Esc를 잡고, 뒤로 가기는 history.pushState로 가짜 히스토리를 끼워 넣는 꼼수를 써야 했습니다. 지저분하고 잘 깨졌어요.
기본 사용
const watcher = new CloseWatcher();
watcher.addEventListener('close', () => {
closeMyMenu(); // 실제로 UI 닫는 로직
});
// 코드로 닫을 때도 같은 경로 태우기
function onCloseButton() {
watcher.requestClose();
}
이게 핵심입니다. Esc를 누르든, 안드로이드 뒤로 가기를 누르든, 내가 requestClose()를 부르든 전부 하나의 close 이벤트로 모입니다. 닫는 로직을 한 곳에 두면 되죠.
취소도 가능하다
실제로 닫기 전에 확인 절차를 넣고 싶을 때가 있습니다. cancel 이벤트를 preventDefault()로 막으면 닫힘을 보류할 수 있습니다.
watcher.addEventListener('cancel', (e) => {
if (hasUnsavedChanges && !confirm('저장 안 된 내용이 있어요. 닫을까요?')) {
e.preventDefault(); // 닫힘 취소
}
});
다만 cancel 취소는 사용자 활성화(직전에 상호작용이 있었을 때)가 있어야 먹힙니다. 사용자가 아무 조작 없이 뒤로 가기만 연타하는 상황에서 앱이 인질처럼 붙잡는 걸 막기 위한 안전장치죠.
기존 방식과 비교
| 닫기 트리거 | 예전 방식 | CloseWatcher |
|---|---|---|
| Esc 키 | keydown 수동 처리 | 자동 |
| 안드로이드 뒤로 가기 | 가짜 history 조작 | 자동 |
| 중첩 UI 순서 닫기 | 직접 스택 관리 | watcher 스택 자동 |
중첩이 특히 편합니다. 모달 위에 드롭다운을 띄운 상태에서 뒤로 가기를 누르면, 가장 최근에 만든 watcher부터 하나씩 닫힙니다. LIFO로 알아서 처리돼요.
지원과 폴백
2026년 기준 크로미엄 계열에서 먼저 안착했고 다른 브라우저는 도입 중입니다. 그래서 점진적 향상으로 씁니다. 존재 여부를 확인하고, 없으면 예전처럼 keydown으로 Esc만이라도 처리하는 폴백을 두면 됩니다.
if ('CloseWatcher' in window) {
// 최신 경로
} else {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMyMenu();
});
}
자주 묻는 질문
dialog나 popover가 있는데 왜 필요한가요?
네이티브 <dialog>·Popover를 쓸 수 있으면 그게 최선이고, 닫기 동작도 이미 내장돼 있습니다. CloseWatcher는 그것들을 쓸 수 없는 완전 커스텀 UI에 동일한 닫기 경험을 주려는 API입니다. 도구 선택의 문제죠.
뒤로 가기를 가로채면 사용자가 갇히지 않나요?
그 남용을 막는 장치가 있습니다. 열려 있는 watcher 없이 사용자 활성화가 없으면 뒤로 가기를 가로채지 못하고, cancel로 닫힘을 막는 것도 최근 상호작용이 있어야 가능합니다. 사용자를 무한히 붙잡을 수 없도록 설계돼 있습니다.
다 쓴 watcher는 정리해야 하나요?
네. UI를 닫았으면 watcher.destroy()로 정리하는 게 좋습니다. 안 그러면 watcher가 스택에 남아 다음 Esc·뒤로 가기가 엉뚱한 대상에 소비될 수 있습니다. 열 때 생성하고 닫을 때 파기하는 짝을 지키세요.

댓글 0