본문 바로가기
Study

[JavaScript] 디바운스 구현하기 (feat. 더블 클릭 이슈 해결하기)

by 안자두 2024. 8. 31.

1. 계기

유지보수 진행 중인 프로젝트에서 캘린더를 조작하면 API를 바로 요청하여 테이블을 조회하는 기능이 있었다. 이 경우, 캘린더를 키보드나 마우스로 빠르게 조작할 시, 변경되는 날짜마다 API가 요청되어 서버 쪽 부하가 생길 것 같아 로직 수정이 필요해 보였다. 

추가로 최근 더블 클릭 이슈가 발생하여, 해당 문제를 해결하고자 다양한 방법을 시도하였다. 그 중 채택된 방법이 현재 작성할 디바운스를 활용한 방법이다.
현재 프로젝트의 규모와 이후 작업을 고려해봤을 때, 해당 방법이 가장 최선이라 선택하게 되었다.
앞서 말한 디바운스 구현 시에는 딱 한 군데에서만 필요했고, 코드 양이 적어 코드를 분리하지 않았는데, 이번 디바운스 적용으로 코드를 모듈화 하였다.

라이브러리를 사용하지 않고 직접 구현한 이유는, 디바운스 자체가 간단하기 때문에 직접 구현할 수 있고, lodash 라이브러리를 사용하려고 하니 필요한 기능이 조금 달라 커스텀이 필요하였기 때문이었다. 

 


2. 개발 환경

함수 자체는 자바스크립트로 구현하여 어떤 프레임워크에서 구현하든 동일하겠지만, 나는 Vue2에서 구현하였다. 
각 함수를 플러그인으로 구현해 Vue의 프로토타입에 추가해 사용 시에는 import 없이 사용할 수 있었다.

큰 틀은 적용하는 방법마다 다를 수도 있기 때문에 코드 로직 위주로 설명하였다.

 


3. 디바운스 설명

우선, 로직 설명에 앞서, 디바운스에 대해 간단히 설명하려고 한다.

더보기

디바운스는 일정 시간 전에 이벤트가 발생하게 되면, 기존의 타이머를 취소하고 시간을 재설정하여 이벤트 호출을 방지하고, 일정 시간이 경과된 후에 이벤트 핸들러가 호출되도록 한다.

 

디바운스 특징

  • delay보다 짧은 간격으로 이벤트가 발생하면 계속 타이머를 재설정한다.
  • delay 동안 이벤트가 발생하지 않을 때, 비로소 이벤트 핸들러가 단 한 번, 호출된다.

디바운스 실행 예시

사용 이점

기본적으로 디바운스는 다음과 같은 이점을 위해 사용된다.

  • 타이머 함수가 적용되어 있어, 짧은 시간 동안 연속적으로 발생하는 이벤트를 제어할 때 효율적이다.
  • 이벤트 핸들러가 과도하게 호출되어 발생되는 성능 문제를 해결할 수 있다.

 

디바운스 이벤트 예시

  • resize 이벤트 처리
  • input 요소에 입력된 값으로 ajax 요청을 하는 입력 필드 자동완성 UI 구현
  • 버튼 중복 클릭 방지 처리

등 이벤트 핸들러 호출이 한 번만 필요할 때 유용하다.

일반적인 상황에서는 underscore 라이브러리나 lodash 라이브러리를 사용해 안전하고 편하게 사용할 수 있다.

 


4. 함수 로직 설명

우리 프로젝트에는 두 가지의 다른 디바운스가 필요했다.

첫 번째는 일정 시간 내에 재요청이 없을 시, 파라미터로 받은 함수 호출
두 번째는 파라미터로 받은 함수 호출 후, 일정 시간 동안 동일한 요청 시 타이머 초기화

이렇게 두 가지가 필요했는데, 첫 번째 디바운스부터 차례대로 설명하겠다.

 

📑 첫 번째 로직

첫 번째 로직은 함수 호출 후 일정 시간 내에 재호출이 없을 시, 요청된 API만 호출하는 로직이다. 

우리 프로젝트에서는 위에서 설명했던 것처럼 캘린더 날짜 조작 시, 바로 API가 요청되어 그리드에 데이터가 그려지는 로직이 있었다.
이 경우, 스크롤이나 방향키로 빠르게 날짜를 변경할 때마다 API가 호출되었는데, 호출이 반복되면 서버 부하로 이어질 수 있겠다는 생각이 들어 해당 로직을 추가하기로 하였다.

로직은 아래와 같다.

let timeoutID = null;
const afterDebounce = (fn, time = 1000, ...args) => {
	clearTimeout(timeoutID);
	timeoutID = setTimeout(() => {
		timeoutID = null;
		fn(...args);
	}, time);
};

setTimeout()은 생성 시, 해당 함수의 key를 반환해 주기 때문에, 이 값을 timeoutID라는 변수에 저장한 후, 사용해 주었다. clearTimeout()은 timeoutID에 저장된 값을 key로 갖는 setTimeout()을 제거해 준다.

 

더보기

지정한 `time` 이전에 afterDebounce 함수가 재호출 되면,


1. clearTimeout이 먼저 호출되어, 이전 afterDebounce 함수 호출 시 저장해 둔 timeoutID를 key로 갖는 setTimeout 함수를 제거한다.
2. setTimeout 함수의 콜백 함수가 실행되지 못한 채,
timeoutID에 새 setTimeout 함수가 할당된다.

지정한 `time`이 경과하면 setTimeout의 콜백함수가 실행되며 timeoutID가 초기화되고, 넘겨받은 함수를 실행하게 된다. 나의 경우에는 이 파라미터로 받은 함수가 API를 요청하는 함수이다.

 

이 과정을 그림으로 나타내면 아래와 같다.

첫 번째 디바운스 함수 로직

 

 

📑 두 번째 로직

두 번째 로직은 먼저 파라미터로 받은 함수 호출 후 일정 시간 경과 전 동일한 요청 시 타이머를 초기화하는 로직이다. 

이 로직은 더블 클릭 이슈가 발생하는 모든 함수에 래핑해 사용하였다. 가장 처음 요청된 함수만 실행하고 이후 지정한 시간이 경과하기 전까지는 재요청이 가지 않도록 하였다.

버튼을 클릭할 때마다 API가 요청되어 의도치 않은 결과가 발생할 수도 있고, 사용자가 혼란스러울 수 있겠다는 생각이 들어 적용하게 되었다.

로직은 아래와 같다.

let timeoutID = null;
const beforeDebounce = (fn, time = 1000, ...args) => {
    if (!timeoutID) {
        fn(...args);
    }
    clearTimeout(timeoutID);
    timeoutID = setTimeout(() => {
        timeoutID = null;
    }, time);
};

위와 동일하게 setTimeout의 key를 timeoutID에 저장한 후, 사용해 주었다.

 

더보기

처음 afterDebounce 함수 호출 시, timeoutID는 null이기 때문에 파라미터로 받은 함수를 실행한다.

지정한 `time` 이전에 afterDebounce 함수가 재호출 되면,

1. clearTimeout이 먼저 호출되어, 이전 afterDebounce 함수 호출 시 저장해 둔 timeoutID를 key로 갖는 setTimeout 함수를 제거한다.
2. setTimeout 함수의 콜백 함수가 실행되지 못한 채,
 timeoutID에 새 setTimeout 함수가 할당된다.

지정한 `time`이 경과하면 timeoutID가 초기화되어, 다시 afterDebounce 함수 호출 시 파라미터로 받은 함수를 실행할 수 있게 된다.

 

이 과정을 그림으로 나타내면 아래와 같다.

두 번째 디바운스 함수 로직

 


 

마치며

두 디바운스 적용으로 다량의 API 호출도 방지하고 예기치 못한 상황도 방지할 수 있었다 ! 플러그인으로 분리하고 재사용할 수 있게 했다는 점도 뿌듯했다. 최근 UI 수정만 하다가 작업해서 굉장히 재미있게 진행했던 작업이었다~

야호

(뿌듯했던 작업이라 좀 더 공들여 글을 쓰고 싶다는 생각에 벌써 초고보다 한 달이나 지나버렸지만...)


REF

https://github.com/lodash/lodash/blob/main/src/debounce.ts

https://stackoverflow.com/questions/24004791/what-is-the-debounce-function-in-javascript

728x90