From 2ee6d3e63ebc9b4159899232b5921077b0beb5b9 Mon Sep 17 00:00:00 2001 From: foresec Date: Wed, 31 Jul 2024 18:12:08 +0900 Subject: [PATCH 01/18] =?UTF-8?q?=E2=9C=A8=20feat:=20useDetectInactivity?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/hooks/useDetectInactivity.ts diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts new file mode 100644 index 0000000..3acc49f --- /dev/null +++ b/src/hooks/useDetectInactivity.ts @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from 'react'; +import useTimer from './useTimer'; + +const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { + const [isInactive, setIsInactive] = useState(false); + const { start } = useTimer(() => setIsInactive(true), time); + + const clientEvent = ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; + + const resetTimer = useCallback(() => { + setIsInactive(false); + start(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + start(); + + clientEvent.forEach((event) => window.addEventListener(event, resetTimer)); + + if (isInactive) { + onInactivity(); + } + + return () => { + clientEvent.forEach((event) => + window.removeEventListener(event, resetTimer) + ); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isInactive) { + onInactivity(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInactive]); + + return isInactive; +}; + +export default useDetectInactivity; From 9c87c7cc6fd9ab4766b52915cb6d24490c326c98 Mon Sep 17 00:00:00 2001 From: foresec Date: Wed, 31 Jul 2024 18:31:30 +0900 Subject: [PATCH 02/18] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=84=B0=EC=B9=98?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=88=98=ED=96=89=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EB=A5=BC=20=EA=B0=90=EC=A7=80,=20=EB=8F=99=EB=A1=9D?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A5=BC=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 3acc49f..1f29b48 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -1,11 +1,15 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; +// 시간 바꾸고 + const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { const [isInactive, setIsInactive] = useState(false); const { start } = useTimer(() => setIsInactive(true), time); - const clientEvent = ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; + const clientEvents = isTouchDevice() + ? ['touchstart'] + : ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; const resetTimer = useCallback(() => { setIsInactive(false); @@ -16,14 +20,16 @@ const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { useEffect(() => { start(); - clientEvent.forEach((event) => window.addEventListener(event, resetTimer)); + clientEvents.forEach((event) => { + window.addEventListener(event, resetTimer); + }); if (isInactive) { onInactivity(); } return () => { - clientEvent.forEach((event) => + clientEvents.forEach((event) => window.removeEventListener(event, resetTimer) ); }; @@ -41,3 +47,10 @@ const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { }; export default useDetectInactivity; + +const isTouchDevice = () => { + if (typeof window === 'undefined') { + return false; + } + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +}; From 6c47cd11703653f504d9ee4c580a89fe8c6d7775 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 03:03:46 +0900 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20=EB=A7=A4?= =?UTF-8?q?=EA=B0=9C=EB=B3=80=EC=88=98=20default=EA=B0=92=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 1f29b48..1771d75 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -1,9 +1,8 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; +import { Fn } from '../types'; -// 시간 바꾸고 - -const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { +const useDetectInactivity = (time: number, onInactivity: Fn) => { const [isInactive, setIsInactive] = useState(false); const { start } = useTimer(() => setIsInactive(true), time); @@ -24,10 +23,6 @@ const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { window.addEventListener(event, resetTimer); }); - if (isInactive) { - onInactivity(); - } - return () => { clientEvents.forEach((event) => window.removeEventListener(event, resetTimer) @@ -40,8 +35,7 @@ const useDetectInactivity = (time = 10000, onInactivity = () => {}) => { if (isInactive) { onInactivity(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInactive]); + }, [isInactive, onInactivity]); return isInactive; }; From b15a968ad56a6be08bcb42666be4f3ef7eef742f Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 03:11:47 +0900 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=92=AC=20comment:=20useDetectInacti?= =?UTF-8?q?vity=20jsdocs=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 1771d75..f22bc2d 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -2,6 +2,19 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; import { Fn } from '../types'; +/** + * 일정 시간(ms) 동안 활동이 없을 때 지정된 콜백 함수를 실행하는 훅. + * + * @param {number} time - 비활성 상태로 간주되기까지의 시간(밀리초). 양의 정수로 지정. + * @param {Fn} onInactivity - 비활성 상태가 감지되었을 때 호출되는 콜백 함수. + * + * @returns {boolean} - 현재 비활동 상태 여부를 나타내는 boolean 값. + * + * @description + * 사용자가 정의한 시간(time) 동안 활동이 없으면 비활성 상태로 간주하고, 지정된 콜백 함수(onInactivity)를 호출합니다. + * 해당 환경에 맞게 설정된 이벤트를 리스닝하여, 활동이 감지될 시 타이머를 리셋합니다. + */ + const useDetectInactivity = (time: number, onInactivity: Fn) => { const [isInactive, setIsInactive] = useState(false); const { start } = useTimer(() => setIsInactive(true), time); From 093d0f12923e5d51632ca7cf4eafa065c0dff431 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 16:36:32 +0900 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=85=20test:=20useDetectInactivity?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.test.ts | 126 ++++++++++++++++++++++++++ src/hooks/useDetectInactivity.ts | 2 +- 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useDetectInactivity.test.ts diff --git a/src/hooks/useDetectInactivity.test.ts b/src/hooks/useDetectInactivity.test.ts new file mode 100644 index 0000000..2dde2dd --- /dev/null +++ b/src/hooks/useDetectInactivity.test.ts @@ -0,0 +1,126 @@ +import { act, renderHook } from '@testing-library/react'; +import useDetectInactivity, { isTouchDevice } from './useDetectInactivity'; +import useTimer from './useTimer'; + +jest.mock('./useTimer'); + +describe('useDetectInactivity', () => { + let startMock: jest.Mock; + let onInactivityMock: jest.Mock; + + beforeEach(() => { + jest.useFakeTimers(); + startMock = jest.fn(); + onInactivityMock = jest.fn(); + (useTimer as jest.Mock).mockImplementation((callback, time) => ({ + start: () => { + startMock(); + // 타이머 만료 후 콜백 호출 + setTimeout(() => { + callback(); + }, time); + }, + })); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test('타이머에 설정된 시간 후에는 비활동 상태가 감지된다.', () => { + const { result } = renderHook(() => + useDetectInactivity(5000, onInactivityMock) + ); + act(() => { + jest.advanceTimersByTime(5000); + }); + + expect(onInactivityMock).toHaveBeenCalled(); + expect(result.current).toBe(true); + }); + + test('비활동 상태일때 onInactivity콜백은 호출되지 않는다.', () => { + renderHook(() => useDetectInactivity(5000, onInactivityMock)); + + act(() => { + jest.advanceTimersByTime(4500); + }); + + expect(onInactivityMock).not.toHaveBeenCalled(); + }); + + test('활동(설정된 이벤트)이 감지되면 타이머는 리셋된 후 다시 실행된다.', () => { + const { result } = renderHook(() => + useDetectInactivity(5000, onInactivityMock) + ); + + act(() => { + jest.advanceTimersByTime(3000); + }); + + expect(startMock).toHaveBeenCalledTimes(1); + expect(result.current).toBe(false); + + act(() => { + window.dispatchEvent(new Event('keyup')); + }); + + expect(startMock).toHaveBeenCalledTimes(1); + expect(result.current).toBe(false); + + act(() => { + window.dispatchEvent(new Event('mousemove')); + }); + + expect(startMock).toHaveBeenCalledTimes(2); + expect(result.current).toBe(false); + + act(() => { + jest.advanceTimersByTime(5000); + }); + + expect(onInactivityMock).toHaveBeenCalled(); + expect(result.current).toBe(true); + }); + + test('환경에 맞게 이벤트 리스너가 추가/제거된다.', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + const { unmount } = renderHook(() => + useDetectInactivity(5000, onInactivityMock) + ); + + const expectedClientEvents = isTouchDevice() + ? ['touchstart'] + : ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; + + const addedEvents = addEventListenerSpy.mock.calls.map( + ([event, callback]) => ({ event, callback }) + ); + + expectedClientEvents.forEach((event) => { + expect(addedEvents.some((e) => e.event === event)).toBe(true); + }); + + act(() => { + unmount(); + }); + + const removedEvents = removeEventListenerSpy.mock.calls.map( + ([event, callback]) => ({ event, callback }) + ); + // 제거된 이벤트 리스트가 올바른지 확인 + expectedClientEvents.forEach((event) => { + expect(removedEvents.some((e) => e.event === event)).toBe(true); + }); + + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); +}); diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index f22bc2d..caa7dfb 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -55,7 +55,7 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { export default useDetectInactivity; -const isTouchDevice = () => { +export const isTouchDevice = () => { if (typeof window === 'undefined') { return false; } From 7936cfdbc67ca0b1fd52ea579b7b861758c3d333 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 16:59:53 +0900 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=85=20test:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/useDetectInactivity.test.ts b/src/hooks/useDetectInactivity.test.ts index 2dde2dd..8830e74 100644 --- a/src/hooks/useDetectInactivity.test.ts +++ b/src/hooks/useDetectInactivity.test.ts @@ -15,7 +15,6 @@ describe('useDetectInactivity', () => { (useTimer as jest.Mock).mockImplementation((callback, time) => ({ start: () => { startMock(); - // 타이머 만료 후 콜백 호출 setTimeout(() => { callback(); }, time); @@ -115,7 +114,7 @@ describe('useDetectInactivity', () => { const removedEvents = removeEventListenerSpy.mock.calls.map( ([event, callback]) => ({ event, callback }) ); - // 제거된 이벤트 리스트가 올바른지 확인 + expectedClientEvents.forEach((event) => { expect(removedEvents.some((e) => e.event === event)).toBe(true); }); From 265c25579c767ab01d0e66a1bdb093dabf05ef13 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 17:20:10 +0900 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20isInactive?= =?UTF-8?q?=EA=B0=80=20true=EC=9D=BC=EB=95=8C=EB=A7=8C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=9E=AC=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index caa7dfb..08c9e14 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -24,7 +24,9 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { : ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; const resetTimer = useCallback(() => { - setIsInactive(false); + if (isInactive) { + setIsInactive(false); + } start(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 125dc2d439499e4d4df98052535c99f65689f6bf Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 18:39:43 +0900 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EC=8A=A4=EB=84=88=20=EC=A3=BC=EA=B8=B0?= =?UTF-8?q?=EC=97=90=20throttle=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 17 +++++++++++++---- src/hooks/useMousePosition.ts | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 08c9e14..c42f748 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; import { Fn } from '../types'; +import { throttle } from './useMousePosition'; /** * 일정 시간(ms) 동안 활동이 없을 때 지정된 콜백 함수를 실행하는 훅. @@ -23,14 +24,22 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { ? ['touchstart'] : ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; - const resetTimer = useCallback(() => { - if (isInactive) { - setIsInactive(false); - } + const handleChange = useCallback(() => { + setIsInactive(false); start(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const throttledHandleChange = throttle(handleChange, 1000); + + const resetTimer = useCallback( + (event: Event) => { + throttledHandleChange(event); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + useEffect(() => { start(); diff --git a/src/hooks/useMousePosition.ts b/src/hooks/useMousePosition.ts index 1568ea3..9cecf0d 100644 --- a/src/hooks/useMousePosition.ts +++ b/src/hooks/useMousePosition.ts @@ -160,7 +160,7 @@ const useMousePosition = ({ export default useMousePosition; -const throttle = ( +export const throttle = ( callbackFn: (event: T) => void, delayTime: number ) => { From 90b1445ddb91e81659424add103c7c945714ed99 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 18:50:23 +0900 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20useCallback=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index c42f748..954b77e 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -24,32 +24,24 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { ? ['touchstart'] : ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; - const handleChange = useCallback(() => { + const resetTimer = useCallback(() => { setIsInactive(false); start(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const throttledHandleChange = throttle(handleChange, 1000); - - const resetTimer = useCallback( - (event: Event) => { - throttledHandleChange(event); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - useEffect(() => { start(); + const throttledResetTimer = throttle(resetTimer, 5000); + clientEvents.forEach((event) => { - window.addEventListener(event, resetTimer); + window.addEventListener(event, throttledResetTimer); }); return () => { clientEvents.forEach((event) => - window.removeEventListener(event, resetTimer) + window.removeEventListener(event, throttledResetTimer) ); }; // eslint-disable-next-line react-hooks/exhaustive-deps From 863e10096f520a4314a26302aae64d0760e668f1 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 19:12:24 +0900 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20useMousePositi?= =?UTF-8?q?on=EC=97=90=EC=84=9C=20throttle=EC=9D=84=20util=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 3 ++- src/hooks/useMousePosition.ts | 16 +--------------- src/utils/index.ts | 1 + src/utils/throttle.ts | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 src/utils/throttle.ts diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 954b77e..85ffb20 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; import { Fn } from '../types'; -import { throttle } from './useMousePosition'; +import { throttle } from '../utils'; /** * 일정 시간(ms) 동안 활동이 없을 때 지정된 콜백 함수를 실행하는 훅. @@ -33,6 +33,7 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { useEffect(() => { start(); + // 이벤트 리스너는 5초마다 감지 const throttledResetTimer = throttle(resetTimer, 5000); clientEvents.forEach((event) => { diff --git a/src/hooks/useMousePosition.ts b/src/hooks/useMousePosition.ts index 9cecf0d..8eff2d5 100644 --- a/src/hooks/useMousePosition.ts +++ b/src/hooks/useMousePosition.ts @@ -6,6 +6,7 @@ import { useRef, useState, } from 'react'; +import { throttle } from '../utils'; interface MousePosOptions { delayTime?: number; @@ -160,21 +161,6 @@ const useMousePosition = ({ export default useMousePosition; -export const throttle = ( - callbackFn: (event: T) => void, - delayTime: number -) => { - let lastTime = 0; - - return (event: T) => { - const now = Date.now(); - if (now - lastTime >= delayTime) { - lastTime = now; - callbackFn(event); - } - }; -}; - const animationFrameHandler = ( callbackFn: (event: T) => void ) => { diff --git a/src/utils/index.ts b/src/utils/index.ts index f88a5ae..6aa017a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ export { delayExecution, type CancelToken } from './delayExecution'; +export { throttle } from './throttle'; diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts new file mode 100644 index 0000000..89a14a2 --- /dev/null +++ b/src/utils/throttle.ts @@ -0,0 +1,14 @@ +export const throttle = ( + callbackFn: (event: T) => void, + delayTime: number +) => { + let lastTime = 0; + + return (event: T) => { + const now = Date.now(); + if (now - lastTime >= delayTime) { + lastTime = now; + callbackFn(event); + } + }; +}; From bad80020ec3e640d6af7048cb642b60a966ab724 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 19:28:49 +0900 Subject: [PATCH 11/18] =?UTF-8?q?=E2=9C=A8=20feat:=20throttle=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EB=B3=B4=EB=8B=A4=20time=EC=9D=B4=20=EC=9E=91?= =?UTF-8?q?=EC=9D=80=20=EA=B2=BD=EC=9A=B0=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 85ffb20..5097c9d 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -20,6 +20,15 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { const [isInactive, setIsInactive] = useState(false); const { start } = useTimer(() => setIsInactive(true), time); + // 이벤트 리스너는 5초마다 감지 + const MIN_THROTTLE_TIME = 5000; + + if (time < MIN_THROTTLE_TIME) { + throw new Error( + `The 'time' parameter must be at least ${MIN_THROTTLE_TIME}ms. The provided value was too short and has been adjusted.` + ); + } + const clientEvents = isTouchDevice() ? ['touchstart'] : ['mousemove', 'keydown', 'click', 'dblclick', 'scroll']; @@ -33,8 +42,7 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { useEffect(() => { start(); - // 이벤트 리스너는 5초마다 감지 - const throttledResetTimer = throttle(resetTimer, 5000); + const throttledResetTimer = throttle(resetTimer, MIN_THROTTLE_TIME); clientEvents.forEach((event) => { window.addEventListener(event, throttledResetTimer); From 721453f27ca98c14966b8ca278f6ea8e8b19ddb4 Mon Sep 17 00:00:00 2001 From: foresec Date: Thu, 1 Aug 2024 19:38:50 +0900 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=92=AC=20comment:=20throttle?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20jsdocs=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 5097c9d..a3c334f 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -6,14 +6,14 @@ import { throttle } from '../utils'; /** * 일정 시간(ms) 동안 활동이 없을 때 지정된 콜백 함수를 실행하는 훅. * - * @param {number} time - 비활성 상태로 간주되기까지의 시간(밀리초). 양의 정수로 지정. + * @param {number} time - 비활성 상태로 간주되기까지의 시간(밀리초). 양의 정수로 지정. (최소값 5000ms) * @param {Fn} onInactivity - 비활성 상태가 감지되었을 때 호출되는 콜백 함수. * * @returns {boolean} - 현재 비활동 상태 여부를 나타내는 boolean 값. * * @description * 사용자가 정의한 시간(time) 동안 활동이 없으면 비활성 상태로 간주하고, 지정된 콜백 함수(onInactivity)를 호출합니다. - * 해당 환경에 맞게 설정된 이벤트를 리스닝하여, 활동이 감지될 시 타이머를 리셋합니다. + * 해당 환경에 맞게 설정된 이벤트를 5초마다 리스닝하여, 활동이 감지될 시 타이머를 리셋합니다. */ const useDetectInactivity = (time: number, onInactivity: Fn) => { From ed901bf50ffa80f995a88c1121e84d990c77540c Mon Sep 17 00:00:00 2001 From: foresec Date: Sun, 4 Aug 2024 10:12:06 +0900 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20throttle?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A7=8C=20?= =?UTF-8?q?=EB=B0=9B=EB=8A=94=20=ED=98=95=ED=83=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=EC=BD=9C=EB=B0=B1=ED=95=A8=EC=88=98=EB=A1=9C?= =?UTF-8?q?=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/throttle.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts index 89a14a2..b2f3e01 100644 --- a/src/utils/throttle.ts +++ b/src/utils/throttle.ts @@ -1,14 +1,18 @@ -export const throttle = ( - callbackFn: (event: T) => void, +type GenericCallback = (...args: T) => void; + +export const throttle = ( + callbackFn: GenericCallback, delayTime: number ) => { let lastTime = 0; - return (event: T) => { + const throttledFunction = (...args: T) => { const now = Date.now(); if (now - lastTime >= delayTime) { lastTime = now; - callbackFn(event); + callbackFn(...args); } }; + + return throttledFunction; }; From e6ce675fbc69d9b9a24136e5dc536136507c300c Mon Sep 17 00:00:00 2001 From: foresec Date: Sun, 4 Aug 2024 10:18:15 +0900 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=9A=9A=20rename:=20genericCallback?= =?UTF-8?q?=EC=9D=84=20GenericFn=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD,?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/index.ts | 2 ++ src/utils/throttle.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 8d59b53..1f255c1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1 +1,3 @@ export type Fn = () => void; + +export type GenericFn = (...args: T) => void; diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts index b2f3e01..0ee5622 100644 --- a/src/utils/throttle.ts +++ b/src/utils/throttle.ts @@ -1,7 +1,7 @@ -type GenericCallback = (...args: T) => void; +import { GenericFn } from '../types'; export const throttle = ( - callbackFn: GenericCallback, + callbackFn: GenericFn, delayTime: number ) => { let lastTime = 0; From a68b5e53ebda24853a330bd6e16d80b837e660ab Mon Sep 17 00:00:00 2001 From: foresec Date: Sun, 4 Aug 2024 16:10:54 +0900 Subject: [PATCH 15/18] =?UTF-8?q?=E2=9C=A8=20feat:=20time=20=EC=9E=AC?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=8B=9C=20=EB=8F=99=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=98=EC=98=81=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=B0=B0=EC=97=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index a3c334f..812b3c0 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -37,7 +37,7 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { setIsInactive(false); start(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [start]); useEffect(() => { start(); @@ -54,7 +54,7 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { ); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [resetTimer]); useEffect(() => { if (isInactive) { From e64ec91bc4dea9958cc8414162b84a3752d55745 Mon Sep 17 00:00:00 2001 From: foresec Date: Sun, 4 Aug 2024 16:12:31 +0900 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=A9=B9=20chore:=20throw=20error=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=ED=95=9C=EA=B5=AD=EC=96=B4=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 812b3c0..1db2902 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -25,7 +25,7 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { if (time < MIN_THROTTLE_TIME) { throw new Error( - `The 'time' parameter must be at least ${MIN_THROTTLE_TIME}ms. The provided value was too short and has been adjusted.` + `'time'은 최소 ${MIN_THROTTLE_TIME}ms 이상으로 설정되어야 합니다.` ); } From 441daf9fd288ac05fb71029f42b9bbcef784cae9 Mon Sep 17 00:00:00 2001 From: foresec Date: Sun, 4 Aug 2024 22:49:37 +0900 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20isClient=20uti?= =?UTF-8?q?l=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 7 +++---- src/utils/index.ts | 1 + src/utils/isClient.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 src/utils/isClient.ts diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 1db2902..1994ef7 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; import { Fn } from '../types'; -import { throttle } from '../utils'; +import { isClient, throttle } from '../utils'; /** * 일정 시간(ms) 동안 활동이 없을 때 지정된 콜백 함수를 실행하는 훅. @@ -36,7 +36,6 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { const resetTimer = useCallback(() => { setIsInactive(false); start(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [start]); useEffect(() => { @@ -67,8 +66,8 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { export default useDetectInactivity; -export const isTouchDevice = () => { - if (typeof window === 'undefined') { +const isTouchDevice = () => { + if (!isClient) { return false; } return 'ontouchstart' in window || navigator.maxTouchPoints > 0; diff --git a/src/utils/index.ts b/src/utils/index.ts index 6aa017a..78c4bcb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export { delayExecution, type CancelToken } from './delayExecution'; export { throttle } from './throttle'; +export { isClient } from './isClient'; diff --git a/src/utils/isClient.ts b/src/utils/isClient.ts new file mode 100644 index 0000000..a656c39 --- /dev/null +++ b/src/utils/isClient.ts @@ -0,0 +1 @@ +export const isClient = typeof window !== 'undefined'; From fcdc10383fa6feaedef187103c93068b94ee7ac0 Mon Sep 17 00:00:00 2001 From: foresec Date: Sun, 4 Aug 2024 22:53:04 +0900 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20isTouchDevice?= =?UTF-8?q?=20util=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectInactivity.ts | 9 +-------- src/utils/index.ts | 1 + src/utils/isTouchDevice.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 src/utils/isTouchDevice.ts diff --git a/src/hooks/useDetectInactivity.ts b/src/hooks/useDetectInactivity.ts index 1994ef7..507106b 100644 --- a/src/hooks/useDetectInactivity.ts +++ b/src/hooks/useDetectInactivity.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import useTimer from './useTimer'; import { Fn } from '../types'; -import { isClient, throttle } from '../utils'; +import { isTouchDevice, throttle } from '../utils'; /** * 일정 시간(ms) 동안 활동이 없을 때 지정된 콜백 함수를 실행하는 훅. @@ -65,10 +65,3 @@ const useDetectInactivity = (time: number, onInactivity: Fn) => { }; export default useDetectInactivity; - -const isTouchDevice = () => { - if (!isClient) { - return false; - } - return 'ontouchstart' in window || navigator.maxTouchPoints > 0; -}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 78c4bcb..ff44333 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export { delayExecution, type CancelToken } from './delayExecution'; export { throttle } from './throttle'; export { isClient } from './isClient'; +export { isTouchDevice } from './isTouchDevice'; diff --git a/src/utils/isTouchDevice.ts b/src/utils/isTouchDevice.ts new file mode 100644 index 0000000..f5f1d80 --- /dev/null +++ b/src/utils/isTouchDevice.ts @@ -0,0 +1,8 @@ +import { isClient } from './isClient'; + +export const isTouchDevice = () => { + if (!isClient) { + return false; + } + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +};