선요약 - 클로저를 이용한 쓰로틀링 util함수
const numberElem = document.querySelector(".number");
const buttonElem = document.querySelector(".add-button");
function add(elem) {
elem.innerText++;
}
function createThrottle() {
const timeoutRef = { id: null };
const throttle = (fn, timeout = 500) => {
if (timeoutRef.id) {
return;
}
fn();
timeoutRef.id = setTimeout(() => {
timeoutRef.id = null;
}, timeout);
};
return throttle;
}
const throttle = createThrottle();
function handleFirstNumberClick() {
throttle(() => add(numberElem));
}
function initFirstNumberClickEvent() {
buttonElem.addEventListener("click", handleFirstNumberClick);
}
initFirstNumberClickEvent();
클로저를 이용해 쓰로틀링 util함수화 하기
1. 기초적인 방식의 쓰로틀링
count를 올리는 클릭이벤트에 대해 쓰로틀링을 걸어보자
클릭을 연달아 빠르게 해도 쓰로틀링이 걸려 count는 클릭 수 보다 적게 올라간다.
잘 작동하지만 로직이 한군데에 뭉쳐있어 어지럽다. 로직을 분리해 보자.
2. 로직을 분리한 쓰로틀링과 문제점
function add() {
numberElem.innerText++;
}
let timeoutID;
function throttle(fn, time) {
if (timeoutID) return;
timeoutID = setTimeout(() => {
fn();
timeoutID = null;
}, time);
}
function handleClick() {
throttle(add, 500);
}
buttonElem.addEventListener("click", handleClick);
보기좋게 나누었고 얼핏 throttle 이란 함수도 재사용할 수 있어 보이지만 전역변수 timeoutID 에서 기인하는 몇 가지 문제가 있다.
- 전역공간을 더럽힌다는 것 자체로 한가지 문제
- throttle함수를 재사용하게 된다면 여러 이벤트에서 timeoutID를 공유하게 되어 잠재적으로 문제발생
문제상황
두번째 이벤트가 작동하지않는다. 두번째 이벤트가 발생하기 이전에 첫번째 이벤트에 의해 타임아웃ID가 부여되기 때문이다. 이를 해결하기 위해 전역번수 timeoutID를 캡슐화 해보자
3. 캡슐화
const firstNumberElem = document.querySelector(".number");
const secondNumberElem = document.querySelector(".second-number");
const buttonElem = document.querySelector(".add-button");
function add(elem) {
elem.innerText++;
}
function throttle(fn, timeoutRef, timeout = 500) {
if (timeoutRef.id) {
return;
}
timeoutRef.id = setTimeout(() => {
fn();
timeoutRef.id = null;
}, timeout);
}
function handleFirstNumberClick(timeoutRef) {
throttle(() => add(firstNumberElem), timeoutRef);
}
function handleSecondNumberClick(timeoutRef) {
throttle(() => add(secondNumberElem), timeoutRef);
}
function initFirstNumberClickEvent() {
let timeoutRef = { id: null }; // 원시값을 사용해선 안됨
buttonElem.addEventListener("click", () => handleFirstNumberClick(timeoutRef));
}
function initSecondNumberClickEvent() {
let timeoutRef = { id: null };
buttonElem.addEventListener("click", () => handleSecondNumberClick(timeoutRef));
}
initFirstNumberClickEvent();
initSecondNumberClickEvent();
전역 변수를 없앴고 잘 작동한다. timetoutRef 자리에 이전처럼 timeoutID로 원시값(null)을 넣게되면 이벤트핸들러의 인자로 값 자체가 들어가기 때문에 항상 id는 항상 null이 될 것이고 정상작동하지 않을것이다. 그를방지하기 위해 객체를 만들어 참조주소를 이벤트핸들러의 인자로 전달한다. 하지만 아직도 문제는 존재한다.
- throttle이라는 함수를 사용 할 때마다 객체를 수동으로 하나씩 만들어야함
- 함수가 동작하기 위해서 외부요인이 필요하기 때문에 유틸함수로 사용하기에 애매하고 불편
클로저를 이용해 이를 해결해 보자.
4. 클로저를 이용한 캡슐화 및 재사용성 확보
전역변수 캡슐화에 성공했고 재사용가능한 형태의 throttle 함수도 만들어졌다.
예전에 만들어두었던 웹 포트폴리오의 풀스크린 스크롤 기능을 리팩토링 하려다 이렇게까지 오게됐다....
lodash를 사용했다면 고민거리도 없었겠지만 그래도 클로저도 나름 실용적으로 써보고 좋은 공부가 된 듯하다.