DOM이벤트를 호출했을 때, 너무 많은 이벤트는 불필요한 메모리를 소모해서 웹 성능을 떨어트릴 수 있다. 그래서 이벤트를 제어(제한) 해줘야하는데 내가 개발하면서 경험했던 웹 성능 최적화에 대해서 정리해보려고 한다.
개발을 하던 중 고정되는 Header 밑으로 고정되는 요소들이 많아지면서 사용자가 콘텐츠를 보는데 불편함을 느낄 수 있을 것 같아 스크롤을 내릴 때는 Header가 사라지고 올릴 때는 다시 나타나는 동작을 구현했다. (상단에 고정되는 영역의 비율을 조금이라도 줄여서 사용자가 콘텐츠를 보는데 불편함을 덜어주기 위함이었다.)
위와 같은 동작은 addEventListener()
의 scroll
이벤트를 이용하여 특정 위치에 도달했을 때 어떤 액션을 취하도록 했다. 내가 설계한 로직은 현재의 scrollTop 값과 바로 직전의 scrollTop 값을 계속 비교하여 현재의scrollTop값이 더 클 때는 header를 숨기고 현재의 scrollTop값이 더 작을 때는 header가 나타나게 했다.
💡 현재의 scrollTop값이 크다는 것은 scroll을 내리고 있다는 것을 의미하고, 현재 scrollTop값이 직전의 scrollTop 값보다 작다는 것은 scroll을 올리고 있다는 것을 의미하는 원리를 이용했다.
// ❗️이해를 돕기 위해 코드의 일부분만 가져왔기 때문에 코드의 흐름이 자연스럽지 않을 수 있다.
const NavContainer = ({children} : Props) => {
const [height, setHeight] = useState(0);
const callbackRef = (el: HTMLDivElement) => {
if (el) {
setHeight(el.getBoundingClientRect().height) // element의 세로 길이
}
}
useEffect(() => {
const updateScroll = () => {
setScrollPosition(window.scrollY || document.documentElement.scrollTop)
}
window.addEventListener("scroll", updateScroll)
return () => { // useEffect clean-up
window.removeEventListener("scroll", updateScroll)
}
}, [])
useEffect(() => {
const nav: HTMLElement = document.getElementById("nav")
const navBar: HTMLElement = document.getElementById("nav_bar")
let prevScrollPos = 0
let NAV_BAR = 56
const scrollEvent = () => {
let currentScrollPos = window.scrollY
if (Math.abs(prevScrollPos - currentScrollPos) <= NAV_BAR) return
if (prevScrollPos < currentScrollPos) {
navBar.style.visibility = "hidden"
navBar.style.opacity = "0%"
nav.style.top = "-56px"
} else {
navBar.style.visibility = "visible"
navBar.style.opacity = "100%"
nav.style.top = "0px"
}
prevScrollPos = currentScrollPos
}
if (scroll) {
window.addEventListener("scroll", scrollEvent)
}
return () => { // useEffect clean-up
window.removeEventListener("scroll", scrollEvent)
}
}, [])
return(
<div ref={callbackRef} id="nav">
{children}
</div>
)
}
하지만 이렇게 코드를 작성했을 때 동작은 잘 됐지만, scroll
이벤트는 스크롤 할 때마다 이벤트를 발생시키며 몇 초만에 수백, 수천번 이벤트가 호출될 수 있어 서버와 클라이언트 모두에게 부담을 줄 수 있다는 문제점이 있었다.
const updateScroll = () => {
setScrollPosition(window.scrollY || document.documentElement.scrollTop)
console.log("scroll")
}
window.addEventListener("scroll", updateScroll)
scroll
이벤트에 console.log 를 찍어 보면 위와 같이 스크롤을 할때마다 이벤트가 호출 되는 것을 볼 수 있다. 만약 스크롤 이벤트에 무거운 코드를 작성하게 된다면 최악의 상황에는 브라우저가 뻗을 수 도 있다.
이를 해결하기 위해 throttle
과 debounce
함수를 사용했고 함수 호출 횟수를 줄이고 웹 성능이 저하되는 것을 방지 할 수 있었다.
두 함수 모두 이벤트 핸들러가 많은 연산(예 : 무거운 계산 및 기타 DOM 조작)을 수행하는 경우에 대해 제약을 걸어 제어할 수 있는 수준으로 이벤트를 발생시키는 것을 목표로 한다는 공통점이 있다.
const onScroll = () => {
// 실행 함수
}
document.addEventListener('scroll', throttle(onScroll, 300))
무한 스크롤링 페이지의 원리는 사용자가 footer
에서 얼마나 떨어져 있는지 확인하고 사용자가 맨 아래로 스크롤 했다면 Ajax
를 통해 더 많은 콘텐츠를 요청하여 페이지를 추가하는 것이다.
따라서, 사용자가 footer에 도달하지 전에 콘텐츠를 가져와야한다. 이는 throttle
을 통해 사용자 위치가 얼마나 foote로 부터 떨어져 있는지 항상 확인할 수 있다.
const onScroll = () => {
// 실행 함수
}
document.addEventListener('scroll', debounce(onScroll, 300))
브라우저 창 크기를 조정하는 경우에 크기 창 조정 이벤트를 내보낼 수 있다. resize 이벤트에 대한 마지막을 추적하여 최종 값에 대한 이벤트를 실행시킨다.
요즘 서비스들은 검색어를 치자마자 엔터 없이도 결과가 바로 나온다.
항상 input 이벤트를 대기하고 있어야 하기 때문에 키보드를 칠때마다 ajax 요청을 보내게 된다. 이는 큰 낭비가 될 수 있기 때문에 debounce를 사용하여 검색할 단어가 모두 완성됐을 때 ajax 요청을 할 수 있도록 해야한다.
throttle
은 적어도 일정 주기(밀리 초)마다 정기적으로 기능 실행을 보장한다.debounce
는 아무리 많은 이벤트가 발생해도 모두 무시하고 특정 시간 사이에 어떤 이벤트도 발생하지 않았을 때 딱 한번만 마지막 이벤트를 발생시킨다.throttle
을 사용하여 scroll
이벤트에 대한 부분을 최적화 시킬 수 있었다.
그러면 스크롤 할 때마다 이벤트가 실행되는 것이 아닌 내가 설정한 주기 300ms 마다 이벤트를 실행시킬 수 있다.
const updateScroll = _.throttle(() => {
setScrollPosition(window.scrollY || document.documentElement.scrollTop)
}, 300)
window.addEventListener("scroll", updateScroll)
...
const scrollEvent = _.throttle(() => {
let currentScrollPos = window.scrollY
if (Math.abs(prevScrollPos - currentScrollPos) <= NAV_BAR) return
if (prevScrollPos < currentScrollPos) {
navBar.style.visibility = "hidden"
navBar.style.opacity = "0%"
nav.style.top = "-56px"
} else {
navBar.style.visibility = "visible"
navBar.style.opacity = "100%"
nav.style.top = "0px"
}
prevScrollPos = currentScrollPos
}, 300)
if (scroll) {
window.addEventListener("scroll", scrollEvent)
}
...