ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프론트엔드 UI개발 01_Scroll Spy(스크롤스파이)
    웹개발 2023. 6. 16. 11:05

    스크롤 스파이(Scroll Spy)는 웹사이트의 스크롤 위치를 기반으로 내비게이션 상태를 업데이트 함으로써 어떤 탭이 활성화되어 화면상에 노출이 되는지 알려줍니다.

    document.addEventListener("DOMContentLoaded", function () {
        const navElem = document.querySelector("#nav");
        const contentsElem = document.querySelector("#contents");
    
        const scrollspy = new Scrollspy(navElem, contentsElem);
    
        scrollspy.setEventListeners(scrollspy);
    });
    • DOMContentLoaded가 완료되면 스크롤 스파이에 필요한 #nav와 #contents 엘리먼트를 querySelector를 통해서 각각 navElem, contentsElem 변수에 저장합니다.
    • 클래스로 정의한 Scrollspy 앞에 new키워드를 사용해서 객체(인스턴스) scrollspy를 생성합니다.
    • scrollspy에서 setEventListeners를 실행하여, 세 가지 이벤트(resize, scroll, click) 타입에 따른 함수를 실행합니다. 각각의 이벤트에 따른 함수실행은 다음 페이지에서 더 자세히 알아보도록 하겠습니다.

    다음으로, 클래스로 정의한 Scrollspy의 전체적인 구조를 간단하게 살펴 보도록 하겠습니다.

    class Scrollspy {
        constructor(navElem, contentsElem) {
            ...
        }
    
        getOffsetTops() {
            ...
        }
    
        initScrollspy() {
            ...
        }
    
        setEventListeners(scrollspy) {
            ...
        }
    }

    처음 실행에 변수로 저장해 놓은 navElem, contentsElem를 constructor(생성자)로 받아 왔으며, getOffsetTops, initScrollspy, setEventListeners 총 세 가지 함수를 선언하였습니다. 클래스 문법에서 constructor 메서드는 인스턴스 객체를 생성하며 초기값들을 세팅하는 데 사용됩니다. 클래스 내부에서 constructor를 통해 생성된 변수와 함수는 this.(변수)를 통해서 손쉽게 접근이 가능합니다. 그럼 constructor에서 어떤 값들을 초기화했는지 살펴보도록 하겠습니다.

    constructor(navElem, contentsElem) {
        this.navElem = navElem;
        this.navItems = Array.from(this.navElem.children);
        this.contentsElem = contentsElem;
        this.contentItems = Array.from(this.contentsElem.children);
        this.offsetTops = [];
    
        this.initScrollspy();
    }

    스크롤 스파이에서 가장 중요한 엘리먼트는 바로 네비게이션과 각 탭에 해당되는 화면입니다.

    • 각각의 엘리먼트 요소를 가져오기 위해서 우선, navElem과 contentsElem이 사용된 것을 코드를 통해 알 수 있습니다.
    • 다음으로, 해당 엘리먼트에 포함되어 있는 자식 요소들을 가져와서 배열에 저장을 해야 하는데요, 이때 Array.from()이라는 메서드가 사용되었습니다.
    • 이는 this.navElem.children을 통해서 가져온 HTMLCollection을 배열로 만들 수 있습니다. 배열로 만드는 이유는 배열에서 제공해 주는 다양한 메서드를 사용하여 필요한 값들을 쉽게 얻기 위해서입니다.
    • 그리고, 각각의 탭에 해당되는 화면들의 화면상 위치값을 저장하기 위한 offsetTops 빈배열을 하나 생성합니다.
    • 마지막으로, initScrollspy()를 통해서 각 엘리먼트의 offsetTops값을 구해서 배열로 저장해 보도록 하겠습니다.
    getOffsetTops() {
        this.offsetTops = this.contentItems.map(elem => {
             const [ofs, clh] = [elem.offsetTop, elem.clientHeight];
            return [ofs - clh / 2, ofs + clh / 2];
        });
    }
    
    initScrollspy() {
        this.getOffsetTops();
    }

    이제 스크롤스파이에서 가장 중요한 각각의 엘리먼트의 위치값을 구하는 방법에 대해서 알아보도록 하겠습니다.

    • 엘리먼트의 위치값을 저장하기 위해서 constructor에 선언해 놓은 this.offsetTops를 가져옵니다.
    • 참고로, this.getOffsetTops()는 처음 실행될 때와 브라우저 화면이 리사이즈 될때 작동이 되야합니다.
    • 기존에 저장해 놓은 this.contentItems를 map을 돌려서 해당 엘리먼트의 offsetTop과 clientHeight값을 가져옵니다.
    • offsetTop은 전체 문서(offsetParent)를 기준으로 상단으로부터의 거리를 측정합니다.
    • 리턴 값은 from, top 값으로 생각하면 되며, 이때 스크롤이 진행되면서 중간정도 지점에 왔을때 targetIndex값을 저장하기 위해서, ofs - clh/2, ofs + clh/2 계산이 필요합니다.

    지금까지, 필요한 로직은 대부분 확인을 하였습니다. 그럼, 각각의 이벤트에 따라 필요한 함수를 동작시켜 보도록 하겠습니다.

    setEventListeners(scrollspy) {
        window.addEventListener('resize', this.getOffsetTops.bind(scrollspy));
    
        window.addEventListener("scroll", (e) => {
            const { scrollTop } = e.target.scrollingElement;
            const targetIndex = this.offsetTops.findIndex(([from, to]) => (
                scrollTop >= from && scrollTop < to
            ));
    
            this.navItems.forEach((c, i) => {
                if (i !== targetIndex)
                    c.classList.remove("on");
                else
                    c.classList.add("on");
            });
        });
    
        this.navElem.addEventListener("click", (e) => {
            const targetElem = e.target;
            if (targetElem.tagName === "BUTTON") {
                const targetIndex = this.navItems.indexOf(targetElem.parentElement);
                this.contentItems[targetIndex].scrollIntoView({
                    block: "start",
                    behavior: "smooth",
                });
            }
        });
    }

    이벤트 함수는 모두 3개로 정의합니다. 화면 resize, 화면 스크롤, 내비게이션 탭 클릭입니다. 

    • resize같은 경우 주의할점은, 이벤트 헨들러 내부의 this는 이벤트 객체의 currentTarget 프로퍼티와 동일하기 대문에 bind를 사용해서 다시 연결해야 합니다.
    • scroll 이벤트 실행시에는 e.target.scrollingElement 속성을 사용해서 현재 scrollTop값을 가져올 수 있습니다.
    • 현재 저장해 놓은 offsetTops에서 현재 조건과 일치하는 index값을 찾아내기 위해서 findIndex를 사용합니다.
    • scrollTop >= from && scrollTop < to 조건에 맞는 index를 리턴하여 targetIndex에 저장합니다.
    • 해당 값과 forEach를 사용하여 내비게이션 아이템의 스타일을 제거/추가 할 수 있습니다.
    • 마지막으로 내비게이션 탭을 클릭하면 콜백함수가 실행되면서 해당되는 화면으로 이동하는 것을 확인할 수 있습니다.
    • 마크업 구조를 보면 li>button 으로 되어 있기 때문에 사용자가 button을 눌렀을 경우 e.target은 해당 button이 됩니다.
    • 해당 버튼의 부모 요소인 li 엘리먼트가 navItems에서 몇 번째인지 indexOf 메서드를 통해 알 수 있습니다.
    • 현재 index를 this.contentItems[현재인덱스].scrollIntoView()를 통해 부드럽게 이동이 가능합니다.
Designed by Tistory.