React에서 Browser Notification 구현하기
date
Aug 17, 2025
slug
react-admin-notification-and-chat-cooldown
author
status
Public
tags
펑타이그레이터차이나
React
summary
관리자 페이지에 브라우저 알림을 붙이는 기본 훅부터, 채팅처럼 빈번한 이벤트에 쿨다운 패턴까지 단계적으로 구현하는 방법
type
Post
thumbnail
category
✍️ Study
updatedAt
Jan 22, 2026 03:32 PM
관리자 페이지에선 “신규 알림”, “에러 발생”, “고객 문의” 같은 이벤트를 빠르게 알려야 합니다. 이때 브라우저 Notification API를 사용하면 페이지 밖(혹은 탭 비활성화 상태)에서도 눈에 띄는 알림을 보낼 수 있습니다.
하지만 채팅처럼 이벤트가 빈번해지면 알림이 쏟아져 UX를 망칩니다. 그래서 두 단계로 나눠서 설명할려고 합니다.
- 기본형: 관리자 페이지에 알림 띄우기
- 고도화형: 첫 이벤트 즉시 알림 → 일정 시간 동안 추가 알림 무시(쿨다운) 패턴
1) 관리자 페이지: 기본 알림 붙이기
핵심 포인트
Notification지원 여부 확인
- 권한 요청
- 클릭 시 현재 탭 포커싱/닫기
재사용 훅
// useBrowserNotification.ts import { useCallback, useRef } from 'react'; type Options = NotificationOptions & {}; // 관리자 페이지에서 재사용할 수 있는 기본 훅 export function useBrowserNotification() { const lastRef = useRef<Notification | null>(null); const isSupported = typeof window !== 'undefined' && 'Notification' in window; const ensurePermission = useCallback(async () => { if (!isSupported) return false; if (Notification.permission === 'granted') return true; if (Notification.permission === 'denied') return false; // 최신 브라우저(Promise) 우선, 사파리 콜백 폴백 try { const result = await Notification.requestPermission(); return result === 'granted'; } catch { return await new Promise<boolean>(resolve => { // @ts-expect-error Safari callback signature Notification.requestPermission((status: NotificationPermission) => { resolve(status === 'granted'); }); }); } }, [isSupported]); const attachClickHandler = useCallback((n: Notification) => { n.onclick = evt => { evt.preventDefault?.(); window.focus(); n.close(); }; }, []); const fire = useCallback( async (title: string, options?: Options) => { const ok = await ensurePermission(); if (!ok) return; const n = new Notification(title, options); lastRef.current = n; attachClickHandler(n); }, [ensurePermission, attachClickHandler] ); const closeLast = useCallback(() => { lastRef.current?.close(); lastRef.current = null; }, []); return { supported: isSupported, ensurePermission, fire, closeLast, }; }
관리자 페이지에서 사용
// AdminDashboard.tsx import { useEffect } from 'react'; import { useBrowserNotification } from './useBrowserNotification'; export default function AdminDashboard() { const { supported, ensurePermission, fire } = useBrowserNotification(); useEffect(() => { // 페이지 진입 시 한 번 권한 안내 ensurePermission(); }, [ensurePermission]); const onNewIncident = (msg: string) => { if (!supported) { // 대체 UI (배지/토스트 등) // toast.info(msg); return; } fire('관리자 알림', { body: msg, // HTTPS 권장, 실제 경로로 교체 // icon: '/icons/notification.png', // badge: '/icons/badge.png', requireInteraction: false, // 필요 시 true로 고정 표시 }); }; // 예시 // onNewIncident('신규 신고가 접수되었습니다'); return null; }
활성 탭에 사용자가 이미 보고 있다면 알림을 생략하고 토스트로 대체하는 것도 UX에 좋습니다.
2) 고도화: 채팅 등 “빈번한 이벤트”에 쿨다운 적용
채팅처럼 메시지가 빠르게 들어오면 알림이 너무 많이 발생합니다.
해결책은 leading throttle 패턴:
- 첫 이벤트는 즉시 알림
- 이후 쿨다운 기간 동안 들어오는 추가 이벤트는 무시
- 쿨다운이 끝나면 다시 첫 이벤트 허용
쿨다운 알림 훅
// useBrowserNotificationCooldown.ts import { useCallback, useRef } from 'react'; type Options = NotificationOptions & {}; export function useBrowserNotificationCooldown() { const notificationRef = useRef<Notification | null>(null); const cooldownRef = useRef<ReturnType<typeof setTimeout> | null>(null); const isSupported = typeof window !== 'undefined' && 'Notification' in window; const ensurePermission = useCallback(async () => { if (!isSupported) return false; if (Notification.permission === 'granted') return true; if (Notification.permission === 'denied') return false; try { const result = await Notification.requestPermission(); return result === 'granted'; } catch { return await new Promise<boolean>(resolve => { // @ts-expect-error Safari callback signature Notification.requestPermission((status: NotificationPermission) => { resolve(status === 'granted'); }); }); } }, [isSupported]); const attachClick = useCallback((n: Notification) => { n.onclick = e => { e.preventDefault?.(); window.focus(); n.close(); }; }, []); /** * 첫 알림 즉시 → cooldownMs 동안 추가 알림 무시 * closeDurationMs: 알림 자동 닫기 시간(기본 쿨다운과 동기화 권장) */ const fireWithCooldown = useCallback( async ( title: string, cooldownMs: number, options?: Options, closeDurationMs: number = cooldownMs ) => { const ok = await ensurePermission(); if (!ok) return; // 쿨다운 중이면 무시 if (cooldownRef.current) return; // 첫 이벤트 즉시 발사 const n = new Notification(title, options); notificationRef.current = n; attachClick(n); // 알림 자동 종료(선택) const closeTimer = setTimeout(() => { n.close(); if (notificationRef.current === n) notificationRef.current = null; }, closeDurationMs); // 쿨다운 시작 cooldownRef.current = setTimeout(() => { cooldownRef.current = null; clearTimeout(closeTimer); }, cooldownMs); }, [ensurePermission, attachClick] ); return { supported: isSupported, fireWithCooldown, }; }
채팅 화면에서 사용
// ChatNotifier.tsx import { useEffect } from 'react'; import { useBrowserNotificationCooldown } from './useBrowserNotificationCooldown'; export function ChatNotifier({ lastMessage }: { lastMessage?: { sender: string; text: string } }) { const { supported, fireWithCooldown } = useBrowserNotificationCooldown(); useEffect(() => { if (!lastMessage || !supported) return; // 탭이 비활성화된 경우에만 알림 (선택) if (document.visibilityState !== 'visible') { fireWithCooldown('새 채팅 메시지', 5000, { body: `${lastMessage.sender}: ${lastMessage.text}`, // icon: '/icons/chat.png', }); } }, [lastMessage, supported, fireWithCooldown]); return null; }
쿨다운 길이 가이드
- 3~8초 구간에서 실험해 보세요.
- 너무 짧으면 사용자에게 불편함을 주고, 너무 길면 알람을 놓치는 듯한 경험을 줄 수 있습니다.
마치며
관리자 페이지처럼 알림이 적을때는 기본 알림 훅만으로도 충분히 유용합니다. 이벤트가 많이 발생하는 채팅/피드 영역은 “첫 알림 즉시 + 쿨다운” 패턴으로 UX를 지킬 수 있습니다. 훅으로 추상화해 두면 어디서든 재사용 가능하고, 정책(시간대, 합산 문구, 포커스 여부)에 맞게 쉽게 확장할 수 있습니다.