React와 Intersection Observer로 목차(TOC) 만들기
FE/React2023. 9. 30. 15:55Intersection Observer API
이전에는 스크롤 위치에 따라 화면을 변화시키기 위해, 아래와 같이 scroll event listener 를 추가하여 사용했습니다.
document.addEventListener("scroll", callback);
이렇게 스크롤로 화면 내의 요소(element)의 변화를 감지하는 일은, 스크롤을 조금만 움직여도 이벤트가 발생하며 사이트에 부하를 줄 수 있기 때문에 비효율적이었습니다.
비교적 최근 브라우저에서는 Intersection Observer 라는 API를 지원해주기 때문에 이전 방법(스크롤 이벤트를 이용한 방법)보다 효율적이고, 쉽게 컨트롤 할 수 있게 됐습니다.
Intersection Observer 사용하기
사용하는 방법은 관찰자(Observer)를 생성하고, 관측하고 싶은 요소(element)에 관찰자를 붙여주면 됩니다. 간단한 코드를 살펴보면,
IntersectionObserver
를 이용하여 관측자를 생성합니다.
이때 첫번째 인자로는 이벤트가 발생했을때 수행할 콜백 함수가 들어가고, 2번째 인자로는 Observer의 옵션값이 들어갑니다.
// inersection observer 옵션
const options = {
root: document.querySelector('#article'),
rootMargin: '0px',
threshold: 1.0
}
const observer = new IntersectionObserver(callback, options);
옵션 값에는 root
, rootMargin
, threshold
가 있는데, 각 옵션의 의미는 아래와 같습니다
root
: 뷰 포트(viewport)로 대상(target) 요소와 교차하는 요소를 말하며, 반드시 대상 요소보다 상위 요소에 있어야 함. 값이 null인 경우 기본값은 브라우저 뷰포트로 설정 됨rootMargin
: root에 마진값을 설정하여 교차 범위를 설정할 수 있습니다. css의 margin 값과 같이 (top, right, botton, left) 순으로 설정이 가능함 (e.g.10px 20px 30px 10px
)threshold
: 대상(target) 요소가 보이는 부분의 퍼센티지를 나타내며, 대상 요소가 절반만 뷰포트에 보인다면 0.5 안보인다면 0, 완전히 보인다면 1이 됨. 따라서, 완전히 보일때 콜백함수를 실행하고 싶다면 1로 설정.
const options = {
root: null, // 뷰포트 요소 (e.g. document.querySelector('#article'))
rootMargin: '0px',
threshold: 1.0
}
const observer = new IntersectionObserver(callback, options);
Intersection Observer를 사용하여 TOC 생성
toc를 생성하기 위해 글 본문에 있는 heading 태그들을 가져와야 합니다.
const headingList = article.querySelectorAll('h2, h3');
그리고, 가져온 heading 태그에 observer를 붙여줄건데, 화면에 나타나거나 사라졌을때 어떤 행위를 할지 observer 정의부터 해야합니다.
우선, IntersectionObserver
의 옵션 값부터 설정하면, 뷰포트를 null
값을 줘서 브라우저 뷰토프로 설정했고,margin bottom
값은 -40% 설정했습니다. 그리고, threshold
는 타겟 요소가 화면에 전체 노출되었을때, 이벤트가 발생하도록 했습니다.
const options = {
root: null,
rootMargin: '0px 0px -40% 0px',
threshold: 1.0,
}
그리고 이벤트가 발생했을때, 수행할 콜백 함수를 작성할건데, TOC에 highlight를 주기 위해, className을 추가해주는 작업을 합니다.
하나의 화면에 여러개의 heading 태그가 보일 수 있기 때문에 가장 밑에 있는 heading 태그에 highlight를 주려고 합니다.
const observer = new IntersectionObserver(
(entries) => {
const visibleHeadings = entries.filter((entry) => entry.isIntersecting);
if (visibleHeadings.length === 0) return;
const activeHeading = visibleHeadings[visibleHeadings.length - 1];
// ...
},
options
);
그리고, 가장 밑에 있는 heading 태그의 text 값과 toc의 text 값이 동일한 요소를 찾습니다.
찾은 요소에 className을 추가하여, highlight를 줍니다. 마지막으로 원하는 스타일로 className에 css 작성하면 됩니다.
const observer = new IntersectionObserver(
(entries) => {
// ...
const activeHeading = visibleHeadings[visibleHeadings.length - 1];
const headingText = activeHeading.target?.textContent || '';
const tocList = document.querySelectorAll('#toc-list a');
if (!headingText || !tocList) return;
Array.from(tocList).filter((toc) => {
const tocText = toc?.textContent || '';
if (headingText === tocText) {
toc.classList.add('toc-active-item');
} else {
toc.classList.remove('toc-active-item');
}
});
},
options
);
전체 코드는 아래와 같습니다. 콜백함수 내부 로직은 자유롭게 원하는 방향으로 작성하면 됩니다.
const observer = new IntersectionObserver(
(entries) => {
const visibleHeadings = entries.filter((entry) => entry.isIntersecting);
if (visibleHeadings.length === 0) return;
const activeHeading = visibleHeadings[visibleHeadings.length - 1];
const id =
activeHeading.target.getAttribute('id')?.toLowerCase()?.replace(/\.?-/g, ' ') || '';
const tocList = document.querySelectorAll('#toc-list a');
if (!id || !tocList) return;
Array.from(tocList).filter((toc) => {
const href = toc.innerHTML?.toLowerCase()?.replace(/\.\s/, ' ') || '';
if (id === href) {
toc.classList.add('toc-active-item');
} else {
toc.classList.remove('toc-active-item');
}
});
},
options
);
'FE > React' 카테고리의 다른 글
Vite 기반 React 프로젝트에 Tailwind CSS 적용하기 (0) | 2024.08.25 |
---|---|
Bun 설치 및 React 프로젝트 생성하기 (0) | 2024.08.24 |
React와 WebRTC를 활용하여 실시간 화상 채팅 구현하기 (1) | 2024.04.28 |
react와 tailwind를 사용하여 다크 테마 구현하기 (0) | 2023.09.25 |
react를 사용해서 크롬(chrome) extension 만들기 (1) | 2023.09.24 |
IT 기술에 대한 글을 주로 작성하고, 일상 내용, 맛집/숙박/제품 리뷰 등 여러가지 주제를작성하는 블로그입니다. 티스토리 커스텀 스킨도 개발하고 있으니 관심있으신분은 Berry Skin을 검색바랍니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!