-
Slide In on Scroll 💻Web/JS30 2020. 11. 12. 18:02반응형
오늘은 스크롤을 하면서 viewport 내로 이미지가 들어오면
이미지가 서서히 슬라이드되면서 나타나는 효과를 구현해보는 예제이다.
사실 이런 효과는 글을 읽는데 방해가 된다고 생각해서 썩 좋아하진 않지만
그래도 몇 가지 배울 점이 있는 예제였다.
우선, querySelectorAll로 이미지들을 모두 변수에 담아준 후,
스크롤 이벤트 리스너를 window에다 붙여준다.
const sliderImages = document.querySelectorAll('.slide-in'); function checkSlide(e) { console.count(e); } window.addEventListener('scroll', checkSlide);
위에서 console.count(e)의 결과를 보면, 스크롤을 할 때마다 이벤트 리스너가 미친듯이 호출되는 걸 알 수 있다.
페이지의 맨 위에서 맨 아래로 스크롤하는 데에 이벤트가 108번 정도 호출되었다.
지금은 별 문제가 없지만, 효과가 붙기 시작하면 성능 문제를 일으킬 가능성이 농후하다.
이는 'debounce' 함수를 사용함으로써 해결이 가능한데
debounce 함수는 시간 단위를 두고, 그 시간동안 발생하는 이벤트들 중
가장 마지막 이벤트만 실행되도록 하는 함수이다.
흔히 Lodash같은 라이브러리에 있는 debounce 함수를 많이들 쓰지만
이 예제는 framework-free 예제이므로 미리 만들어 놓은 debounce 함수를 사용하기로 한다.
function debounce(func, wait = 20, immediate = true) { var timeout; return function() { var context = this; var args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; }
대충, 10ms를 단위로 이벤트가 실행된다고 보면 될 듯.
window.addEventListener('scroll', debounce(checkSlide, 10));
이제, 이미지가 slide-in 하도록, CSS가 미리 구현되어있는 'active'라는 클래스를 붙여줘야한다.
어떤 상황에서 클래스가 붙어야 하는지, 그리고 제거되어야 하는지를 생각해보자.
1) 페이지를 아래로 스크롤할 때, 페이지의 하단부가 해당 이미지를 절반만큼 지났을 때 이미지가 나타나도록 한다.
2) 페이지를 아래로 스크롤할 때, 이미지의 하단부가 화면 밖으로 나가게 되면 이미지가 사라지도록 한다.
const slideInAt = window.scrollY + window.innerHeight - sliderImage.height / 2; // 1) const imageBottom = sliderImage.offsetTop + sliderImage.height; // 2)
1번은 slideInAt이라는 변수로 표현되었다.
window.scrollY는 현재 viewport의 최상단이 전체 페이지의 최상단에서 Y축으로 얼만큼 멀어졌는지를 의미한다.
window.innerHeight는 현재 viewport의 높이를 의미한다.
따라서, window.scrollY + window.innerHeight는 전체 페이지의 최상단에서
현재 viewport의 최하단이 어느 정도 떨어져 있는지를 나타낸다.
거기서 sliderImage.height / 2만큼, 즉 이미지의 절반만큼 빼줌으로써
'이 지점을 지나야 이미지가 slide-in 된다'라는 의미의 slideInAt 변수가 만들어지는 것.
2번은 imageBottom, 즉 이미지의 최하단이 전체 페이지에서 어느 정도에 위치하는지를 나타낸다.
sliderImage는 하나의 이미지를 의미하는데, sliderImage.offsetTop은 해당 이미지의 최상단이
전체 페이지에서 Y축으로 얼만큼 떨어져있는지를 의미한다.
따라서, sliderImage.offsetTop + sliderImage.height는 전체 페이지에서
해당 이미지의 최하단이 Y축으로 어느만큼 떨어져있는지를 나타낸다.
이 두 변수를 이용해서 새로운 조건문 변수를 두 개 만든다.
참고로, 이 변수는 조건문에 들어갈 변수인데, 변수 이름을 길게 하더라도 무슨 조건을 의미하는지를
명확히 표시해주면 해당 변수명만으로도 어떤 조건인지 한 눈에 알아보기가 훨씬 수월해진다.
const isHalfShown = slideInAt > sliderImage.offsetTop; // 이미지가 절반 이상 보여졌는지 const isNotScrolledPast = window.scrollY < imageBottom; // 이미지가 아직 덜 사라졌는지.
isHalfShown은 특정 이미지의 상단부가 slideInAt보다 위에 있는지를 의미한다.
만약 그렇다면, viewport의 최하단이 이미지의 절반 이상 지났다는 의미이므로, 이미지가 slide-in 되어야 하는 상태이다.
isNotScrolledPast는 이미지의 하단부가 현재 viewport의 상단부보다 더 아래에 있는지를 의미한다.
즉, 이미지가 아직 화면에서 완전히 사라지지 않았는지를 나타내는 변수이다.
이 두 조건을 만족한다면 이미지가 viewport 안으로 들어왔고, 아직 viewport 밖으로 나가지는 않은 상태이므로
이미지가 viewport 내부에 존재하는 상태를 의미한다.
if(isHalfShown && isNotScrolledPast) { sliderImage.classList.add('active'); } else { sliderImage.classList.remove('active'); }
만약 이미지가 viewport 밖으로 나가게되면 active 클래스를 없애서
나중에 다시 위로 스크롤할 때도 동일한 효과를 볼 수 있도록 만들어준다.
전체 자바스크립트 코드
<script> function debounce(func, wait = 20, immediate = true) { var timeout; return function() { var context = this; var args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } const sliderImages = document.querySelectorAll('.slide-in'); function checkSlide(e) { sliderImages.forEach(sliderImage => { // half way through the image const slideInAt = window.scrollY + window.innerHeight - sliderImage.height / 2; // bottom of the image const imageBottom = sliderImage.offsetTop + sliderImage.height; const isHalfShown = slideInAt > sliderImage.offsetTop; const isNotScrolledPast = window.scrollY < imageBottom; if(isHalfShown && isNotScrolledPast) { sliderImage.classList.add('active'); } else { sliderImage.classList.remove('active'); } }); } window.addEventListener('scroll', debounce(checkSlide, 10)); </script>
수치와 관련된 프로퍼티를 많이 사용해서
말로 줄줄 풀어쓰는 게 상당히 복잡하고 어렵다.
그래도 나중에 다시 봤을 때 알아먹을 수 있는 정도는 되는 듯.
Debounce라는 개념에 대해 조금 더 자세하게 다룬 글이 있어서 참고로 걸어놓는다.
Debounce와 비슷한 개념 중에 'Throttle'이라는 개념도 있는데
Throttle과 Debounce 모두 자주 사용되는 이벤트 함수들의 실행 빈도를 줄여서
성능 상의 이점을 가져오기 위해 만든 개념이다.
이벤트 자체를 제어하는 건 아니고, 이벤트에 붙여진 함수의 실행을 제어하는 거라고 한다.
차이점이 있다면
Debounce는 일정 시간이 지난 후, 연이어 발생하는 이벤트들 중
가장 마지막에 발생한 이벤트의 함수만 호출하는 것이다.
브라우저의 크기를 리사이징하는 이벤트라고 치면, 마우스로 리사이징을 막 하는 도중에는 아무런 반응이 없고,
리사이징하다가 마우스를 딱 놓는 시점에서 마지막에 발생한 이벤트에 대해서만 딱 한 번 함수 호출이 이루어진다.
만약 시간 간격이 20ms라고 치면, 20ms보다 적은 시간동안 이벤트가 연이어 발생할 경우에도
20ms가 될 때까지 기다렸다가, 20ms 때 딱 한 번 함수가 호출된다.
만약 20ms보다 긴 시간동안 이벤트가 연이어 발생할 경우, 20ms를 넘어가도 함수가 호출되지 않다가
이벤트가 딱 끊기는 그 순간 함수가 호출된다.
Throttle은 무조건 일정한 주기마다 이벤트 함수가 호출되도록 하는 방법이다.
마지막 함수가 호출된 이후, 일정 시간이 지나기 전에는 다시 호출되지 않도록 한다.
참고로 스크롤 이벤트는, Debouncing의 경우 스크롤이 멈출 때만 이벤트를 발생시키기 때문에
Debouncing보다는 Throttling이 더 적합하다고 한다.
(Throttle은 적어도 일정 시간마다 정기적으로 기능 실행을 보장한다.
반면 Debounce는 일정 시간이 지나도 이벤트가 계속해서 발생하면 함수가 실행되지 않을 수 있다.)
참고
반응형'Web > JS30' 카테고리의 다른 글
💿 LocalStorage & Event Delegation 📤 (0) 2020.11.17 🍴 References VS Copying 🎎 (0) 2020.11.13 Key Sequence Detection(KONAMI CODE) 🎮🎲 (0) 2020.11.11 Custom HTML 5 Video Player 🎬🎥 (0) 2020.11.10 Hold Shift to Check Multiple Checkboxes 📦 (0) 2020.11.09