금쪽이 육아방법 : throttle, debounce에 관하여
🙃 들어가며
최근에 스웨거챗이라는 오픈소스 프로젝트를 진행했다. 스웨거 문서에 대해 더이상 백엔드 개발자에게 물어보는 것이 아닌, 챗을 통해 쉽게 질문하고 답변을 얻을 수 있는 서비스이다.
MVP 제작 후 받은 피드백 중 하나는 채팅에 디바운스 걸어주세요!
, 엔터가 빠르면 여러번 눌립니다!
였다. 해당 피드백을 받고나서 아차!싶었다. 뭔가 기본적인 걸 놓친 느낌? 그러면서도 기본적인 거라고 생각했으면서도 그동안 내가 한 프로젝트에서 디바운스 신경을 얼마나 썼었나? 생각이 들었다. 더 나아가서 debounce가 뭐고 throttle이 뭔지 내가 제대로 알기는 하나 라는 생각까지 이어졌다.
그래서 오늘은 debounce와 throttle, 그리고 useDebounce, useThrottle까지 살펴볼 계획이다.
🙃 debounce, throttle
나는 debounce와 throttle 필요성을 절실히 느낄 때는 동료 개발자들에게 프로젝트 피드백을 받을 때이다. ‘제가 이번에 이런 거 만들어봤는데 한 번 봐주세요’하면 열 중에 아홉은 미친듯한 버튼 광클을 한다. 내 프로젝트를 터지게 하기 위한 그들의 미친듯한 노력을 보며 항상 디바운스 혹은 쓰로틀을 걸어야겠다고 다짐했던 것 같다.
이게 무슨 이야기인지 조금 더 자세하게 이야기해보면 다음과 같다. 스웨거챗에는 AI에게 스웨거에 대한 질문 혹은 대화를 챗으로 보낼 수가 있다. 이때 유저가 광클을 했다고 가정해보자. 그러면 광클의 횟수대로 유저의 메세지는 AI에게 보내질 것이며 그만큼 AI 사용 기회가 차감될 것이다. 광클을 막지 않으면 무분별한 서버 요청이 발생하고 이는 큰 손실로 이어질 것이다.
그러면 이런 금쪽이같은 유저를 막기 위해서는 어떻게 해야할까? 광클 금지라는 문구로는 그들을 막을 수 없을 것이다. 그때 우리에게 필요한 것이 바로 debounce와 throttle이다.
광클을 막는다는 같은 목표 아해 debounce와 throttle이 존재하지만, 약간의 차이가 있다. 광클을 좋아하는 금쪽이 자식이 있다고 생각해보자.
그러면 먼저 throttle 부모(throttledNo)를 보면 아래와 같다.
throttle은 즉각적인 반응을 보인다. 하지만 이후 3초 동안은 무슨 일이 있어도 반응을 안 한다. 그러다가 3초가 지나면 다시 반응을 하는 것이다.
반면에 debounce 부모(debouncedNo)는 약간의 차이가 있다.
throttle 부모보다 더 인내심이 있다고 볼 수 있다. 난리를 피우면 아무런 반응도 해주지 않다가 금쪽이가 멈춰야 그제서야 반응을 해준다.
🙃 lodash 속 debounce, throttle
그러면 이제 비유에서 더 발전하여 lodash 실제 코드를 보면서 debounce와 throttle에 대해서 알아보자
debounce와 throttle은 프로그래밍에서 오래전부터 존재해온 일반적인 프로그래밍 패턴이다. 그런데 아무래도 Lodash는 이 패턴들을 더 안정적이고 사용하기 쉽게 구현하여 인기를 얻었기 때문에 이를 기준으로 살펴보고자 한다.
debounce
기본 매개변수부터 살펴보면
func
는 실행하고자 하는 함수이다. 그리고 wait
는 대기 시간 (밀리초)을 의미한다. 이때 설정 객체 options
가 존재하는데 첫 번째 호출 즉시 실행 여부인 leading
, 마지막 호출 후 실행 여부인 trailing
, 최대 대기 시간인 maxWait
가 있다.
상태 변수 역시 여러 개가 존재하지만 주요 상태 변수만 뽑아보면 아래와 같다.
여러 함수들이 있지만 핵심인 debounced()만 보고자 한다.
금쪽이가 처음 떼를 쓰기 시작한다. (leading: true
일 때) 여기서 leading 옵션 true는 일반적인 debounce와 약간의 차이가 있다. 일반적인 debounce는 조용히 기다리지만, leading: true
로 설정하면 첫 호출에는 즉시 반응하고, 그 이후 호출들은 무시하는 방식으로 동작한다.
금쪽이가 “으아아아ㅏㅏㅏㅏ악” 하면서 떼쓰기 예열을 준비하면, 다시 말해 leading이 true면 부모는 가차없이 “안돼”라고 외치면서 타이머를 시작한다.
하지만 여기서 멈추면 금쪽이가 아니다. 금쪽이는 계속 떼를 쓴다. 이제 악다구를 넘어서서 바닥에 널브러진다.
debounce 부모는 강하다. 타이머가 이미 실행 중이면 새로운 타이머로 리셋해버린다.
어느새 금쪽이도 진정을 하고 울음을 그친다. 마지막 떼쓰기가 끝나고 wait 시간이 지나면 debounce 부모는 "이제 진짜 안돼!"라고 말하면서 끝낸다.
정리하면 위와 같은 느낌이다.
throttle
기본 구조는 아래와 같다.
throttle의 경우 debounce를 사용하고 있다는 점이 특이하다.
이 코드를 보면 결국 throttle은 maxWait: wait
를 설정한 debounce라고도 할 수 있다는 사실을 알게 된다. 이게 무슨 의미나면, debounce는 원래 "금쪽이가 조용해질 때까지" 기다리지만 maxWait
을 설정하면 "아무리 금쪽이가 떼를 써도 이 시간이 지나면 반응한다"는 의미이다. 오은영 박사의 훈육 방식에 더 가깝다고 볼 수 있다. deounce 부모 보다 더 반응을 잘 해준다.
금쪽이가 처음 떼를 쓰면 즉시 "안돼!" (leading: true)라고 throttle 부모는 외친다. 3초 동안은 아무리 떼를 써도 무시한다. 하지만 3초가 지나면 반응을 해준다. "안돼!" (trailing: true)를 외치고 또 3초 동안 무시하고 이런 과정이 계속 반복되는 것이다.
정리하면 위와 같은 느낌이다.
🙃 useDebounce, useThrottle
나는 그동안 debounce, throttle 개념을 어떻게 구현했을까? lodash를 통해서 구현하는 방식을 택했었다. 그런데 이번에 새로 알게된 것이 바로 useDebounce와 useThrottle이다.
그러면 왜 훅을 활용하는걸까? 이유는 간단하다.
lodash를 직접 사용할 때보다 React의 생명주기에 맞게 동작한다. lodash 직접 사용하면 컴포넌트가 리렌더링될 때마다 새로운 throttle 함수 생성된다. 하지만 훅을 사용할 경우 의존성 배열이 변경될 때만 새로운 throttle 함수 생성되게 된다.
또한 메모리 누수 방지도 가능해진다. 컴포넌트가 언마운트되어도 throttle의 타이머가 계속 남아있을수도 있다. 하지만 훅을 사용할 경우 useThrottle 내부에서 자동으로 throttle.cancel() 호출해서 메모리 누수를 방지한다.
마지막으로 불필요한 리렌더링 방지까지 된다.
그러면 잘 만든 useDebounce, useThrottle 훅 예시를 어디서 볼 수 있을까? 나는 @toss/slash의 useDebounce와 useThrottle을 선정하여 살펴보기로 했다.
두 훅은 구조가 매우 비슷하다.
매개변수부터 보면
그리고 사용하고 있는 핵심 훅 같은 경우 아래와 같다.
usePreservedCallback
은 콜백 함수의 안정적인 참조를 유지한다. 리렌더링되어도 함수의 참조가 유지되어서 불필요한 리렌더링을 방지하게 한다. usePreservedReference
는 options 객체의 안정적인 참조를 유지한다. 객체의 내용이 같다면 참조도 같게 유지해준다.
이제 debounce/throttle 함수 생성하는데
useMemo
를 사용해서 디바운스/쓰로틀된 함수를 메모이제이션하는 것을 볼 수 있다. 의존성 배열의 값([preservedCallback, wait, preservedOptions])이 변경될 때만 새로운 함수를 생성하는 식으로 React의 생명주기에 맞게 동작한다.
마지막으로 컴포넌트가 언마운트되거나 디바운스/쓰로틀된 함수가 새로 생성될 때 이전 타이머를 정리해서 메모리 누수를 방지한다.
🙃 SwaggerChat에 적용해보기
마지막으로 간단한 채팅 입력창 예시를 들어보도록 하겠다.
성능 최적화 (불필요한 함수 생성과 호출 방지), 메모리 관리 (자동 클린업), React의 생명주기와의 조화, 타입 안정성 (TypeScript 지원). 매우 귀찮고 어려워 보이는 이 모든 것들을 훅을 사용해서 debounce와 throttle을 구현하면 이 모든 것을 자동으로 처리해준다. 나는 이제 단순히 훅을 사용하기만 하면 되고, 복잡한 내부 로직은 신경 쓰지 않아도 되게 되는 것이다.
열심히 글을 썼는데 마무리를 어떻게 해야할지 모르겠다. 스웨거챗 많관부.