Original article: Debounce – How to Delay a Function in JavaScript (JS ES6 Example)

JavaScript에서 debounce(디바운스)는 유저가 입력할 때마다 코드를 오직 한 번씩만 실행되도록 해주는 함수입니다. 검색 박스의 제안, 텍스트 필드의 자동 저장, 버튼의 더블 클릭의 제거가 모두 debounce를 이용하는 사례입니다.

이번 튜토리얼에서는 JavaScript에서 어떻게 debounce 함수를 만들어보는지 배워볼 것입니다.

debounce가 뭔가요?

debounce는 전자 공학에서 온 용어입니다. 버튼을 누를 때, 예를 들어 TV 리모컨이라고 해본다면, 버튼에서 손을 떼려고 하기도 전에 신호는 아주 빠르게 리모컨의 마이크로칩으로 흐르고, 이것이 디바운스(debounce) 되어 마이크로칩은 이렇게 여러 번 '클릭'한 것을 등록해버립니다.

버튼이 클릭되었을 때 신호가 흐르는 양상을 보여주는 그림으로, 고점과 저점 사이 신호가 무시되는 지점이 파란색 칠이 되어 있다. 신호가 고점과 저점을 여러 번 반복하는 지점이 무시되는 지점으로 설명되고 있다.
버튼이 클릭되었을 때 신호가 흐르는 양상을 보여주는 그림으로, 고점과 저점 사이 신호가 무시되는 지점이 파란색 칠이 되어 있다. 신호가 고점과 저점을 여러 번 반복하는 지점이 무시되는 지점으로 설명되고 있다.

이 과정을 줄이려면, 일단 버튼으로부터 신호를 받았다면, 마이크로칩은 물리적으로 다시 버튼을 누르는 것이 불가능한, 약 몇 마이크로초 동안 버튼으로부터 온 신호를 처리하지 않습니다.

JavaScript에서의 Debounce

JavaScript에서의 사용 예도 비슷합니다. 사용 사례에 따라 오직 한 번만 함수를 실행하고 싶습니다.

방문자가 타이핑을 끝내고 난 뒤에만 검색 질의(query)에 대한 제안 옵션을 보여주고 싶다고 해봅시다.

아니면 양식의 내용을 저장하고자 하지만, 매번 '저장'이 발생한다면 데이터베이스를 거쳐야 하니, 사용자가 해당 내용을 적극적으로 변경하지 않을 때만 이를 하고 싶다고 해봅시다.

어떤 분들은 Window 95에 너무 익숙해진 나머지 이제 모든 곳에 더블 클릭하는, 제가 정말 좋아하는 이야기도 있고 말이죠. 😁

간단하게 debounce 함수를 구현해보았습니다. CodePen은 이곳에서 확인해보세요.

function debounce(func, timeout = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, timeout);
  };
}
function saveInput() {
  console.log('Saving data');
}
const processChange = debounce(() => saveInput());

입력창에서는 이렇게 쓰입니다.

<input type="text" onkeyup="processChange()" />

버튼에서는 이렇게 쓰입니다.

<button onclick="processChange()">Click me</button>

window 이벤트

window.addEventListener('scroll', processChange);

간단한 JS 함수 같은 요소에도 쓸 수 있습니다.

자 그럼 어떤 일이 일어날까요? debounce는 특별한 함수로, 아래 두 가지 일을 처리합니다.

  • timer 값의 스코프(scope)를 할당하고
  • 함수가 지정된 시간에 작동되도록 스케줄링합니다.

가장 먼저 다룬 텍스트 입력창의 사례로 어떻게 이것이 작동하는지 설명해보도록 하겠습니다.

방문자가 첫 글자를 입력하고 키에서 손을 뗄 때, debounce는 우선 clearTimeout(timer)을 가지고 timer를 재설정합니다. 이때, 스케줄에 잡은 것이 아무것도 없으니, 이 단계가 꼭 필수적이진 않습니다. 이후 주어진 함수인 saveInput()을 300 밀리세컨드마다 실행되도록 스케줄 합니다.

그런데 만일 방문자가 계속 작성하는 동안 키에서 손을 뗄 때마다 debounce가 한 번 더 실행된다고 해봅시다. 매번 timer를 재설정한다는 것은 다시 말해 saveInput()과 함께 직전에 예정한 것을 취소하고 이다음의 300 밀리세컨드라는 새로운 시간으로 다시 스케줄 한다는 것입니다. 이는 방문자가 300 밀리세컨드 이내에 계속 키를 입력하는 한 지속됩니다.

마지막 스케줄은 제거되지 않으므로, 마침내 saveInput()이 호출됩니다.

반대의 경우 - 뒤이은 이벤트를 무시하는 방법

방금까지의 방법은 자동 저장이나 제안 옵션을 보여줄 때 좋습니다. 하지만 버튼 하나를 여러 번 클릭하는 사례에서는 어떨까요? 우리는 마지막 클릭까지 기다리는 대신 첫 번째 클릭에서 이를 등록하고, 나머지를 무시하고자 할 것입니다. CodePen은 이곳에서

function debounce_leading(func, timeout = 300) {
  let timer;
  return (...args) => {
    if (!timer) {
      func.apply(this, args);
    }
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = undefined;
    }, timeout);
  };
}

여기에서는 첫 번째로 버튼이 클릭 되었을 때 발생한 첫 번째 debounce_leading 호출에서 saveInput() 함수가 동작합니다. timer는 300 밀리세컨드 이후 사라지도록 설정했습니다. 이 시간 내 뒤이은 모든 버튼 클릭은 이미 지정된 timer가 있으므로, timer가 사라지기까지 남은 시간을 계속해서 300 밀리세컨드로 초기화합니다.

라이브러리에서 구현된 Debounce

이 글에서는 여러분이 어떻게 JavaScript에서 debounce를 구현하고, 웹사이트 요소에서 발생하는 이벤트를 잘 debounce 하기 위한 사용 방법을 보여드렸습니다.

그러나 여러분이 원하지 않는다면 직접 프로젝트에서 debounce 구현하지 않아도 됩니다. 이미 널리 쓰이는 JS 라이브러리에 구현되어 있습니다. 아래는 몇 가지 예시입니다.

라이브러리 사용 예
jQuery (라이브러리를 통한) $.debounce(300, saveInput);
Lodash _.debounce(saveInput, 300)
Underscore _.debounce(saveInput, 300);