-
visibilitychange의 사용 (화면을 닫았을 때만 이벤트 동작)Front-end/Next.js 2025. 4. 28. 09:08반응형
새 탭을 닫는 경우 이벤트가 동작해야 하는 경우가 필요했다.
단순히 화면을 닫을 때를 상정하면 많은 정보가 있지만 이런 경우 문제는 새로고침만 해도 이벤트가 적용이 된다는 문제가 있다.
브라우저에서 페이지를 닫는 이벤트와 새로고침 이벤트는 기술적으로 매우 유사하다.
두 경우 모두 beforeunload 이벤트가 발생하기 때문에 두 상황을 구분 짓는 것은 쉽지 않다.
내가 원하는 것은 "화면을 닫았을 때 이벤트 동작" + "새로고침 시에는 이벤트 동작하지 않음" 이다.
웹 브라우저에서 창 닫기와 새로고침 구분하기
웹 애플리케이션에서 사용자의 다양한 행동을 내가 원하는 스코프 만큼만 정확하게 감지하는 것은 쉽지 않다.
특히 브라우저 창을 닫는 행동과 페이지를 새로고침하는 행동은 둘 다 unload를 발생시키지만 새로고침은 즉시 페이지가 다시 로드되고, 창 닫기는 완전한 세션의 종료로 이어진다.
그렇다면 창 닫기와 페이지 새로고침을 구분하기 위해서는 어떤 방법을 사용할 수 있을까?
visibilitychange라는 이벤트가 있다.이 이벤트는 사용자가 페이지를 보고 있는지, 다른 탭으로 이동 했는지 또는 창을 최소화 했는지 등의 상태 변화를 감지할 수 있다.
visiblitychange는 어떤 상황에서 발생할까?
1. 사용자가 다른 탭으로 이동할 때
2. 브라우저 창을 최소화 할 때
3. 브라우저 창(혹은 탭)을 닫을 때
4. 페이지를 새로고침 할 때
3번과 4번이 함께 있으므로 visiblitychange 하나만을 이용해서 만들 수는 없다.
그래서 하나더 필요한 것이 document.visiblityState 속성이다.
이를 이용하면 페이지가 visible인지, hidden인지 알 수 있다.거기에 더해 setTimeout을 이용하면 된다.
그래서 내가 만든 훅은 아래와 같다.
import { useEffect } from 'react'; import { getLocalStorage, removeLocalStorage } from '@/lib/storage'; import { usePathname, useRouter } from 'next/navigation'; /** * 페이지를 떠날 때(탭/창 닫기) localStorage의 특정 항목을 제거하는 커스텀 훅 * 새로고침 시에는 데이터가 유지됩니다. */ export function useBeforeUnload(paths?: string[]) { const router = useRouter(); const pathname = usePathname(); useEffect(() => { const currentPath = pathname.split('/')[1]; const isLoggedIn = getLocalStorage('admin-session'); const hasSelectedUser = getLocalStorage('selected-user'); // 로그인되지 않은 경우, 선택된 사용자 정보 삭제 후 대시보드로 리다이렉트 if (!isLoggedIn) { removeLocalStorage('selected-user'); router.replace('/dashboard'); return; } // 선택된 사용자가 없거나 현재 경로가 지정된 경로에 포함되지 않으면 아무 작업도 하지 않음 if (!hasSelectedUser || !paths?.includes(currentPath)) { return; } // 페이지 가시성 변화 감지 플래그 let isClosing = false; // 페이지 가시성 변화 핸들러 const handleVisibilityChange = () => { // 페이지가 숨겨질 때 (탭/창 닫기, 다른 탭으로 전환 등) if (document.visibilityState === 'hidden') { isClosing = true; // 일정 시간 후에도 여전히 숨겨져 있다면 선택된 사용자 정보 삭제 // 이 지연 시간은 새로고침과 탭/창 닫기를 구분하는 데 도움이 됨 setTimeout(() => { if (isClosing && document.visibilityState === 'hidden') { removeLocalStorage('selected-user'); } }, 1000); } else { // 페이지가 다시 보이게 되면 (새로고침, 다른 탭에서 돌아옴) 플래그 초기화 isClosing = false; } }; // 이벤트 리스너 등록 document.addEventListener('visibilitychange', handleVisibilityChange); // 컴포넌트 언마운트 시 이벤트 리스너 제거 return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [paths, pathname, router]); }
위 코드의 동작 원리는 우선 첫 번째로 가시성 이벤트를 감지한다. 즉, visiblitychange 이벤트를 사용해 페이작 숨겨지거나 표시되는 것을 감지한다. 페이지가 숨겨지면 isClosing 플래그를 설정하고 타이머를 시작한다.
새로고침의 경우 페이지가 숨겨진 후 거의 즉시 다시 표시되므로 타이머가 실행되기 전에isClosing 플래그가 재설정 되도록 했고, 창(혹은 탭)을 닫는 경우에는 페이지가 계속 숨겨진 상태로 남아있어서 타이머가 실행되고 원하는 로직일 실행되도록 하는 것이다.
사실 setTimeout을 이용해서 설정하는 것을 좋아하지 않는다.
사용자 환경에 따라서, 네트워크 환경에 따라서 타이밍 이슈가 발생할 수 있기 때문이다.
그러나 이번에는 이렇게 사용한 이유가 회사 내부에서 사용하는 웹 어플리케이션이고 사용자가 10명도 안 되는 환경이므로 우선은 이렇게 가도 괜찮다고 판단하고 만든 것이다.
반응형'Front-end > Next.js' 카테고리의 다른 글
NextJS 15에서 MDX 파일 렌더링 (3) 2024.12.18 프론트엔드 개발자의 화면 렌더링의 변화와 의미 (1) 2024.11.01 [NextJS] Props must be serializable for components in "use client" file 문제 해결 (0) 2024.07.03