서비스에서 잦은 리렌더링이 발생한다면 성능 저하의 원인이 될수도 있습니다. 잦은 리렌더링의 문제점은 다음과 같습니다.
- 성능 저하: 컴포넌트가 불필요하게 재렌더링되면 성능이 저하될 수 있습니다. 특히 컴포넌트 계층 구조가 깊거나 복잡한 경우에 잦은 리렌더링은 성능에 미치는 영향이 더 커집니다.
- 불필요한 데이터 요청: 컴포넌트가 리렌더링되면 해당 컴포넌트 내의 데이터 요청도 발생할 수 있습니다. 불필요한 리렌더링은 따라서 불필요한 데이터 요청을 유발할 수 있습니다.
- 컴포넌트 state 손실: 잦은 리렌더링으로 인해 컴포넌트의 상태(state)가 예상치 못하게 잃어버릴 수 있습니다.
그러니 잦은 리렌더링을 하면 안됩니다.
이벤트 호출도 예외는 아닙니다. 이벤트 발생할때 관련 콜백 이벤트 함수를 자주 호출하게 된다면 성능 저하의 원인이 될 것입니다. 예를 들어 클릭이벤트처럼 단순하지 않고 키보드의 입력이 지속적으로 발생하거나 스크롤 이벤트가 생기면 (특히, 스크롤 이벤트) 한번의 마우스 롤링으로도 몇십-몇백번의 이벤트가 발생해버립니다.
만약, 이런 이벤트가 일어날때마다 api요청을 보내게 된다면 서버 과부하가 일어납니다. 그렇기 때문에 유의미한 시점에 이벤트를 처리하기 위해 아래 두 개념을 적용합니다.
Debounce
연이은 이벤트 중 마지막 이벤트만 인식한다.
디바운스를 적용하기 좋은 예제는 바로 검색 입력 이벤트입니다.
예를들어 검색창에 ‘유시온’을 입력할 경우 모든 입력에 요청을 보내는 것이 아닌 입력이 마무리 되었을 때 요청을 보내는 것입니다.
만약 디바운스가 적용이 안되어있다면 무의미안 단어에도 불필요한 요청을 보낼 것입니다. ex. ㅇ, 유ㅅ, 유시오, 유시온
Debounce 구현
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} />
</>
);
}
주요 코드는 아래와 같습니다.
useEffect(() => {
const debounce = setTimeout(() => {
return setQuery(tmpQuery);
}, 300); //->setTimeout 설정
return () => clearTimeout(debounce); //->clearTimeout 바로 타이머 제거
}, [tmpQuery]); //->결국 마지막 이벤트에만 setTimeout이 실행됨
- useEffect에서 tmpQuery의 변경을 감지한다. tmpQuery가 변경될 때마다 내부 코드 블록이 실행된다.
- setTimeout 함수를 호출하여 300ms 후에 실행되는 타이머를 설정한다. 이 타이머는 setQuery(tmpQuery); 를 호출한다.
- 이때, tmpQuery는 useEffect의 의존성 배열에 포함되어 있으므로, tmpQuery가 변겨될때마다 새로운 타이머가 설정된다.
- 마지막 변경된 tmpQuery에 대한 setQuery가 호출된다.
- useEffect의 반환문에서는 이전 타이머가 존재하면(clearTimeout) 그 타이머를 취소한다. 이로써 이전에 설정된 타이머는 실행되지 않게 됩니다.
⇒ 따라서, 연속적으로 tmpQuery가 변경되더라도, 마지막 변경 이후 300ms가 지난 후에setQuery(tmpQuery)가 실행되므로, 마지막 이벤트에 대한 처리만 보장됩니다.
Debounce → Throttle
디바운스를 적용한 예제를 쓰로틀링으로 바꾸어 보겠습니다. 주요코드를 다음과 같이 바꾸었습니다.
const [throttle, setThrottle] = useState<boolean>(false);
useEffect(() => {
if (throttle) return;
if (!throttle) {
setThrottle(true);
setTimeout(async () => {
setSearchValue(tmpValue);
setThrottle(false);
}, 300);
}
}, [tmpValue]);
입력할 때 마다 아래 리스트가 깜빡거리는 현상을 확인하실 수 있습니다.
입력 이벤트를 쓰로틀링으로 구현한다면 큰 문제가 있습니다. 0.3초 간격으로 이벤트를 감지하는데 입력 값을 ‘이ㅅ’으로 인지할 수 있다는 것입니다. 입력을 ‘이승제’라고 했어도 찰나의 순간으로 tmpValue를 이상한 값으로 인지하여 사용자는 원하지 않는 화면을 볼 수 있습니다.
Throttle
이벤트가 발생하고서 일정 주기마다 이벤트가 발생되도록 한다. (일정주기가 끝나지 않으면 이벤트를 호출하지 않는다.)
쓰로틀링을 적용하기 좋은 예제는 바로 스크롤이나 페이지 resize 이벤트입니다.
만약 스크롤 이벤트에 큰 연산 이벤트 함수가 있는데 페이지를 위에서 아래로 한꺼번에 스크롤 한다고 생각해봅시다. 그렇다면 모든 초마다 스크롤 이벤트가 호출되어서 서비스 성능 저하의 원인이 될것입니다. 그래서 스크롤을 1초동안할때 계속 이벤트 함수를 호출하는 것이 아닌 특정 시간마다 이벤트가 발생되는 쓰로틀링을 적용할 수 있습니다.
Throttle 구현
export default function useGetWindowScrollHeight() {
const [windowScrollHeight, setWindowScrollHeight] = useState<number>(0);
let throttling = false;
function onScroll() {
if (throttling) return;
throttling = true;
setTimeout(() => {
setWindowScrollHeight(window.scrollY);
throttling = false;
}, 200);
}
useEffect(() => {
setWindowScrollHeight(window.scrollY);
window.addEventListener('scroll', onScroll, { passive: true });
return () => {
window.removeEventListener('scroll', onScroll);
};
}, []);
return windowScrollHeight;
}
주요 코드는 아래와 같습니다.
let throttling = false;
function onScroll() {
if (throttling) return;
throttling = true;
setTimeout(() => {
setWindowScrollHeight(window.scrollY);
throttling = false;
}, 200);
}
- throttling 값이 true일 때는 이벤트를 실행하지 않는다.
- 0.2초 간격으로 throttling값이 바뀐다.
- 그래서 0.2초 마다 이벤트가 호출된다.
Throttle → Debounce
아래 화면은 스크롤 할때마다 0.3초씩 이벤트를 호출하고 있습니다. 그런데 만약 쓰로틀링을 디바운스로 바꾸게 된다면 어떻게 될까요?
디바운스를 적용한다면 스크롤을 하고 스크롤이 멈출 때 호출하기 때문에, 스크롤 하는 과정에서 애니메이션을 보지 못할 것입니다. 그렇기 때문에 중간 중간 이벤트를 호출해야
되는 상황(ex. 스크롤 이벤트)에서는 쓰로틀링을 사용하는게 좋을 것 입니다.
마무리
Debounce와 throttle를 프로젝트에 적용시킨다면 사소하지만 프로젝트의 성능을 높이고 사용자 경험을 향상시킬 수 있을 것입니다. 두 기법 중 어떠한 것을 적용시키는게 좋을 지 고민해보시고 해당 글이 도움되셨길 바랍니다!
References
'DEV' 카테고리의 다른 글
git rebase 전략 (0) | 2024.02.21 |
---|---|
Shadcn/ui가 무엇이고, 왜 사용할까? (0) | 2024.02.21 |
JavaScript IIFE란? (0) | 2024.01.02 |
Git 왜 쓰는거야? (1) | 2024.01.02 |
PNPM으로 모노레포 구축하기 (0) | 2024.01.02 |