Programming/☕ JavaScript

Debounce, Throttle 이해하고 구현해보기

남남이루 2024. 12. 31. 18:15

Debounce

Debounce란, 입력이 연달아 들어올 때 여러 입력을 그룹으로 묶어서 처리하는 것이다. 예를 들어 1ms 단위로 10번 들어오고 10초 쉬었다가 1ms 단위로 100번 들어왔다면 각각을 단 두번의 입력으로 그룹지어서 처리한다.
검색창 input 입력시 매 입력 이벤트마다 조회 요청을 하는 것이 아니라, input 단위를 시간(interval)을 기준으로 그룹지어서 조회 요청을 줄일 수 있다.

핵심

debounce의 핵심은 "마지막 호출로부터 일정 시간이 지난 후 실행"

 

구현 (PR)

setTimeout

setTimeout으로 함수 호출할 때 시간 지연을 걸어두고, 시간 지연이 진행중일 때는 함수 호출을 취소한다. (clearTimeout)
시간지연이 끝난 요청에 대해서만 함수가 실행된다.

this

반환 함수를 호출할 때 클로저를 활용해 객체의 context를 바인딩해주었다. 이를 통해 객체 메서드로도 사용할 수 있게 되고, UI 컴포넌트에서의 활용도도 높일 수 있게 된다. 결과적으로, this 컨텍스트 처리를 통해 debounce된 함수가 원래 함수의 문맥(context)을 그대로 유지하게 해주어, 객체 지향 프로그래밍에서 더 자연스럽고 예측 가능한 동작을 보장할 수 있다.

호출할 함수.apply(context, args)

이미지로 연상해보기

처음 전기가 통하면서 전구가 들어올 때, 파사사삭 하고 쨍 켜지는 것처럼, 무수한 요청이 빠른 시간 안에 들어왔을 때 가장 처음 요청이나 가장 마지막 요청에 대해서만 진행한다.

Kapture 2024-12-31 at 13 50 01

const searchInput = {
    value: "",
    name: "검색창",

    search() {
        console.log(`${this.name}에서 "${this.value}" 검색 중...`);
    }
};

// debounce 적용
searchInput.debouncedSearch = debounce(searchInput.search, 1000);

// 이렇게 실행하면:
searchInput.value = "자바스크립트";
searchInput.debouncedSearch();  
// "검색창에서 '자바스크립트' 검색 중..." 출력
// 왜? debouncedSearch 호출 시 this가 searchInput을 가리키므로!

// 하지만 이렇게 하면:
const justSearch = searchInput.debouncedSearch;
justSearch();  
// "undefined에서 'undefined' 검색 중..." 출력
// 왜? 일반 함수로 호출되어 this가 window를 가리키므로!

Throttle

일정 시간 동안 한 번만 함수가 실행되도록 제한하는 기술을 의미한다. 첫 번째 호출을 실행하고, 설정된 시간 동안은 추가 호출을 무시한다.
일정 시간이 지나지 않았을 때 들어오는 요청에 대해서 무시함으로써 과도한 호출을 막는 것이다.

  • 프로그래밍이 아닌 기기 설계에서도 쓰이는데, 리소스의 과다 사용시 '의도적인 성능 낮추기'의 실행 같은 의미로 쓰인다.

핵심

throttle은 "마지막 호출로부터 일정 시간이 지난 후 실행"이라는 점은 debounce와 동일하다고 볼 수 있지만,
중간 호출을 아예 무시한다는 점에서 debounce와 차이가 있다.

 

구현 (PR)

lastCallTime

throttle은 시간 간격에 따라 조건적으로 함수를 실행하기 때문에 마지막 호출 시점을 변수에 저장해두었다.
Date.now()로 현재 시간을 구하고, 마지막 실행 시간과의 차이를 비교한다.
정해진 대기 시간(wait)이 지나지 않았다면 함수 실행을 무시하고, 시간이 지났다면 함수를 실행하고 마지막 실행 시간을 갱신한다.

this

debounce와 마찬가지로 반환 함수를 호출할 때 클로저를 활용해 객체의 context를 바인딩해준다.

호출할 함수.apply(context, args)

이미지로 연상해보기

본래 의미는 목을 조르는 행위를 지칭한다. 이 점에서 유래하여 무언가의 출력을 조절하는 뜻도 가지고 있는데, 기계장치에 연결된 액체나 기체가 흐르는 관에 달린 밸브를 조절하는 광경을 생각해보면 적절한 의미 확장인 셈이다.

실시간 검색과 throttle

throttle에 대해 검색하다보면 실시간 검색이 예시로 나오는 경우가 있다. 개인적으로 실시간 검색을 throttle 로 할 경우 중간에 발생하는 유저의 input 이벤트나 request 요청에 대해 아예 무시될 수가 있어서 UX 측면에서 바람직하지 않다고 생각한다.

const handleSearch = throttle((value) => {
    console.log('API 호출:', value);
}, 1000);

// 사용자가 빠르게 입력: "안녕하세요"
handleSearch('안');     // 실행됨
handleSearch('안녕');   // 무시됨 (1초 안지남)
handleSearch('안녕하'); // 무시됨
handleSearch('안녕하세');// 무시됨
handleSearch('안녕하세요');// 무시됨 
// => "안" 으로만 검색됨!
const handleSearch = debounce((value) => {
    console.log('API 호출:', value);
}, 500);

// 사용자가 빠르게 입력: "안녕하세요"
handleSearch('안');      // 대기
handleSearch('안녕');    // 대기
handleSearch('안녕하');  // 대기
handleSearch('안녕하세');// 대기
handleSearch('안녕하세요');// 타이핑 멈추면 이 값으로 API 호출
// => "안녕하세요" 로 검색됨!

활용

무한 스크롤 - 페이지 하단 체크
실시간 게임 - 주기적인 상태 업데이트
윈도우 리사이즈 처리
실시간 차트 데이터 업데이트

무한 스크롤 예시

  • 사용자가 컨텐츠의 최하단에 있다는 것을 이벤트로 감지했을 때 새로 컨텐츠를 불러오는 구현을 한다고 하자.
  • throttle을 이용해 일정 시간마다 호출이 일어나도록 제어할 수 있다.
  window.addEventListener('scroll', throttle(() => {
      checkScrollPosition(); // 200ms마다 한 번씩 실행
  }, 200));

In Summary

  • Debounce, Throttle, RequestAnimationFrame의 목적과 차이점
  • 이벤트 핸들러를 처리하기 위한 세 가지 기술!

Debounce

  • 목적: 연속된 여러 이벤트를 하나의 이벤트로 그룹화
  • 동작: 마지막 이벤트 후 일정 시간이 지나면 실행
  • 예시: 검색창 자동완성
    searchInput.addEventListener('input', debounce(() => {
        fetchResults(input.value); // 타이핑 멈추고 500ms 후 1번만 실행
    }, 500));

Throttle

  • 목적: 일정 시간 간격으로 실행을 보장
  • 동작: X밀리초마다 정확히 한 번씩 실행
  • 예시: 스크롤 위치 체크
    window.addEventListener('scroll', throttle(() => {
        checkScrollPosition(); // 200ms마다 한 번씩 실행
    }, 200));

RequestAnimationFrame (rAF)

  • 목적: 부드러운 애니메이션을 위한 최적화
  • 동작: 브라우저의 리페인트 타이밍에 맞춰 실행 (보통 60fps)
  • 예시: 스크롤 애니메이션
    function updateScroll() {
        element.style.transform = `translateY(${window.scrollY}px)`;
        requestAnimationFrame(updateScroll);
    }
    requestAnimationFrame(updateScroll);

선택 기준

  • Debounce 사용: 연속된 이벤트의 '마지막' 또는 '처음' 실행만 필요할 때
    • 검색어 자동완성
    • 폼 유효성 검사
  • Throttle 사용: 일정 주기로 실행을 보장해야 할 때
    • 무한 스크롤
    • 실시간 게임 상태 업데이트
  • rAF 사용: 화면 렌더링과 관련된 작업일 때
    • 스크롤 애니메이션
    • 요소 크기/위치 계산

==이 세 가지는 각자의 용도가 있고, 서로 보완적인 관계입니다. 상황에 맞게 적절한 기술을 선택하는 것이 중요합니다! (by. Claude)==

참고

Debouncing and Throttling Explained Through Examples

javascript debounce 구현 문제