본문 바로가기
JavaScript

IntersectionObserver API로 TOC 만들기

by Vintz 2021. 9. 20.
반응형

IntersectionObserver API

특정 요소에 관측자(observer)를 생성해서 교차점을 관측할 수 있게 해주는 Web API이다. 따라서 클라이언트의 뷰포트(옵션 기본값)에 따라 다양한 동작을 넣을 수 있다.

  • 이미지나 컨텐츠의 지연 로딩(lazy-loading) -> 초기 렌더링 시간 ⬇️ 사용자 경험 ⬆️
  • 사용자가 다른 페이지에 이동하지 않아도 되는 무한 스크롤(infinite-scroll) -> 예를 들면 유튜브, 앱 사용 시간 ⬆️
  • 뷰포트에 따라 애니메이션 효과 구현 -> 예를 들면 페이드인(fade-in), 페이드아웃(fade-out)
  • 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고 -> 사용자가 해당 광고를 봤는지 안봤는지 관측

나는 IntersectionObserver()로 TOC(Table Of Contents)를 만들어 보고 싶었다. 벨로그나 다른 블로그들을 보면 가끔 오른쪽 여백에 목차가 뜨고 내가 현재 해당 목록의 내용을 보고있다는 것을 알려준다.

 

TOC(Table Of Contents)

TOC에서 구현할 사항은 다음과 같다.

  1. 내가 본 섹션과 아직 못본 섹션을 색상 차이로 구별할 수 있도록 한다.
  2. 목차의 제목을 클릭하면 그것에 해당하는 내용을 보여준다.

생각보다 기능이 별로 없지만 강의와 구글링 그리고 많은 시도 끝에 그나마 잘 동작하는 기능을 구현했다. 의외로 다양한 상황(빠른 스크롤링, 섹션 이동 등)에 잘 동작하지 않아서 여기에 많은 시간을 할애하고 여러번 코드를 바꿨었다.

 

결국 배열, 노드리스트의 인덱스와 HTML id, data 속성의 조합으로 문제를 해결 할 수 있었다.

내가 본 섹션과 아직 못본 섹션을 색상 차이로 구별할 수 있도록 한다

const sections = document.querySelectorAll('section');
const navItems = document.querySelectorAll('.menu-list li');
const sectionIds = [...sections].map((section) => section.id); // id만 뽑아 배열로 변환
let navItem = navItems[0]; // 첫 섹션 목차의 제목 색상 변경을 위함

섹션들은 전개연산자를 통해 배열로 바꿔준 후 map()을 통해 섹션의 id만 뽑아왔다. navItems는 배열은 아니지만 nodeList[idx]로 반환이 가능하기 때문에 그대로 사용했다.

 

이제 섹션들을 관측하게 하고, 특정 범위 이상 교차할 경우 목차의 제목 색상이 변경되도록 구현한다.

// 콜백 설정값들
const observerOptions = { 
  root: null, // 기본값, 뷰포트 기준
  rootMargin: '0px', // root의 범위를 확장하거나 축소할 수 있다
  threshold: 0.4, // 타겟이 40% 이상 보여질 때 실행
};

// 타겟이 최초 등록될 시 그리고 가시성에 변화가 생길 시 실행되는 콜백
const observerCallback = (entries) => { 
  entries.forEach((entry) => {
    if (entry.isIntersecting && entry.intersectionRatio > 0) {
      // 콜백이 실행될 때마다 배열(sectionIds)의 id에 해당하는 인덱스 번호로 초기화
      const index = sectionIds.indexOf(entry.target.id);
      // 목차의 제목 초기화(인덱스 변경) 및 색상 변경
      navItem = navItems[index];
      navItem.style.color = '#66bb6a';

      if (index == 4) {
        navItem = navItems[index - 1];
      } else {
        navItem = navItems[index + 1];
        navItem.style.color = '#868e96';
      }
    }
  });
};

// new IntersectionObserver()를 통해 생성한 인스턴스로 관측자를 초기화
const observer = new IntersectionObserver(
  // 생성자는 2개의 인수를 가진다
  observerCallback,
  observerOptions
);

// 관측할 타겟들(섹션들)을 지정
sections.forEach((section) => observer.observe(section));

관측자의 콜백은 타겟이 최초 등록될 시에도 실행되기 때문에 그것을 막기 위해 조건문으로 제약을 건다. 

  1. 관찰 타겟이 교차가 되었는지? -> isIntersecting, boolean type
  2. 관찰 타겟의 교차한 영역 백분율이 0이상 되는지? -> intersectionRatio, number type

이 두 가지 조건 모두를 만족해야 콜백을 실행하도록 제약을 걸면 처음 뷰포트에 보이는 타겟만 콜백을 실행하도록 할 수 있다.

목차의 제목을 클릭하면 그것에 해당하는 내용을 보여준다

const navMenu = document.querySelector('.nav-menu');
const scrollIntoView = (selector) => {
  const scrollInto = document.querySelector(selector);
  //scrollIntoView()로 호출된 요소가 사용자에게 보여지도록 스크롤
  scrollInto.scrollIntoView({ behavior: 'smooth', block: 'center' });
};

navMenu.addEventListener('click', (event) => {
  const target = event.target;
  const link = target.dataset.link;
  if (link == null) { // 목차의 제목이 아닌 다른 영역(상위 요소)을 클릭했을 시 return
    return;
  }
  scrollIntoView(link);
});

✅ 디테일 추가

if (history.scrollRestoration) {
  history.scrollRestoration = 'manual';
} else {
  window.onbeforeunload = () => {
    window.scrollTo(0, 0);
  };
}

위 코드를 추가하면 새로고침 시 스크롤이 제일 위로 향하도록 할 수 있다. 이렇게 초기화 하지 않으면 목차가 제대로 동작하지 않아서 추가하게 되었다.

 

 

반응형