본문 바로가기

Language & Framework/실습

IntersectionObserver를 활용하여 스크롤시 Navbar에 효과 주기

 

기존에 만들었던 드림코딩 포트폴리오, 그 마지막 강의에서 Intersection Observer를 활용하여 navbar에 active 효과를 주는 방법을 배우는데, 당시 자바스크립트의 ㅈ도 몰랐던 나는 (욕 아님) 도저히 강의가 이해되지 않았고 받아쓰기 하듯 따라 적어 포트폴리오 웹사이트를 완성했었다. 심지어 따라치기도 똑바로 못했는지 정상적으로 작동하지도 않았다.

아직도 자바스크립트의 ㅈ도 모르지만 ㅈ의 한 획 정도는 알게 되어서 직접 다시 해결해보고 싶다는 생각이 들었고 결국 해내긴 했는데 상당히 해멨다.. 나중에 보기 위해 그 과정을 기록하고 싶었지만 혹시나 저작권 문제가 있을까 싶어 완성해놓고 간단한 예제를 하나 더 만들어 기록하게 되었다.

근데 진짜 간단하게 만들었더니 내가 해맸던 부분은 안 만들었네..?

 

 

HTML

<nav>
    <ul class="navItems">
        <li class="navitem active">1</li>
        <li class="navitem">2</li>
        <li class="navitem">3</li>
        <li class="navitem">4</li>
        <li class="navitem">5</li>
        <li class="navitem">6</li>
        <li class="navitem">7</li>
    </ul>
</nav>
<div class="unlimitedPage">
    <ul class="pageItemContiner">
        <li class="pageItem">1</li>
        <li class="pageItem">2</li>
        <li class="pageItem">3</li>
        <li class="pageItem">4</li>
        <li class="pageItem">5</li>
        <li class="pageItem">6</li>
        <li class="pageItem">7</li>
    </ul>

별 거 없습니다.

실제 사용할 때 navvbar의 메뉴 이름이 1234567일 경우는 거의 없으니 dataset 속성을 지정해주는 것이 좋다. (이유는 나중에)

 

 

 

CSS

 

* {
  box-sizing: border-box;
}

body {
  margin: 0px;
}

li {
  list-style: none;
}

.navItems {
  position: fixed;
  top: 0px;
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  padding: 0px;
  margin: 0px;
}

.navitem {
  width: 100%;
  height: 100%;
  margin: 0px;
  text-align: center;
  padding: 30px;
}

.navitem.active {
  transform: scale(1.5);
  border: 5px white solid;
  border-radius: 10px;
  transition: 300ms;
}

.pageItemContiner {
  margin: 0px;
  padding: 0px;
  width: 100%;
}

.pageItem {
  background-color: blue;
  height: 100vh;
  width: 100%;
  margin: 0px;
  padding: 0px;
  font-size: 100px;
  text-align: center;
  color: white;
  line-height: 100vh;
}

별거 없다.

사실 css는 너무 만들기 귀찮아서 후다다닥 만들어서 언제 만들었는지도 기억이 안 난다..

 

 

JS

 

// 필요한 변수 생성

const unlimitedPage = document.querySelector(".unlimitedPage");
const pageItems = document.querySelectorAll(".pageItem");
const itemsContainer = document.querySelectorAll(".pageItemContiner");
const newPageItems = document.querySelector(".newPageItemContainer");
const navItems = document.querySelector(".navItems");
const navItem = document.querySelectorAll(".navitem");


// 별 이유 없이 랜덤 색상으로 지정함. 무시하십시오.
pageItems.forEach((item) => {
  item.style.backgroundColor =
    "#" + parseInt(Math.random() * 0xffffff).toString(16);
});
navItem.forEach((item) => {
  item.style.backgroundColor =
    "#" + parseInt(Math.random() * 0xffffff).toString(16);
});

let navIndexNum = 1;


// 이 부분 때문에 dataset 속성이 있으면 편하다.
function ActiveNavItem() {
  navItem.forEach((e) => {
    if (e.innerHTML == navIndexNum) {
      e.classList.add("active");
    } else {
      e.classList.remove("active");
    }
  });
}

// 옵저버의 콜백함수
function observerCallback(entries, observer) {
  entries.forEach((entry) => {
    if (entry.isIntersecting == true && entry.intersectionRatio > 0) {
      if (entry.boundingClientRect.y > 0) {
        console.log(entry.boundingClientRect);
        ++navIndexNum;
        ActiveNavItem();
        console.log(navIndexNum);
      } else if (entry.boundingClientRect.y < 0) {
        --navIndexNum;
        ActiveNavItem();
      }
    }
  });
}

// IntersectionObserverEntry의 읽기 전용 속성들
// target element:
//   entry.boundingClientRect
//   entry.intersectionRatio
//   entry.intersectionRect
//   entry.isIntersecting
//   entry.rootBounds
//   entry.target
//   entry.time


// 옵저버 옵션
const observerOption = {
  root: null,
  rootMargin: "0px",
  threshold: 0.3,
};

// 옵저버 생성
const observer = new IntersectionObserver(observerCallback, observerOption);

// 감시 대상 지정
pageItems.forEach((item) => {
  observer.observe(item);
});

 

 

필요한 변수들을 호출해준다.

지금 보니까 예전에 무한 스크롤 만들었던 html 파일을 복사 붙여넣기해서 이름이 unlimitedPage다..

 

 

나의 소소한 즐거움을 위해 backgroundColor는 랜덤으로 지정해줬다.

 

현재 내 화면에 표시되고 있는 요소의 순서를 체크해줄 변수다.

index인데 왜 1부터 시작하는지 굉장히 불편할 것이다.

내가 빨리 만들고 싶어서 html을 이상하게 짜서 그렇습니다. 미안합니다.

 

dataset 속성을 이용해 만들 경우 0부터 시작하면 된다.

 

일단 옵저버를 생성해주려고 하는데 콜백 함수와 옵션을 지정해줘야 한다.

 

 

entries는 감시하고 있는 모든 요소들이고 entry는 그 중 한 개의 요소라고 생각하면 된다.

이 콜백함수의 작동 순서는 아래와 같다.

 

1. entry가 isIntersecting (== 화면에 들어오고 있을 때 true), 그리고 entry.intersectionRatio (== 화면에 표시되고 있는 비율)이 0 이상일 때 포함된 조건문을 실행한다.

insIntersecting이 true이면 당연히 intersectionRatio도 0보다 큰 거 아닌가요? 당연합니다. 

근데 문제는 페이지를 처음 로딩할 때 화면에 들어오려고 대기하는 다른 친구들도 true를 반환한다는 게 문제다.  따라서 intersectionRatio>0인 entry에만 적용하게 만들어준다.

 

2. 현재 내 뷰포트에 새롭게 잡히는 감시 대상의 y값이 +라는 말은 밑에서 요소가 올라오고 있다는 것이다. (화면이 아래로 스크롤되는 중)

그리고 그와 반대로 y값이 -라면 요소가 위에서 밑으로 내려오고 있다는 뜻이다. (화면이 위로 스크롤되는 중)

위에서 선언해준 navIndexNum 변수를 활용해서 navbar에 active를 줄 생각이니 당연히 내려갈 때는 ++, 올라갈 때는 --를 해주면 된다.

 

 

옵저버의 옵션을 설정해줘야 한다.

root은 쉽게 말해서 옵저버가 감시할 한 페이지의 기준점을 잡아주는 것이다.

null을 입력할 경우 뷰포트 기준으로 움직인다.

 

rootMargin을 지정해주면 설정한 수치만큼 화면에 들어오기 전에 미리 작동한다. 

 

threshold는 요소가 화면에 몇 % 노출되었을 때 콜백함수를 실행할 것인지 지정해주는 것이다.

 

 

nav의 요소들에 active 클래스를 지정해주거나 지워주는 함수.

나는 nav 요소들의 innerHTML을 1,2,3...6,7로 만들었기 때문에 navIndexNum 변수를 1부터 시작해서 7까지 더하거나 빼서 둘을 대치해주기만 하면 됐다.

 

만약 dataset 속성을 이용할 경우, dataset 속성의 배열을 만들어주고 dataset 배열의 인덱스와 navIndexNum를 대치해주면 된다.

 

이제 각 요소를 감시하도록 명령만 해주면 끝.

 

참고로 이건 요소의 height가 100vh라서 굉장히 속 편한 상황이고, 실상황에서는 스크롤이 가장 아래로 내려갔을 때 혹은 가장 위로 올라갔을 때 active가 똑바로 부착되지 않을 수 있다.

그럴 경우 스크롤 이벤트 리스너를 만들어 widow.scrollY + window.innerheight > document.body.innerheight -2로 가장 아래까지 스크롤 됐는지 체크하거나 window.scrollY==0일 경우 첫번째 요소에 active를 주도록 코드를 작성하면 된다.

 

마지막에 -2를 해주는 이유는.. 이렇게 해야 맥에서 정상적으로 코드가 작동한다.

 

정리 끝.