[실무개발]/Frontend

스크롤 이벤트 최적화

_niel 2022. 4. 4. 12:57

네비게이션 바가 특정 위치만큼 내려왔을 때  CSS를 변경해야 하는 순간이 있다.

window.addEventListener('scroll', () => console.log(window.scrollY));

이러면 스크롤이 되는 매 순간마다 함수가 호출되기 때문에 이를 최적화 해줄 필요가 있다.

 

 

보통은 throttle 함수를 사용하여 최적화를 했으나 이번엔 새로운 방식으로 최적화를 하려고 한다.

 

쓰로틀 함수

export default function throttle(fn, threshhold, scope) {
    threshhold || (threshhold = 250);
    let last, deferTimer;
    return function () {
        let context = scope || this;

        let now = +new Date(),
            args = arguments;
        if (last && now < last + threshhold) {
            // hold on to it
            clearTimeout(deferTimer);
            deferTimer = setTimeout(function () {
                last = now;
                fn.apply(context, args);
            }, threshhold);
        } else {
            last = now;
            fn.apply(context, args);
        }
    };
}
window.addEventListener('scroll', throttle(onScroll, 300))

이유는 자바스크립트의 동작방식에 있는데,

 

싱글 스레드로 동작하는 JavaScript는 setTimeout API의 비동기 task들을 Task Queue에 넣어둔 후 순차적으로 처리합니다. Queue에 저장된 비동기 task를 처리하는 시점은 Call stack이 비어져있을 경우 입니다. 이 시점이 setTimeout 또는 setInterval에 할당해준 delay와 맞지 않는다면 등록해둔 callback은 trigger 되지 않을 수 있습니다.

 

다시말해 300ms가 지난 시점에서 Call stack에서 다른 함수들을 처리하고 있다면, 그 함수가 끝난 뒤에서야 스크롤 함수가 동작 할 것이기 때문이다. 또한 기존의 방식은 new Date() 를 지속적으로 호출하게 되며 이는 불필요한 callstack을 지속적으로 생성한다. 화면에 많은 그리기 동작을 애니메이션으로 구현해도 실제로 표시 가능한 주사율에 영향을 받기에 지나치게 높은 부하가 발생할 수 있다.

 

requestAnimationFrame? (rAF)

rAF는 자바스크립트의 내장 메소드이다. 이를 사용하게 되면 브라우저가 렌더링 할 수 있는 ‘능력’에 맞춰 이벤트를 트리거 할 수 있다. 즉 일부러 300ms 씩 trigger 하려고 하지 않아도 된다는 뜻. 브라우저는 60fps(초당 60회)로 화면을 렌더링, 이 렌더링에 최적화하기 위해 rAF 이라는 API를 사용할 수 있다.

 

 

 rAF API도 setTimeout 과 마찬가지로 callback으로 넘겨지는 function을 비동기 task로 분류하여 처리. 다만 rAF  macro queue가 아니라 animation frame에서 처리된다. 또한 setTimeout 두번째 parameter로 전달되는 delay 값이 브라우저 렌더링에 최적화되어 있다는 차이가 있다.

 

export function toFit(
  cb,
  { dismissCondition = () => false, triggerCondition = () => true }
) {
  if (!cb) {
    throw Error('Invalid required arguments')
  }

  let tick = false

  return function() {

    if (tick) {
      return
    }

    tick = true
    return requestAnimationFrame(() => {
      if (dismissCondition()) {
        tick = false
        return
      }

      if (triggerCondition()) {
        tick = false
        return cb()
      }
    })
  }
}

위의 코드를 설명하자면,

 

1. 스크롤이 될 때 발생시킬 함수 cb를 인자로 받음

2. dissmissCondition, triggerCondition 함수를 옵션으로 받아 트리거를 원하는 상황에 대한 핸들링을 해 준다

3. tick 변수는 클로저로, 이를 통해 브라우저가 렌더링 할 수 있는 능력 이상의 callback 함수 호출을 막는다. 

4. requestAnimationFrame 메서드의 콜백 인자는 앞서 언급한 macro queue가 아니라 animation frame에서 처리된다.

5. 실제로 실행되기 전 까지는 tick이 true 이므로 tigger가 아무리 호출되도 실행이 되지 않는다.

6. 함수가 event loop에 의해 실행된다. 이때 tick은 false로 바뀌면서 실제 동작을 한다.

 

 

 

 

 

 

 

본글은

https://jbee.io/web/optimize-scroll-event/ https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame

를 인용하여 구성되었습니다.

 

window.requestAnimationFrame() - Web API | MDN

화면에 새로운 애니메이션을 업데이트할 준비가 될때마다 이 메소드를 호출하는것이 좋습니다. 이는 브라우저가 다음 리페인트를 수행하기전에 호출된 애니메이션 함수를 요청합니다. 콜백의

developer.mozilla.org