debounceTime, throttleTime, auditTime: 시간 간격을 기준으로 이벤트를 걸러내는 세 가지 방식
debounceTime, throttleTime, auditTime은 모두 짧은 시간 안에 몰려오는 이벤트를 줄여주는 연산자지만 어떤 값을 언제 내보내는지가 다르다. debounceTime은 조용해진 이후의 마지막 값을, throttleTime은 시작 시점의 값을 주기적으로, auditTime은 구간이 끝날 때의 최신 값을 내보낸다. 세 연산자의 차이를 이
1. 개념
세 연산자는 모두 "짧은 시간 안에 여러 번 발생하는 이벤트를 줄여서 내보낸다"는 공통 목적을 가진다. 하지만 언제, 어떤 값을 내보내는지에 따라 성격이 완전히 다르다.
debounceTime(ms)은 소스(source)에서 값이 들어올 때마다 타이머를 리셋하고, 지정한 시간(ms) 동안 새로운 값이 안 들어오면 그때 마지막 값을 한 번 내보낸다. 즉 "조용해진 뒤"에 반응하는 연산자다.
throttleTime(ms)은 값이 들어오면 즉시 내보내고, 그 시점부터 지정한 시간 동안 들어오는 값은 무시한다. 시간이 지나면 다시 다음 값을 즉시 내보낸다. 즉 "구간의 시작"에 반응하는 연산자다.
auditTime(ms)은 값이 들어오면 타이머를 시작하고, 그 시간이 흐르는 동안 들어온 값들은 무시하다가 시간이 다 되는 순간 가장 최근에 들어온 값을 내보낸다. 즉 "구간의 끝"에 반응하는 연산자다.
2. 왜 사용하는가
사용자 입력, 스크롤, 마우스 이동, 윈도우 리사이즈 같은 이벤트는 짧은 시간에 수십, 수백 번 발생할 수 있다. 이런 이벤트를 그대로 처리하면 불필요한 API 호출이 발생하거나 렌더링이 과도하게 일어나 성능 문제가 생긴다.
세 연산자는 이런 이벤트 스트림의 발생 빈도를 줄이는 역할을 하지만, 목적에 따라 선택이 달라진다. 사용자가 타이핑을 멈춘 후에 검색을 실행하고 싶다면 debounceTime이 적합하고, 버튼 연타를 막아 첫 클릭만 처리하고 싶다면 throttleTime이 적합하며, 스크롤 중에도 일정 간격으로 최신 위치를 반영하고 싶다면 auditTime이 적합하다.
3. 동작 원리
debounceTime의 내부 동작은 다음과 같다. 소스에서 값이 도착하면 이전에 설정된 타이머가 있다면 취소하고 새 타이머를 시작한다. 타이머가 만료될 때까지 새 값이 오지 않으면 그 시점에 가장 마지막으로 받은 값을 내보낸다. 계속 값이 들어오는 동안에는 아무것도 내보내지 않는다.
throttleTime의 기본 동작은 다음과 같다. 값이 도착하면 즉시 내보내고 동시에 "무시 구간" 타이머를 시작한다. 이 구간 동안 들어오는 값은 버려진다. 구간이 끝나면 다음에 도착하는 값을 다시 즉시 내보내고 같은 과정을 반복한다. 참고로 { leading, trailing } 옵션을 통해 구간 끝에 마지막 값을 한 번 더 내보내는 동작을 추가할 수도 있다.
auditTime의 동작은 다음과 같다. 값이 도착하면 타이머를 시작하고, 타이머가 도는 동안 들어오는 값은 계속 최신 값으로 갱신만 해둔다. 타이머가 만료되면 그 시점에 저장된 가장 최신 값을 내보내고 타이머는 종료된다. 이후 새 값이 들어오면 다시 같은 과정을 반복한다.
세 연산자의 차이를 한 문장으로 정리하면, debounceTime은 "정지 이후", throttleTime은 "시작 시점", auditTime은 "구간 종료 시점"에 값을 내보낸다.
4. 예제
검색어 입력을 debounceTime으로 처리하는 예제를 살펴본다. 먼저 fromEvent(inputElement, 'input')으로 입력 이벤트 스트림을 만들고, 그다음 pipe(map(e => e.target.value), debounceTime(300))을 연결한다. 이렇게 하면 사용자가 타이핑을 멈춘 후 300밀리초가 지났을 때만 최종 입력값이 흘러나오므로, 매 키 입력마다 API를 호출하는 대신 타이핑이 끝난 시점에 한 번만 호출할 수 있다.
버튼 연타 방지를 throttleTime으로 처리하는 예제를 살펴본다. 먼저 fromEvent(button, 'click') 스트림을 만들고, 그다음 pipe(throttleTime(1000))을 연결한다. 이렇게 하면 첫 클릭은 즉시 처리되고, 이후 1초 동안 발생하는 추가 클릭은 모두 무시되어 중복 요청을 막을 수 있다.
스크롤 위치 추적을 auditTime으로 처리하는 예제를 살펴본다. 먼저 fromEvent(window, 'scroll') 스트림을 만들고, 그다음 pipe(map(() => window.scrollY), auditTime(200))을 연결한다. 이렇게 하면 스크롤이 발생하는 동안 200밀리초 간격으로 그 시점의 최신 스크롤 위치가 전달되므로, 매 스크롤 이벤트마다 처리하지 않으면서도 최신 상태를 놓치지 않고 반영할 수 있다.
세 코드를 나란히 비교하면, 세 연산자 모두 pipe 안에 한 줄만 추가하면 되는 간단한 구조지만 결과로 나오는 이벤트의 타이밍은 완전히 다르다는 점을 확인할 수 있다.
5. 실무 사용 사례
검색 자동완성 기능에서는 debounceTime을 사용해 사용자가 입력을 멈춘 후에만 서버에 요청을 보낸다. 보통 300에서 500밀리초 정도의 지연을 준다.
폼 저장(autosave) 기능에서도 debounceTime을 사용해 사용자가 타이핑을 멈춘 시점에 변경 사항을 서버에 저장한다.
버튼 중복 클릭 방지, 결제 요청 같은 민감한 액션에는 throttleTime을 사용해 짧은 시간 안에 같은 요청이 여러 번 나가지 않도록 막는다.
무한 스크롤이나 스크롤 위치에 따른 UI 갱신(예: 헤더 숨김/표시)에는 auditTime을 사용해 스크롤이 진행되는 동안에도 일정 주기로 최신 상태를 반영하면서 이벤트 처리 부담은 줄인다.
마우스 드래그나 리사이즈 이벤트 처리에도 throttleTime이나 auditTime이 자주 쓰인다. 리사이즈가 끝난 후 최종 크기만 필요하다면 debounceTime을, 리사이즈 도중에도 주기적으로 갱신이 필요하다면 throttleTime이나 auditTime을 선택한다.
6. 주의할 점
debounceTime은 값이 계속 들어오면 결과가 한 번도 나오지 않을 수 있다. 예를 들어 사용자가 쉬지 않고 타이핑하면 debounceTime 구간이 끝나기 전에 계속 새 값이 들어와 타이머가 계속 리셋되므로, 타이핑을 멈추기 전까지는 아무 값도 방출되지 않는다. 이는 의도된 동작이지만 "왜 값이 안 나오지"라는 오해를 만들 수 있다.
throttleTime은 기본 설정(leading true, trailing false)에서 마지막 값을 놓칠 수 있다. 예를 들어 무시 구간 안에서 사용자가 입력을 끝냈다면 그 마지막 입력값은 내보내지지 않는다. 마지막 값도 필요하다면 trailing: true 옵션을 함께 고려해야 한다.
auditTime은 스로틀(throttle)과 비슷해 보이지만 값을 내보내는 시점이 다르다는 점을 혼동하기 쉽다. throttleTime은 구간 시작 값을, auditTime은 구간 종료 시점의 최신 값을 내보낸다는 차이를 정확히 구분해야 한다.
세 연산자 모두 시간(ms) 단위는 프로젝트의 사용자 경험 요구사항에 맞춰 실험적으로 조정해야 하며, 너무 짧으면 최적화 효과가 없고 너무 길면 사용자가 반응이 느리다고 느낄 수 있다.
7. 핵심 정리
debounceTime, throttleTime, auditTime은 모두 이벤트 빈도를 줄이는 연산자지만 값을 내보내는 시점이 다르다. debounceTime은 이벤트가 멈춘 후, throttleTime은 이벤트가 시작된 순간, auditTime은 일정 구간이 끝나는 순간에 값을 내보낸다. 검색 입력처럼 "완료된 후" 처리가 필요하면 debounceTime, 중복 실행 방지가 목적이면 throttleTime, 진행 중에도 최신 상태를 주기적으로 반영하고 싶으면 auditTime을 선택한다.