스크롤 등의 이벤트 최적화하기
개발 •

사용자의 스크롤, 클릭 등에 반응하는 페이지를 만들면, 1초에도 몇십 번씩 특정 함수가 동작해야 할 때가 많습니다.
간단한 애니메이션을 출력하는 정도라면 상관없겠지만, 복잡한 그래픽 연산이 들어가는 작업을 아무런 최적화 없이 이벤트 리스너만 추가해두면 시스템 자원을 갉아먹고 디스플레이의 주사율보다 더 많이 실행되어 오히려 프레임 방어를 못 해주는 상황까지 발생하기도 합니다.
이렇게 무거운 함수를 돌리는 와중에도 event.preventDefault() 사용 여부를 확인해야 하니, 브라우저의 메인 쓰레드에까지 악영향을 끼쳐 사용자가 페이지 전체가 버벅인다 느낄 수 있습니다.
개인적으로 스크롤 이벤트에선 한 번도 문제가 생긴 적 없는데, 슬라이더 등을 제작하며 마우스 / 터치에 반응해 여러 돔 요소의 스타일을 조작해야 하는 상황에선 절실하더라고요.
Passive Listener
window.addEventListener("scroll", foo, { passive: true });요즘엔 scroll이나 touchstart 같은 이벤트에 passive 옵션을 추가하지 않으면 콘솔에 경고까지 뜨기에, 아마 대부분 사용하는 옵션일 거라 봅니다.
passive가 true인 이벤트 리스너는 preventDefault()를 호출할 수 없습니다.
preventDefault()를 호출할 일이 없으니, 상술한 것처럼 브라우저의 메인 쓰레드에까지 악영향을 끼치는 참사는 막을 수 있습니다.
Window.requestAnimationFrame()
아직 브라우저가 렌더링할 수 있는 능력보다 함수가 실행되는 횟수가 더 많단 문제를 해결하지 못했습니다.
이는 requestAnimationFrame(MDN)을 이용해 해결할 수 있습니다.

requestAnimationFrame 없인 200회가량 실행됨
위 스크린샷처럼 requestAnimationFrame에 콜백을 넘겨주면 브라우저가 화면을 다시 그리기 전에 해당 함수를 호출하니, 함수가 초당 200회씩 호출되는 참사를 방지할 수 있습니다.
!(function () {
let ticking = false;
function foo() {
if (!ticking) {
ticking = true;
requestAnimationFrame(() => {
console.log("Scrolled!");
ticking = false;
});
}
}
window.addEventListener("scroll", foo, { passive: true });
})();함수의 실행이 끝났는지 확인하기 위해 ticking이란 변수를 추가했습니다.
매 스크롤에 foo가 실행되고, ticking이 false이면 requestAnimationFrame에 실행할 함수를 콜백으로 넘겨줍니다.
콜백으로 넘겨준 함수의 마지막에 ticking을 다시 false로 변경하는 코드를 추가해, 함수의 실행이 끝나고 다음 스크롤에 foo가 호출되면 다시 함수를 콜백으로 넘겨줄 수 있게 합니다.
재사용 가능한 함수 만들기
매번 상술한 방식으로 이벤트를 등록하는 건 상당히 귀찮을뿐더러 직관적이지 않으니, 어떤 상황에서건 쓸 수 있는 함수를 제작해보겠습니다.
function outer() {
let counter = 0;
function increaseCounter() {
counter++;
console.log(counter);
}
return increaseCounter;
}
const myFunction = outer();
myFunction(); // 1
myFunction(); // 2
myFunction(); // 3
myFunction(); // 4그에 앞서 알아야하는 개념이 자바스크립트의 클로저(Closure)(MDN)입니다.
위 코드는 언뜻 보면 counter가 실행 컨텍스트에도, window에도 없어 제대로 작동하지 않을 것처럼 보이지만, 실제론 outer에서 increaseCounter를 넘겨줄 때 해당 함수가 couter를 사용한단 걸 판단하고 counter도 함께 넘겨줍니다.
function optimizeAnimation(callback) {
let ticking = false;
return () => {
if (!ticking) {
ticking = true;
requestAnimationFrame(() => {
callback();
ticking = false;
});
}
};
}위를 활용해 만든 함수입니다.
window.addEventListener(
"scroll",
optimizeAnimation(() => {
console.log("Hi there 👋");
}),
{ passive: true }
);다소 이상해 보일 수 있지만, 이렇게 optimizeAnimation 함수가 아닌 optimizeAnimation의 실행 결과를 넘겨주면, ticking이 동봉되어 함수가 브라우저가 렌더링할 수 있는 능력을 벗어나는 횟수만큼 실행되는 것을 방지할 수 있습니다.
