티스토리 뷰

반응형

자바스크립트 쓰로틀링(Throttling), 디바운싱(Debouncing) 성능 최적화

자바스크립트 쓰로틀링(Throttling)과 디바운싱(Debouncing)은 성능 최적화를 위한 핵심 기법이다.

고빈도 이벤트(스크롤, 리사이즈, 입력 등)에서 과도한 호출을 방지하고 성능을 높이는데 유용하다.

 

 

 

쓰로틀링 (Throttling)

일정 시간 간격으로 함수를 강제로 실행하게 제한한다. 

즉, N초에 한번만 실행되게 만든다는게 핵심이다.

사용자의 행동이 빈번하지만 일정 간격으로만 반응하면 충분한 경우에 사용된다. (주기적으로 처리)

  • 스크롤 위치 저장
  • 윈도우 리사이즈

 

 

예시

사용자가 아무리 스크롤을 해도, 200ms에 한번만 로그 출력

window.addEventListener('scroll', throttle(() => {
	console.log('스크롤 위치', window.scrollY);
}, 200);

 

흐름

  • 0ms 호출 -> 실행 (lastCall = 0)
  • 50ms 호출 -> 무시
  • 100ms 호출 -> 무시
  • 200ms 호출 -> 실행 (lastCall = 200)
  • 300ms 호출 -> 무시 
  • 400ms 호출 -> 실행 (lastCall = 400)

 

 

 

디바운싱 (Debouncing)

마지막 이벤트 이후 일정 시간이 지나면 함수를 실행한다.

그 전에 이벤트가 또 발생하면 타이머를 리셋한다.

사용자의 행동이 멈출 때까지 기다렸다가 실행해야 할때 사용한다.

  • 검색어 자동 완성
  • 윈도우 리사이즈 후 렌더링

 

디바운싱의 핵심은 타이머를 계속 덮어쓰기 때문에 마지막 호출만 남는다는 것

내부에서 일어나는 동작

function dbounce(fn, delay) {
	let timer = null;
    
    return function(...args) {
    	clearTimeout(timer); // 1. 이전 타이머 제거
        timer = setTimeout(() => {
        	fn.apply(this, args) // 3. delay 후 함수 실행
        }, delay); // 2. 새로운 타이머 설정
    }
}

 

  1. 첫번째 이벤트 발생
    1. timer가 null이기에 clearTimeout(timer)는 무시됨
    2. setTimeout(fn, 300) 실행되며, timer에 새로운 ID 저장
  2. 100ms 후 두번째 이벤트 발생
    1. clearTimeout(timer)에서 이전 타이머 취소
    2. setTimeout(fn, 300) 실행되며, timer에 새로운 ID 저장
  3. 100ms 후 세번째 이벤트 발생
    1. 2번 반복
  4. 입력을 멈추고 300ms 지나면
    1. 더 이상 clearTimeout 호출이 없기에 마지막 타이머가 만료되어 fn()이 실행된다.

 

 

디바운싱의 동작을 제한하고 싶다면 maxWait과 같은 옵션을 사용하는 라이브러리도 있다.

'최대 기다림 시간을 넘기면 무조건 실행'이라는 안전 장치고, 

디바운싱이 가지는 본래의 특성 (= 너무 안 실행될 수 있는 상황) 때문에 존재하는 옵션이다.

 

 

예시

사용자가 계속 이벤트를 발생한다.

input.addEventListener('input', debounce(doSearch, 300));

 

사용자가 계속 타자를 치게되면 300ms내에 입력이 끊기지 않기 때문에 doSearch()는 영원히 실행되지 않을 수 있다.

이때 아래와 같이 특정 라이브러리에서 maxWait을 사용할 수 있다.

const debouncedSearch = debounce(searchAPI, 300, { maxWait: 1000 });

 

사용자가 1초 이상 계속 타이핑 중일때, maxWait에 의해 1초 이상 검색을 안보내는 일은 없어진다.

로딩이 없는 UI가 계속 지연되는 것을 방지하며 사용자 인터랙션에 적절한 피드백을 제공할때 중요하게 사용될 수 있다.

 

 

 


 

 

Q. 만약 300ms가 흐를동안의 여유를 주지 않고 계속 호출하게되면, 무한루프야?

A. 무한루프라고 하지 않는다. 스케쥴링된 동작.

타이머가 끝나기 전에 계속 이벤트가 발생하면 타이머는 계속 취소되고 새로 설정되지만, 무한루프라고 부르지 않는다.

타이머 자체는 비동기이며 반복적으로 재설정될 뿐이고, setTimeout은 이벤트 루프 큐에 등록되는 것이지, 재귀처럼 호출 스택이 쌓이지 않는다.

clearTimeout과 setTimeout 모두 콜스택에 한번 올라간 뒤 바로 실행되기에 호출 스택이 쌓이지 않기에 메모리도 터지지 않고 콜스택 오버플로우도 안난다. 

즉, 자바스크립트 콜스택이 쌓여서 메모리 터지는 그런 '무한 루프'는 아니다. (계속 지연되는건 맞다)

루프가 아니라 계속 스케줄된 함수 호출로 볼 수 있는데, 재귀함수 같은 경우 콜스택이 계속 쌓이기 때문에 무한 루프가 된다. (선입 후출로 인해 함수가 쌓이기만하고 실행과 종료를 못함)

하지만 debounce 같은 함수는 비동기 + 타이머 + 이벤트 루프에 의해 처리되기에 콜스택은 깔끔하게 유지되기에 루프처럼 보여도 진짜 루프라고 할 수 없다.

 

 

예시

function loop() {
	setTimeout(loop, 0);
};

loop();

 

흐름

  1. loop() 콜스택에 올라감
  2. setTimeout(loop, 0) 브라우저 Web API 타이머 영역으로 등록
  3. loop() 실행 종료
  4. 0ms 후 loop 콜백이 태스크큐로 이동
  5. 이벤트 루프가 콜스택이 비어있는지 확인 -> 비었다면 다시 loop() 실행

 

여기서 중요한건 매 loop() 호출은 이전 호출이 종료된 후 일어난다.

콜스택에는 한번에 함수 하나만 올라감 (위 2~5번까지 반복해야 다시 하나의 함수가 콜스택에 올라감)

때문에 선입후출 구조는 영향이 없음

 

즉, 함수가 스스로 종료되기 전에 직접 호출하는 구조가 아니고, 2~5번의 과정이 있기 때문에 다음 이벤트 루프 틱에서 다시 호출이 일어나고 실행되는 방식이기 때문에 콜스택에 쌓이는 연속 호출이 없고 이벤트 루프가 순서대로 처리가된다.

 

 


 

 

  • 너무 짧은 딜레이는 큰 효과가없다.
  • 상황에 맞는 ms 설정이 필요하다.
  • React에서는 useCallback과 useRef 또는 외부 라이브러리(lodash, use-debounce, use-throttle)에서 활용 가능하며, cleanup 해주는게 안전하다.

 

반응형
최근에 올라온 글
최근에 달린 댓글
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Total
Today
Yesterday