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