자바스크립트의 이벤트를 제어하는 Debounce와 Throttle에 대해 알아보자! 🌱🌿🍀🌵
사용자와 상호작용하는 동적 페이지를 구현하기 위해 event는 무척 유용한 도구다. 하지만! event가 불필요하게 자주 실행되면 동적이고 뭐고... 성능 저하의 원인이 된다. 대표적인 예가 scroll, drag 이벤트와 input(혹은 change) 이벤트.
🔼 scroll 이벤트의 콜백함수에 console.log(scrollTop)을 추가했을 때
🔼 input 이벤트의 콜백함수에 console.log(event.target.value)를 추가했을 때
위 두 경우에서 콘솔창에 찍히는 것들을 보면 이벤트가 과도하게 많이 발생하고 있다는 걸 알 수 있다. 따라서 이러한 낭비를 줄이고 성능을 개선하기 위해 사용할 수 있는 두 가지 옵션이 바로 debounce와 throttle!
DEBOUNCE
debounce는 이벤트가 와다다다 발생하면, 그중 마지막 이벤트만 발생하도록 해 준다. 연속적이고 빠른 이벤트들을 그룹화해서 하나의 이벤트로 처리한다는 뜻!
THROTTLE
throttle은 이벤트를 일정 주기마다 한 번씩만 발생하도록 해 준다. 이벤트가 한 번 실행되면 설정한 시간이 지나고 나야 다음 이벤트를 실행할 수 있다.
뭔가 비슷해 보이는데... debounce는 이벤트가 끊기지 않고(끊기지 않음의 기준은 설정시간!) 연속적으로 발생하면 전부 퉁쳐서 하나로 실행하는 반면, throttle은 이벤트가 계에에속 발생하면 그걸 설정시간마다 한 번씩 끊어서 실행해 준다고 보면 된다.
그래서 debounce는 사용자의 입력을 받는 input이나 change 이벤트에 주로 사용되고, throttle은 이벤트가 중단되지 않아도 계속적으로 감지해야 하는 scroll 이벤트에 많이 사용된다고 한다.
이론만 봐서는 정확히 이해가 안 가기 때문에...... 실제로 적용해 봄!
보편적으로 사용되는 라이브러리로 Underscore와 Lodash가 있는데, 파생인 Lodash가 더 최신이고 많이 쓰인다고 해서 Lodash를 이용했다.(단순~~) 설치와 사용법은 여기서!
(더 쉬운) throttle 먼저 도전했다. debounce가 어려웠던 이유는 아래에...🤦♀️
- 이벤트에 전달할 콜백함수를 throttle(이벤트에 전달할 콜백함수, 설정시간)로 덮어씌운다. 나는 필요한 함수만 import해서 썼는데, 만약 언더스코어(
_
)로 라이브러리 전체를 불러왔다면 _.throttle()의 형태로 써 준다.infiniteScroll = throttle(infiniteScroll, 1000);
- 그리고 원래 해당 콜백함수가 들어갈 자리에 그대로 넣어주면 끝! 완전 간단~~~
window.addEventListener('scroll', infiniteScroll);
사실 infiniteScroll이 인자를 받지 않는 함수이기 때문에 1번 과정 없이 addEventListner안에 그대로 throttle(infiniteScroll, 1000)을 써 줘도 된다!
근데 설정시간을 1초로 했더니 무한스크롤 기능에서 스크롤이 늦게 인식되어서 피드 데이터가 한 박자 늦게 불러와지는 불편함이 있었다😟 그렇다고 시간을 단축하자니 뭔가 성능 개선의 의미가 별로 없을 것 같아서... 고민하다가 스크롤이 맨 아래까지 갔을 때 말고, 한 500px정도 남았을 때 미리 fetch를 하면 되지 않을까?! 싶어서 infiniteScroll 내부의 조건식을 아래처럼 수정했다.
[수정 전]
if (scrollTop >= scrollHeight - clientHeight)
[수정 후]
if (scrollTop + 500 >= scrollHeight - clientHeight)
그랬더니 큰 불편함 없이 잘 작동함 >.<
콘솔창을 보면 throttle 적용 전보다 확연히 찍히는 빈도가 줄었다!
쪼끔 더 복잡했던 input에 debounce 적용.
앞선 scroll 이벤트는 window 전역 객체에 이벤트리스너를 걸어야 해서 addEventListner
를 사용한 반면, input 이벤트는 React로 프로젝트를 수정하면서 onInput
property로 부여했다. 전자는 Browser Native Event이고, 후자는 Synthetic Event(합성이벤트)라고 한다. 그게 그건 줄 알았는데....😐
Synthetic Event?
합성이벤트의 가장 큰 특징은 Event Pooling! 대충 요약하자면 이벤트마다 각각의 객체를 가지는 브라우저 네이티브 이벤트와 달리, 합성이벤트는 모든 이벤트의 정보를 합성 이벤트 풀(Synthetic Event Pool)에 담아 놓고, 해당 이벤트들이 하나의 합성 이벤트 객체를 재사용한다고 한다. 따라서 특정 이벤트가 발생하면 합성 이벤트 풀에서 합성 이벤트 객체에 해당 정보를 넘겨 이벤트를 실행하고, 실행이 끝나면 합성 이벤트 객체는 초기화되어 모든 속성값이 null이 된다.
그런데 throttle과 debounce는 이벤트의 동기적 실행을 막는 메서드이기 때문에, 이벤트를 강제로 중단시켰다가 후에 다시 발생시키려고 하면 이미 이벤트 객체가 초기화되어서 에러가 뜨는 것...!
구글링을 통해 이벤트 핸들러를 두 부분으로 나눠서 해결하는 방법을 알아냈다.
onInput
에는 debounce 되지 않은 inputHandler를 전달하고,기존의 inputHandler는 이런 형태였는데,
inputHandler = (e) => { setState({ comment: e.target.value }); setInput(e.target.value); }
이 inputHandler를 바로 debounce하는 게 아니라 inputHandler 내에서 e.target.value를 전달해 호출할 별도의 함수(debounceHandler)를 생성했다.
inputHandler = event => { debounceHandler(event.target.value); }; debounceHandler = (value) => { setState({ comment: value }); setInput(value); };
그리고 throttle에서 했던 것처럼 debounceHandler를 debounce로 덮어씌운다! 마찬가지로 Lodash 전체를 임포트했다면 _.debounce(함수, 설정시간).
debounceHandler = debounce(debounceHandler, 500);
끝~~ 이 아니라 이 코드에는 아주아주 큰 문제가 있었는데...... 댓글창에서 아무리 멀 입력해도 input창에 출력이 되지 않았음ㅠ!!!
그 이유는 바로 setState({ comment: value }) 때문이었다ㅡ.ㅠ 저게 input창의 value를 설정하는 코드인데, 댓글을 입력한 후 댓글창을 비워주기 위해 설정해 둔 거였다. 근데 쟤는 타이핑할 때마다 당연히 반영이 되어야 하는데, 쟤까지 debounce 시켜버렸으니 입력이 제대로 될 리가 업지.....
inputHandler = event => {
debounceHandler(event.target.value);
setState({ comment: value });
};
debounceHandler = (value) => {
setInput(value);
};
그래서 쟤는 inputHandler 안에 다시 넣어주고, 댓글내용을 저장할 input값을 바꿔주는 함수만 debounce되도록 했다.
우여곡절 끝에 완성~~~ 비록 겉에서 볼 땐 아무 차이도 없지만...😞 성능이 향상(사실 이것도 필요없지만...)되었을 거라고 믿자ㅎ.ㅎ 직접 적용해 보니까 제대로 이해되는 느낌! 나름 재밌고 유익했다🥰
Lodahs
JS Lodash 튜토리얼 :: PROGRAMMING ETC
[React] Debounce SyntheticEvent | DailyEngineering
React | 리액트 합성 이벤트와 이벤트 풀링 | react-native-seoul
와 도은님 한 박자 늦게 불러와지는거 저는 그냥 위코드 와이파이가 느리고 우분투고 해서 그런줄 알았는데~!!! 배워갑니다 ❤👏👍💯