새로운 과제를 하면서 debounce를 접할 일이 생겼다.
예전 쇼핑하우 프로젝트를 할 때 처음 썼던 debounce..
제대로 정리하지 못하고 넘어간 것 같아 항상 머리 속에 정리해야지라는 미련(?)이 떠나질 않았는데 이번 과제를 정리할 기회를 얻게 되어 기쁜 마음으로 과제를 했다.
그리고 리액트로 직접 디바운스나 쓰로틀링을 정리한 자료는 거의 찾기 힘들었었는데, loadash나 underscore를 사용하지 않고 나처럼 직접 구현하는 사람들에게 도움이되는 자료가 되길 바란다.
그럼 이제 진짜 정리해봐야지!
이벤트 발생과 그에 따른 이벤트핸들러 콜백함수를 다루는데 있어 중요한 개념이다.
클릭이벤트처럼 단순하지 않고 키보드의 입력이 지속적으로 발생하거나 스크롤 이벤트가 생기면
(특히, 스크롤 이벤트) 한번의 마우스 롤링으로도 몇십-몇백번의 이벤트가 발생해버린다.
만약, 이런 이벤트가 일어날때마다 api요청을 보내게 된다면 서버 과부하가 일어난다.
때문에 유의미한 시점에 이벤트를 처리하기 위해 아래 두 개념을 적용한다.
debounce : 연이은 이벤트 중 마지막 이벤트만 인식!
throttle : 이벤트가 발생하고서 일정 주기마다 이벤트가 발생되도록 한다.
(일정 주기가 끝나지 않으면 이벤트를 호출하지 않는다.)
이번 과제 중 이 두 개념을 적용할만한 부분은
과제를 수행하며 무한스크롤을 디바운스로 적용한 점이 아쉬웠다.
디바운스가 이벤트가 끝나고 나서 발생하는 것이니 검색어를 입력할 때는
사용자가 검색하고자 하는 단어 타이핑이 마쳤을 때 api요청을 하는게 자연스러운데
(만약 디바운스가 적용안되어있었다면
무의미한 검색어에 대해서도 api요청을 보낼 것이다.
ex_디바운스 입력: 딥,디방,디바운ㅅ 와 같이...)
스크롤의 디바운스는 사용자가 스크롤 동작을 마쳤을 때 api요청을 보내니 다음 데이터를 보기 위해 잠시의 로딩시간을 겪어야 한다. (스크롤이 연이어 발생하는게 아니라 잠시 정지(->디바운스 시간 + 데이터 요청시간)하는 느낌이 든다)
반면, 스크롤 쓰로틀링이 적용된 경우 스크롤 이벤트가 발생하고 주기적인 시간별로 계속 스크롤 좌표를 확인한 후 요청을 보내니 정지시간없이 다음 데이터를 부드럽게 볼 수 있다. (물론 주기가 길면 정지텀이 있겠다)
과제 적용 케이스) 검색창에 검색어(permission) 입력
function SearchInput() {
const [query, setQuery] = useState('');
const [tmpQuery, setTmpQuery] = useState(query);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => setTmpQuery(e.target.value);
useEffect(() => {
const debounce = setTimeout(() => {
return setQuery(tmpQuery);
}, 300); //->setTimeout 설정
return () => clearTimeout(debounce); //->clearTimeout 바로 타이머 제거
}, [tmpQuery]); //->결국 마지막 이벤트에만 setTimeout이 실행됨
return (
<>
<SearchInputBlock>
<div className='search-Input'>
<SearchIcon />
<input value={tmpQuery} onChange={handleChange} />
</div>
</SearchInputBlock>
<SearchResult query={query} />
</>
);
}
실제 입력 비교하기)
콘솔창 확인~!
과제 적용 케이스) 무한스크롤
이 부분은 원래 디바운스롤 구현했었는데 위에서 얘기한 바와 같이
스크롤 이벤트가 끝난 이후 약간의 정지시간이 눈에 보여👀 쓰로틀링으로 재 구현했다.
결론 먼저 말하자면 스크롤은 쓰로틀링으로 구현하는 것이 👍훨씬 자연스러웠다.
로딩이 멈춘듯한 지연시간이 해결되었고
기존에는 스크롤의 좌표값을 구해서 다음 페이지 정보를 요청할지 말지를 정해야했는데,
리팩토링 이후 스크롤 좌표값 따위는 필요하지 않았다.)
코드가 훨 깔끔해져 만족스럽다.
function SearchResult({ query }: searchResultProp) {
const [page, setPage] = useState(1);
const [result, setResult] = useState<Array<any>>([]);
const [throttle, setThrottle] = useState(false);
const handleScroll = () => {
if (throttle) return;
if (!throttle) {
setThrottle(true);
setTimeout(async () => {
setPage((page) => page + 1);
setThrottle(false);
}, 300);
}
};
useEffect(() => {
if (query.length === 0 || isEmpty || page === 0) return;
(async () => {
try {
const data = await getSearchResult(query, page);
if (data.length) setResult((result) =>
(result === data ? [...data] : [...result, ...data]));
} catch (e) {
setResult([]);
}
})();
}, [page]);
return (
<SearchResultBlock onScroll={handleScroll}>
{result.map((data: searchDataType) =><ProductCard key={data.id}/>}
</SearchResultBlock>
);
}
실제 쓰로틀 비교하기)
쓰로틀을 통해 이벤트를 처리할 때 page박스가 5장씩 추가되게 구현하였다. (5단위 증가됨)
연속적인 스크롤에도 나름 부드럽게 다음 페이지들을 보여준다.
직접 디바운스와 쓰로틀링을 구현하고 적용 전후를 비교해보면서 꽤 많이 이해하게 된것 같다.
추가로 더 하고 싶은 작업은 이런 디바운스, 쓰로틀링을 커스텀 훅처럼 함수화 시켜 사용해보고 싶다.
useRef로 적용대상을 지정하고 콜백함수와 time을 인자로 정해주는 방식으로 구현할 것 같다.
근데, 실제 현업에서는 그냥 라이브러리 쓰겠지? 😛
추가공부: TODO
좋은 예시 잘 봤습니다. 그런데 무한 스크롤링을 쓰로틀링로 구현하기에 아쉬운게 유저가 스크롤을 아래로 내리지 않고 특정 지역에서만 위아래로 여러번 움직여도 스크롤 이벤트 함수가 호출되어서 쿼리를 계속 보내기 때문에 문제가 있을거 같네요