Intersection Observer API 사용해보기 - 스크롤링 시 메뉴 활성화
웹사이트 랜딩페이지를 보다 보면, 상단 헤더 메뉴가 사용자가 스크롤 시 진입한 구간에 따라 메뉴의 스타일도 이동되는 것을 볼 수 있다. 그동안 프로젝트에서는 이러한 스타일을 적용할 페이지가 없었기에 한 번도 만들어보지 못했었다. 하지만 최근에 포트폴리오를 만들면서 관련 API를 알게 되어 기능도 익힐 겸 간단하게 적용해 보려고 한다.
Observer API의 필요성
MDN공식문서에서 확인하면 타겟 요소가 상위요소 혹은 최상위 요소(=뷰포트)와 교차하는 변경사항을 비동기적으로 관찰하는 방법을 제공한다고 한다. 말 그대로 관찰자 API이다. 🧐 대상을 감시하다가 대상이 정해 놓은 구역에 진입하면 그때부터 만들어 놓은 함수를 시작한다. 과거부터 어떤 특정 요소의 가시성을 확인하고 두 요소들의 가시성, 즉, 서로 관련된 두 요소들의 상대적 가시성을 파악하기 위한 솔루션들이 있었지만 브라우저 속도를 느리게 만들었고, 어려운 작업이었기 때문에 결과를 신뢰하기가 어려웠다고 한다. 하지만 점점 웹이 발전됨에 따라 교차 정보의 필요성이 커졌고 다음과 같은 여러 이유들 때문에 더더욱 필요성이 커졌다고 한다.
- 페이지가 스크롤될 때 이미지나 콘텐츠를 Lazy-loading으로 받아오기 위해
- 무한스크롤 웹 사이트를 구현하기 위해
- 광고 수익을 계산하기 위해 광고의 가시성을 측정하기 위해
ㄴ 이 또한 페이지가 스크롤되면 사용자가 광고를 보았는지 아닌지를 측정하기 위해 필요하다는 의미이다. - 사용자가 볼지 아닐지 여부에 따라 애니메이션을 보여준다거나 특정 작업을 수행하는 것을 판단하기 위해
이전에는 교차 정보를 확인하기 위해 요소에 Element.getBoundingClientRect()이라는 메서드를 사용했다고 한다. 나 또한 해당 메서드를 사용해서 스크롤이 어느 지점까지 내려왔는지에 대한 정보를 얻은 적이 있었다. (예를 들어, 뷰포트의 특정 y좌표만큼 스크롤되면 헤더를 불투명하게 바꿔줘!) 하지만 이 메서드를 스크롤될 때 모든 요소에 좌표점을 계산해서 적용한다면 사이트 성능에 좋지 않다고 한다. 왜냐하면 이 작업은 메인스레드에서 실행하기 때문이다.
Observer API 사용 방법
이제 Observer API를 간단한 예제와 함께 사용해 보려 한다. 지금 예시로 만들려고 하는 것은 특정 스크롤 시 뷰포트에 상자가 완전히 보이면 상자의 색상이 변경되도록 해 볼 것이다. HTML, CSS를 사용하여 div태그로 분홍색 상자를 10개 만들어 주었다.
1. Observer 만들기
new 키워드를 사용하여 생성자를 호출하고 한 방향 또는 다른 방향으로 교차할 때마다 실행되는 콜백 함수를 전달하여 교차 관찰자를 만들어 준다. 이때 옵션을 이용해서 정밀한 조건을 걸어줄 수 있다.
// 옵션
const options = {
root: null,
rootMain: "0px",
threshold: 1, // 단계별 콜백함수 호출
};
// 1. 옵져버 생성
const observer = new IntersectionObserver(observerCallback, options);
Options에는 다음과 같은 필드가 있다.
- root: 대상의 가시성을 확인하기 위해(즉, 대상이 교차하는 경계를 만드는 상자) 뷰포트로서 사용되는 요소이다. 반드시 대상의 조상요소여야 하고 root를 지정하지 않거나, 값이 null인 경우는 디폴트 값이 뷰포트이다.
- rootMargin: 루트요소 주의에 여백을 둘 수 있다. CSS 마진 사용법과 똑같고, 백분율 혹은 마이너스의 값도 줄 수 있다. 기본값은 0이다.
- threshold: 콜백이 실행되어야 하는 대상의 가시성 비율이 나타내는 옵션으로 단일 숫자 혹은 숫자 배열로 표현한다. 예를 들어 0은 경계상자에 대상이 나타나는 즉시 콜백함수를 실행한다. 0.5면 경계상자에 대상이 50% 들어오고 50% 나갈 때 콜백함수를 실행한다. 만약, 25%마다 콜백함수를 실행하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 이렇게 배열로 보다 상세하게 지정이 가능하다.
2. 관찰할 대상 요소 감시하기
옵져버를 만든 후에 어떤 대상을 관찰할지를 지정해줘야 한다. 지금은 경계 상자에 분홍색 상자가 들어오고 나가는 것을 관찰해야 하므로 대상은 분홍색 상자가 된다. 옵져버에게 어떤 대상을 관찰(observe)할지를 결정해 주는 단계이다.
// 2. 관찰대상 선택
const targets = document.querySelectorAll(".box");
targets.forEach((target) => observer.observe(target));
3. 콜백함수
관찰할 대상이 교차점에 들어오고 나갈 때 어떤 작업을 수행할지에 대한 콜백함수를 만들어 준다. 콜백함수는 entries를 수신하며 entries는 '경계 상자에 들어온 상자'들로 IntersectionObserverEntry 객체를 담고 있는 리스트, 즉 배열이다. entries는 forEach 반복문을 사용해서 entry의 프로퍼티를 사용하면 여러 가지 정보를 얻을 수 있다.
isIntersecting을 사용하면 각 entry가 뷰포트에 들어왔는지를 boolean값으로 확인할 수 있고, intersectionRatio를 사용하면 상세한 entry 가시성 비율을 알 수 있다.
관찰대상(분홍색 상자)이 뷰포트에 전부 보이면 콜백함수가 호출되고, isIntersecting 프로퍼티를 사용하여 뷰포트에 들어온 상자일 경우(true) active 클래스를 추가하여 상자의 색을 변경하는 콜백함수를 완성했다.
참고로, 처음에는 콜백함수가 모든 상자를 한 번씩 다 순회하는 것을 확인할 수 있는데, console.log(entry)를 확인해 보면 상자 10개에 대한 IntersectionObserverEntry 객체 10개가 처음에 콘솔에 찍혀있는 것을 확인할 수 있다. (물론, isIntersecting 값은 뷰포트에 들어온 값만 true!!)
// 3. 콜백함수 생성
function observerCallback(entries) {
entries.forEach((entry) => {
// console.log(entry);
// console.log(entry.target);
// console.log(entry.isIntersecting);
// console.log(entry.intersectionRatio);
if (entry.isIntersecting) {
entry.target.classList.add("active");
} else {
entry.target.classList.remove("active");
}
});
}
코드를 실행하면, 스크롤이 진행되면서 분홍색 상자가 뷰포트에 전부 들어왔을 때 청록색으로 색상이 변경되는 것을 확인할 수 있다.
Observer API의 사용방법을 익혀보았으니 나중에 헤더에 사용할 것을 대비하여 다른 예제를 만들어 보려 한다. 헤더에 메뉴를 만들고, 스크롤이 진행될 때 상자가 뷰포트에 들어오면 메뉴의 활성화표시가 보이게 하는 작업이다. 다음과 같이 헤더를 간단하게 만들어 주었고, 분홍색 상자는 10개에서 5개로 변경해 주었다.
간단하게 헤더 리스트에 data-target 속성을 만들고, 헤더 안 메뉴의 배경색을 바꿔주는 부분을 activateMenu 따로 만들어서 로직을 분리해 주었다.
// 추가 한 부분
const headerItems = document.querySelectorAll("li");
const activateMenu = (target) => {
headerItems.forEach((item) => {
const data = item.getAttribute("data-target");
if (data === target) {
item.classList.add("active");
} else {
item.classList.remove("active");
}
});
};
// 변경 한 부분
function observerCallback(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
activateMenu(entry.target.innerText);
}
});
}
코드를 실행하면, 스크롤이 진행되면서 분홍색 상자가 뷰포트에 전부 들어왔을 때 상단 헤더 활성화 표시가 변경되는 것을 알 수 있다.
마무리
지금 두 개의 예제는 옵져버 옵션의 threshold를 1로 했을 때여서 뷰포트에 대상 요소가 완전히 들어왔을 때만 콜백함수를 호출하면 되는 상대적으로 간단한 작업이었지만, 옵션을 상세하게 주었을 때는 경계에 들어왔을 때 어떤 값을 더 우선해서 보여주어야 할지 등 추가로 고려해야 할 부분이 있다. 이럴 때는 요소의 인덱스값을 활용해서 isIntersecting 반환값을 추가 배열을 생성해서 무엇을 더 우선하여 보여 줄지를 결정해 주는 방법으로 사용하면 될 것 같다.
지금까지 해본 느낌(?!)을 활용해서 다음에는 리액트에 적용해 보거나 랜딩페이지를 작업할 때 스크롤에 따라 글씨나 이미지가 적절하게 배치되어 마치 사용자를 따라다니는 것 같은 인터랙티브 한 애니메이션 효과를 주는데 활용해 봐야겠다.
참고