
사용자의 입력이 유실되지 않도록 입력 중간 중간마다 입력값을 로컬스토리지에 임시 저장하는 기능을 만들고 있었습죠.
작업 중 매번 이벤트 발생 시마다 저장기능 함수를 호출하거나, setInterval 등으로 끊임없이 저장기능 함수를 호출하게 된다면, 불필요하게 과도한 호출이 발생하는거 아닐까? 라는 생각이 들었습니다.
ㅎㄷㄷ 운명적이게도, 구독하고 있던 기술메일에 디바운스와 쓰로틀과 관련된 내용을 받았고, 이거다! 싶어서 적용해보았습니다.
간단한 개념 정리와 함께 내가 왜 이 기술을 선택했는지 기록해보려고 합니다.
디바운스와 쓰로틀
- 둘 다 이벤트 핸들러가 너무 자주 실행되지 않도록 조절하는 기법. 하지만 동작 방식에 차이가 있습니다.
디바운스?
- 이벤트가 반복 발생했을 때, 마지막 이벤트만 실행되도록 만들어주는 함수입니다.
- 즉, 이벤트가 멈춘 후 일정 시간이 지난 뒤에 실행된다고 보시면 됩니다.
- 예제
// JS
const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
// TS
// return 값 x라 void로 처리해두고,
// this 보존 x의 케이스 입니다. 추가 정보는 하단 더보기 참고
const debounce = <F extends (...args: any[]) => void>(
fn: F,
delay: number
): (...args: Parameters<F>) => void => {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<F>): void => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
// TS
// return 반환 및 체크 버전
const debounce = <F extends (...args: any[]) => any>(
fn: F,
delay: number
): (...args: Parameters<F>) => ReturnType<F> => {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<F>): ReturnType<F> => {
clearTimeout(timer);
return fn(...args);
};
};
+) 리턴 타입이나 this 보존이 필요한 경우
그래서 실제 적용부분에선 추가로 ReturnType 유틸을 쓴다거나, apply(this, args) 처리 같은 경우는 제외했는데, 구현하면서 살펴보니 this 관련해서 이슈가 있는 것 같네요.
Typescript Debounce
Typescript Debounce. GitHub Gist: instantly share code, notes, and snippets.
gist.github.com
쓰로틀?
- 이벤트가 반복 발생 했을 때, 일정 시간 간격으로만 이벤트가 실행되도록 제한하는 함수입니다.
- 즉, 이벤트가 발생하고 지정된 시간마다(일정 주기마다) 실행됩니다.
- 이벤트 끝나면 끝이다. 이벤트 주루룩 계속 받을 때 설정한 기간마다 이벤트가 발생되도록 하는 것이다.
- 예제
// JS
const throttle = (fn, delay) => {
let lastTime = 0;
return (...args) => {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn(...args);
}
};
};
// TS
// 이것도 return void 케이스
const throttle = <F extends (...args: any[]) => void>(
fn: F,
delay: number
): (...args: Parameters<F>) => void => {
let lastTime = 0;
return (...args: Parameters<F>): void => {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn(...args);
}
};
};
// TS
// return 있고 체크 필요할 때
const throttle = <F extends (...args: any[]) => any>(
fn: F,
delay: number
): (...args: Parameters<F>) => ReturnType<F> => {
let lastTime = 0;
let lastResult: ReturnType<F>;
return (...args: Parameters<F>): ReturnType<F> => {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
lastResult = fn(...args);
}
return lastResult!;
};
};
적용기
저는 디바운스를 선택했습니다.
사용자의 연속적인 입력에 대해 불필요한 저장 반복을 방지했어야 하는건 뭐 공통 요소라 치더라도,
키포인트는 쓰로틀 써서 사용자가 계속 입력할 때 굳이 주기적으로 값을 받을 필요가 없었기 때문입니다.
보통 입력 멈추고 브라우저 창을 잘못 닫았을 때나 새로고침을 잘못 눌렀을 때 등에 대응하기 위한 기능이었기 때문이죠. (입력중에 정전나면 어쩔꺼냐 하면 저도 할말이 없습니다)
작업 코드 일부를 보자면 이렇게 적용했습니다.
//... 중략
// 대략 로컬스토리지 저장 함수
const save = () => {
// ex) localStorage.setItem('draft', JSON.stringify(data));
};
// 디바운스 (1.5초)
const debouncedSave = debounce(save, 1500);
// 인풋 이벤트 발생 시
document.querySelector('#write_board')
.addEventListener('input', debouncedSave);
야이놈아 그렇다면 쓰로틀은 그럼 어디서 쓰냐? 물을 수 있겠습니다만..
형님, 이벤트가 주루룩 계속 들어올 때 일정 주기마다 처리해 주는 게 쓰로틀입니다.
대표적인 예로 무한 스크롤이 있겠습니다.
스크롤 이벤트를 그대로 쓰면? 브라우저가 이벤트 폭격을 맞아서 성능이 참도 좋아지겠죠.
이럴 때 쓰로틀로 감싸서 내가 설정한 주기마다 한 번씩만 실행되도록 제한하면 깔끔.
// 무한 스크롤 예시
const loadMore = () => {
console.log('다음 페이지 데이터 로드');
};
window.addEventListener('scroll', throttle(() => { // 쓰로틀 적용
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 5) {
loadMore();
}
}, 200));
참고로 저는 아름다운 사내 시스템 덕분에 생짜 debounce 함수를 하나 만들어서 적용시켰지만요?
여러분들은 이미 잘 나와있는 lodash나(_.debounce, _.throttle) 보니까 npm에 use-debounce 같은것도 있던데 그런걸로 편하고 달콤하게 잘 넣으시길 바라겠습니다.
읽어주셔서 감사합니다!
'개발 > 개발 아카이브' 카테고리의 다른 글
| URL 파라미터 정리하기: JavaScript와 React에서 (0) | 2025.12.30 |
|---|---|
| 동기화 안 되는 main-develop, PR Cherry-pick 전략으로 배포 프로세스 개선해보기 (1) | 2025.10.18 |
| JavaScript 소수점 시리즈 - ceil, round, floor, trunc (1) | 2025.08.07 |
| 웹 스토리지 (로컬 스토리지 / 세션 스토리지) 개념과 조작법 그리고 사용하기 좋은 상황에 대하여 (0) | 2025.07.16 |
| 간단하게 예제로 알아보는 코드의 자유도 (0) | 2025.02.13 |