이전 토이프로젝트에서 setTimeout
을 사용하긴 했었는데 솔직히 말해, 대충 MDN
쓱 보고 돌린거라서
이번 책을 통해 공부해봤다.
함수를 명시적으로 호출하면 함수는 즉시 실행되며, 컨텍스트에서 실행된 순에 따라 실행된다.
이는 자바스크립트는 싱글 스레드 엔진이기 때문이다.
하지만 명시적으로 실행된 순이 아닌, 일정 시간이 경과 된 후에 호출되도록
함수 호출을 예약하려면 타이머 함수를 사용하면 된다.
이를 호출 스케줄링
이라고 한다.
자바스크립트는 타이머 함수인 setTimeout
, setInterval
이 존재한다.
이는 자바스크립트에서 정의된 빌트인 객체가 아닌, 브라우저와 Node.js 에서 제공하는 전역 객체의 메소드로서, 호스트 객체이다.
타이머 함수는 콜백 함수를 인수로 받아 특정 시간 이후 콜백 함수를 실행시킨다.
setTimeout
은 타이머가 지나면 함수를 한 번만 실행하고setInterval
은 타이머가 만료 될 때 마다 반복 호출한다.
자바스크립트는 싱글 스레드로 동작하기 때문에, 타이머 함수는 실행 컨텍스트와 무관하게 실행 되어야 한다.
그런 이유로 타이머 함수는 비동기 처리 방식으로 동작한다.
싱글 스레드 엔진 ?
한 번에 하나의 작업만 처리가 가능한 엔진을 일컫는다.
setTimeout / claerTimeout
setTimeout
의 함수 시그니처를 살펴보자
function setTimeout(handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]): number
setTimeout
은 heandler
, 즉 콜백 함수를 첫 번째 인수로 받는다 , 이후는 timeout
을 통해 딜레이 기간을 인수로 받는다.(ms , 1/1000초
)
이후 인수들은 heandler
에 들어갈 인수들을 명시적으로 지정해줄 수 있다.
setTimeout(() => {
console.log('hi~!');
}, 1000); // hi~!
setTimeout(
(name) => {
console.log(`hi~! ${name}`);
},
1000,
'dongdong',
); // hi~1 dongdong
함수 시그니처에서 setTimeout
의 반환 값은 number
라고 나온다.
이는 setTimeout
을 실행 할 경우 스케줄러를 참조 할 수 있도록
브라우저 환경에서는 숫자 , Node.js
환경에선 객체를 반환한다.
const timer = setTimeout(() => {
console.log('hi!');
});
console.log(timer);
/**
Timeout {
_idleTimeout: 1,
_idlePrev: [TimersList],
_idleNext: [TimersList],
_idleStart: 18,
_onTimeout: [Function (anonymous)],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: true,
[Symbol(kHasPrimitive)]: false,
[Symbol(asyncId)]: 2,
[Symbol(triggerId)]: 1
}
*/
함수가 반환한 객체를 이용해 clearTimeout
함수를 통해 타이머를 제거 할 수 있다.
const timer = setTimeout(() => {
console.log('hi!');
});
clearTimeout(timer); // 타이머가 삭제되어 콜백함수가 실행되지 않음
console.log('방가루'); // 방가루
setInterval / clearInterval
setTimeout / clearTimeInterval
과 동일하다.
다만 setInterval
은 콜백 함수가 반복 호출된다는 점이 다르다.
let num = 0;
const timer = setInterval(() => {
if (num < 5) {
console.log(`${num + 1}번째 호출!`);
} else {
console.log('반복을 종료합니다 ~!');
clearInterval(timer);
}
num += 1;
}, 1000); // 1번째 호출 2번째 호출 ... 5번째 호출 반복을 종료합니다 ~!
짧은 시간 간격 연속해서 발생하는 이벤트들이 있다.
예를 들어 스크롤 움직임이나, 마우스 움직임, 리사이즈 등이 그렇다.
...
<body>
<div>
<p class="result">0</p>
</div>
</body>
<script>
let num = 0;
const $result = document.querySelector('.result');
document.addEventListener('scroll', () => {
$result.textContent = num++;
});
</script>
...
마우스를 아래로 한 번만 내려도 scroll
이벤트 핸들러는 픽셀 단위로 호출된다.
예시를 통해서 디바운스와 스로틀을 이해해보자
현재 나는 다음과 같이 긴 글을 담은 태그와 스크롤바가 존재한다.
이 때 나는 스크롤바를 한 번 움직일 때 마다 배경 색이 변경되기를 기대한다.
여기서 한 번 움직임의 기준은 픽셀 단위가 아니라 마우스 휠을 한 번 돌리는 행위를 의미한다.
<script>
const $div = document.querySelector('div');
const changeBack = () => {
$div.style.backgroundColor = `
rgb(${Math.random() * 255} , ${Math.random() * 255} , ${
Math.random() * 255
})`;
};
$div.addEventListener('scroll', changeBack);
</script>
그래서 스크롤이 움직일 때 마다 배경색이 변경되는 이벤트 핸들러를 등록했다.
한 번 휠을 돌렸는데 돌아가는 동안 배경색이 변경되며 난리가 난다.
내가 원하는 것은 마우스 한 번 휠을 돌리는 액션 동안 한 번만 이벤트 핸들러의 콜백함수가 호출되기를 기대하는 것이다.
그럼 어떻게 해야 할까 ?
아.. 마우스 휠을 돌리는 동안은 변경되지 않고 모두 돌리고 난 후에 배경색이 변했음 좋겠는데 ..
그럴 때 사용하는 것이 디바운스
이다.
디바운스(Debounce
)는 연이어 호출되는 함수들 중 마지막 함수만 실행되도록 하는 패턴이다.
주로 이벤트 핸들러에서 사용되며, 사용자의 입력과 같이 짧은 시간 간격으로 발생하는 이벤트에 대한 성능을 최적화 하는데 유용하다.
주로 setTimeout
과 함께 사용된다.
어떻게 setTimeout
을 이용해서 디바운스를 사용 하는지 예시를 통해 살펴보자
마지막 액션 때만 배경색이 변하도록 changeBack
콜백 함수를 설정해주자
<script>
const $div = document.querySelector('div');
const changeBack = // 이전과 동일
const debounce = (callback, delay) => {
let timer;
return (...rest) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
callback(...rest);
}, delay);
};
};
const newChangeBack = debounce(changeBack, 100);
$div.addEventListener('scroll', newChangeBack);
</script>
함수만 보면 이게 뭔가 싶다.
적어도 나는 그랬다.
debounce
함수는 callback
함수와 얼만큼의 타이머를 가질지인 delay
를 인수로 받는다.
이후 지역 변수인 timer
를 참조하는 클로저인 새로운 콜백 함수를 반환한다.
이 때 return
문에 쓰인 ...rest
는 콜백함수 중 인수를 필요로 하는 콜백함수에게 인수를 넘겨주기 위해 사용되었다.
그러니 addEventListener
에 쓰인 콜백 함수에서의 ...rest
는 event
를 인수로 자동으로 받을 수 있게 해주는 것이다.
const newChangeBack = debounce(changeBack, 100);
$div.addEventListener('scroll', newChangeBack);
이 코드가 처음 실행 될 때를 생각해보자 (스크롤 휠이 1px 움직였을 때
)
const debounce = (callback, delay) => {
let timer; // 1. undefined
return (...rest) => {
if (timer) clearTimeout(timer); // 2. 실행되지 않음
timer = setTimeout(() => { // 3. timer 는 delay 이후 콜백함수를 실행함
// 실행하면서 timer 에는 숫자 혹은 객체가 할당됨
callback(...rest);
}, delay); //4. 째깍 째깍 타이머 시각 가는중 ..
};
};
타이머는 delay
동안 대기하면서 delay
가 지나면 콜백 함수가 실행된다.
만약 delay
가 지나기 전 scroll
이벤트가 또 발생하는 경우를 생각해보자
(...rest) => {
if (timer) clearTimeout(timer); // 1. timer 는 현재 값이 할당되어 있음으로 이전 timer는
// 제거함
timer = setTimeout(() => { // 2. 새로운 타이머 생성 !
callback(...rest);
}, delay);
};
};
와우
그러니 debouce
는 delay
이전에 이벤트가 발생하면 타이머를 삭제하고 새로 생성하기 때문에 마지막 호출 때에만 실행되는구나 !
구우웃 ~~
이처럼 짧은 시간 간격으로 이벤트가 연속해서 발생하면 이벤트 핸들러를 호출하지 않고, 일정 시간 동안 이벤트가 발생하지 않으면 이벤트 핸들러가 한번 만 호출되게 하는 디바운스는
연속된 이벤트에 따라서 ajax 요청 , 버튼 중복 클릭 방지
처리 등 유용하게 사용한다.
하지만 이번에 제시한 함수는 간략하게 구현한 것이며, 실무에서는 모듈을 이용하도록 하자고 한다. (Underscore , Ladash
)
스로틀은 짧은 시간 간격으로 이벤트가 연속해서 발생하더라도, 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 한다.
즉, 스로틀은 연속해서 발생하는 이벤트를 그룹화 해서 일정 시간 단위로 이벤트 핸들러가 호출되도록
호출 주기를 만든다.
위 예시에서 debouce
는 마우스 스크롤이 멈춘 순간 ! 배경색이 변했다.
그럼 이번에는 그게 아니라
마우스가 이동하는 동안 몇 초에 한번씩 색상이 변하길 원한다고 해보자
필요한 알고리즘은 다음과 같다.
해제하는 이유는 콜백 함수 호출 이후 새로운 타이머를 설정 하기 위함이다.
const throtle = (callback, delay) => {
let timer;
return (...rest) => {
if (timer) return;
timer = setTimeout(() => {
callback(...rest);
timer = null;
}, delay);
};
};
const DELAY = 1000;
const newChangeBack = throtle(changeBack, DELAY);
$div.addEventListener('scroll', newChangeBack);
위에서 이야기한 알고리즘으로 쓰로틀을 구현해보았다.
코드에서 이벤트가 실행되면 타이머를 설정하고 타이머가 만료되면 콜백함수를 호출한 후 자기 자신을 null
값으로 만든다.
이 때 해당 타이머가 존재하는 경우엔 이벤트가 발생해도 어떤 일도 발생하지 않는다. 그러니 타이머의 생명주기인 delay
값에 따라 콜백 함수가 호출된다.
타이머를
delay
이후null
값으로 변경하면서 타이머의 생명주기를 만드는 이유는 타이머의 생명주기가 끝나고 새로운 이벤트가 발생하면 새로운 타이머를 생성해주기 위함이다.
이 또한 모듈을 사용하는 것을 권장한다고 한다.
나같은 경우엔 아직 이벤트마다 호출되는 콜백함수가 그렇게 오버헤드가 크지 않지만
오버헤드가 큰 콜백함수의 경우에는 디바운스나 스로틀링을 적절히 사용해야 한다고 한다.
지피티야 도와줘~!
지피티야 도와줘 ~~~!