-
프론트엔드 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()를 통해 부드럽게 이동이 가능합니다.
'웹개발' 카테고리의 다른 글
프론트엔드 UI개발 06_IntersectionObserver (0) 2023.11.15 프론트엔드 UI개발 05_Pagination (0) 2023.09.12 프론트엔드 UI개발 04_Instant Search (0) 2023.08.18 프론트엔드 UI개발 03_Dropdown Menu (0) 2023.08.03 프론트엔드 UI개발 02_Draggable Element (0) 2023.06.26